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