From 902bbef4744bfbc116f57bef25dec6f262895163 Mon Sep 17 00:00:00 2001 From: Vik Vorona Date: Wed, 31 May 2023 11:09:54 +0200 Subject: [PATCH] SONAR-19337 Display configuration validity status --- .../js/api/mocks/AuthenticationServiceMock.ts | 39 ++++ .../sonar-web/src/main/js/api/provisioning.ts | 8 +- .../GitHubConfigurationValidity.tsx | 159 +++++++++++++ .../GithubAuthenticationTab.tsx | 19 +- .../authentication/SamlAuthenticationTab.tsx | 2 +- .../__tests__/Authentication-it.tsx | 213 +++++++++++++++++- .../hook/useGithubConfiguration.ts | 2 +- .../hook/useSamlConfiguration.ts | 2 +- ...entityProvider.ts => identity-provider.ts} | 5 + .../src/main/js/types/provisioning.ts | 26 +++ .../resources/org/sonar/l10n/core.properties | 12 + 11 files changed, 461 insertions(+), 26 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationValidity.tsx rename server/sonar-web/src/main/js/apps/settings/components/authentication/queries/{IdentityProvider.ts => identity-provider.ts} (94%) diff --git a/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts index 6adf7f8010a..54510a6285a 100644 --- a/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts @@ -17,11 +17,14 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { cloneDeep } from 'lodash'; import { mockTask } from '../../helpers/mocks/tasks'; +import { GitHubConfigurationStatus, GitHubProvisioningStatus } from '../../types/provisioning'; import { Task, TaskStatuses, TaskTypes } from '../../types/tasks'; import { activateGithubProvisioning, activateScim, + checkConfigurationValidity, deactivateGithubProvisioning, deactivateScim, fetchGithubProvisioningStatus, @@ -30,14 +33,35 @@ import { jest.mock('../provisioning'); +const defaultConfigurationStatus: GitHubConfigurationStatus = { + application: { + jit: { + status: GitHubProvisioningStatus.Success, + }, + autoProvisioning: { + status: GitHubProvisioningStatus.Success, + }, + }, + installations: [ + { + organization: 'testOrg', + autoProvisioning: { + status: GitHubProvisioningStatus.Success, + }, + }, + ], +}; + export default class AuthenticationServiceMock { scimStatus: boolean; githubProvisioningStatus: boolean; + githubConfigurationStatus: GitHubConfigurationStatus; tasks: Task[]; constructor() { this.scimStatus = false; this.githubProvisioningStatus = false; + this.githubConfigurationStatus = cloneDeep(defaultConfigurationStatus); this.tasks = []; jest.mocked(activateScim).mockImplementation(this.handleActivateScim); jest.mocked(deactivateScim).mockImplementation(this.handleDeactivateScim); @@ -51,6 +75,9 @@ export default class AuthenticationServiceMock { jest .mocked(fetchGithubProvisioningStatus) .mockImplementation(this.handleFetchGithubProvisioningStatus); + jest + .mocked(checkConfigurationValidity) + .mockImplementation(this.handleCheckConfigurationValidity); } addProvisioningTask = (overrides: Partial> = {}) => { @@ -63,6 +90,13 @@ export default class AuthenticationServiceMock { ); }; + setConfigurationValidity = (overrides: Partial = {}) => { + this.githubConfigurationStatus = { + ...this.githubConfigurationStatus, + ...overrides, + }; + }; + handleActivateScim = () => { this.scimStatus = true; return Promise.resolve(); @@ -115,9 +149,14 @@ export default class AuthenticationServiceMock { }); }; + handleCheckConfigurationValidity = () => { + return Promise.resolve(this.githubConfigurationStatus); + }; + reset = () => { this.scimStatus = false; this.githubProvisioningStatus = false; + this.githubConfigurationStatus = cloneDeep(defaultConfigurationStatus); this.tasks = []; }; } diff --git a/server/sonar-web/src/main/js/api/provisioning.ts b/server/sonar-web/src/main/js/api/provisioning.ts index fbadc9dee85..00c9232ff49 100644 --- a/server/sonar-web/src/main/js/api/provisioning.ts +++ b/server/sonar-web/src/main/js/api/provisioning.ts @@ -18,8 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { throwGlobalError } from '../helpers/error'; -import { getJSON, post } from '../helpers/request'; -import { GithubStatus } from '../types/provisioning'; +import { getJSON, post, postJSON } from '../helpers/request'; +import { GitHubConfigurationStatus, GithubStatus } from '../types/provisioning'; export function fetchIsScimEnabled(): Promise { return getJSON('/api/scim_management/status') @@ -47,6 +47,10 @@ export function deactivateGithubProvisioning(): Promise { return post('/api/github_provisioning/disable').catch(throwGlobalError); } +export function checkConfigurationValidity(): Promise { + return postJSON('/api/github_provisioning/check').catch(throwGlobalError); +} + export function syncNowGithubProvisioning(): Promise { return post('/api/github_provisioning/sync').catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationValidity.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationValidity.tsx new file mode 100644 index 00000000000..b3e07c92cec --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationValidity.tsx @@ -0,0 +1,159 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React, { useEffect, useState } from 'react'; +import theme from '../../../../app/theme'; +import Modal from '../../../../components/controls/Modal'; +import { Button } from '../../../../components/controls/buttons'; +import CheckIcon from '../../../../components/icons/CheckIcon'; +import ClearIcon from '../../../../components/icons/ClearIcon'; +import { Alert, AlertVariant } from '../../../../components/ui/Alert'; +import { translate, translateWithParameters } from '../../../../helpers/l10n'; +import { GitHubProvisioningStatus } from '../../../../types/provisioning'; +import { useCheckGitHubConfigQuery } from './queries/identity-provider'; + +const intlPrefix = 'settings.authentication.github.configuration.validation'; + +function ValidityIcon({ valid }: { valid: boolean }) { + const color = valid ? theme.colors.success500 : theme.colors.error500; + + return valid ? ( + + ) : ( + + ); +} + +interface Props { + isAutoProvisioning: boolean; +} + +function GitHubConfigurationValidity({ isAutoProvisioning }: Props) { + const [openDetails, setOpenDetails] = useState(false); + const [messages, setMessages] = useState([]); + const [alertVariant, setAlertVariant] = useState('loading'); + const { data, isFetching, refetch } = useCheckGitHubConfigQuery(); + const modalHeader = translate(`${intlPrefix}.details.title`); + + const applicationField = isAutoProvisioning ? 'autoProvisioning' : 'jit'; + + const isValidApp = + data?.application[applicationField].status === GitHubProvisioningStatus.Success; + + useEffect(() => { + if (isFetching) { + setMessages([translate(`${intlPrefix}.loading`)]); + setAlertVariant('loading'); + return; + } + + const invalidOrgs = + isValidApp && isAutoProvisioning && data + ? data.installations.filter( + (org) => org.autoProvisioning.status === GitHubProvisioningStatus.Error + ) + : []; + + if (isValidApp && invalidOrgs.length === 0) { + setMessages([ + translateWithParameters( + `${intlPrefix}.valid${data.installations.length > 1 ? '.multiple_orgs' : ''}`, + isAutoProvisioning + ? translate('settings.authentication.github.form.provisioning_with_github_short') + : translate('settings.authentication.form.provisioning_at_login_short'), + data.installations.length + ), + ]); + setAlertVariant('success'); + } else { + setMessages([ + translateWithParameters( + `${intlPrefix}.invalid`, + data?.application[applicationField].errorMessage ?? '' + ), + ...invalidOrgs.map((org) => + translateWithParameters( + `${intlPrefix}.invalid_org`, + org.organization, + org.autoProvisioning.errorMessage ?? '' + ) + ), + ]); + setAlertVariant('error'); + } + }, [isFetching, isValidApp, isAutoProvisioning, applicationField, data]); + + return ( + <> + +
+
+ {messages.map((msg) => ( +
{msg}
+ ))} +
+
+ + +
+
+
+ {openDetails && ( + setOpenDetails(false)}> +
+

+ {modalHeader} +

+
+
+ {!isValidApp && ( + {data?.application[applicationField].errorMessage} + )} +
    + {data?.installations.map((inst) => ( +
  • + + {inst.organization} + {isAutoProvisioning && + inst.autoProvisioning.status === GitHubProvisioningStatus.Error && ( + - {inst.autoProvisioning.errorMessage} + )} +
  • + ))} +
+
+
+ +
+
+ )} + + ); +} + +export default GitHubConfigurationValidity; diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx index 1f2348a1f62..214e89f16dd 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx @@ -25,7 +25,6 @@ import ConfirmModal from '../../../../components/controls/ConfirmModal'; import RadioCard from '../../../../components/controls/RadioCard'; import { Button, ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons'; import { Provider } from '../../../../components/hooks/useManageProvider'; -import CheckIcon from '../../../../components/icons/CheckIcon'; import DeleteIcon from '../../../../components/icons/DeleteIcon'; import EditIcon from '../../../../components/icons/EditIcon'; import { Alert } from '../../../../components/ui/Alert'; @@ -36,8 +35,9 @@ import { ExtendedSettingDefinition } from '../../../../types/settings'; import { DOCUMENTATION_LINK_SUFFIXES } from './Authentication'; import AuthenticationFormField from './AuthenticationFormField'; import ConfigurationForm from './ConfigurationForm'; +import GitHubConfigurationValidity from './GitHubConfigurationValidity'; import useGithubConfiguration, { GITHUB_JIT_FIELDS } from './hook/useGithubConfiguration'; -import { useIdentityProvierQuery } from './queries/IdentityProvider'; +import { useIdentityProvierQuery } from './queries/identity-provider'; interface GithubAuthenticationProps { definitions: ExtendedSettingDefinition[]; @@ -102,6 +102,11 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps )} + {enabled && ( + + )} {!hasConfiguration && !hasLegacyConfiguration && (
{translate('settings.authentication.github.form.not_configured')} @@ -130,16 +135,6 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
{translateWithParameters('settings.authentication.github.appid_x', appId)}

{url}

-

- {enabled ? ( - - - {translate('settings.authentication.form.enabled')} - - ) : ( - translate('settings.authentication.form.not_enabled') - )} -