]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20392 Add Edit mapping button for auto-provisioning
authorViktor Vorona <viktor.vorona@sonarsource.com>
Wed, 13 Sep 2023 09:54:16 +0000 (11:54 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 15 Sep 2023 20:03:05 +0000 (20:03 +0000)
13 files changed:
server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts
server/sonar-web/src/main/js/api/provisioning.ts
server/sonar-web/src/main/js/app/index.ts
server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormField.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormFieldWrapper.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubMappingModal.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/queries/identity-provider.ts
server/sonar-web/src/main/js/queries/settings.ts
server/sonar-web/yarn.lock
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 4a427162eadb5fdc5bc1613acaca806455031597..03c59cede6bbfed8b350afc12a6f876393d6475b 100644 (file)
@@ -241,11 +241,11 @@ export default class AuthenticationServiceMock {
 
   handleUpdateGithubRolesMapping: typeof updateGithubRolesMapping = (id, data) => {
     this.githubMapping = this.githubMapping.map((mapping) =>
-      mapping.id === id ? { ...mapping, ...data } : mapping
+      mapping.id === id ? { ...mapping, ...data } : mapping,
     );
 
     return Promise.resolve(
-      this.githubMapping.find((mapping) => mapping.id === id) as GitHubMapping
+      this.githubMapping.find((mapping) => mapping.id === id) as GitHubMapping,
     );
   };
 
index d196a32eee24d7bf5d2c823f5f37cc698afb24de..756bf57dfa452e05b0a1cdbe2b17ca4c5ab1055e 100644 (file)
@@ -56,13 +56,17 @@ export function syncNowGithubProvisioning(): Promise<void> {
   return post('/api/github_provisioning/sync').catch(throwGlobalError);
 }
 
-export function fetchGithubRolesMapping(): Promise<GitHubMapping[]> {
-  return axios.get('/api/v2/github-permissions-mapping');
+export function fetchGithubRolesMapping() {
+  return axios
+    .get<unknown, { githubPermissionsMappings: GitHubMapping[] }>(
+      '/api/v2/github-permission-mappings',
+    )
+    .then((data) => data.githubPermissionsMappings);
 }
 
 export function updateGithubRolesMapping(
   role: string,
-  data: Partial<Pick<GitHubMapping, 'permissions'>>
+  data: Partial<Pick<GitHubMapping, 'permissions'>>,
 ): Promise<GitHubMapping> {
-  return axios.patch(`/api/v2/github-permissions-mapping/${role}`, data);
+  return axios.patch(`/api/v2/github-permission-mappings/${role}`, data);
 }
index 9cb5f85850178c6d3d34e0ff5ef70841c8e36bed..9fb5a9c46e2c06fc9a4cf59dbfb169e9ec8b84fd 100644 (file)
@@ -40,13 +40,15 @@ initApplication();
 
 async function initApplication() {
   axiosToCatch.interceptors.response.use((response) => response.data);
+  axiosToCatch.defaults.headers.patch['Content-Type'] = 'application/merge-patch+json';
+  axios.defaults.headers.patch['Content-Type'] = 'application/merge-patch+json';
   axios.interceptors.response.use(
     (response) => response.data,
     (error) => {
       const { response } = error;
       addGlobalErrorMessage(parseErrorResponse(response));
       return Promise.reject(response);
-    }
+    },
   );
   const [l10nBundle, currentUser, appState, availableFeatures] = await Promise.all([
     loadL10nBundle(),
index 1467e7a87015a8a56df6e24cbe6eaff7d5513415..26d50fe903d9a4c827d8599142a783024ece153b 100644 (file)
@@ -21,9 +21,9 @@ import React from 'react';
 import ValidationInput, {
   ValidationInputErrorPlacement,
 } from '../../../../components/controls/ValidationInput';
-import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker';
 import { ExtendedSettingDefinition, SettingType } from '../../../../types/settings';
 import { getPropertyDescription, getPropertyName, isSecuredDefinition } from '../../utils';
+import AuthenticationFormFieldWrapper from './AuthenticationFormFieldWrapper';
 import AuthenticationMultiValueField from './AuthenticationMultiValuesField';
 import AuthenticationSecuredField from './AuthenticationSecuredField';
 import AuthenticationToggleField from './AuthenticationToggleField';
@@ -44,58 +44,54 @@ export default function AuthenticationFormField(props: SamlToggleFieldProps) {
   const description = getPropertyDescription(definition);
 
   return (
-    <div className="settings-definition">
-      <div className="settings-definition-left">
-        <label className="h3" htmlFor={definition.key}>
-          {name}
-        </label>
-        {mandatory && <MandatoryFieldMarker />}
-        {definition.description && <div className="markdown small spacer-top">{description}</div>}
-      </div>
-      <div className="settings-definition-right big-padded-top display-flex-column">
-        {definition.multiValues && (
-          <AuthenticationMultiValueField
-            definition={definition}
-            settingValue={settingValue as string[]}
-            onFieldChange={(value) => props.onFieldChange(definition.key, value)}
-          />
+    <AuthenticationFormFieldWrapper
+      title={name}
+      defKey={definition.key}
+      mandatory={mandatory}
+      description={description}
+    >
+      {definition.multiValues && (
+        <AuthenticationMultiValueField
+          definition={definition}
+          settingValue={settingValue as string[]}
+          onFieldChange={(value) => props.onFieldChange(definition.key, value)}
+        />
+      )}
+      {isSecuredDefinition(definition) && (
+        <AuthenticationSecuredField
+          definition={definition}
+          settingValue={String(settingValue ?? '')}
+          onFieldChange={props.onFieldChange}
+          isNotSet={isNotSet}
+        />
+      )}
+      {!isSecuredDefinition(definition) && definition.type === SettingType.BOOLEAN && (
+        <AuthenticationToggleField
+          definition={definition}
+          settingValue={settingValue as string | boolean}
+          onChange={(value) => props.onFieldChange(definition.key, value)}
+        />
+      )}
+      {!isSecuredDefinition(definition) &&
+        definition.type === undefined &&
+        !definition.multiValues && (
+          <ValidationInput
+            error={error}
+            errorPlacement={ValidationInputErrorPlacement.Bottom}
+            isValid={false}
+            isInvalid={Boolean(error)}
+          >
+            <input
+              className="width-100"
+              id={definition.key}
+              maxLength={4000}
+              name={definition.key}
+              onChange={(e) => props.onFieldChange(definition.key, e.currentTarget.value)}
+              type="text"
+              value={String(settingValue ?? '')}
+            />
+          </ValidationInput>
         )}
-        {isSecuredDefinition(definition) && (
-          <AuthenticationSecuredField
-            definition={definition}
-            settingValue={String(settingValue ?? '')}
-            onFieldChange={props.onFieldChange}
-            isNotSet={isNotSet}
-          />
-        )}
-        {!isSecuredDefinition(definition) && definition.type === SettingType.BOOLEAN && (
-          <AuthenticationToggleField
-            definition={definition}
-            settingValue={settingValue as string | boolean}
-            onChange={(value) => props.onFieldChange(definition.key, value)}
-          />
-        )}
-        {!isSecuredDefinition(definition) &&
-          definition.type === undefined &&
-          !definition.multiValues && (
-            <ValidationInput
-              error={error}
-              errorPlacement={ValidationInputErrorPlacement.Bottom}
-              isValid={false}
-              isInvalid={Boolean(error)}
-            >
-              <input
-                className="width-100"
-                id={definition.key}
-                maxLength={4000}
-                name={definition.key}
-                onChange={(e) => props.onFieldChange(definition.key, e.currentTarget.value)}
-                type="text"
-                value={String(settingValue ?? '')}
-              />
-            </ValidationInput>
-          )}
-      </div>
-    </div>
+    </AuthenticationFormFieldWrapper>
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormFieldWrapper.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AuthenticationFormFieldWrapper.tsx
new file mode 100644 (file)
index 0000000..0801569
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React, { PropsWithChildren } from 'react';
+import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker';
+
+interface Props {
+  readonly title: string;
+  readonly description?: string;
+  readonly defKey?: string;
+  readonly mandatory?: boolean;
+}
+
+export default function AuthenticationFormFieldWrapper(props: PropsWithChildren<Props>) {
+  const { mandatory = false, title, description, defKey, children } = props;
+
+  return (
+    <div className="settings-definition">
+      <div className="settings-definition-left">
+        <label className="h3" htmlFor={defKey}>
+          {title}
+        </label>
+        {mandatory && <MandatoryFieldMarker />}
+        {description && <div className="markdown small spacer-top">{description}</div>}
+      </div>
+      <div className="settings-definition-right big-padded-top display-flex-column">{children}</div>
+    </div>
+  );
+}
index 8f38c142b3151c92022bd30d9ccd0bb2e6b23db0..7b1f0306abfb54dcfcf41901b0113cb6d6442e75 100644 (file)
@@ -34,26 +34,20 @@ import { useGithubRolesMappingQuery } from '../../../../queries/identity-provide
 import { GitHubMapping } from '../../../../types/provisioning';
 
 interface Props {
-  mapping: GitHubMapping[] | null;
-  setMapping: React.Dispatch<React.SetStateAction<GitHubMapping[] | null>>;
-  onClose: () => void;
+  readonly mapping: GitHubMapping[] | null;
+  readonly setMapping: React.Dispatch<React.SetStateAction<GitHubMapping[] | null>>;
+  readonly onClose: () => void;
 }
 
 export default function GitHubMappingModal({ mapping, setMapping, onClose }: Props) {
   const { data: roles, isLoading } = useGithubRolesMappingQuery();
   const permissions = convertToPermissionDefinitions(
     PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE,
-    'projects_role'
+    'projects_role',
   );
 
-  React.useEffect(() => {
-    if (!mapping && roles) {
-      setMapping(roles);
-    }
-  }, [roles, mapping, setMapping]);
-
   const header = translate(
-    'settings.authentication.github.configuration.roles_mapping.dialog.title'
+    'settings.authentication.github.configuration.roles_mapping.dialog.title',
   );
 
   return (
@@ -68,7 +62,7 @@ export default function GitHubMappingModal({ mapping, setMapping, onClose }: Pro
               <th scope="col" className="nowrap bordered-bottom sw-pl-[10px] sw-align-middle">
                 <b>
                   {translate(
-                    'settings.authentication.github.configuration.roles_mapping.dialog.roles_column'
+                    'settings.authentication.github.configuration.roles_mapping.dialog.roles_column',
                   )}
                 </b>
               </th>
@@ -77,14 +71,13 @@ export default function GitHubMappingModal({ mapping, setMapping, onClose }: Pro
                   key={
                     isPermissionDefinitionGroup(permission) ? permission.category : permission.key
                   }
-                  onSelectPermission={() => {}}
                   permission={permission}
                 />
               ))}
             </tr>
           </thead>
           <tbody>
-            {mapping?.map(({ id, roleName, permissions }) => (
+            {(mapping ?? roles)?.map(({ id, roleName, permissions }) => (
               <tr key={id}>
                 <th scope="row" className="nowrap text-middle sw-pl-[10px]">
                   <b>{roleName}</b>
@@ -95,11 +88,11 @@ export default function GitHubMappingModal({ mapping, setMapping, onClose }: Pro
                       checked={value}
                       onCheck={(newValue) =>
                         setMapping(
-                          mapping.map((item) =>
+                          (mapping ?? roles)?.map((item) =>
                             item.id === id
                               ? { ...item, permissions: { ...item.permissions, [key]: newValue } }
-                              : item
-                          )
+                              : item,
+                          ) ?? null,
                         )
                       }
                     />
index 9b7326a0a237ffaf4878c556c0b3756177dd8353..9d66fc0ccfe091c66a7bb2b4ce96ec73e3eb5548 100644 (file)
@@ -38,6 +38,7 @@ import { AlmKeys } from '../../../../types/alm-settings';
 import { ExtendedSettingDefinition } from '../../../../types/settings';
 import { AuthenticationTabs, DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
 import AuthenticationFormField from './AuthenticationFormField';
+import AuthenticationFormFieldWrapper from './AuthenticationFormFieldWrapper';
 import AutoProvisioningConsent from './AutoProvisionningConsent';
 import ConfigurationForm from './ConfigurationForm';
 import GitHubConfigurationValidity from './GitHubConfigurationValidity';
@@ -78,12 +79,11 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
     hasGithubProvisioningTypeChange,
     hasGithubProvisioningConfigChange,
     resetJitSetting,
-    saveGroup,
     changeProvisioning,
     toggleEnable,
     rolesMapping,
     setRolesMapping,
-    saveMapping,
+    applyAdditionalOptions,
     hasLegacyConfiguration,
     deleteMutation: { isLoading: isDeleting, mutate: deleteConfiguration },
   } = useGithubConfiguration(definitions);
@@ -106,10 +106,7 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
     if (hasGithubProvisioningTypeChange) {
       setShowConfirmProvisioningModal(true);
     } else {
-      saveGroup();
-      if (newGithubProvisioningStatus ?? githubProvisioningStatus) {
-        saveMapping();
-      }
+      applyAdditionalOptions();
     }
   };
 
@@ -197,6 +194,7 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
                 {enabled ? (
                   <div className="display-flex-column spacer-top">
                     <RadioCard
+                      className="sw-min-h-0"
                       label={translate('settings.authentication.form.provisioning_at_login')}
                       title={translate('settings.authentication.form.provisioning_at_login')}
                       selected={!(newGithubProvisioningStatus ?? githubProvisioningStatus)}
@@ -246,7 +244,7 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
                       )}
                     </RadioCard>
                     <RadioCard
-                      className="spacer-top"
+                      className="spacer-top sw-min-h-0"
                       label={translate(
                         'settings.authentication.github.form.provisioning_with_github',
                       )}
@@ -287,18 +285,17 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
                           </p>
 
                           {githubProvisioningStatus && <GitHubSynchronisationWarning />}
-
-                          <div className="sw-flex sw-flex-1 spacer-bottom">
-                            <Button
-                              className="spacer-top width-30"
-                              onClick={synchronizeNow}
-                              disabled={!canSyncNow}
-                            >
-                              {translate('settings.authentication.github.synchronize_now')}
-                            </Button>
-                          </div>
                           {(newGithubProvisioningStatus ?? githubProvisioningStatus) && (
                             <>
+                              <div className="sw-flex sw-flex-1 spacer-bottom">
+                                <Button
+                                  className="spacer-top width-30"
+                                  onClick={synchronizeNow}
+                                  disabled={!canSyncNow}
+                                >
+                                  {translate('settings.authentication.github.synchronize_now')}
+                                </Button>
+                              </div>
                               <hr />
                               {Object.values(values).map((val) => {
                                 if (!GITHUB_PROVISIONING_FIELDS.includes(val.key)) {
@@ -318,6 +315,23 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
                                   </div>
                                 );
                               })}
+                              <AuthenticationFormFieldWrapper
+                                title={translate(
+                                  'settings.authentication.github.configuration.roles_mapping.title',
+                                )}
+                                description={translate(
+                                  'settings.authentication.github.configuration.roles_mapping.description',
+                                )}
+                              >
+                                <Button
+                                  className="spacer-top"
+                                  onClick={() => setShowMappingModal(true)}
+                                >
+                                  {translate(
+                                    'settings.authentication.github.configuration.roles_mapping.button_label',
+                                  )}
+                                </Button>
+                              </AuthenticationFormFieldWrapper>
                             </>
                           )}
                         </>
@@ -348,12 +362,11 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
                 )}
               </fieldset>
               {enabled && (
-                <>
+                <div className="sw-flex sw-gap-2 sw-h-8 sw-items-center">
                   <SubmitButton disabled={!hasGithubProvisioningConfigChange}>
                     {translate('save')}
                   </SubmitButton>
                   <ResetButtonLink
-                    className="spacer-left"
                     onClick={() => {
                       setProvisioningType(undefined);
                       resetJitSetting();
@@ -362,7 +375,11 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
                   >
                     {translate('cancel')}
                   </ResetButtonLink>
-                </>
+                  <Alert variant="warning" className="sw-w-[300px] sw-mb-0">
+                    {hasGithubProvisioningConfigChange &&
+                      translate('settings.authentication.github.configuration.unsaved_changes')}
+                  </Alert>
+                </div>
               )}
               {showConfirmProvisioningModal && (
                 <ConfirmModal
index 9a57809d84158789595f5031f2493d83b8c94acd..c0d63db413e33300f62cdc4bbf65a42774a1d8f1 100644 (file)
@@ -184,6 +184,7 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {
                 {samlEnabled ? (
                   <div className="display-flex-column spacer-top">
                     <RadioCard
+                      className="sw-min-h-0"
                       label={translate('settings.authentication.saml.form.provisioning_at_login')}
                       title={translate('settings.authentication.saml.form.provisioning_at_login')}
                       selected={!(newScimStatus ?? scimStatus)}
@@ -194,7 +195,7 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {
                       </p>
                     </RadioCard>
                     <RadioCard
-                      className="spacer-top"
+                      className="spacer-top sw-min-h-0"
                       label={translate('settings.authentication.saml.form.provisioning_with_scim')}
                       title={translate('settings.authentication.saml.form.provisioning_with_scim')}
                       selected={newScimStatus ?? scimStatus}
index 30b51acb236e07dfb8b598889cd6ec05b987ee87..db07040a07421930d7308003bd9b42164fec16e6 100644 (file)
@@ -70,7 +70,9 @@ export default function useGithubConfiguration(definitions: ExtendedSettingDefin
     newGithubProvisioningStatus !== undefined &&
     newGithubProvisioningStatus !== githubProvisioningStatus;
   const hasGithubProvisioningConfigChange =
-    some(GITHUB_ADDITIONAL_FIELDS, isValueChange) || hasGithubProvisioningTypeChange || rolesMapping;
+    some(GITHUB_ADDITIONAL_FIELDS, isValueChange) ||
+    hasGithubProvisioningTypeChange ||
+    rolesMapping;
 
   const resetJitSetting = () => {
     GITHUB_ADDITIONAL_FIELDS.forEach((s) => setNewValue(s));
@@ -78,7 +80,7 @@ export default function useGithubConfiguration(definitions: ExtendedSettingDefin
 
   const { mutate: saveSetting } = useSaveValueMutation();
   const { mutate: saveSettings } = useSaveValuesMutation();
-  const { mutate: updateMapping } = useGithubRolesMappingMutation();
+  const { mutateAsync: updateMapping } = useGithubRolesMappingMutation();
 
   const enabled = values[GITHUB_ENABLED_FIELD]?.value === 'true';
   const appId = values[GITHUB_APP_ID_FIELD]?.value as string;
@@ -89,22 +91,20 @@ export default function useGithubConfiguration(definitions: ExtendedSettingDefin
     if (hasGithubProvisioningTypeChange) {
       await toggleGithubProvisioning.mutateAsync(!!newGithubProvisioningStatus);
     }
-    if (!newGithubProvisioningStatus || !githubProvisioningStatus) {
-      saveGroup();
-    }
-    if (newGithubProvisioningStatus ?? githubProvisioningStatus) {
-      saveMapping();
-    }
+    applyAdditionalOptions();
   };
 
-  const saveGroup = () => {
+  const applyAdditionalOptions = () => {
     const newValues = GITHUB_ADDITIONAL_FIELDS.map((settingKey) => values[settingKey]);
     saveSettings(newValues);
-  };
-
-  const saveMapping = () => {
-    if (rolesMapping) {
-      updateMapping(rolesMapping);
+    if (newGithubProvisioningStatus ?? githubProvisioningStatus) {
+      if (rolesMapping) {
+        updateMapping(rolesMapping)
+          .then(() => {
+            setRolesMapping(null);
+          })
+          .catch(() => {});
+      }
     }
   };
 
@@ -132,12 +132,11 @@ export default function useGithubConfiguration(definitions: ExtendedSettingDefin
     hasGithubProvisioningTypeChange,
     hasGithubProvisioningConfigChange,
     changeProvisioning,
-    saveGroup,
     resetJitSetting,
     toggleEnable,
     rolesMapping,
     setRolesMapping,
-    saveMapping,
+    applyAdditionalOptions,
     hasLegacyConfiguration,
   };
 }
index cd5f5fef94eee0841e2b3686b83bf60f80ff3605..7d6702739a7400f993640e3af513a2eb3c190fff 100644 (file)
@@ -126,10 +126,11 @@ export function useGithubRolesMappingQuery() {
   return useQuery(['identity_provider', 'github_mapping'], fetchGithubRolesMapping, {
     staleTime: MAPPING_STALE_TIME,
     select: (data) =>
-      data.sort((a, b) => {
-        const hardcodedValues = ['admin', 'maintain', 'write', 'triage', 'read'];
-        if (hardcodedValues.includes(a.id) || hardcodedValues.includes(b.id)) {
-          return hardcodedValues.indexOf(b.id) - hardcodedValues.indexOf(a.id);
+      [...data].sort((a, b) => {
+        // Order is reversed to put non-existing roles at the end (their index is -1)
+        const defaultRoleOrder = ['admin', 'maintain', 'write', 'triage', 'read'];
+        if (defaultRoleOrder.includes(a.id) || defaultRoleOrder.includes(b.id)) {
+          return defaultRoleOrder.indexOf(b.id) - defaultRoleOrder.indexOf(a.id);
         }
         return a.roleName.localeCompare(b.roleName);
       }),
@@ -147,12 +148,12 @@ export function useGithubRolesMappingMutation() {
             (item) =>
               !isEqual(
                 item,
-                state.find((el) => el.id === item.id)
-              )
+                state.find((el) => el.id === item.id),
+              ),
           )
         : mapping;
       return Promise.all(
-        changedRoles.map((data) => updateGithubRolesMapping(data.id, omit(data, 'id', 'roleName')))
+        changedRoles.map((data) => updateGithubRolesMapping(data.id, omit(data, 'id', 'roleName'))),
       );
     },
     onSuccess: (data) => {
@@ -163,7 +164,7 @@ export function useGithubRolesMappingMutation() {
           state.map((item) => {
             const changed = data.find((el) => el.id === item.id);
             return changed ?? item;
-          })
+          }),
         );
       }
     },
index 3de28aaccdc00b6c1a3c762c633ffa38e715562c..c55f7f1f53c72cf0717faefa4dc26e46667e599c 100644 (file)
@@ -71,8 +71,10 @@ export function useSaveValuesMutation() {
           }),
       );
     },
-    onSuccess: () => {
-      queryClient.invalidateQueries(['settings']);
+    onSuccess: (data) => {
+      if (data.length > 0) {
+        queryClient.invalidateQueries(['settings']);
+      }
     },
   });
 }
index 1af4fa8c09f7741213e470b9bb07f553399de7af..595b17865fbc2d05f4362a1a1d2db46fad1e2b89 100644 (file)
@@ -5247,6 +5247,7 @@ __metadata:
     "@typescript-eslint/parser": 5.59.11
     "@wojtekmaj/enzyme-adapter-react-17": 0.8.0
     autoprefixer: 10.4.15
+    axios: 1.5.0
     chalk: 4.1.2
     chokidar: 3.5.3
     classnames: 2.3.2
@@ -5767,6 +5768,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"axios@npm:1.5.0":
+  version: 1.5.0
+  resolution: "axios@npm:1.5.0"
+  dependencies:
+    follow-redirects: ^1.15.0
+    form-data: ^4.0.0
+    proxy-from-env: ^1.1.0
+  checksum: e7405a5dbbea97760d0e6cd58fecba311b0401ddb4a8efbc4108f5537da9b3f278bde566deb777935a960beec4fa18e7b8353881f2f465e4f2c0e949fead35be
+  languageName: node
+  linkType: hard
+
 "axobject-query@npm:^3.1.1":
   version: 3.1.1
   resolution: "axobject-query@npm:3.1.1"
@@ -8342,6 +8354,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"follow-redirects@npm:^1.15.0":
+  version: 1.15.2
+  resolution: "follow-redirects@npm:1.15.2"
+  peerDependenciesMeta:
+    debug:
+      optional: true
+  checksum: faa66059b66358ba65c234c2f2a37fcec029dc22775f35d9ad6abac56003268baf41e55f9ee645957b32c7d9f62baf1f0b906e68267276f54ec4b4c597c2b190
+  languageName: node
+  linkType: hard
+
 "for-each@npm:^0.3.3":
   version: 0.3.3
   resolution: "for-each@npm:0.3.3"
@@ -11916,6 +11938,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"proxy-from-env@npm:^1.1.0":
+  version: 1.1.0
+  resolution: "proxy-from-env@npm:1.1.0"
+  checksum: ed7fcc2ba0a33404958e34d95d18638249a68c430e30fcb6c478497d72739ba64ce9810a24f53a7d921d0c065e5b78e3822759800698167256b04659366ca4d4
+  languageName: node
+  linkType: hard
+
 "psl@npm:^1.1.33":
   version: 1.8.0
   resolution: "psl@npm:1.8.0"
index 66dd6f922b7aa04fff22d3c51804a15f147f7e0e..bfc920199e9aee4a975621ce3e0c1213a8206950 100644 (file)
@@ -1530,8 +1530,12 @@ settings.authentication.github.configuration.validation.details.title=Configurat
 settings.authentication.github.configuration.validation.details.valid_label=Valid
 settings.authentication.github.configuration.validation.details.invalid_label=Invalid
 settings.authentication.github.configuration.validation.details.org_not_found={0} (not found or app not installed)
+settings.authentication.github.configuration.roles_mapping.title=Role permission mapping
+settings.authentication.github.configuration.roles_mapping.description=When synchronizing users and groups we rely on GitHub user and team roles to assign sonarqube permissions. You can customize how the mapping will be applied. Be aware that the mapping will take effect from the next synchronization.
+settings.authentication.github.configuration.roles_mapping.button_label=Edit mapping
 settings.authentication.github.configuration.roles_mapping.dialog.title=GitHub Roles Mapping
 settings.authentication.github.configuration.roles_mapping.dialog.roles_column=Roles
+settings.authentication.github.configuration.unsaved_changes=You have unsaved changes.
 
 # SAML
 settings.authentication.form.create.saml=New SAML configuration