]> source.dussan.org Git - sonarqube.git/blob
aaf2ec9d9838d4fe12b8e060ef242ee9c0454b1a
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2023 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 {
23   deleteProjectAlmBinding,
24   getAlmSettings,
25   getProjectAlmBinding,
26   setProjectAzureBinding,
27   setProjectBitbucketBinding,
28   setProjectBitbucketCloudBinding,
29   setProjectGithubBinding,
30   setProjectGitlabBinding,
31   validateProjectAlmBinding,
32 } from '../../../../api/alm-settings';
33 import withCurrentUserContext from '../../../../app/components/current-user/withCurrentUserContext';
34 import { throwGlobalError } from '../../../../helpers/error';
35 import { HttpStatus } from '../../../../helpers/request';
36 import { hasGlobalPermission } from '../../../../helpers/users';
37 import {
38   AlmKeys,
39   AlmSettingsInstance,
40   ProjectAlmBindingConfigurationErrors,
41   ProjectAlmBindingResponse,
42 } from '../../../../types/alm-settings';
43 import { Permissions } from '../../../../types/permissions';
44 import { Component } from '../../../../types/types';
45 import { CurrentUser } from '../../../../types/users';
46 import PRDecorationBindingRenderer from './PRDecorationBindingRenderer';
47
48 type FormData = Omit<ProjectAlmBindingResponse, 'alm'>;
49
50 interface Props {
51   component: Component;
52   currentUser: CurrentUser;
53 }
54
55 interface State {
56   formData: FormData;
57   instances: AlmSettingsInstance[];
58   isChanged: boolean;
59   isConfigured: boolean;
60   isValid: boolean;
61   loading: boolean;
62   originalData?: FormData;
63   updating: boolean;
64   successfullyUpdated: boolean;
65   checkingConfiguration: boolean;
66   configurationErrors?: ProjectAlmBindingConfigurationErrors;
67 }
68
69 const REQUIRED_FIELDS_BY_ALM: {
70   [almKey in AlmKeys]: Array<keyof Omit<FormData, 'key'>>;
71 } = {
72   [AlmKeys.Azure]: ['repository', 'slug'],
73   [AlmKeys.BitbucketServer]: ['repository', 'slug'],
74   [AlmKeys.BitbucketCloud]: ['repository'],
75   [AlmKeys.GitHub]: ['repository'],
76   [AlmKeys.GitLab]: ['repository'],
77 };
78
79 const INITIAL_FORM_DATA = { key: '', repository: '', monorepo: false };
80
81 export class PRDecorationBinding extends React.PureComponent<Props, State> {
82   mounted = false;
83   state: State = {
84     formData: cloneDeep(INITIAL_FORM_DATA),
85     instances: [],
86     isChanged: false,
87     isConfigured: false,
88     isValid: false,
89     loading: true,
90     updating: false,
91     successfullyUpdated: false,
92     checkingConfiguration: false,
93   };
94
95   componentDidMount() {
96     this.mounted = true;
97     this.fetchDefinitions();
98   }
99
100   componentWillUnmount() {
101     this.mounted = false;
102   }
103
104   fetchDefinitions = () => {
105     const project = this.props.component.key;
106     return Promise.all([getAlmSettings(project), this.getProjectBinding(project)])
107       .then(([instances, originalData]) => {
108         if (this.mounted) {
109           this.setState(({ formData }) => {
110             const newFormData = originalData || formData;
111             return {
112               formData: newFormData,
113               instances: instances || [],
114               isChanged: false,
115               isConfigured: !!originalData,
116               isValid: this.validateForm(newFormData),
117               loading: false,
118               originalData: newFormData,
119               configurationErrors: undefined,
120             };
121           });
122         }
123       })
124       .catch(() => {
125         if (this.mounted) {
126           this.setState({ loading: false });
127         }
128       })
129       .then(() => this.checkConfiguration());
130   };
131
132   getProjectBinding(project: string): Promise<ProjectAlmBindingResponse | undefined> {
133     return getProjectAlmBinding(project).catch((response: Response) => {
134       if (response && response.status === HttpStatus.NotFound) {
135         return undefined;
136       }
137       return throwGlobalError(response);
138     });
139   }
140
141   catchError = () => {
142     if (this.mounted) {
143       this.setState({ updating: false });
144     }
145   };
146
147   handleReset = () => {
148     const { component } = this.props;
149     this.setState({ updating: true });
150     deleteProjectAlmBinding(component.key)
151       .then(() => {
152         if (this.mounted) {
153           this.setState({
154             formData: {
155               key: '',
156               repository: '',
157               slug: '',
158               monorepo: false,
159             },
160             originalData: undefined,
161             isChanged: false,
162             isConfigured: false,
163             updating: false,
164             successfullyUpdated: true,
165             configurationErrors: undefined,
166           });
167         }
168       })
169       .catch(this.catchError);
170   };
171
172   submitProjectAlmBinding(
173     alm: AlmKeys,
174     key: string,
175     almSpecificFields: Omit<FormData, 'key'>
176   ): Promise<void> {
177     const almSetting = key;
178     const { repository, slug = '', monorepo = false } = almSpecificFields;
179     const project = this.props.component.key;
180
181     switch (alm) {
182       case AlmKeys.Azure: {
183         return setProjectAzureBinding({
184           almSetting,
185           project,
186           projectName: slug,
187           repositoryName: repository,
188           monorepo,
189         });
190       }
191       case AlmKeys.BitbucketServer: {
192         return setProjectBitbucketBinding({
193           almSetting,
194           project,
195           repository,
196           slug,
197           monorepo,
198         });
199       }
200       case AlmKeys.BitbucketCloud: {
201         return setProjectBitbucketCloudBinding({
202           almSetting,
203           project,
204           repository,
205           monorepo,
206         });
207       }
208       case AlmKeys.GitHub: {
209         // By default it must remain true.
210         const summaryCommentEnabled = almSpecificFields?.summaryCommentEnabled ?? true;
211         return setProjectGithubBinding({
212           almSetting,
213           project,
214           repository,
215           summaryCommentEnabled,
216           monorepo,
217         });
218       }
219
220       case AlmKeys.GitLab: {
221         return setProjectGitlabBinding({
222           almSetting,
223           project,
224           repository,
225           monorepo,
226         });
227       }
228
229       default:
230         return Promise.reject();
231     }
232   }
233
234   checkConfiguration = async () => {
235     const {
236       component: { key: projectKey },
237     } = this.props;
238
239     const { isConfigured } = this.state;
240
241     if (!isConfigured) {
242       return;
243     }
244
245     this.setState({ checkingConfiguration: true, configurationErrors: undefined });
246
247     const configurationErrors = await validateProjectAlmBinding(projectKey).catch((error) => error);
248
249     if (this.mounted) {
250       this.setState({ checkingConfiguration: false, configurationErrors });
251     }
252   };
253
254   handleSubmit = () => {
255     this.setState({ updating: true });
256     const {
257       formData: { key, ...additionalFields },
258       instances,
259     } = this.state;
260
261     const selected = instances.find((i) => i.key === key);
262     if (!key || !selected) {
263       return;
264     }
265
266     this.submitProjectAlmBinding(selected.alm, key, additionalFields)
267       .then(() => {
268         if (this.mounted) {
269           this.setState({
270             updating: false,
271             successfullyUpdated: true,
272           });
273         }
274       })
275       .then(this.fetchDefinitions)
276       .catch(this.catchError);
277   };
278
279   isDataSame(
280     { key, repository = '', slug = '', summaryCommentEnabled = false, monorepo = false }: FormData,
281     {
282       key: oKey = '',
283       repository: oRepository = '',
284       slug: oSlug = '',
285       summaryCommentEnabled: osummaryCommentEnabled = false,
286       monorepo: omonorepo = false,
287     }: FormData
288   ) {
289     return (
290       key === oKey &&
291       repository === oRepository &&
292       slug === oSlug &&
293       summaryCommentEnabled === osummaryCommentEnabled &&
294       monorepo === omonorepo
295     );
296   }
297
298   handleFieldChange = (id: keyof ProjectAlmBindingResponse, value: string | boolean) => {
299     this.setState(({ formData, originalData }) => {
300       const newFormData = {
301         ...formData,
302         [id]: value,
303       };
304
305       return {
306         formData: newFormData,
307         isValid: this.validateForm(newFormData),
308         isChanged: !this.isDataSame(newFormData, originalData || cloneDeep(INITIAL_FORM_DATA)),
309         successfullyUpdated: false,
310       };
311     });
312   };
313
314   validateForm = ({ key, ...additionalFields }: State['formData']) => {
315     const { instances } = this.state;
316     const selected = instances.find((i) => i.key === key);
317     if (!key || !selected) {
318       return false;
319     }
320     return REQUIRED_FIELDS_BY_ALM[selected.alm].reduce(
321       (result: boolean, field) => result && Boolean(additionalFields[field]),
322       true
323     );
324   };
325
326   handleCheckConfiguration = async () => {
327     await this.checkConfiguration();
328   };
329
330   render() {
331     const { currentUser } = this.props;
332
333     return (
334       <PRDecorationBindingRenderer
335         onFieldChange={this.handleFieldChange}
336         onReset={this.handleReset}
337         onSubmit={this.handleSubmit}
338         onCheckConfiguration={this.handleCheckConfiguration}
339         isSysAdmin={hasGlobalPermission(currentUser, Permissions.Admin)}
340         {...this.state}
341       />
342     );
343   }
344 }
345
346 export default withCurrentUserContext(PRDecorationBinding);