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 * as React from 'react';
22 deleteProjectAlmBinding,
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';
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';
47 type FormData = Omit<ProjectAlmBindingResponse, 'alm'>;
51 currentUser: CurrentUser;
56 instances: AlmSettingsInstance[];
58 isConfigured: boolean;
61 originalData?: FormData;
63 successfullyUpdated: boolean;
64 checkingConfiguration: boolean;
65 configurationErrors?: ProjectAlmBindingConfigurationErrors;
68 const REQUIRED_FIELDS_BY_ALM: {
69 [almKey in AlmKeys]: Array<keyof Omit<FormData, 'key'>>;
71 [AlmKeys.Azure]: ['repository', 'slug'],
72 [AlmKeys.BitbucketServer]: ['repository', 'slug'],
73 [AlmKeys.BitbucketCloud]: ['repository'],
74 [AlmKeys.GitHub]: ['repository'],
75 [AlmKeys.GitLab]: ['repository'],
78 export class PRDecorationBinding extends React.PureComponent<Props, State> {
81 formData: { key: '', monorepo: false },
88 successfullyUpdated: false,
89 checkingConfiguration: false,
94 this.fetchDefinitions();
97 componentWillUnmount() {
101 fetchDefinitions = () => {
102 const project = this.props.component.key;
103 return Promise.all([getAlmSettings(project), this.getProjectBinding(project)])
104 .then(([instances, originalData]) => {
106 this.setState(({ formData }) => {
107 const newFormData = originalData || formData;
109 formData: newFormData,
110 instances: instances || [],
112 isConfigured: !!originalData,
113 isValid: this.validateForm(newFormData),
115 originalData: newFormData,
116 configurationErrors: undefined,
123 this.setState({ loading: false });
126 .then(() => this.checkConfiguration());
129 getProjectBinding(project: string): Promise<ProjectAlmBindingResponse | undefined> {
130 return getProjectAlmBinding(project).catch((response: Response) => {
131 if (response && response.status === HttpStatus.NotFound) {
134 return throwGlobalError(response);
140 this.setState({ updating: false });
144 handleReset = () => {
145 const { component } = this.props;
146 this.setState({ updating: true });
147 deleteProjectAlmBinding(component.key)
157 originalData: undefined,
161 successfullyUpdated: true,
162 configurationErrors: undefined,
166 .catch(this.catchError);
169 submitProjectAlmBinding(
172 almSpecificFields?: Omit<FormData, 'key'>
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;
181 return Promise.reject();
185 case AlmKeys.Azure: {
187 return Promise.reject();
189 return setProjectAzureBinding({
193 repositoryName: repository,
197 case AlmKeys.BitbucketServer: {
199 return Promise.reject();
201 return setProjectBitbucketBinding({
209 case AlmKeys.BitbucketCloud: {
210 return setProjectBitbucketCloudBinding({
217 case AlmKeys.GitHub: {
218 // By default it must remain true.
219 const summaryCommentEnabled = almSpecificFields?.summaryCommentEnabled ?? true;
220 return setProjectGithubBinding({
224 summaryCommentEnabled,
229 case AlmKeys.GitLab: {
230 return setProjectGitlabBinding({
239 return Promise.reject();
243 checkConfiguration = async () => {
245 component: { key: projectKey },
248 const { isConfigured } = this.state;
254 this.setState({ checkingConfiguration: true, configurationErrors: undefined });
256 const configurationErrors = await validateProjectAlmBinding(projectKey).catch((error) => error);
259 this.setState({ checkingConfiguration: false, configurationErrors });
263 handleSubmit = () => {
264 this.setState({ updating: true });
266 formData: { key, ...additionalFields },
270 const selected = instances.find((i) => i.key === key);
271 if (!key || !selected) {
275 this.submitProjectAlmBinding(selected.alm, key, additionalFields)
280 successfullyUpdated: true,
284 .then(this.fetchDefinitions)
285 .catch(this.catchError);
289 { key, repository = '', slug = '', summaryCommentEnabled = false, monorepo = false }: FormData,
292 repository: oRepository = '',
294 summaryCommentEnabled: osummaryCommentEnabled = false,
295 monorepo: omonorepo = false,
300 repository === oRepository &&
302 summaryCommentEnabled === osummaryCommentEnabled &&
303 monorepo === omonorepo
307 handleFieldChange = (id: keyof ProjectAlmBindingResponse, value: string | boolean) => {
308 this.setState(({ formData, originalData }) => {
309 const newFormData = {
315 formData: newFormData,
316 isValid: this.validateForm(newFormData),
317 isChanged: !this.isDataSame(newFormData, originalData || { key: '', monorepo: false }),
318 successfullyUpdated: false,
323 validateForm = ({ key, ...additionalFields }: State['formData']) => {
324 const { instances } = this.state;
325 const selected = instances.find((i) => i.key === key);
326 if (!key || !selected) {
329 return REQUIRED_FIELDS_BY_ALM[selected.alm].reduce(
330 (result: boolean, field) => result && Boolean(additionalFields[field]),
335 handleCheckConfiguration = async () => {
336 await this.checkConfiguration();
340 const { currentUser } = this.props;
343 <PRDecorationBindingRenderer
344 onFieldChange={this.handleFieldChange}
345 onReset={this.handleReset}
346 onSubmit={this.handleSubmit}
347 onCheckConfiguration={this.handleCheckConfiguration}
348 isSysAdmin={hasGlobalPermission(currentUser, Permissions.Admin)}
355 export default withCurrentUserContext(PRDecorationBinding);