]> source.dussan.org Git - sonarqube.git/commitdiff
[NO_JIRA] Use react-query to get the identity provider
authorMathieu Suen <mathieu.suen@sonarsource.com>
Tue, 16 May 2023 07:59:41 +0000 (09:59 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 22 May 2023 20:02:56 +0000 (20:02 +0000)
server/sonar-web/package.json
server/sonar-web/src/main/js/apps/settings/components/SettingsApp.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts
server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts
server/sonar-web/src/main/js/apps/settings/components/authentication/queries/IdentityProvider.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
server/sonar-web/yarn.lock

index c9dd15014ffb3e761eb2056cb66151b255997d21..0ffafc16ab7c85fd53d2ae061eae0adfe4b5c149 100644 (file)
@@ -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",
index e05ce398856b9b7411bd008dd034a2296f18f05d..f10b4a70348d6d7cfd1c4cc0a9f7f0998b6ef743 100644 (file)
@@ -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<Props, State> {
 
   render() {
     const { component } = this.props;
-    return <SettingsAppRenderer component={component} {...this.state} />;
+    return (
+      <QueryClientProvider client={queryClient}>
+        <SettingsAppRenderer component={component} {...this.state} />
+      </QueryClientProvider>
+    );
   }
 }
 
index 96d3160caef68e4a65fd46584e38e2e7796e39b8..7d9b411e7aef209709795f8f1449d0c76756830a 100644 (file)
  */
 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<string>();
-
-  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 && (
                     <SamlAuthenticationTab
                       definitions={definitions.filter((def) => def.subCategory === SAML)}
-                      provider={provider}
-                      onReload={() => loadProvider()}
                     />
                   )}
 
                   {tab.key === AlmKeys.GitHub && (
                     <GithubAuthenticationTab
                       definitions={definitions.filter((def) => def.subCategory === AlmKeys.GitHub)}
-                      provider={provider}
-                      onReload={() => loadProvider()}
                     />
                   )}
 
index ca32c00991bd2e6de547eb2d2c391bb8c4b196b7..44f704a755b94ef0f9d225272336472fde71b2f8 100644 (file)
@@ -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);
index 44722f68f3705503487a40de51fc03f905eff8d1..092ae89ede28b3ae86076ea6d7e59e26c8efe9e0 100644 (file)
 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();
index dea619fd54ae78dbdee4d0bd744d6011476a612d..8c9c255467b0de7db72c1d8673310f45d007ba68 100644 (file)
  * 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<boolean>();
   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,
index 034c8ee5cfa37f240925a595dba0ba821fda3b3e..aed3fa84031348547383f9d8aa9660c7f51a5e8f 100644 (file)
  * 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<boolean>(false);
+export default function useSamlConfiguration(definitions: ExtendedSettingDefinition[]) {
   const [newScimStatus, setNewScimStatus] = React.useState<boolean>();
   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 (file)
index 0000000..9a0687c
--- /dev/null
@@ -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'] });
+    },
+  });
+}
index 1d0ffcf5f5cc4dd2a0de030169d0df955b730747..765b22ef21c67f6934910a279d53a3ea4c04f153 100644 (file)
@@ -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<Metric>;
   appState?: AppState;
@@ -98,15 +100,17 @@ export function renderComponent(
   function Wrapper({ children }: { children: React.ReactElement }) {
     return (
       <IntlProvider defaultLocale="en" locale="en">
-        <HelmetProvider>
-          <AppStateContextProvider appState={appState}>
-            <MemoryRouter initialEntries={[pathname]}>
-              <Routes>
-                <Route path="*" element={children} />
-              </Routes>
-            </MemoryRouter>
-          </AppStateContextProvider>
-        </HelmetProvider>
+        <QueryClientProvider client={queryClient}>
+          <HelmetProvider>
+            <AppStateContextProvider appState={appState}>
+              <MemoryRouter initialEntries={[pathname]}>
+                <Routes>
+                  <Route path="*" element={children} />
+                </Routes>
+              </MemoryRouter>
+            </AppStateContextProvider>
+          </HelmetProvider>
+        </QueryClientProvider>
       </IntlProvider>
     );
   }
index 90e4d51539df1501a7f0babc8f07c44128ef2712..5e21aa42090024c7285266e2d19c8309500e24df 100644 (file)
@@ -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"