]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19790 Hide Apply Template button and show banners for GH provisioning
authorViktor Vorona <viktor.vorona@sonarsource.com>
Wed, 12 Jul 2023 14:46:01 +0000 (16:46 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 18 Jul 2023 20:03:22 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/Template.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/PermissionTemplatesApp-it.tsx
server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx
server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx
server/sonar-web/src/main/js/apps/permissions/test-utils.ts
server/sonar-web/src/main/js/helpers/UseQuery.tsx
server/sonar-web/src/main/js/helpers/testSelector.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 86da435b83da110e50a3d8267b3516a42203c6eb..176ac9ce6ebc2e8fea593a32108490bee40fe3fb 100644 (file)
  * 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<Props, State> {
-  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 (
+    <>
       <header className="page-header" id="project-permissions-header">
         <h1 className="page-title">{translate('permission_templates.page')}</h1>
 
-        <DeferredSpinner loading={!this.props.ready} />
+        <DeferredSpinner loading={!ready} />
 
         <div className="page-actions">
-          <Button onClick={this.handleCreateClick}>{translate('create')}</Button>
+          <Button onClick={() => setCreateModal(true)}>{translate('create')}</Button>
 
-          {this.state.createModal && (
+          {createModal && (
             <Form
               confirmButtonText={translate('create')}
               header={translate('permission_template.new_template')}
-              onClose={this.handleCreateModalClose}
-              onSubmit={this.handleCreateModalSubmit}
+              onClose={() => setCreateModal(false)}
+              onSubmit={handleCreateModalSubmit}
             />
           )}
         </div>
 
         <p className="page-description">{translate('permission_templates.page.description')}</p>
       </header>
-    );
-  }
+      {gitHubProvisioningStatus && (
+        <Alert variant="warning" className="sw-w-fit">
+          {translate('permission_templates.github_warning')}
+        </Alert>
+      )}
+    </>
+  );
 }
 
 export default withRouter(Header);
index e679c512546366fce9040a7b8ebc41878cdef6bf..0c0c0819785b764bbcbccdc626e2f2447bf83502 100644 (file)
@@ -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<Props, State> {
         />
         <main>
           <TemplateDetails template={template} />
+          <UseQuery query={useGithubStatusQuery}>
+            {({ data: githubProvisioningStatus }) =>
+              githubProvisioningStatus ? (
+                <Alert variant="warning" className="sw-w-fit">
+                  {translate('permission_templates.github_warning')}
+                </Alert>
+              ) : null
+            }
+          </UseQuery>
 
           <AllHoldersList
             filter={filter}
index e3710f2ed8350a26ef15b06f1f2f448a6e83fa21..32f0bc74bfbc4167f94b22973634cf529f5a13da 100644 (file)
@@ -21,24 +21,25 @@ 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 { uniq } from 'lodash';
+import AuthenticationServiceMock from '../../../../api/mocks/AuthenticationServiceMock';
 import PermissionsServiceMock from '../../../../api/mocks/PermissionsServiceMock';
 import { mockPermissionGroup, mockPermissionUser } from '../../../../helpers/mocks/permissions';
 import { PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE } from '../../../../helpers/permissions';
 import { mockAppState } from '../../../../helpers/testMocks';
-import {
-  findTooltipWithContent,
-  renderAppWithAdminContext,
-} from '../../../../helpers/testReactTestingUtils';
-import { byLabelText, byRole } from '../../../../helpers/testSelector';
+import { renderAppWithAdminContext } from '../../../../helpers/testReactTestingUtils';
+import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
 import { ComponentQualifier } from '../../../../types/component';
+import { Feature } from '../../../../types/features';
 import { Permissions } from '../../../../types/permissions';
 import { PermissionGroup, PermissionUser } from '../../../../types/types';
 import routes from '../../routes';
 
 const serviceMock = new PermissionsServiceMock();
+const authServiceMock = new AuthenticationServiceMock();
 
 beforeEach(() => {
   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 <th> 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,
   });
 }
index 210c9f0af94290dd22dfa5efe42241c7e432ed33..f9b0bcf125c9d00eee07dd300d7807236ca0e44e 100644 (file)
@@ -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);
index 57372d4f0c7204c2497e332f19297d7083dc99f8..01b17d95ee43f57cce78900b38d3b48b9f4593fe 100644 (file)
@@ -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')
index 7ce2af3edaa6628c51dc742fa215ad611b5f96cf..15f971e558bc91ed6254c33872a5eee8c67b9a9a 100644 (file)
@@ -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());
index ea75cd9422dd51ad71ea6c1f027a8eb902a7eab3..7b8542ed2c83b2bdb382ea12b72c3f259e24703e 100644 (file)
@@ -25,7 +25,7 @@ type QueryHook<TData, TArgs extends any[]> = (...args: TArgs) => UseQueryResult<
 interface Props<TData, TArgs extends any[]> {
   query: QueryHook<TData, TArgs>;
   args?: TArgs;
-  children: (value: UseQueryResult<TData>) => ReactElement;
+  children: (value: UseQueryResult<TData>) => ReactElement | null;
 }
 
 export default function UseQuery<TData, TArgs extends any[]>(props: Props<TData, TArgs>) {
index 832aca63258caa53f9e8f2a68c1959c1ae821917..5a7a99f8e66b5ea645dd0bc22e29ba804ecc0b41 100644 (file)
@@ -127,7 +127,11 @@ class ChainDispatch extends ChainingQuery {
   }
 
   getAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
-    return this.elementQuery.getAll<T>(this.insideQuery.get(container));
+    const containers = this.insideQuery.getAll(container);
+    return containers.reduce(
+      (acc, item) => [...acc, ...(this.elementQuery.queryAll<T>(item) ?? [])],
+      []
+    );
   }
 
   query<T extends HTMLElement = HTMLElement>(container?: HTMLElement) {
index 45f9ceac607c719c3f915333476fd9b6a377cce8..3392c1cb4387e051869462d442045e1f057d4a8c 100644 (file)
@@ -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}"?