diff options
15 files changed, 319 insertions, 372 deletions
diff --git a/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx b/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx index 83e933e4858..411b4ae79f8 100644 --- a/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx +++ b/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx @@ -17,12 +17,11 @@ * 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')}; +`; diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx index d8d409bd004..4ea23f1fec2 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -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() { diff --git a/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx b/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx index 24cbae5c7c6..16bf3fcaac7 100644 --- a/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx +++ b/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx b/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx index b1d64c35f15..1c9a5ac1611 100644 --- a/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx @@ -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(); diff --git a/server/sonar-web/src/main/js/apps/groups/components/DeleteGroupForm.tsx b/server/sonar-web/src/main/js/apps/groups/components/DeleteGroupForm.tsx index a4b4ad084a2..a9b29459e8a 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/DeleteGroupForm.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/DeleteGroupForm.tsx @@ -17,10 +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 { 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')} + /> ); } diff --git a/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx b/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx index c1818628841..5abe6bf8160 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx @@ -17,14 +17,13 @@ * 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} + /> ); } diff --git a/server/sonar-web/src/main/js/apps/groups/components/GroupForm.tsx b/server/sonar-web/src/main/js/apps/groups/components/GroupForm.tsx index 97faf125d87..3852cebe389 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/GroupForm.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/GroupForm.tsx @@ -17,13 +17,10 @@ * 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')} + /> ); } diff --git a/server/sonar-web/src/main/js/apps/groups/components/Header.tsx b/server/sonar-web/src/main/js/apps/groups/components/Header.tsx index 70f64ed4912..951b2a792e8 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/Header.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/Header.tsx @@ -17,11 +17,10 @@ * 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 />} diff --git a/server/sonar-web/src/main/js/apps/groups/components/List.tsx b/server/sonar-web/src/main/js/apps/groups/components/List.tsx index e90e3cf1cf0..a3ac8892030 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/List.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/List.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx index d0a6b8f6c60..dd8d428cf6c 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx @@ -17,13 +17,22 @@ * 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> ); } diff --git a/server/sonar-web/src/main/js/apps/groups/components/Members.tsx b/server/sonar-web/src/main/js/apps/groups/components/Members.tsx index 64814df42f7..9a66634f8e6 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/Members.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/Members.tsx @@ -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} /> diff --git a/server/sonar-web/src/main/js/apps/groups/components/ViewMembersModal.tsx b/server/sonar-web/src/main/js/apps/groups/components/ViewMembersModal.tsx index dd5f62a36c1..6cd3c75c1e2 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/ViewMembersModal.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/ViewMembersModal.tsx @@ -17,12 +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 { 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 index 6fa08f30676..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/groups.css +++ /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%; -} diff --git a/server/sonar-web/src/main/js/components/controls/ManagedFilter.tsx b/server/sonar-web/src/main/js/components/controls/ManagedFilter.tsx index c328bbd3fbe..8397199bdf1 100644 --- a/server/sonar-web/src/main/js/components/controls/ManagedFilter.tsx +++ b/server/sonar-web/src/main/js/components/controls/ManagedFilter.tsx @@ -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> ); } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index a5204ec5bf3..3a6bf83af90 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -69,6 +69,7 @@ date=Date days=Days default=Default delete=Delete +delete_x=Delete {0} deprecated=Deprecated descending=Descending description=Description |