]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21068 - Update permissions to adapt the new UI
authorKevin Silva <kevin.silva@sonarsource.com>
Thu, 30 Nov 2023 09:07:35 +0000 (10:07 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 1 Dec 2023 20:02:43 +0000 (20:02 +0000)
24 files changed:
server/sonar-web/design-system/src/components/Table.tsx
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/ListItem.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/Template.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/TemplateDetails.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/TemplateHeader.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/PermissionTemplatesApp-it.tsx
server/sonar-web/src/main/js/apps/permissions/global/components/PageHeader.tsx
server/sonar-web/src/main/js/apps/permissions/global/components/PermissionsGlobalApp.tsx
server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx
server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx
server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx
server/sonar-web/src/main/js/apps/permissions/project/components/PublicProjectDisclaimer.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/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx
server/sonar-web/src/main/js/components/permissions/AllHoldersList.tsx
server/sonar-web/src/main/js/components/permissions/GroupHolder.tsx
server/sonar-web/src/main/js/components/permissions/HoldersList.tsx
server/sonar-web/src/main/js/components/permissions/PermissionCell.tsx
server/sonar-web/src/main/js/components/permissions/PermissionHeader.tsx
server/sonar-web/src/main/js/components/permissions/SearchForm.tsx
server/sonar-web/src/main/js/components/permissions/UserHolder.tsx

index 332dd65230e1ec6eabeeabab8bee29904076bc6d..03c528826c3bf678c7452bbb2f3df506e6b27a8c 100644 (file)
@@ -86,6 +86,11 @@ export function Table(props: TableProps) {
   );
 }
 
