3 * Copyright (C) 2009-2024 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 import { cloneDeep } from 'lodash';
21 import * as React from 'react';
22 import { getAlmSettings, validateProjectAlmBinding } from '../../../../api/alm-settings';
23 import withCurrentUserContext from '../../../../app/components/current-user/withCurrentUserContext';
24 import { hasGlobalPermission } from '../../../../helpers/users';
26 useDeleteProjectAlmBindingMutation,
27 useProjectBindingQuery,
28 useSetProjectBindingMutation,
29 } from '../../../../queries/devops-integration';
33 ProjectAlmBindingConfigurationErrors,
34 ProjectAlmBindingResponse,
35 } from '../../../../types/alm-settings';
36 import { Permissions } from '../../../../types/permissions';
37 import { Component } from '../../../../types/types';
38 import { CurrentUser } from '../../../../types/users';
39 import PRDecorationBindingRenderer from './PRDecorationBindingRenderer';
41 type FormData = Omit<ProjectAlmBindingResponse, 'alm'>;
45 currentUser: CurrentUser;
50 instances: AlmSettingsInstance[];
52 isConfigured: boolean;
55 originalData?: FormData;
57 successfullyUpdated: boolean;
58 checkingConfiguration: boolean;
59 configurationErrors?: ProjectAlmBindingConfigurationErrors;
62 const REQUIRED_FIELDS_BY_ALM: {
63 [almKey in AlmKeys]: Array<keyof Omit<FormData, 'key'>>;
65 [AlmKeys.Azure]: ['repository', 'slug'],
66 [AlmKeys.BitbucketServer]: ['repository', 'slug'],
67 [AlmKeys.BitbucketCloud]: ['repository'],
68 [AlmKeys.GitHub]: ['repository'],
69 [AlmKeys.GitLab]: ['repository'],
72 const INITIAL_FORM_DATA = { key: '', repository: '', monorepo: false };
74 export function PRDecorationBinding(props: Props) {
75 const { component, currentUser } = props;
76 const [formData, setFormData] = React.useState<FormData>(cloneDeep(INITIAL_FORM_DATA));
77 const [instances, setInstances] = React.useState<AlmSettingsInstance[]>([]);
78 const [configurationErrors, setConfigurationErrors] = React.useState(undefined);
79 const [loading, setLoading] = React.useState(true);
80 const [successfullyUpdated, setSuccessfullyUpdated] = React.useState(false);
81 const [checkingConfiguration, setCheckingConfiguration] = React.useState(false);
82 const { data: originalData } = useProjectBindingQuery(component.key);
83 const { mutateAsync: deleteMutation, isLoading: isDeleting } = useDeleteProjectAlmBindingMutation(
86 const { mutateAsync: updateMutation, isLoading: isUpdating } = useSetProjectBindingMutation();
88 const isConfigured = !!originalData;
89 const updating = isDeleting || isUpdating;
91 const isValid = React.useMemo(() => {
92 const validateForm = ({ key, ...additionalFields }: State['formData']) => {
93 const selected = instances.find((i) => i.key === key);
94 if (!key || !selected) {
97 return REQUIRED_FIELDS_BY_ALM[selected.alm].reduce(
98 (result: boolean, field) => result && Boolean(additionalFields[field]),
103 return validateForm(formData);
104 }, [formData, instances]);
107 { key, repository = '', slug = '', summaryCommentEnabled = false, monorepo = false }: FormData,
110 repository: oRepository = '',
112 summaryCommentEnabled: osummaryCommentEnabled = false,
113 monorepo: omonorepo = false,
118 repository === oRepository &&
120 summaryCommentEnabled === osummaryCommentEnabled &&
121 monorepo === omonorepo
125 const isChanged = !isDataSame(formData, originalData ?? cloneDeep(INITIAL_FORM_DATA));
127 React.useEffect(() => {
131 React.useEffect(() => {
132 checkConfiguration();
135 React.useEffect(() => {
136 setFormData((formData) => originalData ?? formData);
139 const fetchDefinitions = () => {
140 const project = component.key;
142 return getAlmSettings(project)
143 .then((instances) => {
144 setInstances(instances || []);
145 setConfigurationErrors(undefined);
153 const handleReset = () => {
162 setSuccessfullyUpdated(true);
163 setConfigurationErrors(undefined);
168 const submitProjectAlmBinding = (
171 almSpecificFields: Omit<FormData, 'key'>,
172 ): Promise<void> => {
173 const almSetting = key;
174 const { repository, slug = '', monorepo = false } = almSpecificFields;
175 const project = component.key;
185 if (alm === AlmKeys.Azure || alm === AlmKeys.BitbucketServer) {
191 } else if (alm === AlmKeys.GitHub) {
195 summaryCommentEnabled: almSpecificFields?.summaryCommentEnabled ?? true,
204 return updateMutation(updateParams);
207 const checkConfiguration = async () => {
208 const projectKey = component.key;
214 setCheckingConfiguration(true);
215 setConfigurationErrors(undefined);
217 const configurationErrors = await validateProjectAlmBinding(projectKey).catch((error) => error);
219 setCheckingConfiguration(false);
220 setConfigurationErrors(configurationErrors);
223 const handleSubmit = () => {
224 const { key, ...additionalFields } = formData;
226 const selected = instances.find((i) => i.key === key);
227 if (!key || !selected) {
231 submitProjectAlmBinding(selected.alm, key, additionalFields)
233 setSuccessfullyUpdated(true);
235 .then(fetchDefinitions)
239 const handleFieldChange = (id: keyof ProjectAlmBindingResponse, value: string | boolean) => {
240 setFormData((formData) => ({
244 setSuccessfullyUpdated(false);
247 const handleCheckConfiguration = async () => {
248 await checkConfiguration();
252 <PRDecorationBindingRenderer
253 onFieldChange={handleFieldChange}
254 onReset={handleReset}
255 onSubmit={handleSubmit}
256 onCheckConfiguration={handleCheckConfiguration}
257 isSysAdmin={hasGlobalPermission(currentUser, Permissions.Admin)}
258 instances={instances}
260 isChanged={isChanged}
262 isConfigured={isConfigured}
265 successfullyUpdated={successfullyUpdated}
266 checkingConfiguration={checkingConfiguration}
267 configurationErrors={configurationErrors}
272 export default withCurrentUserContext(PRDecorationBinding);