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