]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19462 Use react-query for fetching authentication settings
authorMathieu Suen <mathieu.suen@sonarsource.com>
Thu, 1 Jun 2023 15:52:23 +0000 (17:52 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 7 Jun 2023 20:02:43 +0000 (20:02 +0000)
server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts
server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.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/__tests__/Authentication-it.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useConfiguration.ts
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/queries/settings.ts [new file with mode: 0644]

index cc1d04f61dc3f090fef047578b573dc75a0ba872..c58f7d8473ebecaaeff991e0953fcc4a1ab478ce 100644 (file)
@@ -160,6 +160,10 @@ export default class SettingsServiceMock {
     }
 
     this.set(definition.key, value);
+    const def = this.#definitions.find((d) => d.key === definition.key);
+    if (def === undefined) {
+      this.#definitions.push(definition as ExtendedSettingDefinition);
+    }
 
     return this.reply(undefined);
   };
index 8cb254886982b8febf9712d52b56a47a53d97517..ae6d06d143227c18b6941f1466f4363553a4c7a4 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { isEmptyArray } from 'formik';
-import { isEmpty, keyBy } from 'lodash';
+import { keyBy } from 'lodash';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
-import { resetSettingValue, setSettingValue } from '../../../../api/settings';
 import DocLink from '../../../../components/common/DocLink';
 import Modal from '../../../../components/controls/Modal';
 import { ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
@@ -29,6 +27,7 @@ import { Alert } from '../../../../components/ui/Alert';
 import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
 import { translate } from '../../../../helpers/l10n';
 import { Dict } from '../../../../types/types';
+import { useSaveValuesMutation } from '../../queries/settings';
 import { AuthenticationTabs, DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
 import AuthenticationFormField from './AuthenticationFormField';
 import { SettingValue } from './hook/useConfiguration';
@@ -40,7 +39,6 @@ interface Props {
   setNewValue: (key: string, value: string | boolean) => void;
   canBeSave: boolean;
   onClose: () => void;
-  onReload: () => Promise<void>;
   tab: AuthenticationTabs;
   excludedField: string[];
   hasLegacyConfiguration?: boolean;
@@ -64,34 +62,22 @@ export default function ConfigurationForm(props: Props) {
   } = props;
   const [errors, setErrors] = React.useState<Dict<ErrorValue>>({});
 
+  const { mutateAsync: changeConfig } = useSaveValuesMutation();
+
   const headerLabel = translate('settings.authentication.form', create ? 'create' : 'edit', tab);
 
   const handleSubmit = async (event: React.SyntheticEvent<HTMLFormElement>) => {
     event.preventDefault();
 
     if (canBeSave) {
-      const r = await Promise.all(
-        Object.values(values)
-          .filter((v) => v.newValue !== undefined)
-          .map(async ({ key, newValue, definition }) => {
-            try {
-              if (isEmptyArray(newValue)) {
-                await resetSettingValue({ keys: definition.key });
-              } else {
-                await setSettingValue(definition, newValue);
-              }
-              return { key, success: true };
-            } catch (error) {
-              return { key, success: false };
-            }
-          })
-      );
-      const errors = r
+      const data = await changeConfig(Object.values(values));
+      const errors = data
         .filter(({ success }) => !success)
         .map(({ key }) => ({ key, message: translate('default_save_field_error_message') }));
+
       setErrors(keyBy(errors, 'key'));
-      if (isEmpty(errors)) {
-        await props.onReload();
+
+      if (errors.length === 0) {
         props.onClose();
       }
     } else {
@@ -122,11 +108,11 @@ export default function ConfigurationForm(props: Props) {
             <Alert variant={hasLegacyConfiguration ? 'warning' : 'info'}>
               <FormattedMessage
                 id={`settings.authentication.${
-                  hasLegacyConfiguration ? 'legacy_help.github' : 'help'
+                  hasLegacyConfiguration ? `legacy_help.${tab}` : 'help'
                 }`}
                 defaultMessage={translate(
                   `settings.authentication.${
-                    hasLegacyConfiguration ? 'legacy_help.github' : 'help'
+                    hasLegacyConfiguration ? `legacy_help.${tab}` : 'help'
                   }`
                 )}
                 values={{
index 214e89f16ddf47ae07decf825fe20919686ee295..a6814babf7c09a732e1a812b68931fb8a514fe2d 100644 (file)
@@ -37,7 +37,7 @@ import AuthenticationFormField from './AuthenticationFormField';
 import ConfigurationForm from './ConfigurationForm';
 import GitHubConfigurationValidity from './GitHubConfigurationValidity';
 import useGithubConfiguration, { GITHUB_JIT_FIELDS } from './hook/useGithubConfiguration';
-import { useIdentityProvierQuery } from './queries/identity-provider';
+import { useCheckGitHubConfigQuery, useIdentityProvierQuery } from './queries/identity-provider';
 
 interface GithubAuthenticationProps {
   definitions: ExtendedSettingDefinition[];
@@ -59,15 +59,13 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
     hasConfiguration,
     hasGithubProvisioning,
     githubProvisioningStatus,
-    loading,
+    isLoading,
     values,
     setNewValue,
     canBeSave,
-    reload,
     url,
     appId,
     enabled,
-    deleteConfiguration,
     newGithubProvisioningStatus,
     setNewGithubProvisioningStatus,
     hasGithubProvisioningConfigChange,
@@ -76,16 +74,19 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
     changeProvisioning,
     toggleEnable,
     hasLegacyConfiguration,
+    deleteMutation: { isLoading: isDeleting, mutate: deleteConfiguration },
   } = useGithubConfiguration(definitions);
 
   const hasDifferentProvider = data?.provider !== undefined && data.provider !== Provider.Github;
   const { canSyncNow, synchronizeNow } = useSyncNow();
+  const { refetch } = useCheckGitHubConfigQuery(enabled);
 
   const handleCreateConfiguration = () => {
     setShowEditModal(true);
   };
 
-  const handleCancelConfiguration = () => {
+  const handleCloseConfiguration = () => {
+    refetch();
     setShowEditModal(false);
   };
 
@@ -150,7 +151,11 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
                 <EditIcon />
                 {translate('settings.authentication.form.edit')}
               </Button>
-              <Button className="button-red" disabled={enabled} onClick={deleteConfiguration}>
+              <Button
+                className="button-red"
+                disabled={enabled || isDeleting}
+                onClick={deleteConfiguration}
+              >
                 <DeleteIcon />
                 {translate('settings.authentication.form.delete')}
               </Button>
@@ -320,13 +325,12 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
         <ConfigurationForm
           tab={AlmKeys.GitHub}
           excludedField={GITHUB_EXCLUDED_FIELD}
-          loading={loading}
+          loading={isLoading}
           values={values}
           setNewValue={setNewValue}
           canBeSave={canBeSave}
-          onClose={handleCancelConfiguration}
+          onClose={handleCloseConfiguration}
           create={!hasConfiguration}
-          onReload={reload}
           hasLegacyConfiguration={hasLegacyConfiguration}
         />
       )}
index f56729201614261edfd01a7a8c4d98c5a4f43796..f51e9f893c33a0032e1b73bd4bd307d5b0b6ee3b 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { isEmpty } from 'lodash';
 import React from 'react';
 import { FormattedMessage } from 'react-intl';
-import { resetSettingValue, setSettingValue } from '../../../../api/settings';
 import DocLink from '../../../../components/common/DocLink';
 import Link from '../../../../components/common/Link';
 import ConfirmModal from '../../../../components/controls/ConfirmModal';
@@ -34,6 +32,7 @@ import { Alert } from '../../../../components/ui/Alert';
 import { translate } from '../../../../helpers/l10n';
 import { getBaseUrl } from '../../../../helpers/system';
 import { ExtendedSettingDefinition } from '../../../../types/settings';
+import { useSaveValueMutation } from '../../queries/settings';
 import { getPropertyName } from '../../utils';
 import DefinitionDescription from '../DefinitionDescription';
 import ConfigurationForm from './ConfigurationForm';
@@ -60,7 +59,7 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {
   const {
     hasScim,
     scimStatus,
-    loading,
+    isLoading,
     samlEnabled,
     name,
     groupValue,
@@ -73,12 +72,12 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {
     newScimStatus,
     setNewScimStatus,
     setNewGroupSetting,
-    reload,
-    deleteConfiguration,
+    deleteMutation: { isLoading: isDeleting, mutate: deleteConfiguration },
   } = useSamlConfiguration(definitions);
   const toggleScim = useToggleScimMutation();
 
   const { data } = useIdentityProvierQuery();
+  const { mutate: saveSetting } = useSaveValueMutation();
 
   const hasDifferentProvider = data?.provider !== undefined && data.provider !== Provider.Scim;
 
@@ -90,29 +89,22 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {
     setShowEditModal(false);
   };
 
-  const handleToggleEnable = async () => {
+  const handleToggleEnable = () => {
     const value = values[SAML_ENABLED_FIELD];
-    await setSettingValue(value.definition, !samlEnabled);
-    await reload();
+    saveSetting({ newValue: !samlEnabled, definition: value.definition });
   };
 
-  const handleSaveGroup = async () => {
+  const handleSaveGroup = () => {
     if (groupValue.newValue !== undefined) {
-      if (isEmpty(groupValue.newValue)) {
-        await resetSettingValue({ keys: groupValue.definition.key });
-      } else {
-        await setSettingValue(groupValue.definition, groupValue.newValue);
-      }
-      await reload();
+      saveSetting({ newValue: groupValue.newValue, definition: groupValue.definition });
     }
   };
 
   const handleConfirmChangeProvisioning = async () => {
     await toggleScim.mutateAsync(!!newScimStatus);
     if (!newScimStatus) {
-      await handleSaveGroup();
+      handleSaveGroup();
     }
-    await reload();
   };
 
   return (
@@ -168,7 +160,11 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {
                 <EditIcon />
                 {translate('settings.authentication.form.edit')}
               </Button>
-              <Button className="button-red" disabled={samlEnabled} onClick={deleteConfiguration}>
+              <Button
+                className="button-red"
+                disabled={samlEnabled || isDeleting}
+                onClick={deleteConfiguration}
+              >
                 <DeleteIcon />
                 {translate('settings.authentication.form.delete')}
               </Button>
@@ -323,13 +319,12 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {
         <ConfigurationForm
           tab={SAML}
           excludedField={SAML_EXCLUDED_FIELD}
-          loading={loading}
+          loading={isLoading}
           values={values}
           setNewValue={setNewValue}
           canBeSave={canBeSave}
           onClose={handleCancelConfiguration}
           create={!hasConfiguration}
-          onReload={reload}
         />
       )}
     </div>
index 02a8b996a332a983a7072b25b33af370fb72e2c4..01996b13ff156a5cb75b9d34936e440cf774cbaa 100644 (file)
@@ -17,7 +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 { act, screen, within } from '@testing-library/react';
+import { act, screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
 import React from 'react';
@@ -288,7 +288,7 @@ describe('SAML tab', () => {
 
     expect(await saml.disableConfigButton.find()).toBeInTheDocument();
     await user.click(saml.disableConfigButton.get());
-    expect(saml.disableConfigButton.query()).not.toBeInTheDocument();
+    await waitFor(() => expect(saml.disableConfigButton.query()).not.toBeInTheDocument());
 
     expect(await saml.enableConfigButton.find()).toBeInTheDocument();
   });
@@ -309,7 +309,7 @@ describe('SAML tab', () => {
     await user.type(saml.groupAttribute.get(), 'group');
     expect(saml.saveScim.get()).toBeEnabled();
     await user.click(saml.saveScim.get());
-    expect(await saml.saveScim.find()).toBeDisabled();
+    await waitFor(() => expect(saml.saveScim.query()).toBeDisabled());
 
     await user.click(saml.scimProvisioningButton.get());
     expect(saml.saveScim.get()).toBeEnabled();
@@ -393,7 +393,7 @@ describe('Github tab', () => {
 
     expect(await github.disableConfigButton.find()).toBeInTheDocument();
     await user.click(github.disableConfigButton.get());
-    expect(github.disableConfigButton.query()).not.toBeInTheDocument();
+    await waitFor(() => expect(github.disableConfigButton.query()).not.toBeInTheDocument());
 
     expect(await github.enableConfigButton.find()).toBeInTheDocument();
   });
@@ -403,6 +403,7 @@ describe('Github tab', () => {
     const user = userEvent.setup();
 
     renderAuthentication();
+    await user.click(await github.tab.find());
 
     await github.createConfiguration(user);
     await user.click(await github.enableConfigButton.find());
@@ -431,7 +432,8 @@ describe('Github tab', () => {
 
     expect(github.saveGithubProvisioning.get()).toBeEnabled();
     await user.click(github.saveGithubProvisioning.get());
-    expect(await github.saveGithubProvisioning.find()).toBeDisabled();
+
+    await waitFor(() => expect(github.saveGithubProvisioning.query()).toBeDisabled());
 
     await user.click(github.githubProvisioningButton.get());
 
@@ -515,7 +517,7 @@ describe('Github tab', () => {
       renderAuthentication([Feature.GithubProvisioning]);
       await github.enableConfiguration(user);
 
-      expect(github.configurationValiditySuccess.get()).toBeInTheDocument();
+      await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument());
     });
 
     it('should display that config is valid for both provisioning with multiple orgs', async () => {
@@ -528,7 +530,7 @@ describe('Github tab', () => {
       renderAuthentication([Feature.GithubProvisioning]);
       await github.enableConfiguration(user);
 
-      expect(github.configurationValiditySuccess.get()).toBeInTheDocument();
+      await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument());
       expect(github.configurationValiditySuccess.get()).toHaveTextContent('2');
 
       await act(() => user.click(github.viewConfigValidityDetailsButton.get()));
@@ -560,7 +562,7 @@ describe('Github tab', () => {
       renderAuthentication([Feature.GithubProvisioning]);
       await github.enableConfiguration(user);
 
-      expect(github.configurationValidityError.get()).toBeInTheDocument();
+      await waitFor(() => expect(github.configurationValidityError.query()).toBeInTheDocument());
       expect(github.configurationValidityError.get()).toHaveTextContent(errorMessage);
 
       await act(() => user.click(github.viewConfigValidityDetailsButton.get()));
@@ -586,7 +588,7 @@ describe('Github tab', () => {
       renderAuthentication([Feature.GithubProvisioning]);
       await github.enableConfiguration(user);
 
-      expect(github.configurationValiditySuccess.get()).toBeInTheDocument();
+      await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument());
       expect(github.configurationValiditySuccess.get()).not.toHaveTextContent(errorMessage);
 
       await act(() => user.click(github.viewConfigValidityDetailsButton.get()));
@@ -622,7 +624,7 @@ describe('Github tab', () => {
       renderAuthentication([Feature.GithubProvisioning]);
       await github.enableConfiguration(user);
 
-      expect(github.configurationValiditySuccess.get()).toBeInTheDocument();
+      await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument());
 
       await act(() => user.click(github.viewConfigValidityDetailsButton.get()));
       github.getOrgs().forEach((org) => {
index 73eb9d79c9cff3d7e887908c8c1fc031893f5a47..e1d87ca09098f9edf129d0099f875e4d881e0b42 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { every, isEmpty, keyBy } from 'lodash';
-import React, { useCallback, useState } from 'react';
-import { getValues, resetSettingValue } from '../../../../../api/settings';
+import { UseMutationResult } from '@tanstack/react-query';
+import { every, isEmpty, keyBy, update } from 'lodash';
+import { useCallback, useEffect, useState } from 'react';
 import { ExtendedSettingDefinition } from '../../../../../types/settings';
 import { Dict } from '../../../../../types/types';
+import { useGetValuesQuery, useResetSettingsMutation } from '../../../queries/settings';
 
 export type SettingValue =
   | {
@@ -47,23 +48,17 @@ export default function useConfiguration(
   definitions: ExtendedSettingDefinition[],
   optionalFields: string[]
 ) {
-  const [loading, setLoading] = useState(true);
+  const keys = definitions.map((definition) => definition.key);
   const [values, setValues] = useState<Dict<SettingValue>>({});
 
-  const reload = useCallback(async () => {
-    const keys = definitions.map((definition) => definition.key);
-
-    setLoading(true);
-
-    try {
-      const values = await getValues({
-        keys,
-      });
+  const { isLoading, data } = useGetValuesQuery(keys);
 
+  useEffect(() => {
+    if (data !== undefined) {
       setValues(
         keyBy(
           definitions.map((definition) => {
-            const value = values.find((v) => v.key === definition.key);
+            const value = data.find((v) => v.key === definition.key);
             const multiValues = definition.multiValues ?? false;
             if (multiValues) {
               return {
@@ -87,16 +82,8 @@ export default function useConfiguration(
           'key'
         )
       );
-    } finally {
-      setLoading(false);
     }
-  }, [...definitions]);
-
-  React.useEffect(() => {
-    (async () => {
-      await reload();
-    })();
-  }, [...definitions]);
+  }, [data, definitions]);
 
   const setNewValue = (key: string, newValue?: string | boolean | string[]) => {
     const value = values[key];
@@ -133,10 +120,11 @@ export default function useConfiguration(
     (v) => !v.isNotSet
   );
 
-  const deleteConfiguration = useCallback(async () => {
-    await resetSettingValue({ keys: Object.keys(values).join(',') });
-    await reload();
-  }, [reload, values]);
+  const deleteMutation = update(
+    useResetSettingsMutation(),
+    'mutate',
+    (mutate) => () => mutate(Object.keys(values))
+  ) as Omit<UseMutationResult<void, unknown, void, unknown>, 'mutateAsync'>;
 
   const isValueChange = useCallback(
     (setting: string) => {
@@ -148,12 +136,11 @@ export default function useConfiguration(
 
   return {
     values,
-    reload,
     setNewValue,
     canBeSave,
-    loading,
+    isLoading,
     hasConfiguration,
     isValueChange,
-    deleteConfiguration,
+    deleteMutation,
   };
 }
index bd90eec9a9b2e371aeede0067d3a2d7436e43272..384dfbf8c484a02860918d17cb1915f606b26cc7 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { isEmpty, some } from 'lodash';
-import { useCallback, useContext, useState } from 'react';
-import { resetSettingValue, setSettingValue } from '../../../../../api/settings';
+import { some } from 'lodash';
+import { useContext, useState } from 'react';
 import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
 import { Feature } from '../../../../../types/features';
 import { ExtendedSettingDefinition } from '../../../../../types/settings';
+import { useSaveValueMutation, useSaveValuesMutation } from '../../../queries/settings';
 import {
-  useCheckGitHubConfigQuery,
   useGithubStatusQuery,
   useToggleGithubProvisioningMutation,
 } from '../queries/identity-provider';
@@ -55,7 +54,7 @@ export interface SamlSettingValue {
 
 export default function useGithubConfiguration(definitions: ExtendedSettingDefinition[]) {
   const config = useConfiguration(definitions, OPTIONAL_FIELDS);
-  const { values, isValueChange, setNewValue, reload: reloadConfig } = config;
+  const { values, isValueChange, setNewValue } = config;
 
   const hasGithubProvisioning = useContext(AvailableFeaturesContext).includes(
     Feature.GithubProvisioning
@@ -72,52 +71,37 @@ export default function useGithubConfiguration(definitions: ExtendedSettingDefin
     GITHUB_JIT_FIELDS.forEach((s) => setNewValue(s));
   };
 
+  const { mutate: saveSetting } = useSaveValueMutation();
+  const { mutate: saveSettings } = useSaveValuesMutation();
+
   const enabled = values[GITHUB_ENABLED_FIELD]?.value === 'true';
-  const { refetch } = useCheckGitHubConfigQuery(enabled);
   const appId = values[GITHUB_APP_ID_FIELD]?.value as string;
   const url = values[GITHUB_API_URL_FIELD]?.value;
   const clientIdIsNotSet = values[GITHUB_CLIENT_ID_FIELD]?.isNotSet;
 
-  const reload = useCallback(async () => {
-    await reloadConfig();
-    // Temporary solution that will be solved once we migrate to react-query
-    refetch();
-  }, [reloadConfig]);
-
   const changeProvisioning = async () => {
     if (newGithubProvisioningStatus !== githubProvisioningStatus) {
       await toggleGithubProvisioning.mutateAsync(!!newGithubProvisioningStatus);
     }
-    await saveGroup();
+    if (!newGithubProvisioningStatus || !githubProvisioningStatus) {
+      saveGroup();
+    }
   };
 
-  const saveGroup = async () => {
-    await Promise.all(
-      GITHUB_JIT_FIELDS.map(async (settingKey) => {
-        const value = values[settingKey];
-        if (value.newValue !== undefined) {
-          if (isEmpty(value.newValue) && typeof value.newValue !== 'boolean') {
-            await resetSettingValue({ keys: value.definition.key });
-          } else {
-            await setSettingValue(value.definition, value.newValue);
-          }
-        }
-      })
-    );
-    await reload();
+  const saveGroup = () => {
+    const newValues = GITHUB_JIT_FIELDS.map((settingKey) => values[settingKey]);
+    saveSettings(newValues);
   };
 
-  const toggleEnable = async () => {
+  const toggleEnable = () => {
     const value = values[GITHUB_ENABLED_FIELD];
-    await setSettingValue(value.definition, !enabled);
-    await reload();
+    saveSetting({ newValue: !enabled, definition: value.definition });
   };
 
   const hasLegacyConfiguration = appId === undefined && !clientIdIsNotSet;
 
   return {
     ...config,
-    reload,
     url,
     enabled,
     appId,
index 2204d44ec834adfc8ff6e24120836630302ebaba..d80ffeeda92f737826d5f927fd79498b54f5df72 100644 (file)
@@ -43,7 +43,7 @@ export default function useSamlConfiguration(definitions: ExtendedSettingDefinit
   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;
+  const { values, setNewValue, isValueChange } = config;
 
   const { data: scimStatus } = useScimStatusQuery();
 
@@ -59,10 +59,6 @@ export default function useSamlConfiguration(definitions: ExtendedSettingDefinit
   const hasScimConfigChange =
     isValueChange(SAML_GROUP_NAME) || (newScimStatus !== undefined && newScimStatus !== scimStatus);
 
-  const reload = React.useCallback(async () => {
-    await reloadConfig();
-  }, [reloadConfig]);
-
   return {
     ...config,
     hasScim,
@@ -73,7 +69,6 @@ export default function useSamlConfiguration(definitions: ExtendedSettingDefinit
     groupValue,
     values,
     setNewValue,
-    reload,
     hasScimConfigChange,
     newScimStatus,
     setNewScimStatus,
diff --git a/server/sonar-web/src/main/js/apps/settings/queries/settings.ts b/server/sonar-web/src/main/js/apps/settings/queries/settings.ts
new file mode 100644 (file)
index 0000000..6c88431
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * 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 { getValues, resetSettingValue, setSettingValue } from '../../../api/settings';
+import { ExtendedSettingDefinition } from '../../../types/settings';
+
+type SettingValue = string | boolean | string[];
+
+export function useGetValuesQuery(keys: string[]) {
+  return useQuery(['settings', 'values', keys] as const, ({ queryKey: [_a, _b, keys] }) => {
+    return getValues({ keys });
+  });
+}
+
+export function useResetSettingsMutation() {
+  const queryClient = useQueryClient();
+  return useMutation({
+    mutationFn: (keys: string[]) => resetSettingValue({ keys: keys.join(',') }),
+    onSuccess: () => {
+      queryClient.invalidateQueries(['settings']);
+    },
+  });
+}
+
+export function useSaveValuesMutation() {
+  const queryClient = useQueryClient();
+  return useMutation({
+    mutationFn: (
+      values: {
+        newValue?: SettingValue;
+        definition: ExtendedSettingDefinition;
+      }[]
+    ) => {
+      return Promise.all(
+        values
+          .filter((v) => v.newValue !== undefined)
+          .map(async ({ newValue, definition }) => {
+            try {
+              if (isDefaultValue(newValue as string | boolean | string[], definition)) {
+                await resetSettingValue({ keys: definition.key });
+              } else {
+                await setSettingValue(definition, newValue);
+              }
+              return { key: definition.key, success: true };
+            } catch (error) {
+              return { key: definition.key, success: false };
+            }
+          })
+      );
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries(['settings']);
+    },
+  });
+}
+
+export function useSaveValueMutation() {
+  const queryClient = useQueryClient();
+  return useMutation({
+    mutationFn: async ({
+      newValue,
+      definition,
+    }: {
+      newValue: SettingValue;
+      definition: ExtendedSettingDefinition;
+    }) => {
+      if (isDefaultValue(newValue, definition)) {
+        await resetSettingValue({ keys: definition.key });
+      } else {
+        await setSettingValue(definition, newValue);
+      }
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries(['settings']);
+    },
+  });
+}
+
+function isDefaultValue(value: SettingValue, definition: ExtendedSettingDefinition) {
+  const defaultValue = definition.defaultValue ?? '';
+  if (definition.multiValues) {
+    return defaultValue === (value as string[]).join(',');
+  }
+
+  return defaultValue === String(value);
+}