]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22862 Can add/view/delete GitLab custom role (#11616)
authorSarath Nair <91882341+sarath-nair-sonarsource@users.noreply.github.com>
Tue, 27 Aug 2024 13:18:11 +0000 (15:18 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 29 Aug 2024 20:02:47 +0000 (20:02 +0000)
server/sonar-web/src/main/js/api/gitlab-provisioning.ts
server/sonar-web/src/main/js/api/mocks/GitlabProvisioningServiceMock.ts
server/sonar-web/src/main/js/apps/settings/components/authentication/DevopsRolesMappingModal.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubMappingModal.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx
server/sonar-web/src/main/js/queries/identity-provider/github.ts
server/sonar-web/src/main/js/queries/identity-provider/gitlab.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 8311008ec2e653607117dabc25f838d87e666cf2..59f10624eb0b2dc60b845b05bcd92a8fb08851df 100644 (file)
@@ -82,3 +82,11 @@ export function updateGitlabRolesMapping(
     data,
   );
 }
+
+export function addGitlabRolesMapping(data: Omit<GitLabMapping, 'id'>) {
+  return axios.post<GitLabMapping>(GITLAB_PERMISSION_MAPPINGS, data);
+}
+
+export function deleteGitlabRolesMapping(role: string) {
+  return axios.delete(`${GITLAB_PERMISSION_MAPPINGS}/${encodeURIComponent(role)}`);
+}
index 121c16a67020948054c1fe14177822d658cb26dc..e7b9529143b31c81489ed02edbf9ddc30d4684d9 100644 (file)
@@ -22,8 +22,10 @@ import { mockGitlabConfiguration } from '../../helpers/mocks/alm-integrations';
 import { mockPaging } from '../../helpers/testMocks';
 import { GitLabMapping, GitlabConfiguration } from '../../types/provisioning';
 import {
+  addGitlabRolesMapping,
   createGitLabConfiguration,
   deleteGitLabConfiguration,
+  deleteGitlabRolesMapping,
   fetchGitLabConfiguration,
   fetchGitLabConfigurations,
   fetchGitlabRolesMapping,
@@ -37,9 +39,14 @@ const defaultGitlabConfiguration: GitlabConfiguration[] = [
   mockGitlabConfiguration({ id: '1', enabled: true }),
 ];
 
-const gitlabMappingMock = (id: string, permissions: (keyof GitLabMapping['permissions'])[]) => ({
+const gitlabMappingMock = (
+  id: string,
+  permissions: (keyof GitLabMapping['permissions'])[],
+  baseRole = false,
+) => ({
   id,
   gitlabRole: id,
+  baseRole,
   permissions: {
     user: permissions.includes('user'),
     codeViewer: permissions.includes('codeViewer'),
@@ -51,30 +58,23 @@ const gitlabMappingMock = (id: string, permissions: (keyof GitLabMapping['permis
 });
 
 const defaultMapping: GitLabMapping[] = [
-  gitlabMappingMock('guest', ['user', 'codeViewer']),
-  gitlabMappingMock('reporter', ['user', 'codeViewer']),
-  gitlabMappingMock('developer', [
-    'user',
-    'codeViewer',
-    'issueAdmin',
-    'securityHotspotAdmin',
-    'scan',
-  ]),
-  gitlabMappingMock('maintainer', [
-    'user',
-    'codeViewer',
-    'issueAdmin',
-    'securityHotspotAdmin',
-    'scan',
-  ]),
-  gitlabMappingMock('owner', [
-    'user',
-    'codeViewer',
-    'issueAdmin',
-    'securityHotspotAdmin',
-    'admin',
-    'scan',
-  ]),
+  gitlabMappingMock('guest', ['user', 'codeViewer'], true),
+  gitlabMappingMock('reporter', ['user', 'codeViewer'], true),
+  gitlabMappingMock(
+    'developer',
+    ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'scan'],
+    true,
+  ),
+  gitlabMappingMock(
+    'maintainer',
+    ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'scan'],
+    true,
+  ),
+  gitlabMappingMock(
+    'owner',
+    ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'admin', 'scan'],
+    true,
+  ),
 ];
 
 export default class GitlabProvisioningServiceMock {
@@ -147,6 +147,22 @@ export default class GitlabProvisioningServiceMock {
     );
   };
 
+  handleAddGitlabRolesMapping: typeof addGitlabRolesMapping = (data) => {
+    const newRole = { ...data, id: data.gitlabRole };
+    this.gitlabMapping = [...this.gitlabMapping, newRole];
+
+    return Promise.resolve(newRole);
+  };
+
+  handleDeleteGitlabRolesMapping: typeof deleteGitlabRolesMapping = (id) => {
+    this.gitlabMapping = this.gitlabMapping.filter((el) => el.id !== id);
+    return Promise.resolve();
+  };
+
+  addGitLabCustomRole = (id: string, permissions: (keyof GitLabMapping['permissions'])[]) => {
+    this.gitlabMapping = [...this.gitlabMapping, gitlabMappingMock(id, permissions)];
+  };
+
   reset = () => {
     this.gitlabConfigurations = cloneDeep(defaultGitlabConfiguration);
   };
index 571303fdc78c82d1db5296ac23f109058673f092..4fe480028e4060380091c39a97eec49ac9eb7ba6 100644 (file)
@@ -47,7 +47,6 @@ import { GitHubMapping, GitLabMapping } from '../../../../types/provisioning';
 type RolesMapping = GitHubMapping[] | GitLabMapping[] | null;
 
 interface Props {
-  canAddCustomRole?: boolean;
   isLoading: boolean;
   mapping: RolesMapping;
   mappingFor: AlmKeys.GitHub | AlmKeys.GitLab;
@@ -71,11 +70,21 @@ const DEFAULT_CUSTOM_ROLE_PERMISSIONS: GitHubMapping['permissions'] = {
 };
 
 function PermissionRow(props: Readonly<PermissionCellProps>) {
-  const { mapping } = props;
+  const { list, mapping } = props;
   const isGitHubMapping = 'githubRole' in mapping;
   const role = isGitHubMapping ? mapping.githubRole : mapping.gitlabRole;
-  const isBaseRole = isGitHubMapping ? mapping.baseRole : true;
-  const list = props.list as GitHubMapping[];
+  const isBaseRole = mapping.baseRole;
+
+  const setMapping = () => {
+    if (isGitHubMapping) {
+      return props.setMapping(
+        (list as GitHubMapping[])?.filter((r) => r.githubRole !== role) ?? null,
+      );
+    }
+    return props.setMapping(
+      (list as GitLabMapping[])?.filter((r) => r.gitlabRole !== role) ?? null,
+    );
+  };
 
   return (
     <TableRowInteractive>
@@ -92,9 +101,7 @@ function PermissionRow(props: Readonly<PermissionCellProps>) {
                 'settings.authentication.configuration.roles_mapping.dialog.delete_custom_role',
                 role,
               )}
-              onClick={() => {
-                props.setMapping(list?.filter((r) => r.githubRole !== role) ?? null);
-              }}
+              onClick={setMapping}
               Icon={TrashIcon}
               size="small"
             />
@@ -107,11 +114,11 @@ function PermissionRow(props: Readonly<PermissionCellProps>) {
             checked={value}
             onCheck={(newValue) =>
               props.setMapping(
-                list?.map((item) =>
+                (list?.map((item) =>
                   item.id === mapping.id
                     ? { ...item, permissions: { ...item.permissions, [key]: newValue } }
                     : item,
-                ) ?? null,
+                ) ?? null) as RolesMapping,
               )
             }
           />
@@ -122,7 +129,7 @@ function PermissionRow(props: Readonly<PermissionCellProps>) {
 }
 
 export function DevopsRolesMappingModal(props: Readonly<Props>) {
-  const { canAddCustomRole, isLoading, mapping, mappingFor, onClose, roles, setMapping } = props;
+  const { isLoading, mapping, mappingFor, onClose, roles, setMapping } = props;
   const permissions = convertToPermissionDefinitions(
     PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE,
     'projects_role',
@@ -141,6 +148,7 @@ export function DevopsRolesMappingModal(props: Readonly<Props>) {
     e.preventDefault();
     const value = customRoleInput.trim();
     if (
+      mappingFor === AlmKeys.GitHub &&
       !(list as GitHubMapping[])?.some((el) =>
         el.baseRole ? el.githubRole.toLowerCase() === value.toLowerCase() : el.githubRole === value,
       )
@@ -154,14 +162,29 @@ export function DevopsRolesMappingModal(props: Readonly<Props>) {
         ...((list as GitHubMapping[]) ?? []),
       ]);
       setCustomRoleInput('');
+    } else if (
+      mappingFor === AlmKeys.GitLab &&
+      !(list as GitLabMapping[])?.some((el) =>
+        el.baseRole ? el.gitlabRole.toLowerCase() === value.toLowerCase() : el.gitlabRole === value,
+      )
+    ) {
+      setMapping([
+        {
+          id: customRoleInput,
+          gitlabRole: customRoleInput,
+          permissions: { ...DEFAULT_CUSTOM_ROLE_PERMISSIONS },
+        },
+        ...((list as GitLabMapping[]) ?? []),
+      ]);
+      setCustomRoleInput('');
     } else {
       setCustomRoleError(true);
     }
   };
 
-  const haveEmptyCustomRoles =
-    mappingFor === AlmKeys.GitHub &&
-    !!mapping?.some((el) => !el.baseRole && !Object.values(el.permissions).some(Boolean));
+  const haveEmptyCustomRoles = !!mapping?.some(
+    (el) => !el.baseRole && !Object.values(el.permissions).some(Boolean),
+  );
 
   const formBody = (
     <div className="sw-p-0">
@@ -184,79 +207,68 @@ export function DevopsRolesMappingModal(props: Readonly<Props>) {
         }
       >
         {list
-          ?.filter((r) => ('githubRole' in r ? r.baseRole : true))
+          ?.filter((r) => r.baseRole)
           .map((mapping) => (
             <PermissionRow key={mapping.id} mapping={mapping} setMapping={setMapping} list={list} />
           ))}
-        {canAddCustomRole && (
-          <>
-            <TableRow>
-              <ContentCell colSpan={7}>
-                <div className="sw-flex sw-items-end">
-                  <form className="sw-flex sw-items-end" onSubmit={validateAndAddCustomRole}>
-                    <FormField
-                      htmlFor="custom-role-input"
-                      label={translate(
-                        'settings.authentication.configuration.roles_mapping.dialog.add_custom_role',
-                      )}
-                    >
-                      <InputField
-                        className="sw-w-[300px]"
-                        id="custom-role-input"
-                        maxLength={4000}
-                        value={customRoleInput}
-                        onChange={(event) => {
-                          setCustomRoleError(false);
-                          setCustomRoleInput(event.currentTarget.value);
-                        }}
-                        type="text"
-                      />
-                    </FormField>
-                    <ButtonSecondary
-                      type="submit"
-                      className="sw-ml-2 sw-mr-4"
-                      disabled={customRoleInput.trim() === '' || customRoleError}
-                    >
-                      {translate('add_verb')}
-                    </ButtonSecondary>
-                  </form>
-                  {customRoleError && (
-                    <FlagMessage variant="error">
-                      {translate('settings.authentication.configuration.roles_mapping.role_exists')}
-                    </FlagMessage>
+        <TableRow>
+          <ContentCell colSpan={7}>
+            <div className="sw-flex sw-items-end">
+              <form className="sw-flex sw-items-end" onSubmit={validateAndAddCustomRole}>
+                <FormField
+                  htmlFor="custom-role-input"
+                  label={translate(
+                    'settings.authentication.configuration.roles_mapping.dialog.add_custom_role',
                   )}
-                </div>
-              </ContentCell>
-            </TableRow>
+                >
+                  <InputField
+                    className="sw-w-[300px]"
+                    id="custom-role-input"
+                    maxLength={4000}
+                    value={customRoleInput}
+                    onChange={(event) => {
+                      setCustomRoleError(false);
+                      setCustomRoleInput(event.currentTarget.value);
+                    }}
+                    type="text"
+                  />
+                </FormField>
+                <ButtonSecondary
+                  type="submit"
+                  className="sw-ml-2 sw-mr-4"
+                  disabled={customRoleInput.trim() === '' || customRoleError}
+                >
+                  {translate('add_verb')}
+                </ButtonSecondary>
+              </form>
+              {customRoleError && (
+                <FlagMessage variant="error">
+                  {translate('settings.authentication.configuration.roles_mapping.role_exists')}
+                </FlagMessage>
+              )}
+            </div>
+          </ContentCell>
+        </TableRow>
 
-            {list
-              ?.filter((r) => !r.baseRole)
-              .map((mapping) => (
-                <PermissionRow
-                  key={mapping.id}
-                  mapping={mapping}
-                  setMapping={setMapping}
-                  list={list}
-                />
-              ))}
-          </>
-        )}
+        {list
+          ?.filter((r) => !r.baseRole)
+          .map((mapping) => (
+            <PermissionRow key={mapping.id} mapping={mapping} setMapping={setMapping} list={list} />
+          ))}
       </Table>
-
-      {canAddCustomRole && (
-        <FlagMessage variant="info">
-          {translate(
-            'settings.authentication.github.configuration.roles_mapping.dialog.custom_roles_description',
-          )}
-        </FlagMessage>
-      )}
+      <FlagMessage variant="info">
+        {translateWithParameters(
+          'settings.authentication.configuration.roles_mapping.dialog.custom_roles_description',
+          translate('alm', mappingFor),
+        )}
+      </FlagMessage>
 
       <Spinner isLoading={isLoading} />
     </div>
   );
 
   return (
-    <Modal onClose={onClose} isLarge>
+    <Modal closeOnOverlayClick={!haveEmptyCustomRoles} onClose={onClose} isLarge>
       <Modal.Header title={header} />
       <Modal.Body>{formBody}</Modal.Body>
       <Modal.Footer
index ed50608d1903be5328ce7b711eae28b2284fbeea..8b4a26362c9e3023fd523da06f71590c52838bcb 100644 (file)
@@ -33,7 +33,6 @@ export default function GitHubMappingModal(props: Readonly<Props>) {
   const { data: roles, isPending } = useGithubRolesMappingQuery();
   return (
     <DevopsRolesMappingModal
-      canAddCustomRole
       isLoading={isPending}
       mappingFor={AlmKeys.GitHub}
       roles={roles}
index d8735204ceef2027b05065e43ecb548e2de7da30..a3cdc43ed212a6696826dd06c466d0c2cf0e7775 100644 (file)
@@ -166,6 +166,21 @@ const ui = {
   }).byRole('button', {
     name: 'close',
   }),
+  customRoleInput: byRole('textbox', {
+    name: 'settings.authentication.configuration.roles_mapping.dialog.add_custom_role',
+  }),
+  customRoleAddBtn: byRole('dialog', {
+    name: 'settings.authentication.configuration.roles_mapping.dialog.title.alm.gitlab',
+  }).byRole('button', { name: 'add_verb' }),
+  roleExistsError: byRole('dialog', {
+    name: 'settings.authentication.configuration.roles_mapping.dialog.title.alm.gitlab',
+  }).byText('settings.authentication.configuration.roles_mapping.role_exists'),
+  emptyRoleError: byRole('dialog', {
+    name: 'settings.authentication.configuration.roles_mapping.dialog.title.alm.gitlab',
+  }).byText('settings.authentication.configuration.roles_mapping.empty_custom_role'),
+  deleteCustomRoleCustom2: byRole('button', {
+    name: 'settings.authentication.configuration.roles_mapping.dialog.delete_custom_role.custom2',
+  }),
   getMappingRowByRole: (text: string) =>
     ui.mappingRow.getAll().find((row) => within(row).queryByText(text) !== null),
 };
@@ -697,8 +712,7 @@ describe('Gitlab Provisioning', () => {
     await user.click(ui.autoProvisioningRadioButton.get());
     expect(await ui.editMappingButton.find()).toBeInTheDocument();
     await user.click(ui.editMappingButton.get());
-
-    expect(await ui.mappingRow.findAll()).toHaveLength(6);
+    expect(await ui.mappingRow.findAll()).toHaveLength(7);
 
     let guestCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('guest'));
     let ownerCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('owner'));
@@ -737,6 +751,108 @@ describe('Gitlab Provisioning', () => {
     expect(ownerCheckboxes[5]).not.toBeChecked();
     await user.click(ui.mappingDialogClose.get());
   });
+
+  it('should add/remove/update custom roles', async () => {
+    const user = userEvent.setup();
+    handler.setGitlabConfigurations([
+      mockGitlabConfiguration({
+        allowUsersToSignUp: false,
+        enabled: true,
+        provisioningType: ProvisioningType.auto,
+        allowedGroups: ['D12'],
+        isProvisioningTokenSet: true,
+      }),
+    ]);
+    handler.addGitLabCustomRole('custom1', ['user', 'codeViewer', 'scan']);
+    handler.addGitLabCustomRole('custom2', ['user', 'codeViewer', 'issueAdmin', 'scan']);
+    renderAuthentication([Feature.GitlabProvisioning]);
+
+    expect(await ui.saveProvisioning.find()).toBeDisabled();
+    await user.click(ui.editMappingButton.get());
+
+    const rows = (await ui.mappingRow.findAll()).filter(
+      (row) => within(row).queryAllByRole('checkbox').length > 0,
+    );
+
+    expect(rows).toHaveLength(7);
+
+    let custom1Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom1'));
+
+    expect(custom1Checkboxes[0]).toBeChecked();
+    expect(custom1Checkboxes[1]).toBeChecked();
+    expect(custom1Checkboxes[2]).not.toBeChecked();
+    expect(custom1Checkboxes[3]).not.toBeChecked();
+    expect(custom1Checkboxes[4]).not.toBeChecked();
+    expect(custom1Checkboxes[5]).toBeChecked();
+
+    await user.click(custom1Checkboxes[1]);
+    await user.click(custom1Checkboxes[2]);
+
+    await user.click(ui.deleteCustomRoleCustom2.get());
+
+    expect(ui.customRoleInput.get()).toHaveValue('');
+    await user.type(ui.customRoleInput.get(), 'guest');
+    await user.click(ui.customRoleAddBtn.get());
+    expect(await ui.roleExistsError.find()).toBeInTheDocument();
+    expect(ui.customRoleAddBtn.get()).toBeDisabled();
+    await user.clear(ui.customRoleInput.get());
+    expect(ui.roleExistsError.query()).not.toBeInTheDocument();
+    await user.type(ui.customRoleInput.get(), 'custom1');
+    await user.click(ui.customRoleAddBtn.get());
+    expect(await ui.roleExistsError.find()).toBeInTheDocument();
+    expect(ui.customRoleAddBtn.get()).toBeDisabled();
+    await user.clear(ui.customRoleInput.get());
+    await user.type(ui.customRoleInput.get(), 'custom3');
+    expect(ui.roleExistsError.query()).not.toBeInTheDocument();
+    expect(ui.customRoleAddBtn.get()).toBeEnabled();
+    await user.click(ui.customRoleAddBtn.get());
+
+    let custom3Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom3'));
+    expect(custom3Checkboxes[0]).toBeChecked();
+    expect(custom3Checkboxes[1]).not.toBeChecked();
+    expect(custom3Checkboxes[2]).not.toBeChecked();
+    expect(custom3Checkboxes[3]).not.toBeChecked();
+    expect(custom3Checkboxes[4]).not.toBeChecked();
+    expect(custom3Checkboxes[5]).not.toBeChecked();
+    await user.click(custom3Checkboxes[0]);
+    expect(await ui.emptyRoleError.find()).toBeInTheDocument();
+    expect(ui.mappingDialogClose.get()).toBeDisabled();
+    await user.click(custom3Checkboxes[1]);
+    expect(ui.emptyRoleError.query()).not.toBeInTheDocument();
+    expect(ui.mappingDialogClose.get()).toBeEnabled();
+    await user.click(ui.mappingDialogClose.get());
+
+    expect(await ui.saveProvisioning.find()).toBeEnabled();
+    await user.click(ui.saveProvisioning.get());
+
+    // Clean local mapping state
+    await user.click(ui.jitProvisioningRadioButton.get());
+    await user.click(ui.autoProvisioningRadioButton.get());
+
+    await user.click(ui.editMappingButton.get());
+
+    expect(
+      (await ui.mappingRow.findAll()).filter(
+        (row) => within(row).queryAllByRole('checkbox').length > 0,
+      ),
+    ).toHaveLength(7);
+    custom1Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom1'));
+    custom3Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom3'));
+    expect(ui.getMappingRowByRole('custom2')).toBeUndefined();
+    expect(custom1Checkboxes[0]).toBeChecked();
+    expect(custom1Checkboxes[1]).not.toBeChecked();
+    expect(custom1Checkboxes[2]).toBeChecked();
+    expect(custom1Checkboxes[3]).not.toBeChecked();
+    expect(custom1Checkboxes[4]).not.toBeChecked();
+    expect(custom1Checkboxes[5]).toBeChecked();
+    expect(custom3Checkboxes[0]).not.toBeChecked();
+    expect(custom3Checkboxes[1]).toBeChecked();
+    expect(custom3Checkboxes[2]).not.toBeChecked();
+    expect(custom3Checkboxes[3]).not.toBeChecked();
+    expect(custom3Checkboxes[4]).not.toBeChecked();
+    expect(custom3Checkboxes[5]).not.toBeChecked();
+    await user.click(ui.mappingDialogClose.get());
+  });
 });
 
 function renderAuthentication(features: Feature[] = []) {
index dc09ffb9ea894bdc2c3c3d87c6e2b1ca57d2a9db..41407db3f7a18c93ee798494ff3111f137bde103 100644 (file)
@@ -134,7 +134,7 @@ export function useGithubRolesMappingMutation() {
       if (state) {
         const newData = unionBy(
           addedOrChanged,
-          state.filter((s) => !deleted.find((id) => id === s.id)),
+          state.filter((s) => deleted.find((id) => id === s.id) === undefined),
           (el) => el.id,
         );
         client.setQueryData(queryKey, newData);
index bc109a8e03098b1c3875b4b9c38cda3f6cddc7fc..fb1bb25436a9b39c34b820674039fcd716fc7b50 100644 (file)
@@ -22,8 +22,10 @@ import { addGlobalSuccessMessage } from 'design-system';
 import { isEqual, keyBy, partition, pick, unionBy } from 'lodash';
 import { getActivity } from '../../api/ce';
 import {
+  addGitlabRolesMapping,
   createGitLabConfiguration,
   deleteGitLabConfiguration,
+  deleteGitlabRolesMapping,
   fetchGitLabConfigurations,
   fetchGitlabRolesMapping,
   syncNowGitLabProvisioning,
@@ -220,21 +222,32 @@ export function useGitlabRolesMappingMutation() {
     mutationFn: async (mapping: GitLabMapping[]) => {
       const state = keyBy(client.getQueryData<GitLabMapping[]>(queryKey), (m) => m.id);
 
-      const [maybeChangedRoles] = partition(mapping, (m) => state[m.id]);
+      const [maybeChangedRoles, newRoles] = partition(mapping, (m) => state[m.id]);
       const changedRoles = maybeChangedRoles.filter((item) => !isEqual(item, state[item.id]));
+      const deletedRoles = Object.values(state).filter(
+        (m) => !m.baseRole && !mapping.some((cm) => m.id === cm.id),
+      );
 
       return {
         addedOrChanged: await Promise.all([
           ...changedRoles.map((data) =>
             updateGitlabRolesMapping(data.id, pick(data, 'permissions')),
           ),
+          ...newRoles.map((m) => addGitlabRolesMapping(m)),
         ]),
+        deleted: await Promise.all([
+          deletedRoles.map((dm) => deleteGitlabRolesMapping(dm.id)),
+        ]).then(() => deletedRoles.map((dm) => dm.id)),
       };
     },
-    onSuccess: ({ addedOrChanged }) => {
+    onSuccess: ({ addedOrChanged, deleted }) => {
       const state = client.getQueryData<GitLabMapping[]>(queryKey);
       if (state) {
-        const newData = unionBy(addedOrChanged, state, (el) => el.id);
+        const newData = unionBy(
+          addedOrChanged,
+          state.filter((s) => deleted.find((id) => id === s.id) === undefined),
+          (el) => el.id,
+        );
         client.setQueryData(queryKey, newData);
       }
       addGlobalSuccessMessage(
index 314b850b8fe3abf9d0fdc7e60b61bf1d33b3396b..780b6ac602d70fadcaf30c8a8f26f8e238198afa 100644 (file)
@@ -1633,7 +1633,6 @@ settings.authentication.github.configuration.validation.details.valid_label=Vali
 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.description=When synchronizing users and groups, SonarQube assigns permissions based on GitHub user and team roles. You can customize the mapping of permissions. The new mapping will take effect at the next synchronization.
-settings.authentication.github.configuration.roles_mapping.dialog.custom_roles_description=When a custom role name added here matches an existing GitHub custom role in any of your organizations, the mapping applies to all users with this custom role. If an existing GitHub custom role has no exact match in this list, the permissions of its inherited base role are mapped.
 settings.authentication.github.configuration.roles_mapping.save_success=GitHub roles mapping saved successfully.
 settings.authentication.github.configuration.unsaved_changes=You have unsaved changes.
 
@@ -1701,6 +1700,7 @@ settings.authentication.configuration.roles_mapping.dialog.delete_custom_role=De
 settings.authentication.configuration.roles_mapping.dialog.add_custom_role=Add custom role:
 settings.authentication.configuration.roles_mapping.role_exists=Role already exists.
 settings.authentication.configuration.roles_mapping.empty_custom_role=Custom roles should have some permissions.
+settings.authentication.configuration.roles_mapping.dialog.custom_roles_description=When a custom role name added here matches an existing {0} custom role in any of your organizations, the mapping applies to all users with this custom role. If an existing {0} custom role has no exact match in this list, the permissions of its inherited base role are mapped.
 
 # SAML
 settings.authentication.form.create.saml=New SAML configuration