From: Mathieu Suen Date: Tue, 16 May 2023 07:59:41 +0000 (+0200) Subject: [NO_JIRA] Use react-query to get the identity provider X-Git-Tag: 10.1.0.73491~238 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=ba4f89b285cf1cc1d3700ce8955c52a71bebcb17;p=sonarqube.git [NO_JIRA] Use react-query to get the identity provider --- diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index c9dd15014ff..0ffafc16ab7 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -11,6 +11,7 @@ "@emotion/react": "11.10.6", "@emotion/styled": "11.10.6", "@primer/octicons-react": "18.3.0", + "@tanstack/react-query": "4.29.7", "classnames": "2.3.2", "clipboard": "2.0.11", "core-js": "3.29.1", diff --git a/server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx b/server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx index e05ce398856..f10b4a70348 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import * as React from 'react'; import { getDefinitions } from '../../../api/settings'; import withComponentContext from '../../../app/components/componentContext/withComponentContext'; @@ -31,6 +32,8 @@ import { Component } from '../../../types/types'; import '../styles.css'; import SettingsAppRenderer from './SettingsAppRenderer'; +const queryClient = new QueryClient(); + interface Props { component?: Component; } @@ -77,7 +80,11 @@ class SettingsApp extends React.PureComponent { render() { const { component } = this.props; - return ; + return ( + + + + ); } } diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx index 96d3160caef..7d9b411e7ae 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx @@ -19,10 +19,8 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import { useCallback, useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { useSearchParams } from 'react-router-dom'; -import { getSystemInfo } from '../../../../api/system'; import withAvailableFeatures, { WithAvailableFeaturesProps, } from '../../../../app/components/available-features/withAvailableFeatures'; @@ -37,7 +35,6 @@ import { searchParamsToQuery } from '../../../../helpers/urls'; import { AlmKeys } from '../../../../types/alm-settings'; import { Feature } from '../../../../types/features'; import { ExtendedSettingDefinition } from '../../../../types/settings'; -import { SysInfoCluster } from '../../../../types/types'; import { AUTHENTICATION_CATEGORY } from '../../constants'; import CategoryDefinitionsList from '../CategoryDefinitionsList'; import GithubAuthenticationTab from './GithubAuthenticationTab'; @@ -78,16 +75,6 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) { const { definitions } = props; const [query, setSearchParams] = useSearchParams(); - const [provider, setProvider] = useState(); - - const loadProvider = useCallback(async () => { - const info = (await getSystemInfo()) as SysInfoCluster; - setProvider(info.System['External Users and Groups Provisioning']); - }, []); - - useEffect(() => { - loadProvider(); - }, []); const currentTab = (query.get('tab') || SAML) as AuthenticationTabs; @@ -182,16 +169,12 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) { {tab.key === SAML && ( def.subCategory === SAML)} - provider={provider} - onReload={() => loadProvider()} /> )} {tab.key === AlmKeys.GitHub && ( def.subCategory === AlmKeys.GitHub)} - provider={provider} - onReload={() => loadProvider()} /> )} 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 ca32c00991b..44f704a755b 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 @@ -36,11 +36,10 @@ import { DOCUMENTATION_LINK_SUFFIXES } from './Authentication'; import AuthenticationFormField from './AuthenticationFormField'; import ConfigurationForm from './ConfigurationForm'; import useGithubConfiguration, { GITHUB_JIT_FIELDS } from './hook/useGithubConfiguration'; +import { useIdentityProvierQuery } from './queries/IdentityProvider'; interface GithubAuthenticationProps { definitions: ExtendedSettingDefinition[]; - provider: string | undefined; - onReload: () => void; } const GITHUB_EXCLUDED_FIELD = [ @@ -51,7 +50,8 @@ const GITHUB_EXCLUDED_FIELD = [ ]; export default function GithubAuthenticationTab(props: GithubAuthenticationProps) { - const { definitions, provider } = props; + const { definitions } = props; + const { data } = useIdentityProvierQuery(); const [showEditModal, setShowEditModal] = useState(false); const [showConfirmProvisioningModal, setShowConfirmProvisioningModal] = useState(false); @@ -76,9 +76,9 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps changeProvisioning, toggleEnable, hasLegacyConfiguration, - } = useGithubConfiguration(definitions, props.onReload); + } = useGithubConfiguration(definitions); - const hasDifferentProvider = provider !== undefined && provider !== Provider.Github; + const hasDifferentProvider = data?.provider !== undefined && data.provider !== Provider.Github; const handleCreateConfiguration = () => { setShowEditModal(true); diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx index 44722f68f37..092ae89ede2 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx @@ -20,12 +20,7 @@ import { isEmpty } from 'lodash'; import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { - activateScim, - deactivateScim, - resetSettingValue, - setSettingValue, -} from '../../../../api/settings'; +import { resetSettingValue, setSettingValue } from '../../../../api/settings'; import DocLink from '../../../../components/common/DocLink'; import Link from '../../../../components/common/Link'; import ConfirmModal from '../../../../components/controls/ConfirmModal'; @@ -47,11 +42,10 @@ import useSamlConfiguration, { SAML_GROUP_NAME, SAML_SCIM_DEPRECATED, } from './hook/useSamlConfiguration'; +import { useIdentityProvierQuery, useToggleScimMutation } from './queries/IdentityProvider'; interface SamlAuthenticationProps { definitions: ExtendedSettingDefinition[]; - provider: string | undefined; - onReload: () => void; } export const SAML = 'saml'; @@ -60,7 +54,7 @@ const CONFIG_TEST_PATH = '/saml/validation_init'; const SAML_EXCLUDED_FIELD = [SAML_ENABLED_FIELD, SAML_GROUP_NAME, SAML_SCIM_DEPRECATED]; export default function SamlAuthenticationTab(props: SamlAuthenticationProps) { - const { definitions, provider, onReload } = props; + const { definitions } = props; const [showEditModal, setShowEditModal] = React.useState(false); const [showConfirmProvisioningModal, setShowConfirmProvisioningModal] = React.useState(false); const { @@ -81,9 +75,12 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) { setNewGroupSetting, reload, deleteConfiguration, - } = useSamlConfiguration(definitions, onReload); + } = useSamlConfiguration(definitions); + const toggleScim = useToggleScimMutation(); - const hasDifferentProvider = provider !== undefined && provider !== Provider.Scim; + const { data } = useIdentityProvierQuery(); + + const hasDifferentProvider = data?.provider !== undefined && data.provider !== Provider.Scim; const handleCreateConfiguration = () => { setShowEditModal(true); @@ -111,10 +108,8 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) { }; const handleConfirmChangeProvisioning = async () => { - if (newScimStatus) { - await activateScim(); - } else { - await deactivateScim(); + await toggleScim.mutateAsync(!!newScimStatus); + if (!newScimStatus) { await handleSaveGroup(); } await reload(); diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts index dea619fd54a..8c9c255467b 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts @@ -18,17 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { isEmpty, some } from 'lodash'; -import { useCallback, useContext, useEffect, useState } from 'react'; -import { - activateGithubProvisioning, - deactivateGithubProvisioning, - fetchIsGithubProvisioningEnabled, - resetSettingValue, - setSettingValue, -} from '../../../../../api/settings'; +import { useCallback, useContext, useState } from 'react'; +import { resetSettingValue, setSettingValue } from '../../../../../api/settings'; import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext'; import { Feature } from '../../../../../types/features'; import { ExtendedSettingDefinition } from '../../../../../types/settings'; +import { + useGithubStatusQuery, + useToggleGithubProvisioningMutation, +} from '../queries/IdentityProvider'; import useConfiguration from './useConfiguration'; export const GITHUB_ENABLED_FIELD = 'sonar.auth.github.enabled'; @@ -52,16 +50,15 @@ export interface SamlSettingValue { definition: ExtendedSettingDefinition; } -export default function useGithubConfiguration( - definitions: ExtendedSettingDefinition[], - onReload: () => void -) { +export default function useGithubConfiguration(definitions: ExtendedSettingDefinition[]) { const config = useConfiguration(definitions, OPTIONAL_FIELDS); const { values, isValueChange, setNewValue, reload: reloadConfig } = config; + const hasGithubProvisioning = useContext(AvailableFeaturesContext).includes( Feature.GithubProvisioning ); - const [githubProvisioningStatus, setGithubProvisioningStatus] = useState(false); + const { data: githubProvisioningStatus } = useGithubStatusQuery(); + const toggleGithubProvisioning = useToggleGithubProvisioningMutation(); const [newGithubProvisioningStatus, setNewGithubProvisioningStatus] = useState(); const hasGithubProvisioningConfigChange = some(GITHUB_JIT_FIELDS, isValueChange) || @@ -72,14 +69,6 @@ export default function useGithubConfiguration( GITHUB_JIT_FIELDS.forEach((s) => setNewValue(s)); }; - useEffect(() => { - (async () => { - if (hasGithubProvisioning) { - setGithubProvisioningStatus(await fetchIsGithubProvisioningEnabled()); - } - })(); - }, [hasGithubProvisioning]); - const enabled = values[GITHUB_ENABLED_FIELD]?.value === 'true'; const appId = values[GITHUB_APP_ID_FIELD]?.value as string; const url = values[GITHUB_API_URL_FIELD]?.value; @@ -87,20 +76,13 @@ export default function useGithubConfiguration( const reload = useCallback(async () => { await reloadConfig(); - setGithubProvisioningStatus(await fetchIsGithubProvisioningEnabled()); - onReload(); - }, [reloadConfig, onReload]); + }, [reloadConfig]); const changeProvisioning = async () => { - if (newGithubProvisioningStatus && newGithubProvisioningStatus !== githubProvisioningStatus) { - await activateGithubProvisioning(); - await reload(); - } else { - if (newGithubProvisioningStatus !== githubProvisioningStatus) { - await deactivateGithubProvisioning(); - } - await saveGroup(); + if (newGithubProvisioningStatus !== githubProvisioningStatus) { + await toggleGithubProvisioning.mutateAsync(!!newGithubProvisioningStatus); } + await saveGroup(); }; const saveGroup = async () => { @@ -134,7 +116,6 @@ export default function useGithubConfiguration( enabled, appId, hasGithubProvisioning, - setGithubProvisioningStatus, githubProvisioningStatus, newGithubProvisioningStatus, setNewGithubProvisioningStatus, diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts index 034c8ee5cfa..aed3fa84031 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts @@ -18,10 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import { fetchIsScimEnabled } from '../../../../../api/settings'; import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext'; import { Feature } from '../../../../../types/features'; import { ExtendedSettingDefinition } from '../../../../../types/settings'; +import { useScimStatusQuery } from '../queries/IdentityProvider'; import useConfiguration from './useConfiguration'; export const SAML_ENABLED_FIELD = 'sonar.auth.saml.enabled'; @@ -39,23 +39,13 @@ const OPTIONAL_FIELDS = [ SAML_SCIM_DEPRECATED, ]; -export default function useSamlConfiguration( - definitions: ExtendedSettingDefinition[], - onReload: () => void -) { - const [scimStatus, setScimStatus] = React.useState(false); +export default function useSamlConfiguration(definitions: ExtendedSettingDefinition[]) { const [newScimStatus, setNewScimStatus] = React.useState(); const hasScim = React.useContext(AvailableFeaturesContext).includes(Feature.Scim); const config = useConfiguration(definitions, OPTIONAL_FIELDS); const { reload: reloadConfig, values, setNewValue, isValueChange } = config; - React.useEffect(() => { - (async () => { - if (hasScim) { - setScimStatus(await fetchIsScimEnabled()); - } - })(); - }, [hasScim]); + const { data: scimStatus } = useScimStatusQuery(); const name = values[SAML_PROVIDER_NAME]?.value; const url = values[SAML_LOGIN_URL]?.value; @@ -71,9 +61,7 @@ export default function useSamlConfiguration( const reload = React.useCallback(async () => { await reloadConfig(); - setScimStatus(await fetchIsScimEnabled()); - onReload(); - }, [reloadConfig, onReload]); + }, [reloadConfig]); return { ...config, diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/queries/IdentityProvider.ts b/server/sonar-web/src/main/js/apps/settings/components/authentication/queries/IdentityProvider.ts new file mode 100644 index 00000000000..9a0687c630f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/queries/IdentityProvider.ts @@ -0,0 +1,86 @@ +/* + * 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 { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useContext } from 'react'; +import { + activateGithubProvisioning, + activateScim, + deactivateGithubProvisioning, + deactivateScim, + fetchIsGithubProvisioningEnabled, + fetchIsScimEnabled, +} from '../../../../../api/settings'; +import { getSystemInfo } from '../../../../../api/system'; +import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext'; +import { Feature } from '../../../../../types/features'; +import { SysInfoCluster } from '../../../../../types/types'; + +export function useIdentityProvierQuery() { + return useQuery(['identity_provider'], async () => { + const info = (await getSystemInfo()) as SysInfoCluster; + return { provider: info.System['External Users and Groups Provisioning'] }; + }); +} + +export function useScimStatusQuery() { + const hasScim = useContext(AvailableFeaturesContext).includes(Feature.Scim); + + return useQuery(['identity_provider', 'scim_status'], () => { + if (!hasScim) { + return false; + } + return fetchIsScimEnabled(); + }); +} + +export function useGithubStatusQuery() { + const hasGithubProvisioning = useContext(AvailableFeaturesContext).includes( + Feature.GithubProvisioning + ); + + return useQuery(['identity_provider', 'github_status'], () => { + if (!hasGithubProvisioning) { + return false; + } + return fetchIsGithubProvisioningEnabled(); + }); +} + +export function useToggleScimMutation() { + const client = useQueryClient(); + return useMutation({ + mutationFn: (activate: boolean) => (activate ? activateScim() : deactivateScim()), + onSuccess: () => { + client.invalidateQueries({ queryKey: ['identity_provider'] }); + }, + }); +} + +export function useToggleGithubProvisioningMutation() { + const client = useQueryClient(); + return useMutation({ + mutationFn: (activate: boolean) => + activate ? activateGithubProvisioning() : deactivateGithubProvisioning(), + onSuccess: () => { + client.invalidateQueries({ queryKey: ['identity_provider'] }); + }, + }); +} diff --git a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx index 1d0ffcf5f5c..765b22ef21c 100644 --- a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx +++ b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { fireEvent, Matcher, render, RenderResult, screen, within } from '@testing-library/react'; import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; import { omit } from 'lodash'; @@ -43,6 +44,7 @@ import { mockComponent } from './mocks/component'; import { DEFAULT_METRICS } from './mocks/metrics'; import { mockAppState, mockCurrentUser } from './testMocks'; +const queryClient = new QueryClient(); export interface RenderContext { metrics?: Dict; appState?: AppState; @@ -98,15 +100,17 @@ export function renderComponent( function Wrapper({ children }: { children: React.ReactElement }) { return ( - - - - - - - - - + + + + + + + + + + + ); } diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index 90e4d51539d..5e21aa42090 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -3565,6 +3565,32 @@ __metadata: languageName: node linkType: hard +"@tanstack/query-core@npm:4.29.7": + version: 4.29.7 + resolution: "@tanstack/query-core@npm:4.29.7" + checksum: f28441a1fc17881d770ebacbe3a2fcf58bd065ac0cf58f8dc544d367f00d7f65213e09a1a1280e021eef25a50b307cac6884173b5acf32570e755baf29b741aa + languageName: node + linkType: hard + +"@tanstack/react-query@npm:4.29.7": + version: 4.29.7 + resolution: "@tanstack/react-query@npm:4.29.7" + dependencies: + "@tanstack/query-core": 4.29.7 + use-sync-external-store: ^1.2.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-native: "*" + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + checksum: 9db2fb78e18f299c0e58f3c177261f6010fdea63a5f950e4c93a7cf7a46da019aa3171c77759fa7b327e35763e3379f0a885c71dafd9aa99bf2ddda9f264de90 + languageName: node + linkType: hard + "@testing-library/dom@npm:8.20.0": version: 8.20.0 resolution: "@testing-library/dom@npm:8.20.0" @@ -4507,6 +4533,7 @@ __metadata: "@primer/octicons-react": 18.3.0 "@swc/core": 1.3.44 "@swc/jest": 0.2.24 + "@tanstack/react-query": 4.29.7 "@testing-library/dom": 8.20.0 "@testing-library/jest-dom": 5.16.5 "@testing-library/react": 12.1.5 @@ -12574,6 +12601,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.2.0": + version: 1.2.0 + resolution: "use-sync-external-store@npm:1.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 5c639e0f8da3521d605f59ce5be9e094ca772bd44a4ce7322b055a6f58eeed8dda3c94cabd90c7a41fb6fa852210092008afe48f7038792fd47501f33299116a + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2"