From 4dab8ffd7d19d709707b9bae1e284b613c7733f9 Mon Sep 17 00:00:00 2001 From: Viktor Vorona Date: Tue, 26 Sep 2023 16:25:45 +0200 Subject: [PATCH] SONAR-20532 Add custom roles to mapping modal --- .../authentication/GitHubMappingModal.tsx | 184 +++++++++++++++--- .../GithubAuthenticationTab.tsx | 14 +- .../__tests__/Authentication-it.tsx | 59 +++--- server/sonar-web/src/main/js/types/axios.d.ts | 18 +- .../src/main/js/types/provisioning.ts | 5 +- .../resources/org/sonar/l10n/core.properties | 3 + 6 files changed, 216 insertions(+), 67 deletions(-) diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubMappingModal.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubMappingModal.tsx index 7b1f0306abf..7c4026ceba5 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubMappingModal.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubMappingModal.tsx @@ -17,12 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import classNames from 'classnames'; import * as React from 'react'; import Checkbox from '../../../../components/controls/Checkbox'; import Modal from '../../../../components/controls/Modal'; -import { SubmitButton } from '../../../../components/controls/buttons'; +import { DeleteButton, SubmitButton } from '../../../../components/controls/buttons'; import PermissionHeader from '../../../../components/permissions/PermissionHeader'; +import { Alert } from '../../../../components/ui/Alert'; import Spinner from '../../../../components/ui/Spinner'; import { translate } from '../../../../helpers/l10n'; import { @@ -34,22 +34,105 @@ import { useGithubRolesMappingQuery } from '../../../../queries/identity-provide import { GitHubMapping } from '../../../../types/provisioning'; interface Props { - readonly mapping: GitHubMapping[] | null; - readonly setMapping: React.Dispatch>; - readonly onClose: () => void; + mapping: GitHubMapping[] | null; + setMapping: React.Dispatch>; + onClose: () => void; } -export default function GitHubMappingModal({ mapping, setMapping, onClose }: Props) { +interface PermissionCellProps { + mapping: GitHubMapping; + setMapping: React.Dispatch>; + list?: GitHubMapping[]; +} + +const DEFAULT_CUSTOM_ROLE_PERMISSIONS: GitHubMapping['permissions'] = { + user: false, + codeviewer: false, + issueadmin: false, + securityhotspotadmin: false, + admin: false, + scan: false, +}; + +function PermissionRow(props: Readonly) { + const { mapping, list } = props; + + return ( + + +
+ + {mapping.roleName} + + {!mapping.isBaseRole && ( + { + props.setMapping(list?.filter((r) => r.roleName !== mapping.roleName) ?? null); + }} + /> + )} +
+ + {Object.entries(mapping.permissions).map(([key, value]) => ( + + + props.setMapping( + list?.map((item) => + item.id === mapping.id + ? { ...item, permissions: { ...item.permissions, [key]: newValue } } + : item, + ) ?? null, + ) + } + /> + + ))} + + ); +} + +export default function GitHubMappingModal({ mapping, setMapping, onClose }: Readonly) { const { data: roles, isLoading } = useGithubRolesMappingQuery(); const permissions = convertToPermissionDefinitions( PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE, 'projects_role', ); + const [customRoleInput, setCustomRoleInput] = React.useState(''); + const [customRoleError, setCustomRoleError] = React.useState(false); const header = translate( 'settings.authentication.github.configuration.roles_mapping.dialog.title', ); + const list = mapping ?? roles; + + const validateAndAddCustomRole = (e: React.FormEvent) => { + e.preventDefault(); + const value = customRoleInput.trim(); + if ( + !list?.some((el) => + el.isBaseRole ? el.roleName.toLowerCase() === value.toLowerCase() : el.roleName === value, + ) + ) { + setMapping([ + { + id: customRoleInput, + roleName: customRoleInput, + permissions: { ...DEFAULT_CUSTOM_ROLE_PERMISSIONS }, + }, + ...(list ?? []), + ]); + setCustomRoleInput(''); + } else { + setCustomRoleError(true); + } + }; + return (
@@ -58,8 +141,11 @@ export default function GitHubMappingModal({ mapping, setMapping, onClose }: Pro
- - + - {(mapping ?? roles)?.map(({ id, roleName, permissions }) => ( - - - {Object.entries(permissions).map(([key, value]) => ( - - ))} - - ))} + {list + ?.filter((r) => r.isBaseRole) + .map((mapping) => ( + + ))} + + + + {list + ?.filter((r) => !r.isBaseRole) + .map((mapping) => ( + + ))}
+
{translate( 'settings.authentication.github.configuration.roles_mapping.dialog.roles_column', @@ -77,29 +163,65 @@ export default function GitHubMappingModal({ mapping, setMapping, onClose }: Pro
- {roleName} - - - setMapping( - (mapping ?? roles)?.map((item) => - item.id === id - ? { ...item, permissions: { ...item.permissions, [key]: newValue } } - : item, - ) ?? null, - ) - } - /> -
+ + {translate( + 'settings.authentication.github.configuration.roles_mapping.dialog.custom_roles_description', + )} + +
+ + { + setCustomRoleError(false); + setCustomRoleInput(event.currentTarget.value); + }} + type="text" + /> + + {translate('add_verb')} + + + {customRoleError && + translate( + 'settings.authentication.github.configuration.roles_mapping.role_exists', + )} + +
+
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx index 9d66fc0ccfe..6ba0fc587bc 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx @@ -399,14 +399,14 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps )} )} - {showMappingModal && ( - setShowMappingModal(false)} - /> - )} + {showMappingModal && ( + setShowMappingModal(false)} + /> + )}
)} diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx index ef05268b167..a14e5ac963c 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx @@ -154,6 +154,8 @@ const ui = { mappingRow: byRole('dialog', { name: 'settings.authentication.github.configuration.roles_mapping.dialog.title', }).byRole('row'), + getMappingRowByRole: (text: string) => + ui.github.mappingRow.getAll().find((row) => within(row).queryByText(text) !== null), mappingCheckbox: byRole('checkbox'), mappingDialogClose: byRole('dialog', { name: 'settings.authentication.github.configuration.roles_mapping.dialog.title', @@ -850,12 +852,17 @@ describe('Github tab', () => { expect(await github.editMappingButton.find()).toBeInTheDocument(); await user.click(github.editMappingButton.get()); - expect(await github.mappingRow.findAll()).toHaveLength(6); - expect(github.mappingRow.getAt(1)).toHaveTextContent('read'); - expect(github.mappingRow.getAt(2)).toHaveTextContent('triage'); - expect(github.mappingRow.getAt(3)).toHaveTextContent('write'); - expect(github.mappingRow.getAt(4)).toHaveTextContent('maintain'); - expect(github.mappingRow.getAt(5)).toHaveTextContent('admin'); + const rows = (await github.mappingRow.findAll()).filter( + (row) => within(row).queryAllByRole('checkbox').length > 0, + ); + + expect(rows).toHaveLength(5); + + expect(rows[0]).toHaveTextContent('read'); + expect(rows[1]).toHaveTextContent('triage'); + expect(rows[2]).toHaveTextContent('write'); + expect(rows[3]).toHaveTextContent('maintain'); + expect(rows[4]).toHaveTextContent('admin'); }); it('should apply new mapping and new provisioning type at the same time', async () => { @@ -872,18 +879,18 @@ describe('Github tab', () => { expect(await github.editMappingButton.find()).toBeInTheDocument(); await user.click(github.editMappingButton.get()); - expect(await github.mappingRow.findAll()).toHaveLength(6); + expect(await github.mappingRow.findAll()).toHaveLength(7); - let rowOneCheckboxes = github.mappingCheckbox.getAll(github.mappingRow.getAt(1)); - let rowFiveCheckboxes = github.mappingCheckbox.getAll(github.mappingRow.getAt(5)); + let readCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('read')); + let adminCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('admin')); - expect(rowOneCheckboxes[0]).toBeChecked(); - expect(rowOneCheckboxes[5]).not.toBeChecked(); - expect(rowFiveCheckboxes[5]).toBeChecked(); + expect(readCheckboxes[0]).toBeChecked(); + expect(readCheckboxes[5]).not.toBeChecked(); + expect(adminCheckboxes[5]).toBeChecked(); - await user.click(rowOneCheckboxes[0]); - await user.click(rowOneCheckboxes[5]); - await user.click(rowFiveCheckboxes[5]); + await user.click(readCheckboxes[0]); + await user.click(readCheckboxes[5]); + await user.click(adminCheckboxes[5]); await user.click(github.mappingDialogClose.get()); await user.click(github.saveGithubProvisioning.get()); @@ -894,12 +901,12 @@ describe('Github tab', () => { await user.click(github.githubProvisioningButton.get()); await user.click(github.editMappingButton.get()); - rowOneCheckboxes = github.mappingCheckbox.getAll(github.mappingRow.getAt(1)); - rowFiveCheckboxes = github.mappingCheckbox.getAll(github.mappingRow.getAt(5)); + readCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('read')); + adminCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('admin')); - expect(rowOneCheckboxes[0]).not.toBeChecked(); - expect(rowOneCheckboxes[5]).toBeChecked(); - expect(rowFiveCheckboxes[5]).not.toBeChecked(); + expect(readCheckboxes[0]).not.toBeChecked(); + expect(readCheckboxes[5]).toBeChecked(); + expect(adminCheckboxes[5]).not.toBeChecked(); await user.click(github.mappingDialogClose.get()); }); @@ -913,13 +920,13 @@ describe('Github tab', () => { expect(await github.saveGithubProvisioning.find()).toBeDisabled(); await user.click(github.editMappingButton.get()); - expect(await github.mappingRow.findAll()).toHaveLength(6); + expect(await github.mappingRow.findAll()).toHaveLength(7); - let rowOneCheckbox = github.mappingCheckbox.getAll(github.mappingRow.getAt(1))[0]; + let readCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('read'))[0]; - expect(rowOneCheckbox).toBeChecked(); + expect(readCheckboxes).toBeChecked(); - await user.click(rowOneCheckbox); + await user.click(readCheckboxes); await user.click(github.mappingDialogClose.get()); expect(await github.saveGithubProvisioning.find()).toBeEnabled(); @@ -931,9 +938,9 @@ describe('Github tab', () => { await user.click(github.githubProvisioningButton.get()); await user.click(github.editMappingButton.get()); - rowOneCheckbox = github.mappingCheckbox.getAll(github.mappingRow.getAt(1))[0]; + readCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('read'))[0]; - expect(rowOneCheckbox).not.toBeChecked(); + expect(readCheckboxes).not.toBeChecked(); await user.click(github.mappingDialogClose.get()); }); }); diff --git a/server/sonar-web/src/main/js/types/axios.d.ts b/server/sonar-web/src/main/js/types/axios.d.ts index c9b67a26671..7e9132e9ff4 100644 --- a/server/sonar-web/src/main/js/types/axios.d.ts +++ b/server/sonar-web/src/main/js/types/axios.d.ts @@ -20,12 +20,28 @@ import 'axios'; +type IfEquals = (() => T extends X ? 1 : 2) extends () => T extends Y + ? 1 + : 2 + ? A + : B; + +type WritableKeys = { + [P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P>; +}[keyof T]; + +type OmitReadonly = Pick>; + declare module 'axios' { export interface AxiosInstance { get(url: string, config?: AxiosRequestConfig): Promise; delete(url: string, config?: AxiosRequestConfig): Promise; post(url: string, data?: D, config?: AxiosRequestConfig): Promise; - patch(url: string, data?: D, config?: AxiosRequestConfig): Promise; + patch>>( + url: string, + data?: D, + config?: AxiosRequestConfig, + ): Promise; defaults: Omit & { headers: HeadersDefaults & { diff --git a/server/sonar-web/src/main/js/types/provisioning.ts b/server/sonar-web/src/main/js/types/provisioning.ts index d361b8a31cf..f59b1b610de 100644 --- a/server/sonar-web/src/main/js/types/provisioning.ts +++ b/server/sonar-web/src/main/js/types/provisioning.ts @@ -77,8 +77,9 @@ export interface GitHubConfigurationStatus { } export interface GitHubMapping { - id: string; - roleName: string; + readonly id: string; + readonly roleName: string; + readonly isBaseRole?: boolean; permissions: { user: boolean; codeviewer: boolean; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 2291d1fea4f..bccc8c54921 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1539,6 +1539,9 @@ settings.authentication.github.configuration.roles_mapping.description=When sync 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.roles_mapping.dialog.add_custom_role=Add custom role: +settings.authentication.github.configuration.roles_mapping.role_exists=Role already exists +settings.authentication.github.configuration.roles_mapping.dialog.custom_roles_description=Define a custom GitHub role to create a mapping. If the custom role name matches an existing role in any of your organizations, this mapping will apply to all occurrences of that role. If the existing custom role doesn't match any entries in this mapping, it will gracefully fall back to its base role. settings.authentication.github.configuration.unsaved_changes=You have unsaved changes. # SAML -- 2.39.5