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