]> source.dussan.org Git - sonarqube.git/blob
5da48c1bb73c456f853bfdf7b13142c6b9aa558e
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2024 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
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.
10  *
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.
15  *
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.
19  */
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';
25 import {
26   useDeleteProjectAlmBindingMutation,
27   useProjectBindingQuery,
28   useSetProjectBindingMutation,
29 } from '../../../../queries/devops-integration';
30 import {
31   AlmKeys,
32   AlmSettingsInstance,
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';
40
41 type FormData = Omit<ProjectAlmBindingResponse, 'alm'>;
42
43 interface Props {
44   component: Component;
45   currentUser: CurrentUser;
46 }
47
48 interface State {
49   formData: FormData;
50   instances: AlmSettingsInstance[];
51   isChanged: boolean;
52   isConfigured: boolean;
53   isValid: boolean;
54   loading: boolean;
55   originalData?: FormData;
56   updating: boolean;
57   successfullyUpdated: boolean;
58   checkingConfiguration: boolean;
59   configurationErrors?: ProjectAlmBindingConfigurationErrors;
60 }
61
62 const REQUIRED_FIELDS_BY_ALM: {
63   [almKey in AlmKeys]: Array<keyof Omit<FormData, 'key'>>;
64 } = {
65   [AlmKeys.Azure]: ['repository', 'slug'],
66   [AlmKeys.BitbucketServer]: ['repository', 'slug'],
67   [AlmKeys.BitbucketCloud]: ['repository'],
68   [AlmKeys.GitHub]: ['repository'],
69   [AlmKeys.GitLab]: ['repository'],
70 };
71
72 const INITIAL_FORM_DATA = { key: '', repository: '', monorepo: false };
73
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(
84     component.key,
85   );
86   const { mutateAsync: updateMutation, isLoading: isUpdating } = useSetProjectBindingMutation();
87
88   const isConfigured = !!originalData;
89   const updating = isDeleting || isUpdating;
90
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) {
95         return false;
96       }
97       return REQUIRED_FIELDS_BY_ALM[selected.alm].reduce(
98         (result: boolean, field) => result && Boolean(additionalFields[field]),
99         true,
100       );
101     };
102
103     return validateForm(formData);
104   }, [formData, instances]);
105
106   const isDataSame = (
107     { key, repository = '', slug = '', summaryCommentEnabled = false, monorepo = false }: FormData,
108     {
109       key: oKey = '',
110       repository: oRepository = '',
111       slug: oSlug = '',
112       summaryCommentEnabled: osummaryCommentEnabled = false,
113       monorepo: omonorepo = false,
114     }: FormData,
115   ) => {
116     return (
117       key === oKey &&
118       repository === oRepository &&
119       slug === oSlug &&
120       summaryCommentEnabled === osummaryCommentEnabled &&
121       monorepo === omonorepo
122     );
123   };
124
125   const isChanged = !isDataSame(formData, originalData ?? cloneDeep(INITIAL_FORM_DATA));
126
127   React.useEffect(() => {
128     fetchDefinitions();
129   }, []);
130
131   React.useEffect(() => {
132     checkConfiguration();
133   }, [originalData]);
134
135   React.useEffect(() => {
136     setFormData((formData) => originalData ?? formData);
137   }, [originalData]);
138
139   const fetchDefinitions = () => {
140     const project = component.key;
141
142     return getAlmSettings(project)
143       .then((instances) => {
144         setInstances(instances || []);
145         setConfigurationErrors(undefined);
146         setLoading(false);
147       })
148       .catch(() => {
149         setLoading(false);
150       });
151   };
152
153   const handleReset = () => {
154     deleteMutation()
155       .then(() => {
156         setFormData({
157           key: '',
158           repository: '',
159           slug: '',
160           monorepo: false,
161         });
162         setSuccessfullyUpdated(true);
163         setConfigurationErrors(undefined);
164       })
165       .catch(() => {});
166   };
167
168   const submitProjectAlmBinding = (
169     alm: AlmKeys,
170     key: string,
171     almSpecificFields: Omit<FormData, 'key'>,
172   ): Promise<void> => {
173     const almSetting = key;
174     const { repository, slug = '', monorepo = false } = almSpecificFields;
175     const project = component.key;
176
177     const baseParams = {
178       almSetting,
179       project,
180       repository,
181       monorepo,
182     };
183     let updateParams;
184
185     if (alm === AlmKeys.Azure || alm === AlmKeys.BitbucketServer) {
186       updateParams = {
187         alm,
188         ...baseParams,
189         slug,
190       };
191     } else if (alm === AlmKeys.GitHub) {
192       updateParams = {
193         alm,
194         ...baseParams,
195         summaryCommentEnabled: almSpecificFields?.summaryCommentEnabled ?? true,
196       };
197     } else {
198       updateParams = {
199         alm,
200         ...baseParams,
201       };
202     }
203
204     return updateMutation(updateParams);
205   };
206
207   const checkConfiguration = async () => {
208     const projectKey = component.key;
209
210     if (!isConfigured) {
211       return;
212     }
213
214     setCheckingConfiguration(true);
215     setConfigurationErrors(undefined);
216
217     const configurationErrors = await validateProjectAlmBinding(projectKey).catch((error) => error);
218
219     setCheckingConfiguration(false);
220     setConfigurationErrors(configurationErrors);
221   };
222
223   const handleSubmit = () => {
224     const { key, ...additionalFields } = formData;
225
226     const selected = instances.find((i) => i.key === key);
227     if (!key || !selected) {
228       return;
229     }
230
231     submitProjectAlmBinding(selected.alm, key, additionalFields)
232       .then(() => {
233         setSuccessfullyUpdated(true);
234       })
235       .then(fetchDefinitions)
236       .catch(() => {});
237   };
238
239   const handleFieldChange = (id: keyof ProjectAlmBindingResponse, value: string | boolean) => {
240     setFormData((formData) => ({
241       ...formData,
242       [id]: value,
243     }));
244     setSuccessfullyUpdated(false);
245   };
246
247   const handleCheckConfiguration = async () => {
248     await checkConfiguration();
249   };
250
251   return (
252     <PRDecorationBindingRenderer
253       onFieldChange={handleFieldChange}
254       onReset={handleReset}
255       onSubmit={handleSubmit}
256       onCheckConfiguration={handleCheckConfiguration}
257       isSysAdmin={hasGlobalPermission(currentUser, Permissions.Admin)}
258       instances={instances}
259       formData={formData}
260       isChanged={isChanged}
261       isValid={isValid}
262       isConfigured={isConfigured}
263       loading={loading}
264       updating={updating}
265       successfullyUpdated={successfullyUpdated}
266       checkingConfiguration={checkingConfiguration}
267       configurationErrors={configurationErrors}
268     />
269   );
270 }
271
272 export default withCurrentUserContext(PRDecorationBinding);