]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21382 Migrate Groups page to MIUI
authorViktor Vorona <viktor.vorona@sonarsource.com>
Thu, 28 Dec 2023 10:33:01 +0000 (11:33 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 28 Dec 2023 20:03:00 +0000 (20:03 +0000)
15 files changed:
server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx
server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx
server/sonar-web/src/main/js/apps/groups/components/DeleteGroupForm.tsx
server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx
server/sonar-web/src/main/js/apps/groups/components/GroupForm.tsx
server/sonar-web/src/main/js/apps/groups/components/Header.tsx
server/sonar-web/src/main/js/apps/groups/components/List.tsx
server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
server/sonar-web/src/main/js/apps/groups/components/Members.tsx
server/sonar-web/src/main/js/apps/groups/components/ViewMembersModal.tsx
server/sonar-web/src/main/js/apps/groups/groups.css [deleted file]
server/sonar-web/src/main/js/components/controls/ManagedFilter.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 83e933e4858a04ad673372bd6ef7a61d7afdcbbf..411b4ae79f8ceda4498e8094ae1af8be90f084d6 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 styled from '@emotion/styled';
 import { formatDistance } from 'date-fns';
+import { CheckIcon, FlagMessage, FlagWarningIcon, Link, themeColor } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
-import Link from '../../components/common/Link';
-import CheckIcon from '../../components/icons/CheckIcon';
-import WarningIcon from '../../components/icons/WarningIcon';
 import { Alert } from '../../components/ui/Alert';
 import { translate, translateWithParameters } from '../../helpers/l10n';
 import { AlmSyncStatus } from '../../types/provisioning';
@@ -50,13 +49,13 @@ function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) {
   if (short) {
     return status === TaskStatuses.Success ? (
       <div>
-        <span className="authentication-enabled spacer-left">
+        <IconWrapper className="sw-ml-2">
           {warningMessage ? (
-            <WarningIcon className="spacer-right" />
+            <FlagWarningIcon className="sw-mr-2" />
           ) : (
-            <CheckIcon className="spacer-right" />
+            <CheckIcon width={32} height={32} className="sw-mr-2" />
           )}
-        </span>
+        </IconWrapper>
         <i>
           {warningMessage ? (
             <FormattedMessage
@@ -67,7 +66,7 @@ function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) {
               values={{
                 date: formattedDate,
                 details: (
-                  <Link to="/admin/settings?category=authentication&tab=github">
+                  <Link className="sw-ml-2" to="/admin/settings?category=authentication&tab=github">
                     {translate('settings.authentication.github.synchronization_details_link')}
                   </Link>
                 ),
@@ -82,19 +81,19 @@ function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) {
         </i>
       </div>
     ) : (
-      <Alert variant="error">
+      <FlagMessage variant="error">
         <FormattedMessage
           id="settings.authentication.github.synchronization_failed_short"
           defaultMessage={translate('settings.authentication.github.synchronization_failed_short')}
           values={{
             details: (
-              <Link to="/admin/settings?category=authentication&tab=github">
+              <Link className="sw-ml-2" to="/admin/settings?category=authentication&tab=github">
                 {translate('settings.authentication.github.synchronization_details_link')}
               </Link>
             ),
           }}
         />
-      </Alert>
+      </FlagMessage>
     );
   }
 
@@ -165,3 +164,7 @@ export default function AlmSynchronisationWarning({
     </>
   );
 }
+
+const IconWrapper = styled.span`
+  color: ${themeColor('iconSuccess')};
+`;
index d8d409bd00488a021763bacb807aa72bf6e26c01..4ea23f1fec2557f7728d875d4e37f7a323518d6b 100644 (file)
@@ -77,6 +77,7 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE = [
   '/admin/permission_templates',
   '/project/background_tasks',
   '/admin/background_tasks',
+  '/admin/groups',
 ];
 
 export default function GlobalContainer() {
index 24cbae5c7c61403fe1bfeb48202689885a407f4b..16bf3fcaac74d3ef32a9dae5b80b27e35bad933a 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 { InputSearch, LargeCenteredLayout, PageContentFontWrapper } from 'design-system';
 import * as React from 'react';
 import { useState } from 'react';
 import { Helmet } from 'react-helmet-async';
@@ -24,7 +25,6 @@ import GitHubSynchronisationWarning from '../../app/components/GitHubSynchronisa
 import GitLabSynchronisationWarning from '../../app/components/GitLabSynchronisationWarning';
 import ListFooter from '../../components/controls/ListFooter';
 import { ManagedFilter } from '../../components/controls/ManagedFilter';
-import SearchBox from '../../components/controls/SearchBox';
 import Suggestions from '../../components/embed-docs-modal/Suggestions';
 import { translate } from '../../helpers/l10n';
 import { useGroupsQueries } from '../../queries/groups';
@@ -32,7 +32,6 @@ import { useIdentityProviderQuery } from '../../queries/identity-provider/common
 import { Provider } from '../../types/types';
 import Header from './components/Header';
 import List from './components/List';
-import './groups.css';
 
 export default function GroupsApp() {
   const [search, setSearch] = useState<string>('');
@@ -47,42 +46,44 @@ export default function GroupsApp() {
   const groups = data?.pages.flatMap((page) => page.groups) ?? [];
 
   return (
-    <>
-      <Suggestions suggestions="user_groups" />
-      <Helmet defer={false} title={translate('user_groups.page')} />
-      <main className="page page-limited" id="groups-page">
-        <Header manageProvider={manageProvider?.provider} />
-        {manageProvider?.provider === Provider.Github && <GitHubSynchronisationWarning short />}
-        {manageProvider?.provider === Provider.Gitlab && <GitLabSynchronisationWarning short />}
+    <LargeCenteredLayout>
+      <PageContentFontWrapper className="sw-my-8 sw-body-sm">
+        <Suggestions suggestions="user_groups" />
+        <Helmet defer={false} title={translate('user_groups.page')} />
+        <main>
+          <Header manageProvider={manageProvider?.provider} />
+          {manageProvider?.provider === Provider.Github && <GitHubSynchronisationWarning short />}
+          {manageProvider?.provider === Provider.Gitlab && <GitLabSynchronisationWarning short />}
 
-        <div className="display-flex-justify-start big-spacer-bottom big-spacer-top">
-          <ManagedFilter
-            manageProvider={manageProvider?.provider}
-            loading={isLoading}
-            managed={managed}
-            setManaged={setManaged}
-          />
-          <SearchBox
-            id="groups-search"
-            minLength={2}
-            onChange={(q) => setSearch(q)}
-            placeholder={translate('search.search_by_name')}
-            value={search}
-          />
-        </div>
+          <div className="sw-flex sw-my-4">
+            <ManagedFilter
+              manageProvider={manageProvider?.provider}
+              loading={isLoading}
+              managed={managed}
+              setManaged={setManaged}
+              miui
+            />
+            <InputSearch
+              minLength={2}
+              size="large"
+              onChange={(q) => setSearch(q)}
+              placeholder={translate('search.search_by_name')}
+              value={search}
+            />
+          </div>
 
-        <List groups={groups} manageProvider={manageProvider?.provider} />
+          <List groups={groups} manageProvider={manageProvider?.provider} />
 
-        <div id="groups-list-footer">
           <ListFooter
             count={groups.length}
             loading={isLoading}
             loadMore={fetchNextPage}
             ready={!isLoading}
             total={data?.pages[0].page.total}
+            useMIUIButtons
           />
-        </div>
-      </main>
-    </>
+        </main>
+      </PageContentFontWrapper>
+    </LargeCenteredLayout>
   );
 }
index b1d64c35f157c7901ffb3395715fbe0a8ef42619..1c9a5ac1611f0e0e30a47b6e742bc70a9f880d12 100644 (file)
@@ -47,18 +47,19 @@ const ui = {
   allFilter: byRole('radio', { name: 'all' }),
   selectedFilter: byRole('radio', { name: 'selected' }),
   unselectedFilter: byRole('radio', { name: 'unselected' }),
-  localAndManagedFilter: byRole('button', { name: 'all' }),
-  managedFilter: byRole('button', { name: 'managed' }),
-  localFilter: byRole('button', { name: 'local' }),
+  localAndManagedFilter: byRole('radio', { name: 'all' }),
+  managedFilter: byRole('radio', { name: 'managed' }),
+  localFilter: byRole('radio', { name: 'local' }),
   searchInput: byRole('searchbox', { name: 'search.search_by_name' }),
-  updateButton: byRole('button', { name: 'update_details' }),
+  updateButton: byRole('menuitem', { name: 'update_details' }),
   updateDialog: byRole('dialog', { name: 'groups.update_group' }),
   updateDialogButton: byRole('button', { name: 'update_verb' }),
-  deleteButton: byRole('button', { name: 'delete' }),
+  deleteButton: byRole('menuitem', { name: 'delete' }),
+  deleteIconButton: byRole('button', { name: /delete_x/ }),
   deleteDialog: byRole('dialog', { name: 'groups.delete_group' }),
   deleteDialogButton: byRole('button', { name: 'delete' }),
   showMore: byRole('button', { name: 'show_more' }),
-  nameInput: byRole('textbox', { name: 'name field_required' }),
+  nameInput: byRole('textbox', { name: 'name required' }),
   descriptionInput: byRole('textbox', { name: 'description' }),
   createGroupDialogButton: byRole('button', { name: 'create' }),
   editGroupDialogButton: byRole('button', { name: 'groups.create_group' }),
@@ -287,11 +288,10 @@ describe('in manage mode', () => {
     expect(await ui.localGroupRowWithLocalBadge.find()).toBeInTheDocument();
 
     await user.click(await ui.localFilter.find());
-    await user.click(await ui.localEditButton.find());
-
-    expect(ui.updateButton.query()).not.toBeInTheDocument();
+    expect(ui.localEditButton.query()).not.toBeInTheDocument();
+    expect(await ui.localGroupRowWithLocalBadge.by(ui.deleteIconButton).find()).toBeInTheDocument();
 
-    await user.click(await ui.deleteButton.find());
+    await user.click(ui.localGroupRowWithLocalBadge.by(ui.deleteIconButton).get());
 
     expect(await ui.deleteDialog.find()).toBeInTheDocument();
 
index a4b4ad084a27258a0f31291ea06223de6f504746..a9b29459e8a4be75c6b35ea19aa4081e9a5ebe26 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 { DangerButtonPrimary, Modal } from 'design-system';
 import * as React from 'react';
-import SimpleModal from '../../../components/controls/SimpleModal';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
-import Spinner from '../../../components/ui/Spinner';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { useDeleteGroupMutation } from '../../../queries/groups';
 import { Group } from '../../../types/types';
@@ -30,10 +28,10 @@ interface Props {
   onClose: () => void;
 }
 
-export default function DeleteGroupForm(props: Props) {
+export default function DeleteGroupForm(props: Readonly<Props>) {
   const { group } = props;
 
-  const { mutate: deleteGroup } = useDeleteGroupMutation();
+  const { mutate: deleteGroup, isLoading } = useDeleteGroupMutation();
 
   const onSubmit = () => {
     deleteGroup(group.id, {
@@ -41,30 +39,17 @@ export default function DeleteGroupForm(props: Props) {
     });
   };
 
-  const header = translate('groups.delete_group');
   return (
-    <SimpleModal header={header} onClose={props.onClose} onSubmit={onSubmit}>
-      {({ onCloseClick, onFormSubmit, submitting }) => (
-        <form onSubmit={onFormSubmit}>
-          <header className="modal-head">
-            <h2>{header}</h2>
-          </header>
-
-          <div className="modal-body">
-            {translateWithParameters('groups.delete_group.confirmation', group.name)}
-          </div>
-
-          <footer className="modal-foot">
-            <Spinner className="spacer-right" loading={submitting} />
-            <SubmitButton className="button-red" disabled={submitting}>
-              {translate('delete')}
-            </SubmitButton>
-            <ResetButtonLink disabled={submitting} onClick={onCloseClick}>
-              {translate('cancel')}
-            </ResetButtonLink>
-          </footer>
-        </form>
-      )}
-    </SimpleModal>
+    <Modal
+      headerTitle={translate('groups.delete_group')}
+      onClose={props.onClose}
+      body={translateWithParameters('groups.delete_group.confirmation', group.name)}
+      primaryButton={
+        <DangerButtonPrimary autoFocus type="submit" onClick={onSubmit} disabled={isLoading}>
+          {translate('delete')}
+        </DangerButtonPrimary>
+      }
+      secondaryButtonLabel={translate('cancel')}
+    />
   );
 }
index c181862884126140b1662f2a0818bdf003b1e634..5abe6bf816040887a465b4af77c4d83f4506991a 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 { Modal, TextMuted } from 'design-system';
 import { find } from 'lodash';
 import * as React from 'react';
-import Modal from '../../../components/controls/Modal';
 import SelectList, {
   SelectListFilter,
   SelectListSearchParams,
 } from '../../../components/controls/SelectList';
-import { ResetButtonLink } from '../../../components/controls/buttons';
 import { translate } from '../../../helpers/l10n';
 import {
   useAddGroupMembershipMutation,
@@ -90,10 +89,10 @@ export default function EditMembersModal(props: Readonly<Props>) {
     }
 
     return (
-      <div className="select-list-list-item">
+      <div>
         {user.name}
         <br />
-        <span className="note">{user.login}</span>
+        <TextMuted text={user.login} />
       </div>
     );
   };
@@ -111,15 +110,8 @@ export default function EditMembersModal(props: Readonly<Props>) {
 
   return (
     <Modal
-      className="group-menbers-modal"
-      contentLabel={modalHeader}
-      onRequestClose={props.onClose}
-    >
-      <header className="modal-head">
-        <h2>{modalHeader}</h2>
-      </header>
-
-      <div className="modal-body modal-container">
+      headerTitle={modalHeader}
+      body={
         <SelectList
           elements={users.map((user) => user.id)}
           elementsTotalCount={data?.pages[0].page.total}
@@ -134,11 +126,9 @@ export default function EditMembersModal(props: Readonly<Props>) {
           withPaging
           loading={isLoading}
         />
-      </div>
-
-      <footer className="modal-foot">
-        <ResetButtonLink onClick={props.onClose}>{translate('done')}</ResetButtonLink>
-      </footer>
-    </Modal>
+      }
+      secondaryButtonLabel={translate('done')}
+      onClose={props.onClose}
+    />
   );
 }
index 97faf125d87cacc1bfd8db8fbc2ad1be09b58d39..3852cebe38937b6101a23ae7995b6142340ba162 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, FormField, InputField, InputTextArea, Modal } from 'design-system';
 import * as React from 'react';
 import { useState } from 'react';
-import SimpleModal from '../../../components/controls/SimpleModal';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
-import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
-import Spinner from '../../../components/ui/Spinner';
 import { translate } from '../../../helpers/l10n';
 import { useCreateGroupMutation, useUpdateGroupMutation } from '../../../queries/groups';
 import { Group } from '../../../types/types';
@@ -46,8 +43,8 @@ export default function GroupForm(props: Props) {
   const [name, setName] = useState<string>(create ? '' : group.name);
   const [description, setDescription] = useState<string>(create ? '' : group.description ?? '');
 
-  const { mutate: createGroup } = useCreateGroupMutation();
-  const { mutate: updateGroup } = useUpdateGroupMutation();
+  const { mutate: createGroup, isLoading: isCreating } = useCreateGroupMutation();
+  const { mutate: updateGroup, isLoading: isUpdating } = useUpdateGroupMutation();
 
   const handleCreateGroup = () => {
     createGroup({ name, description }, { onSuccess: props.onClose });
@@ -70,61 +67,49 @@ export default function GroupForm(props: Props) {
   };
 
   return (
-    <SimpleModal
-      header={create ? translate('groups.create_group') : translate('groups.update_group')}
+    <Modal
+      headerTitle={create ? translate('groups.create_group') : translate('groups.update_group')}
+      body={
+        <>
+          <MandatoryFieldsExplanation className="sw-block sw-mb-4" />
+          <FormField htmlFor="create-group-name" label={translate('name')} required>
+            <InputField
+              autoFocus
+              id="create-group-name"
+              maxLength={255}
+              name="name"
+              onChange={(event: React.SyntheticEvent<HTMLInputElement>) => {
+                setName(event.currentTarget.value);
+              }}
+              required
+              size="full"
+              type="text"
+              value={name}
+            />
+          </FormField>
+          <FormField htmlFor="create-group-description" label={translate('description')}>
+            <InputTextArea
+              id="create-group-description"
+              name="description"
+              onChange={(event: React.SyntheticEvent<HTMLTextAreaElement>) => {
+                setDescription(event.currentTarget.value);
+              }}
+              size="full"
+              value={description}
+            />
+          </FormField>
+        </>
+      }
       onClose={props.onClose}
-      onSubmit={create ? handleCreateGroup : handleUpdateGroup}
-      size="small"
-    >
-      {({ onCloseClick, onFormSubmit, submitting }) => (
-        <form onSubmit={onFormSubmit}>
-          <header className="modal-head">
-            <h2>{create ? translate('groups.create_group') : translate('groups.update_group')}</h2>
-          </header>
-
-          <div className="modal-body">
-            <MandatoryFieldsExplanation className="modal-field" />
-            <div className="modal-field">
-              <label htmlFor="create-group-name">
-                {translate('name')}
-                <MandatoryFieldMarker />
-              </label>
-              <input
-                autoFocus
-                id="create-group-name"
-                maxLength={255}
-                name="name"
-                onChange={(event: React.SyntheticEvent<HTMLInputElement>) => {
-                  setName(event.currentTarget.value);
-                }}
-                required
-                size={50}
-                type="text"
-                value={name}
-              />
-            </div>
-            <div className="modal-field">
-              <label htmlFor="create-group-description">{translate('description')}</label>
-              <textarea
-                id="create-group-description"
-                name="description"
-                onChange={(event: React.SyntheticEvent<HTMLTextAreaElement>) => {
-                  setDescription(event.currentTarget.value);
-                }}
-                value={description}
-              />
-            </div>
-          </div>
-
-          <footer className="modal-foot">
-            <Spinner className="spacer-right" loading={submitting} />
-            <SubmitButton disabled={submitting}>
-              {create ? translate('create') : translate('update_verb')}
-            </SubmitButton>
-            <ResetButtonLink onClick={onCloseClick}>{translate('cancel')}</ResetButtonLink>
-          </footer>
-        </form>
-      )}
-    </SimpleModal>
+      primaryButton={
+        <ButtonPrimary
+          disabled={isUpdating || isCreating || name === ''}
+          onClick={create ? handleCreateGroup : handleUpdateGroup}
+        >
+          {create ? translate('create') : translate('update_verb')}
+        </ButtonPrimary>
+      }
+      secondaryButtonLabel={translate('cancel')}
+    />
   );
 }
index 70f64ed491264c9009924b813db7b5fe4a3705bf..951b2a792e8f9c220f724cafd5b72773d6263157 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 { FormattedMessage } from 'react-intl';
-import DocLink from '../../../components/common/DocLink';
-import { Button } from '../../../components/controls/buttons';
-import { Alert } from '../../../components/ui/Alert';
+import DocumentationLink from '../../../components/common/DocumentationLink';
 import { translate } from '../../../helpers/l10n';
 import { Provider } from '../../../types/types';
 import GroupForm from './GroupForm';
@@ -35,36 +34,37 @@ export default function Header({ manageProvider }: Readonly<HeaderProps>) {
 
   return (
     <>
-      <div className="page-header null-spacer-bottom" id="groups-header">
-        <h2 className="page-title">{translate('user_groups.page')}</h2>
-
-        <div className="page-actions">
-          <Button
+      <div id="groups-header">
+        <div className="sw-flex sw-justify-between">
+          <Title className="sw-mb-4">{translate('user_groups.page')}</Title>
+          <ButtonPrimary
             id="groups-create"
             disabled={manageProvider !== undefined}
             onClick={() => setCreateModal(true)}
           >
             {translate('groups.create_group')}
-          </Button>
+          </ButtonPrimary>
         </div>
 
         {manageProvider === undefined ? (
-          <p className="page-description">{translate('user_groups.page.description')}</p>
+          <p className="sw-mb-4">{translate('user_groups.page.description')}</p>
         ) : (
-          <Alert className="page-description max-width-100 width-100" variant="info">
-            <FormattedMessage
-              defaultMessage={translate('user_groups.page.managed_description')}
-              id="user_groups.page.managed_description"
-              values={{
-                provider: manageProvider,
-                link: (
-                  <DocLink to="/instance-administration/authentication/overview/">
-                    {translate('documentation')}
-                  </DocLink>
-                ),
-              }}
-            />
-          </Alert>
+          <FlagMessage className="sw-mb-4 sw-max-w-full sw-w-full" variant="info">
+            <div>
+              <FormattedMessage
+                defaultMessage={translate('user_groups.page.managed_description')}
+                id="user_groups.page.managed_description"
+                values={{
+                  provider: manageProvider,
+                  link: (
+                    <DocumentationLink to="/instance-administration/authentication/overview/">
+                      {translate('documentation')}
+                    </DocumentationLink>
+                  ),
+                }}
+              />
+            </div>
+          </FlagMessage>
         )}
       </div>
       {createModal && <GroupForm onClose={() => setCreateModal(false)} create />}
index e90e3cf1cf01df8eeaca1091b89f0ea6710e60d8..a3ac8892030c2ba6d29cd17ed585aaeeb263b7ea 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, NumericalCell, Table, TableRow } from 'design-system';
 import { sortBy } from 'lodash';
 import * as React from 'react';
 import { translate } from '../../../helpers/l10n';
@@ -28,30 +29,25 @@ interface Props {
   manageProvider: Provider | undefined;
 }
 
-export default function List(props: Props) {
+function Header() {
+  return (
+    <TableRow>
+      <ContentCell>{translate('user_groups.page.group_header')}</ContentCell>
+      <NumericalCell>{translate('members')}</NumericalCell>
+      <ContentCell>{translate('description')}</ContentCell>
+      <NumericalCell>{translate('actions')}</NumericalCell>
+    </TableRow>
+  );
+}
+
+export default function List(props: Readonly<Props>) {
   const { groups, manageProvider } = props;
 
   return (
-    <div className="boxed-group boxed-group-inner">
-      <table className="data zebra zebra-hover" id="groups-list">
-        <thead>
-          <tr>
-            <th id="list-group-name">{translate('user_groups.page.group_header')}</th>
-            <th id="list-group-member" className="nowrap width-10">
-              {translate('members')}
-            </th>
-            <th id="list-group-description" className="nowrap">
-              {translate('description')}
-            </th>
-            <th id="list-group-actions">{translate('actions')}</th>
-          </tr>
-        </thead>
-        <tbody>
-          {sortBy(groups, (group) => group.name.toLowerCase()).map((group) => (
-            <ListItem group={group} key={group.name} manageProvider={manageProvider} />
-          ))}
-        </tbody>
-      </table>
-    </div>
+    <Table columnCount={4} header={<Header />} id="groups-list">
+      {sortBy(groups, (group) => group.name.toLowerCase()).map((group) => (
+        <ListItem group={group} key={group.name} manageProvider={manageProvider} />
+      ))}
+    </Table>
   );
 }
index d0a6b8f6c60b684b452b2d3fd2a681136d7df590..dd8d428cf6ceca128e6fcfca558e21d30a3b4f22 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 { Spinner } from 'design-system';
+import {
+  ActionsDropdown,
+  Badge,
+  ContentCell,
+  DestructiveIcon,
+  ItemButton,
+  ItemDangerButton,
+  ItemDivider,
+  NumericalCell,
+  PopupZLevel,
+  Spinner,
+  TableRow,
+  TrashIcon,
+} from 'design-system';
 import * as React from 'react';
 import { useState } from 'react';
-import ActionsDropdown, {
-  ActionsDropdownDivider,
-  ActionsDropdownItem,
-} from '../../../components/controls/ActionsDropdown';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { getBaseUrl } from '../../../helpers/system';
 import { useGroupMembersCountQuery } from '../../../queries/group-memberships';
@@ -37,7 +46,7 @@ export interface ListItemProps {
   manageProvider: Provider | undefined;
 }
 
-export default function ListItem(props: ListItemProps) {
+export default function ListItem(props: Readonly<ListItemProps>) {
   const { manageProvider, group } = props;
   const { name, managed, description } = group;
 
@@ -62,7 +71,7 @@ export default function ListItem(props: ListItemProps) {
     return (
       <img
         alt={identityProvider}
-        className="spacer-left spacer-right"
+        className="sw-ml-2 sw-mr-2"
         height={16}
         src={`${getBaseUrl()}/images/alm/${identityProvider}.svg`}
       />
@@ -70,49 +79,53 @@ export default function ListItem(props: ListItemProps) {
   };
 
   return (
-    <tr data-id={name}>
-      <td className="width-20" headers="list-group-name">
-        <b>{name}</b>
-        {group.default && <span className="little-spacer-left">({translate('default')})</span>}
+    <TableRow data-id={name}>
+      <ContentCell>
+        <div className="sw-body-sm-highlight">{name}</div>
+        {group.default && <span className="sw-ml-1">({translate('default')})</span>}
         {managed && renderIdentityProviderIcon(manageProvider)}
-        {isGroupLocal() && <span className="little-spacer-left badge">{translate('local')}</span>}
-      </td>
+        {isGroupLocal() && <Badge className="sw-ml-1">{translate('local')}</Badge>}
+      </ContentCell>
 
-      <td className="group-members display-flex-justify-end" headers="list-group-member">
-        <Spinner loading={isLoading}>
-          <span>{membersCount}</span>
-        </Spinner>
+      <NumericalCell>
+        <Spinner loading={isLoading}>{membersCount}</Spinner>
         <Members group={group} onEdit={refetch} isManaged={isManaged()} />
-      </td>
+      </NumericalCell>
 
-      <td className="width-40" headers="list-group-description">
-        <span className="js-group-description">{description}</span>
-      </td>
+      <ContentCell>{description}</ContentCell>
 
-      <td className="thin nowrap text-right" headers="list-group-actions">
+      <NumericalCell>
         {!group.default && (!isManaged() || isGroupLocal()) && (
-          <ActionsDropdown label={translateWithParameters('groups.edit', group.name)}>
-            {!isManaged() && (
-              <>
-                <ActionsDropdownItem
-                  className="js-group-update"
-                  onClick={() => setGroupToEdit(group)}
-                >
-                  {translate('update_details')}
-                </ActionsDropdownItem>
-                <ActionsDropdownDivider />
-              </>
-            )}
-            {(!isManaged() || isGroupLocal()) && (
-              <ActionsDropdownItem
-                className="js-group-delete"
-                destructive
+          <>
+            {isManaged() && isGroupLocal() && (
+              <DestructiveIcon
+                Icon={TrashIcon}
+                className="sw-ml-2"
+                aria-label={translateWithParameters('delete_x', name)}
                 onClick={() => setGroupToDelete(group)}
+                size="small"
+              />
+            )}
+            {!isManaged() && (
+              <ActionsDropdown
+                allowResizing
+                id={`group-actions-${group.name}`}
+                ariaLabel={translateWithParameters('groups.edit', group.name)}
+                zLevel={PopupZLevel.Global}
               >
-                {translate('delete')}
-              </ActionsDropdownItem>
+                <ItemButton onClick={() => setGroupToEdit(group)}>
+                  {translate('update_details')}
+                </ItemButton>
+                <ItemDivider />
+                <ItemDangerButton
+                  className="it__quality-profiles__delete"
+                  onClick={() => setGroupToDelete(group)}
+                >
+                  {translate('delete')}
+                </ItemDangerButton>
+              </ActionsDropdown>
             )}
-          </ActionsDropdown>
+          </>
         )}
         {groupToDelete && (
           <DeleteGroupForm group={groupToDelete} onClose={() => setGroupToDelete(undefined)} />
@@ -120,7 +133,7 @@ export default function ListItem(props: ListItemProps) {
         {groupToEdit && (
           <GroupForm create={false} group={groupToEdit} onClose={() => setGroupToEdit(undefined)} />
         )}
-      </td>
-    </tr>
+      </NumericalCell>
+    </TableRow>
   );
 }
index 64814df42f7b9a3c7339bac6c0678641732bddaa..9a66634f8e644171e2f192a526700b9fd87f0d73 100644 (file)
@@ -17,9 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { InteractiveIcon, MenuIcon, PencilIcon } from 'design-system';
 import * as React from 'react';
-import { ButtonIcon } from '../../../components/controls/buttons';
-import BulletListIcon from '../../../components/icons/BulletListIcon';
 import { translateWithParameters } from '../../../helpers/l10n';
 import { Group } from '../../../types/types';
 import EditMembersModal from './EditMembersModal';
@@ -42,21 +41,24 @@ export default function Members(props: Readonly<Props>) {
     }
   };
 
+  const isReadonly = isManaged || group.default;
+
+  const title = translateWithParameters(
+    isReadonly ? 'groups.users.view' : 'groups.users.edit',
+    group.name,
+  );
+
   return (
     <>
-      <ButtonIcon
-        aria-label={translateWithParameters(
-          isManaged || group.default ? 'groups.users.view' : 'groups.users.edit',
-          group.name,
-        )}
-        className="button-small little-spacer-left little-padded"
+      <InteractiveIcon
+        Icon={isReadonly ? MenuIcon : PencilIcon}
+        className="sw-ml-2"
+        aria-label={title}
         onClick={() => setOpenModal(true)}
-        title={translateWithParameters('groups.users.edit', group.name)}
-      >
-        <BulletListIcon />
-      </ButtonIcon>
+        size="small"
+      />
       {openModal &&
-        (isManaged || group.default ? (
+        (isReadonly ? (
           <ViewMembersModal isManaged={isManaged} group={group} onClose={handleModalClose} />
         ) : (
           <EditMembersModal group={group} onClose={handleModalClose} />
index dd5f62a36c187df375e2a8c196138fec4496c054..6cd3c75c1e2d59afca08479dcc80f8a82268d90c 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 { Spinner } from 'design-system';
+import { Badge, InputSearch, Modal, Spinner, TextMuted } from 'design-system';
 import * as React from 'react';
 import ListFooter from '../../../components/controls/ListFooter';
-import Modal from '../../../components/controls/Modal';
-import SearchBox from '../../../components/controls/SearchBox';
-import { ResetButtonLink } from '../../../components/controls/buttons';
 import { translate } from '../../../helpers/l10n';
 import { useGroupMembersQuery } from '../../../queries/group-memberships';
 import { Group } from '../../../types/types';
@@ -47,56 +44,47 @@ export default function ViewMembersModal(props: Readonly<Props>) {
   const modalHeader = translate('users.list');
   return (
     <Modal
-      className="group-menbers-modal"
-      contentLabel={modalHeader}
-      onRequestClose={props.onClose}
-    >
-      <header className="modal-head">
-        <h2>{modalHeader}</h2>
-      </header>
-
-      <div className="modal-body modal-container">
-        <SearchBox
-          className="view-search-box"
-          loading={isLoading}
-          onChange={setQuery}
-          placeholder={translate('search_verb')}
-          value={query}
-        />
-        <div className="select-list-list-container spacer-top sw-overflow-auto">
-          <Spinner loading={isLoading}>
-            <ul className="menu">
-              {users.map((user) => (
-                <li key={user.login} className="display-flex-center">
-                  <span className="little-spacer-left width-100">
-                    <span className="select-list-list-item display-flex-center display-flex-space-between">
-                      <span className="spacer-right">
-                        {user.name}
-                        <br />
-                        <span className="note">{user.login}</span>
+      headerTitle={modalHeader}
+      body={
+        <>
+          <InputSearch
+            className="sw-w-full sw-top-0 sw-sticky"
+            loading={isLoading}
+            onChange={setQuery}
+            placeholder={translate('search_verb')}
+            value={query}
+          />
+          <div className="sw-mt-6">
+            <Spinner loading={isLoading}>
+              <ul>
+                {users.map((user) => (
+                  <li key={user.login} className="sw-flex sw-items-center">
+                    <span className="sw-ml-1 sw-w-full">
+                      <span className="sw-flex sw-justify-between sw-items-center">
+                        <span className="sw-mr-2">
+                          {user.name}
+                          <br />
+                          <TextMuted text={user.login} />
+                        </span>
+                        {!user.managed && isManaged && <Badge>{translate('local')}</Badge>}
                       </span>
-                      {!user.managed && isManaged && (
-                        <span className="badge">{translate('local')}</span>
-                      )}
                     </span>
-                  </span>
-                </li>
-              ))}
-            </ul>
-          </Spinner>
-        </div>
-        {data !== undefined && (
-          <ListFooter
-            count={users.length}
-            loadMore={fetchNextPage}
-            total={data?.pages[0].page.total}
-          />
-        )}
-      </div>
-
-      <footer className="modal-foot">
-        <ResetButtonLink onClick={props.onClose}>{translate('done')}</ResetButtonLink>
-      </footer>
-    </Modal>
+                  </li>
+                ))}
+              </ul>
+            </Spinner>
+            {data !== undefined && (
+              <ListFooter
+                count={users.length}
+                loadMore={fetchNextPage}
+                total={data?.pages[0].page.total}
+                useMIUIButtons
+              />
+            )}
+          </div>
+        </>
+      }
+      onClose={props.onClose}
+    />
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/groups/groups.css b/server/sonar-web/src/main/js/apps/groups/groups.css
deleted file mode 100644 (file)
index 6fa08f3..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-
-#groups-page .group-members {
-  padding-right: 50%;
-}
-
-.group-menbers-modal .modal-container > :last-child {
-  margin-bottom: 0;
-}
-
-.group-menbers-modal .select-list-list-container {
-  height: 350px;
-}
-
-.group-menbers-modal .modal-body {
-  padding: 12px 32px;
-}
-
-.group-members-modal .view-search-box.search-box {
-  max-width: 100%;
-}
index c328bbd3fbe9517b285cdf1d33e3edb349144dd2..8397199bdf11cb8849612179c3443a967ac05144 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 { ToggleButton } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../helpers/l10n';
 import { Provider } from '../../types/types';
@@ -26,34 +27,54 @@ interface ManagedFilterProps {
   manageProvider: Provider | undefined;
   loading: boolean;
   managed: boolean | undefined;
+  miui?: boolean;
   setManaged: (managed: boolean | undefined) => void;
 }
 
-export function ManagedFilter(props: ManagedFilterProps) {
-  const { manageProvider, loading, managed } = props;
+export function ManagedFilter(props: Readonly<ManagedFilterProps>) {
+  const { manageProvider, loading, managed, miui } = props;
 
   if (manageProvider === undefined) {
     return null;
   }
 
   return (
-    <div className="big-spacer-right">
-      <ButtonToggle
-        value={managed ?? 'all'}
-        disabled={loading}
-        options={[
-          { label: translate('all'), value: 'all' },
-          { label: translate('managed'), value: true },
-          { label: translate('local'), value: false },
-        ]}
-        onCheck={(filterOption) => {
-          if (filterOption === 'all') {
-            props.setManaged(undefined);
-          } else {
-            props.setManaged(filterOption as boolean);
-          }
-        }}
-      />
+    <div className="sw-mr-4">
+      {miui ? (
+        <ToggleButton
+          value={managed ?? 'all'}
+          disabled={loading}
+          options={[
+            { label: translate('all'), value: 'all' },
+            { label: translate('managed'), value: true },
+            { label: translate('local'), value: false },
+          ]}
+          onChange={(filterOption) => {
+            if (filterOption === 'all') {
+              props.setManaged(undefined);
+            } else {
+              props.setManaged(filterOption);
+            }
+          }}
+        />
+      ) : (
+        <ButtonToggle
+          value={managed ?? 'all'}
+          disabled={loading}
+          options={[
+            { label: translate('all'), value: 'all' },
+            { label: translate('managed'), value: true },
+            { label: translate('local'), value: false },
+          ]}
+          onCheck={(filterOption) => {
+            if (filterOption === 'all') {
+              props.setManaged(undefined);
+            } else {
+              props.setManaged(filterOption as boolean);
+            }
+          }}
+        />
+      )}
     </div>
   );
 }
index a5204ec5bf3830ce9ea2b17bb9be59b57f472900..3a6bf83af90aaab5948f5a7ab9d3b25e3d24b1bd 100644 (file)
@@ -69,6 +69,7 @@ date=Date
 days=Days
 default=Default
 delete=Delete
+delete_x=Delete {0}
 deprecated=Deprecated
 descending=Descending
 description=Description