+export const TableSeparator = styled.tr`
+  ${tw`sw-h-4`}
+  border-top: ${themeBorder('default')};
+`;
+
 export const TableRow = styled.tr`
   td,
   th {
@@ -182,10 +187,14 @@ export function CellComponent(props: CellComponentProps) {
   return <CellComponentStyled as={containerType} {...props} />;
 }
 
-export function ContentCell({ children, ...props }: CellComponentProps) {
+export function ContentCell({ children, className, ...props }: CellComponentProps) {
   return (
     <CellComponent {...props}>
-      <div className="sw-text-left sw-justify-start sw-flex sw-items-center">{children}</div>
+      <div
+        className={classNames('sw-text-left sw-justify-start sw-flex sw-items-center', className)}
+      >
+        {children}
+      </div>
     </CellComponent>
   );
 }
index 6c8010b6b7c101dfa901b0c470ab0d00c1a2de36..7fbe8586840819147b64ca2f341e66eff04fe14e 100644 (file)
@@ -67,6 +67,9 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE = [
   '/project/quality_profiles',
   '/project/webhooks',
   '/admin/webhooks',
+  '/project_roles',
+  '/admin/permissions',
+  '/admin/permission_templates',
 ];
 
 export default function GlobalContainer() {
index 85cc3137e6744398e47ae7907da5f278f9f6b3a8..7e45df12279b61d385e3aea9bf24247a5a3b82ff 100644 (file)
@@ -17,6 +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 { ActionsDropdown, ItemButton, ItemLink, PopupZLevel } from 'design-system';
 import { difference } from 'lodash';
 import * as React from 'react';
 import {
@@ -24,7 +25,6 @@ import {
   setDefaultPermissionTemplate,
   updatePermissionTemplate,
 } from '../../../api/permissions';
-import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
 import { Router, withRouter } from '../../../components/hoc/withRouter';
 import QualifierIcon from '../../../components/icons/QualifierIcon';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
@@ -121,14 +121,14 @@ class ActionsCell extends React.PureComponent<Props, State> {
 
   renderSetDefaultLink(qualifier: string, child: React.ReactNode) {
     return (
-      <ActionsDropdownItem
+      <ItemButton
         className="js-set-default"
         data-qualifier={qualifier}
         key={qualifier}
         onClick={this.setDefault(qualifier)}
       >
         {child}
-      </ActionsDropdownItem>
+      </ItemButton>
     );
   }
 
@@ -159,27 +159,32 @@ class ActionsCell extends React.PureComponent<Props, State> {
     return (
       <>
         <ActionsDropdown
-          label={translateWithParameters('permission_templates.show_actions_for_x', t.name)}
+          id={`permission-template-actions-${t.id}`}
+          zLevel={PopupZLevel.Global}
+          toggleClassName="it__permission-actions"
+          ariaLabel={translateWithParameters('permission_templates.show_actions_for_x', t.name)}
         >
-          {this.renderSetDefaultsControl()}
-
-          {!this.props.fromDetails && (
-            <ActionsDropdownItem
-              to={{ pathname: PERMISSION_TEMPLATES_PATH, search: queryToSearch({ id: t.id }) }}
-            >
-              {translate('edit_permissions')}
-            </ActionsDropdownItem>
-          )}
-
-          <ActionsDropdownItem className="js-update" onClick={this.handleUpdateClick}>
-            {translate('update_details')}
-          </ActionsDropdownItem>
-
-          {t.defaultFor.length === 0 && (
-            <ActionsDropdownItem className="js-delete" destructive onClick={this.handleDeleteClick}>
-              {translate('delete')}
-            </ActionsDropdownItem>
-          )}
+          <>
+            {this.renderSetDefaultsControl()}
+
+            {!this.props.fromDetails && (
+              <ItemLink
+                to={{ pathname: PERMISSION_TEMPLATES_PATH, search: queryToSearch({ id: t.id }) }}
+              >
+                {translate('edit_permissions')}
+              </ItemLink>
+            )}
+
+            <ItemButton className="js-update" onClick={this.handleUpdateClick}>
+              {translate('update_details')}
+            </ItemButton>
+
+            {t.defaultFor.length === 0 && (
+              <ItemButton className="js-delete" onClick={this.handleDeleteClick}>
+                {translate('delete')}
+              </ItemButton>
+            )}
+          </>
         </ActionsDropdown>
 
         {this.state.updateModal && (
index e8b450c68765a59cd62105bdc877c2642c5362fd..9f78b8a707f47aeffb6606f34360f9b6879103e0 100644 (file)
@@ -40,7 +40,7 @@ export default function ListItem(props: Props) {
 
       {permissions}
 
-      <td className="nowrap thin text-right text-top little-padded-left little-padded-right">
+      <td className="nowrap thin text-right little-padded-left little-padded-right">
         <ActionsCell
           permissionTemplate={props.template}
           refresh={props.refresh}
index 0632302090c212bacff0291267ec4971f770e9f5..51a49681e57d86f1be47d4993732a55eee9aa1eb 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 { FlagMessage, LargeCenteredLayout, PageContentFontWrapper } from 'design-system';
 import { without } from 'lodash';
 import * as React from 'react';
 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 UseQuery from '../../../helpers/UseQuery';
 import { translate } from '../../../helpers/l10n';
 import {
-  convertToPermissionDefinitions,
   PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE,
+  convertToPermissionDefinitions,
 } from '../../../helpers/permissions';
-import UseQuery from '../../../helpers/UseQuery';
 import { useGithubProvisioningEnabledQuery } from '../../../queries/identity-provider';
 import { Paging, PermissionGroup, PermissionTemplate, PermissionUser } from '../../../types/types';
 import TemplateDetails from './TemplateDetails';
@@ -321,48 +321,49 @@ export default class Template extends React.PureComponent<Props, State> {
     }
 
     return (
-      <div className="page page-limited">
-        <Helmet defer={false} title={template.name} />
-
-        <TemplateHeader
-          loading={loading}
-          refresh={this.props.refresh}
-          template={template}
-          topQualifiers={topQualifiers}
-        />
-        <main>
-          <TemplateDetails template={template} />
-          <UseQuery query={useGithubProvisioningEnabledQuery}>
-            {({ data: githubProvisioningStatus }) =>
-              githubProvisioningStatus ? (
-                <Alert variant="warning" className="sw-w-fit">
-                  {translate('permission_templates.github_warning')}
-                </Alert>
-              ) : null
-            }
-          </UseQuery>
-
-          <AllHoldersList
-            filter={filter}
-            onGrantPermissionToGroup={this.grantPermissionToGroup}
-            onGrantPermissionToUser={this.grantPermissionToUser}
-            groups={groups}
-            groupsPaging={groupsPaging}
-            loading={loading}
-            onFilter={this.handleFilter}
-            onLoadMore={this.onLoadMore}
-            onQuery={this.handleSearch}
-            query={query}
-            onRevokePermissionFromGroup={this.revokePermissionFromGroup}
-            onRevokePermissionFromUser={this.revokePermissionFromUser}
-            users={allUsers}
-            usersPaging={usersPagingWithCreator}
-            permissions={permissions}
-            selectedPermission={selectedPermission}
-            onSelectPermission={this.handleSelectPermission}
+      <LargeCenteredLayout id="permission-template">
+        <PageContentFontWrapper className="sw-my-8 sw-body-sm">
+          <Helmet defer={false} title={template.name} />
+
+          <TemplateHeader
+            refresh={this.props.refresh}
+            template={template}
+            topQualifiers={topQualifiers}
           />
-        </main>
-      </div>
+          <main>
+            <TemplateDetails template={template} />
+            <UseQuery query={useGithubProvisioningEnabledQuery}>
+              {({ data: githubProvisioningStatus }) =>
+                githubProvisioningStatus ? (
+                  <FlagMessage variant="warning" className="sw-w-fit">
+                    {translate('permission_templates.github_warning')}
+                  </FlagMessage>
+                ) : null
+              }
+            </UseQuery>
+
+            <AllHoldersList
+              filter={filter}
+              onGrantPermissionToGroup={this.grantPermissionToGroup}
+              onGrantPermissionToUser={this.grantPermissionToUser}
+              groups={groups}
+              groupsPaging={groupsPaging}
+              loading={loading}
+              onFilter={this.handleFilter}
+              onLoadMore={this.onLoadMore}
+              onQuery={this.handleSearch}
+              query={query}
+              onRevokePermissionFromGroup={this.revokePermissionFromGroup}
+              onRevokePermissionFromUser={this.revokePermissionFromUser}
+              users={allUsers}
+              usersPaging={usersPagingWithCreator}
+              permissions={permissions}
+              selectedPermission={selectedPermission}
+              onSelectPermission={this.handleSelectPermission}
+            />
+          </main>
+        </PageContentFontWrapper>
+      </LargeCenteredLayout>
     );
   }
 }
index a5dd0012f9e619fefbc4dacf2705c388fb4e439a..05e42b34a90ace34ba1de2444adf6f45efb6454e 100644 (file)
@@ -27,19 +27,19 @@ interface Props {
 
 export default function TemplateDetails({ template }: Props) {
   return (
-    <div className="big-spacer-bottom">
+    <div className="sw-mb-4">
       {template.defaultFor.length > 0 && (
-        <div className="spacer-top js-defaults">
+        <div className="sw-mt-2 js-defaults">
           <Defaults template={template} />
         </div>
       )}
 
       {!!template.description && (
-        <div className="spacer-top js-description">{template.description}</div>
+        <div className="sw-mt-2 js-description">{template.description}</div>
       )}
 
       {!!template.projectKeyPattern && (
-        <div className="spacer-top js-project-key-pattern">
+        <div className="sw-mt-2 js-project-key-pattern">
           Project Key Pattern: <code>{template.projectKeyPattern}</code>
         </div>
       )}
index 3625d0be7a519f3cc139958346069f8cc5044a16..1b76323f8a26d1e10d9ebbaedd97c722cfe0f4e4 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 { Link, Title } from 'design-system';
 import * as React from 'react';
-import Link from '../../../components/common/Link';
-import Spinner from '../../../components/ui/Spinner';
 import { translate } from '../../../helpers/l10n';
 import { PermissionTemplate } from '../../../types/types';
 import { PERMISSION_TEMPLATES_PATH } from '../utils';
 import ActionsCell from './ActionsCell';
 
 interface Props {
-  loading: boolean;
   refresh: () => void;
   template: PermissionTemplate;
   topQualifiers: string[];
 }
 
 export default function TemplateHeader(props: Props) {
-  const { template, loading } = props;
+  const { template } = props;
   return (
-    <header className="page-header" id="project-permissions-header">
-      <div className="note spacer-bottom">
-        <Link to={PERMISSION_TEMPLATES_PATH}>{translate('permission_templates.page')}</Link>
+    <header className="sw-mb-2 sw-flex sw-justify-between" id="project-permissions-header">
+      <div>
+        <div className="sw-mb-2">
+          <Link to={PERMISSION_TEMPLATES_PATH}>{translate('permission_templates.page')}</Link>
+        </div>
+        <div>
+          <Title>{template.name}</Title>
+        </div>
+        <div>{translate('global_permissions.page.description')}</div>
       </div>
-
-      <h1 className="page-title">{template.name}</h1>
-
-      <Spinner loading={loading} />
-
-      <div className="pull-right">
+      <div>
         <ActionsCell
           fromDetails
           permissionTemplate={template}
index 9d47cfd1169bc0c15de6c0569f53295bd068a883..d6f6c2d7fbd1f14ee684339761a1ba949dcf5aac 100644 (file)
@@ -284,11 +284,11 @@ describe('filtering', () => {
     await ui.openTemplateDetails('Permission Template 1');
     await ui.appLoaded();
 
-    expect(screen.getAllByRole('row').length).toBe(12);
+    expect(screen.getAllByRole('row').length).toBe(11);
     await ui.toggleFilterByPermission(Permissions.Admin);
     expect(screen.getAllByRole('row').length).toBe(3);
     await ui.toggleFilterByPermission(Permissions.Admin);
-    expect(screen.getAllByRole('row').length).toBe(12);
+    expect(screen.getAllByRole('row').length).toBe(11);
   });
 });
 
@@ -368,9 +368,9 @@ it('should correctly handle pagination', async () => {
   await ui.openTemplateDetails('Permission Template 1');
   await ui.appLoaded();
 
-  expect(screen.getAllByRole('row').length).toBe(14);
+  expect(screen.getAllByRole('row').length).toBe(13);
   await ui.clickLoadMore();
-  expect(screen.getAllByRole('row').length).toBe(24);
+  expect(screen.getAllByRole('row').length).toBe(23);
 });
 
 it.each([ComponentQualifier.Project, ComponentQualifier.Application, ComponentQualifier.Portfolio])(
@@ -412,21 +412,21 @@ function getPageObject(user: UserEvent) {
         name: `permission.assign_x_to_y.projects_role.${permission}.${target}`,
       }),
     tableHeaderFilter: (permission: Permissions) =>
-      byRole('link', { name: `projects_role.${permission}` }),
-    onlyUsersBtn: byRole('button', { name: 'users.page' }),
-    onlyGroupsBtn: byRole('button', { name: 'user_groups.page' }),
+      byRole('button', { name: `projects_role.${permission}` }),
+    onlyUsersBtn: byRole('radio', { name: 'users.page' }),
+    onlyGroupsBtn: byRole('radio', { name: 'user_groups.page' }),
     githubWarning: byText('permission_templates.github_warning'),
-    showAllBtn: byRole('button', { name: 'all' }),
+    showAllBtn: byRole('radio', { name: 'all' }),
     searchInput: byRole('searchbox', { name: 'search.search_for_users_or_groups' }),
     loadMoreBtn: byRole('button', { name: 'show_more' }),
     createNewTemplateBtn: byRole('button', { name: 'create' }),
     modal: byRole('dialog'),
     cogMenuBtn: (name: string) =>
       byRole('button', { name: `permission_templates.show_actions_for_x.${name}` }),
-    deleteBtn: byRole('button', { name: 'delete' }),
-    updateDetailsBtn: byRole('button', { name: 'update_details' }),
+    deleteBtn: byRole('menuitem', { name: 'delete' }),
+    updateDetailsBtn: byRole('menuitem', { name: 'update_details' }),
     setDefaultBtn: (qualifier: ComponentQualifier) =>
-      byRole('button', {
+      byRole('menuitem', {
         name:
           qualifier === ComponentQualifier.Project
             ? 'permission_templates.set_default'
index 41fe542f9640f94d46663c39f6b2f8bc9d6f9be5..f60fdac36d86fd4500500fe542e282d3a130d541 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 { Title } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../../../helpers/l10n';
 
-interface Props {
-  loading?: boolean;
-}
-
-export default class PageHeader extends React.PureComponent<Props> {
-  render() {
-    return (
-      <header className="page-header">
-        <h1 className="page-title">{translate('global_permissions.page')}</h1>
-
-        {this.props.loading && <i className="spinner" />}
-
-        <div className="page-description">{translate('global_permissions.page.description')}</div>
-      </header>
-    );
-  }
+export default function PageHeader() {
+  return (
+    <header className="sw-mb-2 sw-flex sw-flex-col sw-justify-between sw-mb-4">
+      <div>
+        <Title>{translate('global_permissions.page')}</Title>
+      </div>
+      <div>{translate('global_permissions.page.description')}</div>
+    </header>
+  );
 }
index b048597dda22c1823b485c4fd145a012908c373c..93be364f5fc47e744256177122c63e9064c42237 100644 (file)
@@ -17,6 +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 { LargeCenteredLayout, PageContentFontWrapper } from 'design-system';
 import { without } from 'lodash';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
@@ -29,9 +30,9 @@ import AllHoldersList from '../../../../components/permissions/AllHoldersList';
 import { FilterOption } from '../../../../components/permissions/SearchForm';
 import { translate } from '../../../../helpers/l10n';
 import {
+  PERMISSIONS_ORDER_GLOBAL,
   convertToPermissionDefinitions,
   filterPermissions,
-  PERMISSIONS_ORDER_GLOBAL,
 } from '../../../../helpers/permissions';
 import { ComponentQualifier } from '../../../../types/component';
 import { Paging, PermissionGroup, PermissionUser } from '../../../../types/types';
@@ -256,28 +257,30 @@ class PermissionsGlobalApp extends React.PureComponent<Props, State> {
       'global_permissions',
     );
     return (
-      <main className="page page-limited">
-        <Suggestions suggestions="global_permissions" />
-        <Helmet defer={false} title={translate('global_permissions.permission')} />
-        <PageHeader loading={loading} />
-        <AllHoldersList
-          permissions={permissions}
-          filter={filter}
-          onGrantPermissionToGroup={this.handleGrantPermissionToGroup}
-          onGrantPermissionToUser={this.handleGrantPermissionToUser}
-          groups={groups}
-          groupsPaging={groupsPaging}
-          loading={loading}
-          onFilter={this.handleFilter}
-          onLoadMore={this.handleLoadMore}
-          onQuery={this.handleSearch}
-          query={query}
-          onRevokePermissionFromGroup={this.handleRevokePermissionFromGroup}
-          onRevokePermissionFromUser={this.handleRevokePermissionFromUser}
-          users={users}
-          usersPaging={usersPaging}
-        />
-      </main>
+      <LargeCenteredLayout id="project-permissions-page">
+        <PageContentFontWrapper className="sw-my-8 sw-body-sm">
+          <Suggestions suggestions="global_permissions" />
+          <Helmet defer={false} title={translate('global_permissions.permission')} />
+          <PageHeader />
+          <AllHoldersList
+            permissions={permissions}
+            filter={filter}
+            onGrantPermissionToGroup={this.handleGrantPermissionToGroup}
+            onGrantPermissionToUser={this.handleGrantPermissionToUser}
+            groups={groups}
+            groupsPaging={groupsPaging}
+            loading={loading}
+            onFilter={this.handleFilter}
+            onLoadMore={this.handleLoadMore}
+            onQuery={this.handleSearch}
+            query={query}
+            onRevokePermissionFromGroup={this.handleRevokePermissionFromGroup}
+            onRevokePermissionFromUser={this.handleRevokePermissionFromUser}
+            users={users}
+            usersPaging={usersPaging}
+          />
+        </PageContentFontWrapper>
+      </LargeCenteredLayout>
     );
   }
 }
index 6a119bd9738ba61c4de0937f7a75107538887d0a..8fbaa59da61954c629421a007e48490fe074d072 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 {
+  ButtonPrimary,
+  FlagMessage,
+  FormField,
+  InputSelect,
+  LabelValueSelectOption,
+  Modal,
+} from 'design-system';
 import * as React from 'react';
 import { applyTemplateToProject, getPermissionTemplates } from '../../../../api/permissions';
-import Select from '../../../../components/controls/Select';
-import SimpleModal from '../../../../components/controls/SimpleModal';
-import { ResetButtonLink, SubmitButton } from '../../../../components/controls/buttons';
-import { Alert } from '../../../../components/ui/Alert';
-import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker';
 import MandatoryFieldsExplanation from '../../../../components/ui/MandatoryFieldsExplanation';
-import Spinner from '../../../../components/ui/Spinner';
 import { translate, translateWithParameters } from '../../../../helpers/l10n';
 import { PermissionTemplate } from '../../../../types/types';
 
@@ -70,8 +72,10 @@ export default class ApplyTemplate extends React.PureComponent<Props, State> {
     );
   };
 
-  handleSubmit = () => {
+  handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+    event.preventDefault();
     if (this.state.permissionTemplate) {
+      this.setState({ loading: true });
       return applyTemplateToProject({
         projectKey: this.props.project.key,
         templateId: this.state.permissionTemplate,
@@ -80,13 +84,13 @@ export default class ApplyTemplate extends React.PureComponent<Props, State> {
           if (this.props.onApply) {
             this.props.onApply();
           }
-          this.setState({ done: true });
+          this.setState({ done: true, loading: false });
         }
       });
     }
   };
 
-  handlePermissionTemplateChange = ({ value }: { value: string }) => {
+  handlePermissionTemplateChange = ({ value }: LabelValueSelectOption<string>) => {
     this.setState({ permissionTemplate: value });
   };
 
@@ -103,63 +107,59 @@ export default class ApplyTemplate extends React.PureComponent<Props, State> {
         }))
       : [];
 
+    const FORM_ID = 'project-permissions-apply-template-form';
+
     return (
-      <SimpleModal
-        header={header}
+      <Modal
+        isOverflowVisible
+        headerTitle={header}
         onClose={this.props.onClose}
-        onSubmit={this.handleSubmit}
-        size="small"
-      >
-        {({ onCloseClick, onFormSubmit, submitting }) => (
-          <form id="project-permissions-apply-template-form" onSubmit={onFormSubmit}>
-            <header className="modal-head">
-              <h2>{header}</h2>
-            </header>
-
-            <div className="modal-body">
+        primaryButton={
+          !this.state.done && (
+            <ButtonPrimary
+              disabled={this.state.loading || !this.state.permissionTemplate}
+              type="submit"
+              form={FORM_ID}
+            >
+              {translate('apply')}
+            </ButtonPrimary>
+          )
+        }
+        secondaryButtonLabel={translate(this.state.done ? 'close' : 'cancel')}
+        body={
+          <form id={FORM_ID} onSubmit={this.handleSubmit}>
+            <div>
               {this.state.done && (
-                <Alert variant="success">{translate('projects_role.apply_template.success')}</Alert>
+                <FlagMessage variant="success">
+                  {translate('projects_role.apply_template.success')}
+                </FlagMessage>
               )}
 
-              {this.state.loading && <i className="spinner" />}
-
               {!this.state.done && !this.state.loading && (
                 <>
-                  <MandatoryFieldsExplanation className="modal-field" />
-                  <div className="modal-field">
-                    <label htmlFor="project-permissions-template-input">
-                      {translate('template')}
-                      <MandatoryFieldMarker />
-                    </label>
+                  <MandatoryFieldsExplanation className="sw-mb-4" />
+                  <FormField
+                    label={translate('template')}
+                    required
+                    htmlFor="project-permissions-template-input"
+                  >
                     {this.state.permissionTemplates && (
-                      <Select
+                      <InputSelect
+                        size="full"
                         id="project-permissions-template"
-                        className="Select"
                         inputId="project-permissions-template-input"
                         onChange={this.handlePermissionTemplateChange}
                         options={options}
                         value={options.filter((o) => o.value === this.state.permissionTemplate)}
                       />
                     )}
-                  </div>
+                  </FormField>
                 </>
               )}
             </div>
-
-            <footer className="modal-foot">
-              <Spinner className="spacer-right" loading={submitting} />
-              {!this.state.done && (
-                <SubmitButton disabled={submitting || !this.state.permissionTemplate}>
-                  {translate('apply')}
-                </SubmitButton>
-              )}
-              <ResetButtonLink onClick={onCloseClick}>
-                {translate(this.state.done ? 'close' : 'cancel')}
-              </ResetButtonLink>
-            </footer>
           </form>
-        )}
-      </SimpleModal>
+        }
+      />
     );
   }
 }
index d6e3ecee691a9076c650561ab99e565a55e0419b..6169fef0977778010acd13e6d7c51dd496c3b064 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 { ButtonPrimary, FlagMessage, Title } from 'design-system';
 import * as React from 'react';
 import GitHubSynchronisationWarning from '../../../../app/components/GitHubSynchronisationWarning';
-import { Button } from '../../../../components/controls/buttons';
-import { Alert } from '../../../../components/ui/Alert';
-import Spinner from '../../../../components/ui/Spinner';
 import { translate } from '../../../../helpers/l10n';
 import { getBaseUrl } from '../../../../helpers/system';
 import { useGithubProvisioningEnabledQuery } from '../../../../queries/identity-provider';
@@ -33,14 +31,13 @@ interface Props {
   component: Component;
   isGitHubProject?: boolean;
   loadHolders: () => void;
-  loading: boolean;
 }
 
 export default function PageHeader(props: Props) {
   const [applyTemplateModal, setApplyTemplateModal] = React.useState(false);
   const { data: githubProvisioningStatus } = useGithubProvisioningEnabledQuery();
 
-  const { component, isGitHubProject, loading } = props;
+  const { component, isGitHubProject } = props;
   const { configuration } = component;
   const provisionedByGitHub = isGitHubProject && !!githubProvisioningStatus;
   const canApplyPermissionTemplate =
@@ -67,27 +64,44 @@ export default function PageHeader(props: Props) {
       : undefined;
 
   return (
-    <header className="page-header">
-      <h1 className="page-title">
-        {translate('permissions.page')}
-        {provisionedByGitHub && (
-          <img
-            alt="github"
-            className="spacer-left spacer-right"
-            aria-label={translate('project_permission.github_managed')}
-            height={16}
-            src={`${getBaseUrl()}/images/alm/github.svg`}
-          />
-        )}
-      </h1>
-
-      <Spinner className="spacer-left" loading={loading} />
+    <header className="sw-mb-2 sw-flex sw-items-center sw-justify-between">
+      <div>
+        <Title>
+          {translate('permissions.page')}
+          {provisionedByGitHub && (
+            <img
+              alt="github"
+              className="sw-mx-2"
+              aria-label={translate('project_permission.github_managed')}
+              height={16}
+              src={`${getBaseUrl()}/images/alm/github.svg`}
+            />
+          )}
+        </Title>
 
+        <div>
+          <p>{description}</p>
+          {visibilityDescription && <p>{visibilityDescription}</p>}
+          {provisionedByGitHub && (
+            <>
+              <p>{translate('roles.page.description.github')}</p>
+              <div className="sw-mt-2">
+                <GitHubSynchronisationWarning short />
+              </div>
+            </>
+          )}
+          {githubProvisioningStatus && !isGitHubProject && (
+            <FlagMessage variant="warning" className="sw-mt-2">
+              {translate('project_permission.local_project_with_github_provisioning')}
+            </FlagMessage>
+          )}
+        </div>
+      </div>
       {canApplyPermissionTemplate && (
-        <div className="page-actions">
-          <Button className="js-apply-template" onClick={handleApplyTemplate}>
+        <div>
+          <ButtonPrimary className="js-apply-template" onClick={handleApplyTemplate}>
             {translate('projects_role.apply_template')}
-          </Button>
+          </ButtonPrimary>
 
           {applyTemplateModal && (
             <ApplyTemplate
@@ -98,24 +112,6 @@ export default function PageHeader(props: Props) {
           )}
         </div>
       )}
-
-      <div className="page-description">
-        <p>{description}</p>
-        {visibilityDescription && <p>{visibilityDescription}</p>}
-        {provisionedByGitHub && (
-          <>
-            <p>{translate('roles.page.description.github')}</p>
-            <div className="sw-mt-2">
-              <GitHubSynchronisationWarning short />
-            </div>
-          </>
-        )}
-        {githubProvisioningStatus && !isGitHubProject && (
-          <Alert variant="warning" className="sw-mt-2">
-            {translate('project_permission.local_project_with_github_provisioning')}
-          </Alert>
-        )}
-      </div>
     </header>
   );
 }
index fe2daf84bcf386391512e51188927dc1033f35a1..a93520936053bad639ac1b851ced6474481bbbe3 100644 (file)
@@ -17,6 +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 { LargeCenteredLayout, PageContentFontWrapper } from 'design-system';
 import { noop, without } from 'lodash';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
@@ -344,63 +345,65 @@ class PermissionsProjectApp extends React.PureComponent<Props, State> {
     const permissions = convertToPermissionDefinitions(order, 'projects_role');
 
     return (
-      <main className="page page-limited" id="project-permissions-page">
-        <Helmet defer={false} title={translate('permissions.page')} />
-
-        <UseQuery query={useIsGitHubProjectQuery} args={[component.key]}>
-          {({ data: isGitHubProject }) => (
-            <>
-              <PageHeader
-                component={component}
-                isGitHubProject={isGitHubProject}
-                loadHolders={this.loadHolders}
-                loading={loading}
-              />
-              <div>
-                <UseQuery query={useGithubProvisioningEnabledQuery}>
-                  {({ data: githubProvisioningStatus, isFetching }) => (
-                    <VisibilitySelector
-                      canTurnToPrivate={canTurnToPrivate}
-                      className="sw-flex big-spacer-top big-spacer-bottom"
-                      onChange={this.handleVisibilityChange}
-                      loading={loading || isFetching}
-                      disabled={isGitHubProject && !!githubProvisioningStatus}
-                      visibility={component.visibility}
+      <LargeCenteredLayout id="project-permissions-page">
+        <PageContentFontWrapper className="sw-my-8 sw-body-sm">
+          <Helmet defer={false} title={translate('permissions.page')} />
+
+          <UseQuery query={useIsGitHubProjectQuery} args={[component.key]}>
+            {({ data: isGitHubProject }) => (
+              <>
+                <PageHeader
+                  component={component}
+                  isGitHubProject={isGitHubProject}
+                  loadHolders={this.loadHolders}
+                />
+                <div>
+                  <UseQuery query={useGithubProvisioningEnabledQuery}>
+                    {({ data: githubProvisioningStatus, isFetching }) => (
+                      <VisibilitySelector
+                        canTurnToPrivate={canTurnToPrivate}
+                        className="sw-flex sw-my-4"
+                        onChange={this.handleVisibilityChange}
+                        loading={loading || isFetching}
+                        disabled={isGitHubProject && !!githubProvisioningStatus}
+                        visibility={component.visibility}
+                      />
+                    )}
+                  </UseQuery>
+
+                  {disclaimer && (
+                    <PublicProjectDisclaimer
+                      component={component}
+                      onClose={this.handleCloseDisclaimer}
+                      onConfirm={this.handleTurnProjectToPublic}
                     />
                   )}
-                </UseQuery>
-
-                {disclaimer && (
-                  <PublicProjectDisclaimer
-                    component={component}
-                    onClose={this.handleCloseDisclaimer}
-                    onConfirm={this.handleTurnProjectToPublic}
-                  />
-                )}
-              </div>
-            </>
-          )}
-        </UseQuery>
-
-        <AllHoldersList
-          filter={filter}
-          onGrantPermissionToGroup={this.handleGrantPermissionToGroup}
-          onGrantPermissionToUser={this.handleGrantPermissionToUser}
-          groups={groups}
-          groupsPaging={groupsPaging}
-          onFilter={this.handleFilterChange}
-          onLoadMore={this.handleLoadMore}
-          onSelectPermission={this.handlePermissionSelect}
-          onQuery={this.handleQueryChange}
-          query={query}
-          onRevokePermissionFromGroup={this.handleRevokePermissionFromGroup}
-          onRevokePermissionFromUser={this.handleRevokePermissionFromUser}
-          selectedPermission={selectedPermission}
-          users={users}
-          usersPaging={usersPaging}
-          permissions={permissions}
-        />
-      </main>
+                </div>
+              </>
+            )}
+          </UseQuery>
+
+          <AllHoldersList
+            loading={loading}
+            filter={filter}
+            onGrantPermissionToGroup={this.handleGrantPermissionToGroup}
+            onGrantPermissionToUser={this.handleGrantPermissionToUser}
+            groups={groups}
+            groupsPaging={groupsPaging}
+            onFilter={this.handleFilterChange}
+            onLoadMore={this.handleLoadMore}
+            onSelectPermission={this.handlePermissionSelect}
+            onQuery={this.handleQueryChange}
+            query={query}
+            onRevokePermissionFromGroup={this.handleRevokePermissionFromGroup}
+            onRevokePermissionFromUser={this.handleRevokePermissionFromUser}
+            selectedPermission={selectedPermission}
+            users={users}
+            usersPaging={usersPaging}
+            permissions={permissions}
+          />
+        </PageContentFontWrapper>
+      </LargeCenteredLayout>
     );
   }
 }
index 455eee1f779d1a81d893f95e496aaceddd3452eb..b7de3743f3311382aaf6a23060415dd166dbd569 100644 (file)
@@ -17,9 +17,9 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { FlagMessage } from 'design-system/lib';
 import * as React from 'react';
 import ConfirmModal from '../../../../components/controls/ConfirmModal';
-import { Alert } from '../../../../components/ui/Alert';
 import { translate, translateWithParameters } from '../../../../helpers/l10n';
 
 interface Props {
@@ -40,9 +40,9 @@ export default function PublicProjectDisclaimer({ component, onClose, onConfirm
       onClose={onClose}
       onConfirm={onConfirm}
     >
-      <Alert variant="warning">
+      <FlagMessage className="sw-mb-4" variant="warning">
         {translate('projects_role.are_you_sure_to_turn_project_to_public.warning', qualifier)}
-      </Alert>
+      </FlagMessage>
       <p>{translate('projects_role.are_you_sure_to_turn_project_to_public', qualifier)}</p>
     </ConfirmModal>
   );
index e3c2cbfceebbff4e10c2b31b4f28033ae3c7457d..5b95545450d4a3e2eec8d130ff2b655a6ec6fef4 100644 (file)
@@ -120,11 +120,11 @@ describe('filtering', () => {
     renderPermissionsProjectApp();
     await ui.appLoaded();
 
-    expect(screen.getAllByRole('row').length).toBe(11);
+    expect(screen.getAllByRole('row').length).toBe(10);
     await ui.toggleFilterByPermission(Permissions.Admin);
     expect(screen.getAllByRole('row').length).toBe(3);
     await ui.toggleFilterByPermission(Permissions.Admin);
-    expect(screen.getAllByRole('row').length).toBe(11);
+    expect(screen.getAllByRole('row').length).toBe(10);
   });
 });
 
@@ -330,15 +330,9 @@ it('should have disabled permissions for GH Project', async () => {
   expect(ui.githubExplanations.get()).toBeInTheDocument();
 
   expect(ui.projectPermissionCheckbox('John', Permissions.Admin).get()).toBeChecked();
-  expect(ui.projectPermissionCheckbox('John', Permissions.Admin).get()).toHaveAttribute(
-    'aria-disabled',
-    'true',
-  );
+  expect(ui.projectPermissionCheckbox('John', Permissions.Admin).get()).toBeDisabled();
   expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).toBeChecked();
-  expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).toHaveAttribute(
-    'aria-disabled',
-    'false',
-  );
+  expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).toBeEnabled();
   await ui.toggleProjectPermission('Alexa', Permissions.IssueAdmin);
   expect(ui.confirmRemovePermissionDialog.get()).toBeInTheDocument();
   expect(ui.confirmRemovePermissionDialog.get()).toHaveTextContent(
@@ -350,10 +344,7 @@ it('should have disabled permissions for GH Project', async () => {
   expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).not.toBeChecked();
 
   expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).toBeChecked();
-  expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).toHaveAttribute(
-    'aria-disabled',
-    'false',
-  );
+  expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).toBeEnabled();
   await ui.toggleProjectPermission('sonar-users', Permissions.Browse);
   expect(ui.confirmRemovePermissionDialog.get()).toBeInTheDocument();
   expect(ui.confirmRemovePermissionDialog.get()).toHaveTextContent(
@@ -365,8 +356,7 @@ it('should have disabled permissions for GH Project', async () => {
   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(
-    'aria-disabled',
-    'true',
+    'disabled',
   );
 
   const johnRow = screen.getAllByRole('row')[4];
@@ -388,7 +378,7 @@ it('should have disabled permissions for GH Project', async () => {
   expect(
     screen
       .getAllByRole('checkbox', { checked: false })
-      .every((item) => item.getAttribute('aria-disabled') === 'true'),
+      .every((item) => item.getAttributeNames().includes('disabled')),
   ).toBe(true);
 });
 
@@ -415,8 +405,8 @@ it('should allow to change permissions for GH Project without auto-provisioning'
 
   // no restrictions
   expect(
-    screen.getAllByRole('checkbox').every((item) => item.getAttribute('aria-disabled') !== 'true'),
-  ).toBe(true);
+    screen.getAllByRole('checkbox').every((item) => item.getAttributeNames().includes('disabled')),
+  ).toBe(false);
 });
 
 it('should allow to change permissions for non-GH Project', async () => {
@@ -434,8 +424,8 @@ it('should allow to change permissions for non-GH Project', async () => {
 
   // no restrictions
   expect(
-    screen.getAllByRole('checkbox').every((item) => item.getAttribute('aria-disabled') !== 'true'),
-  ).toBe(true);
+    screen.getAllByRole('checkbox').every((item) => item.getAttributeNames().includes('disabled')),
+  ).toBe(false);
 });
 
 function renderPermissionsProjectApp(
index 5d010ae94494073609e422b90809e95b813ae708..7de9adca0dd1ca510dcbfe2f6911a1bd4894b376 100644 (file)
@@ -56,10 +56,10 @@ export function getPageObject(user: UserEvent) {
     templateSuccessfullyApplied: byText('projects_role.apply_template.success'),
     confirmApplyTemplateBtn: byRole('button', { name: 'apply' }),
     tableHeaderFilter: (permission: Permissions) =>
-      byRole('link', { name: `projects_role.${permission}` }),
-    onlyUsersBtn: byRole('button', { name: 'users.page' }),
-    onlyGroupsBtn: byRole('button', { name: 'user_groups.page' }),
-    showAllBtn: byRole('button', { name: 'all' }),
+      byRole('button', { name: `projects_role.${permission}` }),
+    onlyUsersBtn: byRole('radio', { name: 'users.page' }),
+    onlyGroupsBtn: byRole('radio', { name: 'user_groups.page' }),
+    showAllBtn: byRole('radio', { name: 'all' }),
     searchInput: byRole('searchbox', { name: 'search.search_for_users_or_groups' }),
     loadMoreBtn: byRole('button', { name: 'show_more' }),
   };
index 5ba6d5fb62a491ad1b20316afbcccc73b2c6e645..ab5cae86f46a0fc8a8757749eee9bff1c81e9d36 100644 (file)
@@ -131,7 +131,7 @@ const ui = {
   applyTemplateDialog: byRole('dialog', {
     name: 'projects_role.apply_template_to_x.Project 1',
   }),
-  selectTemplate: byRole('combobox', { name: 'template field_required' }),
+  selectTemplate: (required: string) => byRole('combobox', { name: `template ${required}` }),
 
   deleteDialog: byRole('dialog', { name: 'qualifiers.delete.TRK' }),
 
@@ -283,7 +283,7 @@ describe('Bulk permission templates', () => {
         .get(),
     ).toBeInTheDocument();
     await selectEvent.select(
-      ui.bulkApplyDialog.by(ui.selectTemplate).get(),
+      ui.bulkApplyDialog.by(ui.selectTemplate('field_required')).get(),
       'Permission Template 2',
     );
     await user.click(ui.bulkApplyDialog.by(ui.apply).get());
@@ -456,7 +456,7 @@ it('should apply template for single object', async () => {
 
   expect(ui.applyTemplateDialog.get()).toBeInTheDocument();
   await selectEvent.select(
-    ui.applyTemplateDialog.by(ui.selectTemplate).get(),
+    ui.applyTemplateDialog.by(ui.selectTemplate('required')).get(),
     'Permission Template 2',
   );
   await user.click(ui.applyTemplateDialog.by(ui.apply).get());
index 0d60b2fa438ff23736990c1bfd6159be43b59463..8dc1cb0daab90f323c935e50eb433361576537e1 100644 (file)
@@ -17,6 +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 { BasicSeparator, Spinner } from 'design-system';
 import * as React from 'react';
 import {
   Paging,
@@ -87,11 +88,31 @@ export default class AllHoldersList extends React.PureComponent<Props> {
   };
 
   render() {
-    const { filter, query, groups, users, permissions, selectedPermission, loading } = this.props;
+    const {
+      filter,
+      query,
+      groups,
+      users,
+      permissions,
+      selectedPermission,
+      loading = false,
+    } = this.props;
     const { count, total } = this.getPaging();
 
     return (
       <>
+        <div>
+          <div className="sw-flex sw-justify-between">
+            <SearchForm
+              filter={filter}
+              onFilter={this.props.onFilter}
+              onSearch={this.props.onQuery}
+              query={query}
+            />
+            <Spinner loading={loading} />
+          </div>
+          <BasicSeparator className="sw-mt-4" />
+        </div>
         <HoldersList
           loading={loading}
           filter={filter}
@@ -103,15 +124,8 @@ export default class AllHoldersList extends React.PureComponent<Props> {
           query={query}
           selectedPermission={selectedPermission}
           users={users}
-        >
-          <SearchForm
-            filter={filter}
-            onFilter={this.props.onFilter}
-            onSearch={this.props.onQuery}
-            query={query}
-          />
-        </HoldersList>
-        <ListFooter count={count} loadMore={this.props.onLoadMore} total={total} />
+        />
+        <ListFooter count={count} loadMore={this.props.onLoadMore} total={total} useMIUIButtons />
       </>
     );
   }
index 512eaebb5c3822f7b47953d649bd2654795d3a0c..f8d07b0ae409b2a9e5f9dedf249509170764bb72 100644 (file)
@@ -17,6 +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 { Badge, ContentCell, TableRowInteractive } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../helpers/l10n';
 import { isPermissionDefinitionGroup } from '../../helpers/permissions';
@@ -57,38 +58,38 @@ export default function GroupHolder(props: Props) {
     removeOnly,
   });
 
+  const description =
+    group.name === ANYONE ? translate('user_groups.anyone.description') : group.description;
+
   return (
-    <tr>
-      <td className="nowrap text-middle">
-        <div className="display-flex-center">
-          <GroupIcon className="big-spacer-right" />
-          <div className="max-width-100">
+    <TableRowInteractive>
+      <ContentCell>
+        <div className="sw-flex sw-items-center">
+          <GroupIcon className="sw-mr-4" />
+          <div className="sw-max-w-abs-800">
             <div className="sw-flex sw-w-fit sw-max-w-full">
-              <div className="sw-flex-1 text-ellipsis">
+              <div className="sw-flex-1 sw-text-ellipsis sw-whitespace-nowrap sw-overflow-hidden  sw-min-w-0">
                 <strong>{group.name}</strong>
               </div>
               {isGitHubProject && group.managed && (
                 <img
                   alt="github"
-                  className="spacer-left spacer-right"
+                  className="sw-my-2"
                   aria-label={translate('project_permission.github_managed')}
                   height={16}
                   src={`${getBaseUrl()}/images/alm/github.svg`}
                 />
               )}
               {group.name === ANYONE && (
-                <span className="spacer-left badge badge-error">{translate('deprecated')}</span>
+                <Badge className="sw-ml-2" variant="deleted">
+                  {translate('deprecated')}
+                </Badge>
               )}
             </div>
-
-            <div className="little-spacer-top" style={{ whiteSpace: 'normal' }}>
-              {group.name === ANYONE
-                ? translate('user_groups.anyone.description')
-                : group.description}
-            </div>
+            {description && <div className="sw-mt-2 sw-whitespace-normal">{description}</div>}
           </div>
         </div>
-      </td>
+      </ContentCell>
       {permissions.map((permission) => {
         const isPermissionGroup = isPermissionDefinitionGroup(permission);
         const permissionKey = isPermissionGroup ? permission.category : permission.key;
@@ -105,11 +106,12 @@ export default function GroupHolder(props: Props) {
             onCheck={handleCheck}
             permission={permission}
             permissionItem={group}
+            prefixID={group.name}
             selectedPermission={selectedPermission}
           />
         );
       })}
       {modal}
-    </tr>
+    </TableRowInteractive>
   );
 }
index fbf92f79843c97c05fe66f25e96cd5c310c34b33..da403626660cdf5b41eb72d32b57440e05592893 100644 (file)
@@ -17,6 +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 { ContentCell, Table, TableRow, TableSeparator } from 'design-system';
 import { partition } from 'lodash';
 import * as React from 'react';
 import UseQuery from '../../helpers/UseQuery';
@@ -146,45 +147,44 @@ export default class HoldersList extends React.PureComponent<
   }
 
   render() {
-    const { permissions, users, groups, loading, children, selectedPermission } = this.props;
+    const { permissions, users, groups, loading, selectedPermission } = this.props;
     const items = [...groups, ...users];
     const [itemWithPermissions, itemWithoutPermissions] = partition(items, (item) =>
       this.getItemInitialPermissionsCount(item),
     );
 
+    const HEADER_COLUMNS = permissions.length + 1;
+
+    const tableHeader = (
+      <TableRow>
+        <ContentCell />
+        {permissions.map((permission) => (
+          <PermissionHeader
+            key={isPermissionDefinitionGroup(permission) ? permission.category : permission.key}
+            onSelectPermission={this.props.onSelectPermission}
+            permission={permission}
+            selectedPermission={selectedPermission}
+          />
+        ))}
+      </TableRow>
+    );
+
     return (
-      <div className="boxed-group boxed-group-inner">
-        <table className="data zebra permissions-table">
-          <thead>
-            <tr>
-              <td className="nowrap bordered-bottom">{children}</td>
-              {permissions.map((permission) => (
-                <PermissionHeader
-                  key={
-                    isPermissionDefinitionGroup(permission) ? permission.category : permission.key
-                  }
-                  onSelectPermission={this.props.onSelectPermission}
-                  permission={permission}
-                  selectedPermission={selectedPermission}
-                />
-              ))}
-            </tr>
-          </thead>
-          <tbody>
-            {items.length === 0 && !loading && this.renderEmpty()}
-            {itemWithPermissions.map((item) => this.renderItem(item, permissions))}
-            {itemWithPermissions.length > 0 && itemWithoutPermissions.length > 0 && (
-              <>
-                <tr>
-                  <td className="divider" colSpan={20} />
-                </tr>
-                <tr />
-                {/* Keep correct zebra colors in the table */}
-              </>
-            )}
-            {itemWithoutPermissions.map((item) => this.renderItem(item, permissions))}
-          </tbody>
-        </table>
+      <div>
+        <Table
+          columnWidths={[500, ...permissions.map(() => 100)]}
+          className="it__permission-list"
+          noHeaderTopBorder
+          columnCount={HEADER_COLUMNS}
+          header={tableHeader}
+        >
+          {items.length === 0 && !loading && this.renderEmpty()}
+          {itemWithPermissions.map((item) => this.renderItem(item, permissions))}
+          {itemWithPermissions.length > 0 && itemWithoutPermissions.length > 0 && (
+            <TableSeparator />
+          )}
+          {itemWithoutPermissions.map((item) => this.renderItem(item, permissions))}
+        </Table>
       </div>
     );
   }
index 06b0ac10018dac4f721087155cad20cc12874604..c9f721c74d915a472453a7c4e801d3811d1e5ae2 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import classNames from 'classnames';
+import { Checkbox, CheckboxCell } from 'design-system';
 import * as React from 'react';
 import { translateWithParameters } from '../../helpers/l10n';
 import { isPermissionDefinitionGroup } from '../../helpers/permissions';
@@ -27,7 +28,6 @@ import {
   PermissionGroup,
   PermissionUser,
 } from '../../types/types';
-import Checkbox from '../controls/Checkbox';
 
 export interface PermissionCellProps {
   disabled?: boolean;
@@ -37,38 +37,50 @@ export interface PermissionCellProps {
   permission: PermissionDefinition | PermissionDefinitionGroup;
   permissionItem: PermissionGroup | PermissionUser;
   selectedPermission?: string;
+  prefixID: string;
 }
 
 export default function PermissionCell(props: PermissionCellProps) {
-  const { disabled, loading, onCheck, permission, permissionItem, selectedPermission, removeOnly } =
-    props;
+  const {
+    disabled,
+    loading,
+    onCheck,
+    permission,
+    permissionItem,
+    selectedPermission,
+    removeOnly,
+    prefixID,
+  } = props;
 
   if (isPermissionDefinitionGroup(permission)) {
     return (
-      <td className="text-middle">
-        {permission.permissions.map((permissionDefinition) => {
-          const isChecked = permissionItem.permissions.includes(permissionDefinition.key);
-          const isDisabled = disabled || loading.includes(permissionDefinition.key);
+      <CheckboxCell>
+        <div className="sw-flex sw-flex-col sw-text-left">
+          {permission.permissions.map((permissionDefinition) => {
+            const isChecked = permissionItem.permissions.includes(permissionDefinition.key);
+            const isDisabled = disabled || loading.includes(permissionDefinition.key);
 
-          return (
-            <div key={permissionDefinition.key}>
-              <Checkbox
-                checked={isChecked}
-                disabled={isDisabled || (!isChecked && removeOnly)}
-                id={permissionDefinition.key}
-                label={translateWithParameters(
-                  'permission.assign_x_to_y',
-                  permissionDefinition.name,
-                  permissionItem.name,
-                )}
-                onCheck={onCheck}
-              >
-                <span className="little-spacer-left">{permissionDefinition.name}</span>
-              </Checkbox>
-            </div>
-          );
-        })}
-      </td>
+            return (
+              <div key={permissionDefinition.key}>
+                <Checkbox
+                  aria-disabled={isDisabled || (!isChecked && removeOnly)}
+                  checked={isChecked}
+                  disabled={isDisabled || (!isChecked && removeOnly)}
+                  id={`${permissionDefinition.key}`}
+                  label={translateWithParameters(
+                    'permission.assign_x_to_y',
+                    permissionDefinition.name,
+                    permissionItem.name,
+                  )}
+                  onCheck={() => onCheck(isChecked, permissionDefinition.key)}
+                >
+                  <span className="sw-ml-2">{permissionDefinition.name}</span>
+                </Checkbox>
+              </div>
+            );
+          })}
+        </div>
+      </CheckboxCell>
     );
   }
 
@@ -76,22 +88,23 @@ export default function PermissionCell(props: PermissionCellProps) {
   const isDisabled = disabled || loading.includes(permission.key);
 
   return (
-    <td
-      className={classNames('permission-column text-center text-middle', {
+    <CheckboxCell
+      className={classNames({
         selected: permission.key === selectedPermission,
       })}
     >
       <Checkbox
+        aria-disabled={isDisabled || (!isChecked && removeOnly)}
         checked={isChecked}
         disabled={isDisabled || (!isChecked && removeOnly)}
-        id={permission.key}
+        id={`${prefixID}-${permission.key}`}
         label={translateWithParameters(
           'permission.assign_x_to_y',
           permission.name,
           permissionItem.name,
         )}
-        onCheck={onCheck}
+        onCheck={() => onCheck(isChecked, permission.key)}
       />
-    </td>
+    </CheckboxCell>
   );
 }
index f7852acd0385fc7cc609167fc5e74e1c13faaaa6..ca73b2251e0009d6df6b367aa94afde794949b28 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import classNames from 'classnames';
+import { BareButton, ContentCell, HelperHintIcon } from 'design-system';
 import * as React from 'react';
 import { translate, translateWithParameters } from '../../helpers/l10n';
 import { isPermissionDefinitionGroup } from '../../helpers/permissions';
 import { PermissionDefinition, PermissionDefinitionGroup } from '../../types/types';
 import InstanceMessage from '../common/InstanceMessage';
+import ClickEventBoundary from '../controls/ClickEventBoundary';
 import HelpTooltip from '../controls/HelpTooltip';
 import Tooltip from '../controls/Tooltip';
 
@@ -33,9 +35,7 @@ interface Props {
 }
 
 export default class PermissionHeader extends React.PureComponent<Props> {
-  handlePermissionClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
-    event.preventDefault();
-    event.currentTarget.blur();
+  handlePermissionClick = () => {
     const { permission } = this.props;
     if (this.props.onSelectPermission && !isPermissionDefinitionGroup(permission)) {
       this.props.onSelectPermission(permission.key);
@@ -65,34 +65,39 @@ export default class PermissionHeader extends React.PureComponent<Props> {
       name = translate('global_permissions', permission.category);
     } else {
       name = onSelectPermission ? (
-        <Tooltip
-          overlay={translateWithParameters(
-            'global_permissions.filter_by_x_permission',
-            permission.name,
-          )}
-        >
-          <a href="#" onClick={this.handlePermissionClick}>
-            {permission.name}
-          </a>
-        </Tooltip>
+        <ClickEventBoundary>
+          <BareButton onClick={this.handlePermissionClick}>
+            <Tooltip
+              overlay={translateWithParameters(
+                'global_permissions.filter_by_x_permission',
+                permission.name,
+              )}
+            >
+              <span>{permission.name}</span>
+            </Tooltip>
+          </BareButton>
+        </ClickEventBoundary>
       ) : (
         permission.name
       );
     }
     return (
-      <th
+      <ContentCell
         scope="col"
-        className={classNames('permission-column text-center text-middle', {
+        className={classNames('sw-justify-center', {
           selected:
             !isPermissionDefinitionGroup(permission) &&
             permission.key === this.props.selectedPermission,
         })}
       >
-        <div className="permission-column-inner">
-          {name}
-          <HelpTooltip className="spacer-left" overlay={this.getTooltipOverlay()} />
+        <div className="sw-flex sw-content-center">
+          <div className="sw-grow-1 sw-text-center">{name}</div>
+
+          <HelpTooltip className="sw-ml-2" overlay={this.getTooltipOverlay()}>
+            <HelperHintIcon aria-label="help-tooltip" />
+          </HelpTooltip>
         </div>
-      </th>
+      </ContentCell>
     );
   }
 }
index 3c5ef9e346b88d21ba7a4696d3d70a80c538a2a5..606d925b3204c7ba9499299ea9b45c7eea7bb564 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 { InputSearch, ToggleButton } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../helpers/l10n';
-import ButtonToggle from '../controls/ButtonToggle';
-import SearchBox from '../controls/SearchBox';
 
 export type FilterOption = 'all' | 'users' | 'groups';
 interface Props {
@@ -38,11 +37,11 @@ export default function SearchForm(props: Props) {
   ];
 
   return (
-    <div className="display-flex-row">
-      <ButtonToggle onCheck={props.onFilter} options={filterOptions} value={props.filter} />
+    <div className="sw-flex sw-flex-row">
+      <ToggleButton onChange={props.onFilter} options={filterOptions} value={props.filter} />
 
-      <div className="flex-1 spacer-left">
-        <SearchBox
+      <div className="sw-flex-1 sw-ml-2">
+        <InputSearch
           minLength={3}
           onChange={props.onSearch}
           placeholder={translate('search.search_for_users_or_groups')}
index 9ab9ae0ee0edcd4336394f78286179c53cbfa688..0b11501bd10c4bec10f9b54fef7f690e882d4cba 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 { Avatar, ContentCell, Note, TableRowInteractive } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../helpers/l10n';
 import { isPermissionDefinitionGroup } from '../../helpers/permissions';
 import { getBaseUrl } from '../../helpers/system';
 import { PermissionDefinitions, PermissionUser } from '../../types/types';
-import LegacyAvatar from '../ui/LegacyAvatar';
 import PermissionCell from './PermissionCell';
 import usePermissionChange from './usePermissionChange';
 
@@ -54,59 +54,62 @@ export default function UserHolder(props: Props) {
       disabled={disabled}
       removeOnly={removeOnly}
       permissionItem={user}
+      prefixID={user.login}
       selectedPermission={selectedPermission}
     />
   ));
 
   if (user.login === '<creator>') {
     return (
-      <tr>
-        <td className="nowrap text-middle">
-          <div>
-            <strong>{user.name}</strong>
-          </div>
-          <div className="little-spacer-top" style={{ whiteSpace: 'normal' }}>
-            {translate('permission_templates.project_creators.explanation')}
+      <TableRowInteractive>
+        <ContentCell>
+          <div className="sw-max-w-abs-800">
+            <div className="sw-flex sw-flex-col sw-w-fit sw-max-w-full">
+              <strong className="sw-text-ellipsis sw-whitespace-nowrap sw-overflow-hidden">
+                {user.name}
+              </strong>
+              <p className="sw-mt-2">
+                {translate('permission_templates.project_creators.explanation')}
+              </p>
+            </div>
           </div>
-        </td>
+        </ContentCell>
         {permissionCells}
-      </tr>
+      </TableRowInteractive>
     );
   }
 
   return (
-    <tr>
-      <td className="nowrap text-middle">
-        <div className="display-flex-center">
-          <LegacyAvatar
-            className="text-middle big-spacer-right flex-0"
-            hash={user.avatar}
-            name={user.name}
-            size={36}
-          />
-          <div className="max-width-100">
+    <TableRowInteractive>
+      <ContentCell>
+        <div className="sw-flex sw-items-center">
+          <Avatar className="sw-mr-4" hash={user.avatar} name={user.name} size="md" />
+          <div className="sw-max-w-abs-800">
             <div className="sw-flex sw-w-fit sw-max-w-full">
-              <div className="sw-flex-1 text-ellipsis">
+              <div className="sw-flex-1 sw-text-ellipsis sw-whitespace-nowrap sw-overflow-hidden">
                 <strong>{user.name}</strong>
-                <span className="note spacer-left">{user.login}</span>
+                <Note className="sw-ml-2">{user.login}</Note>
               </div>
               {isGitHubProject && user.managed && (
                 <img
                   alt="github"
-                  className="spacer-left spacer-right"
+                  className="sw-my-2"
                   height={16}
                   aria-label={translate('project_permission.github_managed')}
                   src={`${getBaseUrl()}/images/alm/github.svg`}
                 />
               )}
             </div>
-
-            <div className="little-spacer-top max-width-100 text-ellipsis">{user.email}</div>
+            {user.email && (
+              <div className="sw-mt-2 sw-max-w-100 sw-text-ellipsis sw-whitespace-nowrap sw-overflow-hidden">
+                {user.email}
+              </div>
+            )}
           </div>
         </div>
-      </td>
+      </ContentCell>
       {permissionCells}
       {modal}
-    </tr>
+    </TableRowInteractive>
   );
 }