* 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';
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);
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';
/>
<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}
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', () => {
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
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', () => {
}
);
+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'),
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' }),
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());
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,
});
}
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);
'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();
'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(
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
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 },
}
);
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')
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')
* 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';
'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'),
});
},
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());
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());
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>) {
}
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) {
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}"?