]> source.dussan.org Git - sonarqube.git/blob
3d2368ed24b691e1dee38a76c8629e83fffc7560
[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
167     switch (alm) {
168       case AlmKeys.Azure: {
169         const projectName = almSpecificFields?.slug;
170         const repositoryName = almSpecificFields?.repository;
171         const monorepo = almSpecificFields?.monorepo ?? false;
172         if (!projectName || !repositoryName) {
173           return Promise.reject();
174         }
175         return setProjectAzureBinding({
176           almSetting,
177           project,
178           projectName,
179           repositoryName,
180           monorepo
181         });
182       }
183       case AlmKeys.Bitbucket: {
184         const repository = almSpecificFields?.repository;
185         const slug = almSpecificFields?.slug;
186         const monorepo = almSpecificFields?.monorepo ?? false;
187         if (!repository || !slug) {
188           return Promise.reject();
189         }
190         return setProjectBitbucketBinding({
191           almSetting,
192           project,
193           repository,
194           slug,
195           monorepo
196         });
197       }
198       case AlmKeys.GitHub: {
199         const repository = almSpecificFields?.repository;
200         // By default it must remain true.
201         const summaryCommentEnabled =
202           almSpecificFields?.summaryCommentEnabled === undefined
203             ? true
204             : almSpecificFields?.summaryCommentEnabled;
205         const monorepo = almSpecificFields?.monorepo ?? false;
206         if (!repository) {
207           return Promise.reject();
208         }
209         return setProjectGithubBinding({
210           almSetting,
211           project,
212           repository,
213           summaryCommentEnabled,
214           monorepo
215         });
216       }
217
218       case AlmKeys.GitLab: {
219         const repository = almSpecificFields && almSpecificFields.repository;
220         return setProjectGitlabBinding({
221           almSetting,
222           project,
223           repository
224         });
225       }
226
227       default:
228         return Promise.reject();
229     }
230   }
231
232   handleSubmit = () => {
233     this.setState({ saving: true });
234     const {
235       formData: { key, ...additionalFields },
236       instances
237     } = this.state;
238
239     const selected = instances.find(i => i.key === key);
240     if (!key || !selected) {
241       return;
242     }
243
244     this.submitProjectAlmBinding(selected.alm, key, additionalFields)
245       .then(() => {
246         if (this.mounted) {
247           this.setState({
248             saving: false,
249             success: true
250           });
251         }
252       })
253       .then(this.fetchDefinitions)
254       .catch(this.catchError);
255   };
256
257   isDataSame(
258     { key, repository = '', slug = '', summaryCommentEnabled = false, monorepo = false }: FormData,
259     {
260       key: oKey = '',
261       repository: oRepository = '',
262       slug: oSlug = '',
263       summaryCommentEnabled: osummaryCommentEnabled = false,
264       monorepo: omonorepo = false
265     }: FormData
266   ) {
267     return (
268       key === oKey &&
269       repository === oRepository &&
270       slug === oSlug &&
271       summaryCommentEnabled === osummaryCommentEnabled &&
272       monorepo === omonorepo
273     );
274   }
275
276   handleFieldChange = (id: keyof ProjectAlmBindingResponse, value: string | boolean) => {
277     this.setState(({ formData, orignalData }) => {
278       const newFormData = {
279         ...formData,
280         [id]: value
281       };
282
283       return {
284         formData: newFormData,
285         isValid: this.validateForm(newFormData),
286         isChanged: !this.isDataSame(newFormData, orignalData || { key: '' }),
287         success: false
288       };
289     });
290   };
291
292   validateForm = ({ key, ...additionalFields }: State['formData']) => {
293     const { instances } = this.state;
294     const selected = instances.find(i => i.key === key);
295     if (!key || !selected) {
296       return false;
297     }
298     return REQUIRED_FIELDS_BY_ALM[selected.alm].reduce(
299       (result: boolean, field) => result && Boolean(additionalFields[field]),
300       true
301     );
302   };
303
304   render() {
305     const { monorepoEnabled } = this.props;
306
307     return (
308       <PRDecorationBindingRenderer
309         onFieldChange={this.handleFieldChange}
310         onReset={this.handleReset}
311         onSubmit={this.handleSubmit}
312         monorepoEnabled={monorepoEnabled}
313         {...this.state}
314       />
315     );
316   }
317 }
318
319 const mapStateToProps = (state: Store): StateProps => ({
320   // This feature trigger will be replaced when SONAR-14349 is implemented
321   monorepoEnabled: [EditionKey.enterprise, EditionKey.datacenter].includes(
322     getAppState(state).edition as EditionKey
323   )
324 });
325
326 export default connect(mapStateToProps)(PRDecorationBinding);