]> source.dussan.org Git - sonarqube.git/blob
90e210bfd84ce0cf84bce92a1c1cb29ec617a3d3
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2023 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 { TextMuted } from 'design-system';
21 import React, { useEffect, useState } from 'react';
22 import theme, { colors } from '../../../../app/theme';
23 import Modal from '../../../../components/controls/Modal';
24 import { Button } from '../../../../components/controls/buttons';
25 import CheckIcon from '../../../../components/icons/CheckIcon';
26 import ClearIcon from '../../../../components/icons/ClearIcon';
27 import HelpIcon from '../../../../components/icons/HelpIcon';
28 import { Alert, AlertVariant } from '../../../../components/ui/Alert';
29 import { translate, translateWithParameters } from '../../../../helpers/l10n';
30 import { useCheckGitHubConfigQuery } from '../../../../queries/identity-provider';
31 import { GitHubProvisioningStatus } from '../../../../types/provisioning';
32
33 const intlPrefix = 'settings.authentication.github.configuration.validation';
34
35 function ValidityIcon({ valid }: { valid: boolean }) {
36   const color = valid ? theme.colors.success500 : theme.colors.error500;
37
38   return valid ? (
39     <CheckIcon fill={color} label={translate(`${intlPrefix}.details.valid_label`)} />
40   ) : (
41     <ClearIcon fill={color} label={translate(`${intlPrefix}.details.invalid_label`)} />
42   );
43 }
44
45 interface Props {
46   isAutoProvisioning: boolean;
47   selectedOrganizations: string[];
48 }
49
50 export default function GitHubConfigurationValidity({
51   isAutoProvisioning,
52   selectedOrganizations,
53 }: Props) {
54   const [openDetails, setOpenDetails] = useState(false);
55   const [messages, setMessages] = useState<string[]>([]);
56   const [alertVariant, setAlertVariant] = useState<AlertVariant>('loading');
57   const { data, isFetching, refetch } = useCheckGitHubConfigQuery(true);
58   const modalHeader = translate(`${intlPrefix}.details.title`);
59
60   const applicationField = isAutoProvisioning ? 'autoProvisioning' : 'jit';
61
62   const isValidApp =
63     data?.application[applicationField].status === GitHubProvisioningStatus.Success;
64
65   const failedOrgs = selectedOrganizations.filter((o) => {
66     return !data?.installations.find((i) => i.organization === o);
67   });
68
69   useEffect(() => {
70     if (isFetching) {
71       setMessages([translate(`${intlPrefix}.loading`)]);
72       setAlertVariant('loading');
73       return;
74     }
75
76     const invalidOrgs =
77       isValidApp && data
78         ? data.installations.filter(
79             (org) => org[applicationField].status === GitHubProvisioningStatus.Failed,
80           )
81         : [];
82
83     const invalidOrgsMessages = invalidOrgs.map((org) =>
84       translateWithParameters(
85         `${intlPrefix}.invalid_org`,
86         org.organization,
87         org[applicationField].errorMessage ?? '',
88       ),
89     );
90
91     if (isValidApp && invalidOrgs.length === 0) {
92       setMessages([
93         translateWithParameters(
94           `${intlPrefix}.valid${data.installations.length === 1 ? '_one' : ''}`,
95           translate(
96             `settings.authentication.github.form.provisioning_with_github_short.${applicationField}`,
97           ),
98           data.installations.length === 1
99             ? data.installations[0].organization
100             : data.installations.length,
101         ),
102       ]);
103       setAlertVariant('success');
104     } else if (isValidApp && !isAutoProvisioning) {
105       setMessages([translate(`${intlPrefix}.valid.short`), ...invalidOrgsMessages]);
106       setAlertVariant('warning');
107     } else {
108       setMessages([
109         translateWithParameters(
110           `${intlPrefix}.invalid`,
111           data?.application[applicationField].errorMessage ?? '',
112         ),
113         ...invalidOrgsMessages,
114       ]);
115       setAlertVariant('error');
116     }
117   }, [isFetching, isValidApp, isAutoProvisioning, applicationField, data]);
118
119   return (
120     <>
121       <Alert
122         title={messages[0]}
123         variant={alertVariant}
124         aria-live="polite"
125         role="status"
126         aria-atomic
127         aria-busy={isFetching}
128       >
129         <div className="sw-flex sw-justify-between sw-items-center">
130           <div>
131             {messages.map((msg) => (
132               <div key={msg}>{msg}</div>
133             ))}
134           </div>
135           <div className="sw-flex">
136             <Button
137               onClick={() => setOpenDetails(true)}
138               disabled={isFetching}
139               className="sw-mr-2 sw-whitespace-nowrap sw-text-center"
140             >
141               {translate(`${intlPrefix}.details`)}
142             </Button>
143             <Button
144               onClick={() => refetch()}
145               disabled={isFetching}
146               className="sw-whitespace-nowrap sw-text-center"
147             >
148               {translate(`${intlPrefix}.test`)}
149             </Button>
150           </div>
151         </div>
152       </Alert>
153       {openDetails && (
154         <Modal size="small" contentLabel={modalHeader} onRequestClose={() => setOpenDetails(false)}>
155           <header className="modal-head">
156             <h2>
157               {modalHeader} <ValidityIcon valid={isValidApp} />
158             </h2>
159           </header>
160           <div className="modal-body modal-container">
161             {!isValidApp && (
162               <Alert variant="error">{data?.application[applicationField].errorMessage}</Alert>
163             )}
164             <ul className="sw-pl-5">
165               {data?.installations.map((inst) => (
166                 <li key={inst.organization}>
167                   <ValidityIcon
168                     valid={inst[applicationField].status === GitHubProvisioningStatus.Success}
169                   />
170                   <span className="sw-ml-2">{inst.organization}</span>
171                   {inst[applicationField].status === GitHubProvisioningStatus.Failed && (
172                     <span> - {inst[applicationField].errorMessage}</span>
173                   )}
174                 </li>
175               ))}
176               {failedOrgs.map((fo) => (
177                 <li key={fo}>
178                   <HelpIcon fillInner={colors.gray60} fill={colors.white} role="img" />
179                   <TextMuted
180                     className="sw-ml-2"
181                     text={translateWithParameters(`${intlPrefix}.details.org_not_found`, fo)}
182                   />
183                 </li>
184               ))}
185             </ul>
186           </div>
187           <footer className="modal-foot">
188             <Button onClick={() => setOpenDetails(false)}>{translate('close')}</Button>
189           </footer>
190         </Modal>
191       )}
192     </>
193   );
194 }