From: Viktor Vorona Date: Wed, 12 Jul 2023 14:46:01 +0000 (+0200) Subject: SONAR-19790 Hide Apply Template button and show banners for GH provisioning X-Git-Tag: 10.2.0.77647~387 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=29bbf09e6ae7b157db05f02a4d95a4b33a28334c;p=sonarqube.git SONAR-19790 Hide Apply Template button and show banners for GH provisioning --- diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx index 86da435b83d..176ac9ce6eb 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx @@ -17,12 +17,15 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import * as React from 'react'; +import React, { useState } from 'react'; import { createPermissionTemplate } from '../../../api/permissions'; import { Button } from '../../../components/controls/buttons'; import { Router, withRouter } from '../../../components/hoc/withRouter'; +import { Alert } from '../../../components/ui/Alert'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; +import { throwGlobalError } from '../../../helpers/error'; import { translate } from '../../../helpers/l10n'; +import { useGithubStatusQuery } from '../../settings/components/authentication/queries/identity-provider'; import { PERMISSION_TEMPLATES_PATH } from '../utils'; import Form from './Form'; @@ -32,71 +35,57 @@ interface Props { router: Router; } -interface State { - createModal: boolean; -} - -class Header extends React.PureComponent { - mounted = false; - state: State = { createModal: false }; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - handleCreateClick = () => { - this.setState({ createModal: true }); - }; +function Header(props: Props) { + const { ready, router } = props; + const [createModal, setCreateModal] = useState(false); + const { data: gitHubProvisioningStatus } = useGithubStatusQuery(); - handleCreateModalClose = () => { - if (this.mounted) { - this.setState({ createModal: false }); - } - }; - - handleCreateModalSubmit = (data: { + const handleCreateModalSubmit = async (data: { description: string; name: string; projectKeyPattern: string; }) => { - return createPermissionTemplate({ ...data }).then((response) => { - this.props.refresh().then(() => { - this.props.router.push({ - pathname: PERMISSION_TEMPLATES_PATH, - query: { id: response.permissionTemplate.id }, - }); + try { + const response = await createPermissionTemplate({ ...data }); + await props.refresh(); + router.push({ + pathname: PERMISSION_TEMPLATES_PATH, + query: { id: response.permissionTemplate.id }, }); - }); + } catch (e) { + throwGlobalError(e); + } }; - render() { - return ( + return ( + <>

{translate('permission_templates.page')}

- +
- + - {this.state.createModal && ( + {createModal && (
setCreateModal(false)} + onSubmit={handleCreateModalSubmit} /> )}

{translate('permission_templates.page.description')}

- ); - } + {gitHubProvisioningStatus && ( + + {translate('permission_templates.github_warning')} + + )} + + ); } export default withRouter(Header); diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/Template.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/Template.tsx index e679c512546..0c0c0819785 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/Template.tsx +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/Template.tsx @@ -23,12 +23,15 @@ import { Helmet } from 'react-helmet-async'; import * as api from '../../../api/permissions'; import AllHoldersList from '../../../components/permissions/AllHoldersList'; import { FilterOption } from '../../../components/permissions/SearchForm'; +import { Alert } from '../../../components/ui/Alert'; import { translate } from '../../../helpers/l10n'; import { convertToPermissionDefinitions, PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE, } from '../../../helpers/permissions'; +import UseQuery from '../../../helpers/UseQuery'; import { Paging, PermissionGroup, PermissionTemplate, PermissionUser } from '../../../types/types'; +import { useGithubStatusQuery } from '../../settings/components/authentication/queries/identity-provider'; import TemplateDetails from './TemplateDetails'; import TemplateHeader from './TemplateHeader'; @@ -329,6 +332,15 @@ export default class Template extends React.PureComponent { />
+ + {({ data: githubProvisioningStatus }) => + githubProvisioningStatus ? ( + + {translate('permission_templates.github_warning')} + + ) : null + } + { serviceMock.reset(); + authServiceMock.reset(); }); describe('rendering', () => { @@ -52,19 +53,13 @@ describe('rendering', () => { expect(ui.templateLink('Permission Template 1').get()).toBeInTheDocument(); expect(ui.templateLink('Permission Template 2').get()).toBeInTheDocument(); - // Shows all permission table headers. - PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE.forEach((permission, i) => { - expect( - ui.getTableHeaderHelpTooltip(i + 1, `projects_role.${permission}.desc`) - ).toBeInTheDocument(); - }); - // Shows warning for browse and code viewer permissions. - [Permissions.Browse, Permissions.CodeViewer].forEach((_permission, i) => { - expect( - ui.getTableHeaderHelpTooltip(i + 1, 'projects_role.public_projects_warning') - ).toBeInTheDocument(); - }); + await expect(ui.getHeaderTooltipIconByIndex(1)).toHaveATooltipWithContent( + 'projects_role.public_projects_warning' + ); + await expect(ui.getHeaderTooltipIconByIndex(2)).toHaveATooltipWithContent( + 'projects_role.public_projects_warning' + ); // Check summaries. // Note: because of the intricacies of these table cells, and the verbosity @@ -91,17 +86,28 @@ describe('rendering', () => { renderPermissionTemplatesApp(); await ui.appLoaded(); + expect(ui.githubWarning.query()).not.toBeInTheDocument(); await ui.openTemplateDetails('Permission Template 1'); await ui.appLoaded(); + expect(ui.githubWarning.query()).not.toBeInTheDocument(); + expect(screen.getByText('This is permission template 1')).toBeInTheDocument(); - PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE.forEach((permission, i) => { - expect(ui.permissionCheckbox('johndoe', permission).get()).toBeInTheDocument(); - expect( - ui.getTableHeaderHelpTooltip(i, `projects_role.${permission}.desc`) - ).toBeInTheDocument(); - }); }); + + it.each(PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE.map((p, i) => [p, i]))( + 'should show the correct tooltips', + async (permission, i) => { + const user = userEvent.setup(); + const ui = getPageObject(user); + renderPermissionTemplatesApp(); + await ui.appLoaded(); + + await expect(ui.getHeaderTooltipIconByIndex(i)).toHaveATooltipWithContent( + `projects_role.${permission}.desc` + ); + } + ); }); describe('CRUD', () => { @@ -391,6 +397,18 @@ it.each([ComponentQualifier.Project, ComponentQualifier.Application, ComponentQu } ); +it('should show github warning', async () => { + const user = userEvent.setup(); + const ui = getPageObject(user); + authServiceMock.githubProvisioningStatus = true; + renderPermissionTemplatesApp(undefined, [Feature.GithubProvisioning]); + + expect(await ui.githubWarning.find()).toBeInTheDocument(); + await ui.openTemplateDetails('Permission Template 1'); + + expect(await ui.githubWarning.find()).toBeInTheDocument(); +}); + function getPageObject(user: UserEvent) { const ui = { loading: byLabelText('loading'), @@ -403,6 +421,7 @@ function getPageObject(user: UserEvent) { byRole('link', { name: `projects_role.${permission}` }), onlyUsersBtn: byRole('button', { name: 'users.page' }), onlyGroupsBtn: byRole('button', { name: 'user_groups.page' }), + githubWarning: byText('permission_templates.github_warning'), showAllBtn: byRole('button', { name: 'all' }), searchInput: byRole('searchbox', { name: 'search.search_for_users_or_groups' }), loadMoreBtn: byRole('button', { name: 'show_more' }), @@ -453,7 +472,7 @@ function getPageObject(user: UserEvent) { await user.click(ui.loadMoreBtn.get()); }, async togglePermission(target: string, permission: Permissions) { - await user.click(ui.permissionCheckbox(target, permission).get()); + await act(() => user.click(ui.permissionCheckbox(target, permission).get())); }, async openCreateModal() { await user.click(ui.createNewTemplateBtn.get()); @@ -516,22 +535,18 @@ function getPageObject(user: UserEvent) { await user.click(ui.cogMenuBtn(name).get()); await user.click(ui.setDefaultBtn(qualifier).get()); }, - getTableHeaderHelpTooltip(i: number, text: string) { - const th = byRole('columnheader').getAll().at(i); - if (th === undefined) { - throw new Error(`Couldn't locate the at index ${i}`); - } - return findTooltipWithContent((_content, element) => { - // For some reason, using the `content` parameter doesn't work for 1 of the - // tests. Explicitly using the element's `textContent` always works. - return Boolean(element?.textContent?.includes(text)); - }, th); + getHeaderTooltipIconByIndex(i: number) { + return byRole('columnheader').byTestId('help-tooltip-activator').getAll()[i]; }, }; } -function renderPermissionTemplatesApp(qualifiers = [ComponentQualifier.Project]) { +function renderPermissionTemplatesApp( + qualifiers = [ComponentQualifier.Project], + featureList: Feature[] = [] +) { renderAppWithAdminContext('admin/permission_templates', routes, { appState: mockAppState({ qualifiers }), + featureList, }); } diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx index 210c9f0af94..f9b0bcf125c 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx @@ -43,9 +43,9 @@ export default function PageHeader(props: Props) { const { component, isGitHubProject, loading } = props; const { configuration } = component; - const canApplyPermissionTemplate = - configuration != null && configuration.canApplyPermissionTemplate; const provisionedByGitHub = isGitHubProject && !!githubProvisioningStatus; + const canApplyPermissionTemplate = + configuration?.canApplyPermissionTemplate && !provisionedByGitHub; const handleApplyTemplate = () => { setApplyTemplateModal(true); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx index 57372d4f0c7..01b17d95ee4 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx @@ -323,12 +323,14 @@ it('should have disabled permissions for GH Project', async () => { 'aria-disabled', 'false' ); - await user.click(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()); + await ui.toggleProjectPermission('Alexa', Permissions.IssueAdmin); expect(ui.confirmRemovePermissionDialog.get()).toBeInTheDocument(); expect(ui.confirmRemovePermissionDialog.get()).toHaveTextContent( `${Permissions.IssueAdmin}Alexa` ); - await user.click(ui.confirmRemovePermissionDialog.byRole('button', { name: 'confirm' }).get()); + await act(() => + user.click(ui.confirmRemovePermissionDialog.byRole('button', { name: 'confirm' }).get()) + ); expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).not.toBeChecked(); expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).toBeChecked(); @@ -336,12 +338,14 @@ it('should have disabled permissions for GH Project', async () => { 'aria-disabled', 'false' ); - await user.click(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()); + await ui.toggleProjectPermission('sonar-users', Permissions.Browse); expect(ui.confirmRemovePermissionDialog.get()).toBeInTheDocument(); expect(ui.confirmRemovePermissionDialog.get()).toHaveTextContent( `${Permissions.Browse}sonar-users` ); - await user.click(ui.confirmRemovePermissionDialog.byRole('button', { name: 'confirm' }).get()); + await act(() => + user.click(ui.confirmRemovePermissionDialog.byRole('button', { name: 'confirm' }).get()) + ); expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).not.toBeChecked(); expect(ui.projectPermissionCheckbox('sonar-admins', Permissions.Admin).get()).toBeChecked(); expect(ui.projectPermissionCheckbox('sonar-admins', Permissions.Admin).get()).toHaveAttribute( @@ -362,6 +366,8 @@ it('should have disabled permissions for GH Project', async () => { expect(adminsGroupRow).toHaveTextContent('sonar-admins'); expect(ui.githubLogo.query(adminsGroupRow)).toBeInTheDocument(); + expect(ui.applyTemplateBtn.query()).not.toBeInTheDocument(); + // not possible to grant permissions at all expect( screen @@ -375,10 +381,9 @@ it('should allow to change permissions for GH Project without auto-provisioning' const ui = getPageObject(user); authHandler.githubProvisioningStatus = false; renderPermissionsProjectApp( - {}, + { visibility: Visibility.Private }, { featureList: [Feature.GithubProvisioning] }, { - component: mockComponent({ visibility: Visibility.Private }), projectBinding: { alm: AlmKeys.GitHub, key: 'test', repository: 'test', monorepo: false }, } ); @@ -387,6 +392,8 @@ it('should allow to change permissions for GH Project without auto-provisioning' expect(ui.pageTitle.get()).toBeInTheDocument(); expect(ui.pageTitle.byRole('img').query()).not.toBeInTheDocument(); + expect(ui.applyTemplateBtn.get()).toBeInTheDocument(); + // no restrictions expect( screen.getAllByRole('checkbox').every((item) => item.getAttribute('aria-disabled') !== 'true') @@ -404,6 +411,8 @@ it('should allow to change permissions for non-GH Project', async () => { expect(ui.nonGHProjectWarning.get()).toBeInTheDocument(); expect(ui.pageTitle.byRole('img').query()).not.toBeInTheDocument(); + expect(ui.applyTemplateBtn.get()).toBeInTheDocument(); + // no restrictions expect( screen.getAllByRole('checkbox').every((item) => item.getAttribute('aria-disabled') !== 'true') diff --git a/server/sonar-web/src/main/js/apps/permissions/test-utils.ts b/server/sonar-web/src/main/js/apps/permissions/test-utils.ts index 7ce2af3edaa..15f971e558b 100644 --- a/server/sonar-web/src/main/js/apps/permissions/test-utils.ts +++ b/server/sonar-web/src/main/js/apps/permissions/test-utils.ts @@ -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 { waitFor } from '@testing-library/react'; +import { act, waitFor } from '@testing-library/react'; import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; import selectEvent from 'react-select-event'; import { byLabelText, byRole, byText } from '../../helpers/testSelector'; @@ -49,7 +49,7 @@ export function getPageObject(user: UserEvent) { 'projects_role.are_you_sure_to_turn_project_to_public.warning.TRK' ), confirmPublicBtn: byRole('button', { name: 'projects_role.turn_project_to_public.TRK' }), - openModalBtn: byRole('button', { name: 'projects_role.apply_template' }), + applyTemplateBtn: byRole('button', { name: 'projects_role.apply_template' }), closeModalBtn: byRole('button', { name: 'close' }), templateSelect: byRole('combobox', { name: /template/ }), templateSuccessfullyApplied: byText('projects_role.apply_template.success'), @@ -71,7 +71,7 @@ export function getPageObject(user: UserEvent) { }); }, async toggleProjectPermission(target: string, permission: Permissions) { - await user.click(ui.projectPermissionCheckbox(target, permission).get()); + await act(() => user.click(ui.projectPermissionCheckbox(target, permission).get())); }, async toggleGlobalPermission(target: string, permission: Permissions) { await user.click(ui.globalPermissionCheckbox(target, permission).get()); @@ -86,7 +86,7 @@ export function getPageObject(user: UserEvent) { await user.click(ui.confirmPublicBtn.get()); }, async openTemplateModal() { - await user.click(ui.openModalBtn.get()); + await user.click(ui.applyTemplateBtn.get()); }, async closeTemplateModal() { await user.click(ui.closeModalBtn.get()); diff --git a/server/sonar-web/src/main/js/helpers/UseQuery.tsx b/server/sonar-web/src/main/js/helpers/UseQuery.tsx index ea75cd9422d..7b8542ed2c8 100644 --- a/server/sonar-web/src/main/js/helpers/UseQuery.tsx +++ b/server/sonar-web/src/main/js/helpers/UseQuery.tsx @@ -25,7 +25,7 @@ type QueryHook = (...args: TArgs) => UseQueryResult< interface Props { query: QueryHook; args?: TArgs; - children: (value: UseQueryResult) => ReactElement; + children: (value: UseQueryResult) => ReactElement | null; } export default function UseQuery(props: Props) { diff --git a/server/sonar-web/src/main/js/helpers/testSelector.ts b/server/sonar-web/src/main/js/helpers/testSelector.ts index 832aca63258..5a7a99f8e66 100644 --- a/server/sonar-web/src/main/js/helpers/testSelector.ts +++ b/server/sonar-web/src/main/js/helpers/testSelector.ts @@ -127,7 +127,11 @@ class ChainDispatch extends ChainingQuery { } getAll(container?: HTMLElement) { - return this.elementQuery.getAll(this.insideQuery.get(container)); + const containers = this.insideQuery.getAll(container); + return containers.reduce( + (acc, item) => [...acc, ...(this.elementQuery.queryAll(item) ?? [])], + [] + ); } query(container?: HTMLElement) { 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 45f9ceac607..3392c1cb438 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3018,6 +3018,7 @@ permission_templates.page=Permission Templates permission_templates.page.description=Manage templates of project permission sets. The default template will be applied to all new projects. permission_templates.set_default=Set Default permission_templates.set_default_for=Set Default For +permission_templates.github_warning=Please note that permission templates will only affect non-GitHub projects due to the enabled automatic provisioning via GitHub. permission_template.new_template=Create Permission Template permission_template.delete_confirm_title=Delete Permission Template permission_template.do_you_want_to_delete_template_xxx=Are you sure that you want to delete permission template "{0}"?