}
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);
};
* 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';
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';
setNewValue: (key: string, value: string | boolean) => void;
canBeSave: boolean;
onClose: () => void;
- onReload: () => Promise<void>;
tab: AuthenticationTabs;
excludedField: string[];
hasLegacyConfiguration?: boolean;
} = 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 {
<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={{
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[];
hasConfiguration,
hasGithubProvisioning,
githubProvisioningStatus,
- loading,
+ isLoading,
values,
setNewValue,
canBeSave,
- reload,
url,
appId,
enabled,
- deleteConfiguration,
newGithubProvisioningStatus,
setNewGithubProvisioningStatus,
hasGithubProvisioningConfigChange,
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);
};
<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>
<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}
/>
)}
* 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';
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';
const {
hasScim,
scimStatus,
- loading,
+ isLoading,
samlEnabled,
name,
groupValue,
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;
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 (
<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>
<ConfigurationForm
tab={SAML}
excludedField={SAML_EXCLUDED_FIELD}
- loading={loading}
+ loading={isLoading}
values={values}
setNewValue={setNewValue}
canBeSave={canBeSave}
onClose={handleCancelConfiguration}
create={!hasConfiguration}
- onReload={reload}
/>
)}
</div>
* 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';
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();
});
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();
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();
});
const user = userEvent.setup();
renderAuthentication();
+ await user.click(await github.tab.find());
await github.createConfiguration(user);
await user.click(await github.enableConfigButton.find());
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());
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 () => {
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()));
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()));
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()));
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) => {
* 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 =
| {
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 {
'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];
(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) => {
return {
values,
- reload,
setNewValue,
canBeSave,
- loading,
+ isLoading,
hasConfiguration,
isValueChange,
- deleteConfiguration,
+ deleteMutation,
};
}
* 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';
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
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,
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();
const hasScimConfigChange =
isValueChange(SAML_GROUP_NAME) || (newScimStatus !== undefined && newScimStatus !== scimStatus);
- const reload = React.useCallback(async () => {
- await reloadConfig();
- }, [reloadConfig]);
-
return {
...config,
hasScim,
groupValue,
values,
setNewValue,
- reload,
hasScimConfigChange,
newScimStatus,
setNewScimStatus,
--- /dev/null
+/*
+ * 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);
+}