3 * Copyright (C) 2009-2023 SonarSource SA
4 * mailto:info AT sonarsource DOT com
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.
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.
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.
20 import { cloneDeep } from 'lodash';
21 import * as React from 'react';
23 deleteProjectAlmBinding,
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';
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';
48 type FormData = Omit<ProjectAlmBindingResponse, 'alm'>;
52 currentUser: CurrentUser;
57 instances: AlmSettingsInstance[];
59 isConfigured: boolean;
62 originalData?: FormData;
64 successfullyUpdated: boolean;
65 checkingConfiguration: boolean;
66 configurationErrors?: ProjectAlmBindingConfigurationErrors;
69 const REQUIRED_FIELDS_BY_ALM: {
70 [almKey in AlmKeys]: Array<keyof Omit<FormData, 'key'>>;
72 [AlmKeys.Azure]: ['repository', 'slug'],
73 [AlmKeys.BitbucketServer]: ['repository', 'slug'],
74 [AlmKeys.BitbucketCloud]: ['repository'],
75 [AlmKeys.GitHub]: ['repository'],
76 [AlmKeys.GitLab]: ['repository'],
79 const INITIAL_FORM_DATA = { key: '', repository: '', monorepo: false };
81 export class PRDecorationBinding extends React.PureComponent<Props, State> {
84 formData: cloneDeep(INITIAL_FORM_DATA),
91 successfullyUpdated: false,
92 checkingConfiguration: false,
97 this.fetchDefinitions();
100 componentWillUnmount() {
101 this.mounted = false;
104 fetchDefinitions = () => {
105 const project = this.props.component.key;
106 return Promise.all([getAlmSettings(project), this.getProjectBinding(project)])
107 .then(([instances, originalData]) => {
109 this.setState(({ formData }) => {
110 const newFormData = originalData || formData;
112 formData: newFormData,
113 instances: instances || [],
115 isConfigured: !!originalData,
116 isValid: this.validateForm(newFormData),
118 originalData: newFormData,
119 configurationErrors: undefined,
126 this.setState({ loading: false });
129 .then(() => this.checkConfiguration());
132 getProjectBinding(project: string): Promise<ProjectAlmBindingResponse | undefined> {
133 return getProjectAlmBinding(project).catch((response: Response) => {
134 if (response && response.status === HttpStatus.NotFound) {
137 return throwGlobalError(response);
143 this.setState({ updating: false });
147 handleReset = () => {
148 const { component } = this.props;
149 this.setState({ updating: true });
150 deleteProjectAlmBinding(component.key)
160 originalData: undefined,
164 successfullyUpdated: true,
165 configurationErrors: undefined,
169 .catch(this.catchError);
172 submitProjectAlmBinding(
175 almSpecificFields: Omit<FormData, 'key'>
177 const almSetting = key;
178 const { repository, slug = '', monorepo = false } = almSpecificFields;
179 const project = this.props.component.key;
182 case AlmKeys.Azure: {
183 return setProjectAzureBinding({
187 repositoryName: repository,
191 case AlmKeys.BitbucketServer: {
192 return setProjectBitbucketBinding({
200 case AlmKeys.BitbucketCloud: {
201 return setProjectBitbucketCloudBinding({
208 case AlmKeys.GitHub: {
209 // By default it must remain true.
210 const summaryCommentEnabled = almSpecificFields?.summaryCommentEnabled ?? true;
211 return setProjectGithubBinding({
215 summaryCommentEnabled,
220 case AlmKeys.GitLab: {
221 return setProjectGitlabBinding({
230 return Promise.reject();
234 checkConfiguration = async () => {
236 component: { key: projectKey },
239 const { isConfigured } = this.state;
245 this.setState({ checkingConfiguration: true, configurationErrors: undefined });
247 const configurationErrors = await validateProjectAlmBinding(projectKey).catch((error) => error);
250 this.setState({ checkingConfiguration: false, configurationErrors });
254 handleSubmit = () => {
255 this.setState({ updating: true });
257 formData: { key, ...additionalFields },
261 const selected = instances.find((i) => i.key === key);
262 if (!key || !selected) {
266 this.submitProjectAlmBinding(selected.alm, key, additionalFields)
271 successfullyUpdated: true,
275 .then(this.fetchDefinitions)
276 .catch(this.catchError);
280 { key, repository = '', slug = '', summaryCommentEnabled = false, monorepo = false }: FormData,
283 repository: oRepository = '',
285 summaryCommentEnabled: osummaryCommentEnabled = false,
286 monorepo: omonorepo = false,
291 repository === oRepository &&
293 summaryCommentEnabled === osummaryCommentEnabled &&
294 monorepo === omonorepo
298 handleFieldChange = (id: keyof ProjectAlmBindingResponse, value: string | boolean) => {
299 this.setState(({ formData, originalData }) => {
300 const newFormData = {
306 formData: newFormData,
307 isValid: this.validateForm(newFormData),
308 isChanged: !this.isDataSame(newFormData, originalData || cloneDeep(INITIAL_FORM_DATA)),
309 successfullyUpdated: false,
314 validateForm = ({ key, ...additionalFields }: State['formData']) => {
315 const { instances } = this.state;
316 const selected = instances.find((i) => i.key === key);
317 if (!key || !selected) {
320 return REQUIRED_FIELDS_BY_ALM[selected.alm].reduce(
321 (result: boolean, field) => result && Boolean(additionalFields[field]),
326 handleCheckConfiguration = async () => {
327 await this.checkConfiguration();
331 const { currentUser } = this.props;
334 <PRDecorationBindingRenderer
335 onFieldChange={this.handleFieldChange}
336 onReset={this.handleReset}
337 onSubmit={this.handleSubmit}
338 onCheckConfiguration={this.handleCheckConfiguration}
339 isSysAdmin={hasGlobalPermission(currentUser, Permissions.Admin)}
346 export default withCurrentUserContext(PRDecorationBinding);