diff options
author | 7PH <benjamin.raymond@sonarsource.com> | 2023-09-11 04:44:27 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-09-12 20:02:41 +0000 |
commit | 172333c558e538ceb6a45392a91d06a348c6eb64 (patch) | |
tree | b8bcc2295a90ea191455681e560f1d294709a73c /server | |
parent | 8df0d85e3c80052d7826c47005c1a83bf6025920 (diff) | |
download | sonarqube-172333c558e538ceb6a45392a91d06a348c6eb64.tar.gz sonarqube-172333c558e538ceb6a45392a91d06a348c6eb64.zip |
SONAR-20366 Migrate quality profile individual QP page to new UI
Diffstat (limited to 'server')
30 files changed, 645 insertions, 584 deletions
diff --git a/server/sonar-web/design-system/src/components/Dropdown.tsx b/server/sonar-web/design-system/src/components/Dropdown.tsx index 6fb33b5853b..1e2f47de597 100644 --- a/server/sonar-web/design-system/src/components/Dropdown.tsx +++ b/server/sonar-web/design-system/src/components/Dropdown.tsx @@ -143,10 +143,11 @@ interface ActionsDropdownProps extends Omit<Props, 'children' | 'overlay'> { ariaLabel?: string; buttonSize?: 'small' | 'medium'; children: React.ReactNode; + toggleClassName?: string; } export function ActionsDropdown(props: ActionsDropdownProps) { - const { children, buttonSize, ariaLabel, ...dropdownProps } = props; + const { children, buttonSize, ariaLabel, toggleClassName, ...dropdownProps } = props; const intl = useIntl(); @@ -155,6 +156,7 @@ export function ActionsDropdown(props: ActionsDropdownProps) { <InteractiveIcon Icon={MenuIcon} aria-label={ariaLabel ?? intl.formatMessage({ id: 'menu' })} + className={toggleClassName} size={buttonSize} stopPropagation={false} /> diff --git a/server/sonar-web/design-system/src/components/icons/UserGroupIcon.tsx b/server/sonar-web/design-system/src/components/icons/UserGroupIcon.tsx new file mode 100644 index 00000000000..9b6cb6d9343 --- /dev/null +++ b/server/sonar-web/design-system/src/components/icons/UserGroupIcon.tsx @@ -0,0 +1,23 @@ +/* + * 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. + */ +import { PeopleIcon } from '@primer/octicons-react'; +import { OcticonHoc } from './Icon'; + +export const UserGroupIcon = OcticonHoc(PeopleIcon, 'UserGroupIcon'); diff --git a/server/sonar-web/design-system/src/components/icons/index.ts b/server/sonar-web/design-system/src/components/icons/index.ts index a10b249dd7a..9cbd5c26118 100644 --- a/server/sonar-web/design-system/src/components/icons/index.ts +++ b/server/sonar-web/design-system/src/components/icons/index.ts @@ -89,4 +89,5 @@ export { TriangleUpIcon } from './TriangleUpIcon'; export { UnfoldDownIcon } from './UnfoldDownIcon'; export { UnfoldIcon } from './UnfoldIcon'; export { UnfoldUpIcon } from './UnfoldUpIcon'; +export { UserGroupIcon } from './UserGroupIcon'; export { VulnerabilityIcon } from './VulnerabilityIcon'; diff --git a/server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts index 237dabd2da8..712447b19c8 100644 --- a/server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts @@ -54,6 +54,7 @@ import { getProfileInheritance, getProfileProjects, getQualityProfile, + getQualityProfileExporterUrl, removeGroup, removeUser, renameProfile, @@ -119,6 +120,9 @@ export default class QualityProfilesServiceMock { jest.mocked(deleteProfile).mockImplementation(this.handleDeleteProfile); jest.mocked(renameProfile).mockImplementation(this.handleRenameProfile); jest.mocked(setDefaultProfile).mockImplementation(this.handleSetDefaultProfile); + jest + .mocked(getQualityProfileExporterUrl) + .mockImplementation(() => '/api/qualityprofiles/export'); } resetQualityProfile() { 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 abcf9c142c4..97aa3eaae2e 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -45,6 +45,7 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND = [ '/project/issues', '/project/activity', '/code', + '/profiles/show', '/project/extension/securityreport/securityreport', '/projects', '/project/information', diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx index 39f2ab60075..0eab39edea8 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx @@ -29,6 +29,10 @@ import routes from '../routes'; jest.mock('../../../api/quality-profiles'); jest.mock('../../../api/rules'); +beforeEach(() => { + serviceMock.reset(); +}); + const serviceMock = new QualityProfilesServiceMock(); const ui = { permissionSection: byRole('region', { name: 'permissions.page' }), @@ -55,16 +59,16 @@ const ui = { name: /quality_profiles.actions/, }), qualityProfilesHeader: byRole('heading', { name: 'quality_profiles.page' }), - deleteQualityProfileButton: byRole('button', { name: 'delete' }), + deleteQualityProfileButton: byRole('menuitem', { name: 'delete' }), activateMoreRulesButton: byRole('button', { name: 'quality_profiles.activate_more' }), activateMoreLink: byRole('link', { name: 'quality_profiles.activate_more' }), - activateMoreRulesLink: byRole('link', { name: 'quality_profiles.activate_more_rules' }), - backUpLink: byRole('link', { name: 'backup_verb' }), - compareLink: byRole('link', { name: 'compare' }), - extendButton: byRole('button', { name: 'extend' }), - copyButton: byRole('button', { name: 'copy' }), - renameButton: byRole('button', { name: 'rename' }), - setAsDefaultButton: byRole('button', { name: 'set_as_default' }), + activateMoreRulesLink: byRole('menuitem', { name: 'quality_profiles.activate_more_rules' }), + backUpLink: byRole('menuitem', { name: 'backup_verb' }), + compareLink: byRole('menuitem', { name: 'compare' }), + extendButton: byRole('menuitem', { name: 'extend' }), + copyButton: byRole('menuitem', { name: 'copy' }), + renameButton: byRole('menuitem', { name: 'rename' }), + setAsDefaultButton: byRole('menuitem', { name: 'set_as_default' }), newNameInput: byRole('textbox', { name: /quality_profiles.new_name/ }), qualityProfilePageLink: byRole('link', { name: 'quality_profiles.page' }), rulesTotalRow: byRole('row', { name: /total/ }), @@ -78,10 +82,6 @@ const ui = { rulesDeprecatedLink: byRole('link', { name: '8' }), }; -beforeEach(() => { - serviceMock.reset(); -}); - describe('Admin or user with permission', () => { beforeEach(() => { serviceMock.setAdmin(); @@ -210,7 +210,7 @@ describe('Admin or user with permission', () => { renderQualityProfile('sonar'); expect(await ui.rulesSection.find()).toBeInTheDocument(); expect(ui.activateMoreRulesButton.get()).toBeInTheDocument(); - expect(ui.activateMoreRulesButton.get()).toHaveClass('disabled'); + expect(ui.activateMoreRulesButton.get()).toBeDisabled(); }); }); @@ -282,7 +282,7 @@ describe('Admin or user with permission', () => { expect(ui.dialog.query()).not.toBeInTheDocument(); expect(screen.getAllByText('Bad new PHP quality profile')).toHaveLength(2); - expect(screen.getAllByText('Good old PHP quality profile')).toHaveLength(2); + expect(screen.getByText('Good old PHP quality profile')).toBeInTheDocument(); }); it('should be able to copy a quality profile', async () => { @@ -437,7 +437,7 @@ describe('Every Users', () => { it('should be able to see a warning when some rules are deprecated', async () => { renderQualityProfile(); - expect(await ui.rulesDeprecatedWarning.findAll()).toHaveLength(2); + expect(await ui.rulesDeprecatedWarning.findAll()).toHaveLength(1); expect(ui.rulesDeprecatedLink.get()).toBeInTheDocument(); expect(ui.rulesDeprecatedLink.get()).toHaveAttribute( 'href', diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx index 8c5a30d4cd9..ec48942dc25 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx @@ -39,19 +39,32 @@ const ui = { cQualityProfileName: 'c quality profile', newCQualityProfileName: 'New c quality profile', newCQualityProfileNameFromCreateButton: 'New c quality profile from create', - profileActions: (name: string, language: string) => + listProfileActions: (name: string, language: string) => byRole('button', { name: `quality_profiles.actions.${name}.${language}`, }), - extendButton: byRole('button', { + profileActions: (name: string, language: string) => + byRole('menuitem', { + name: `quality_profiles.actions.${name}.${language}`, + }), + modalExtendButton: byRole('button', { name: 'extend', }), - copyButton: byRole('button', { + qualityProfileActions: byRole('button', { + name: /quality_profiles.actions/, + }), + extendButton: byRole('menuitem', { + name: 'extend', + }), + modalCopyButton: byRole('button', { + name: 'copy', + }), + copyButton: byRole('menuitem', { name: 'copy', }), createButton: byRole('button', { name: 'create' }), restoreButton: byRole('button', { name: 'restore' }), - compareButton: byRole('link', { name: 'compare' }), + compareButton: byRole('menuitem', { name: 'compare' }), cancelButton: byRole('button', { name: 'cancel' }), compareDropdown: byRole('combobox', { name: 'quality_profiles.compare_with' }), changelogLink: byRole('link', { name: 'changelog' }), @@ -71,8 +84,8 @@ const ui = { namePropupInput: byRole('textbox', { name: 'quality_profiles.new_name required' }), filterByLang: byRole('combobox', { name: 'quality_profiles.filter_by:' }), listLinkCQualityProfile: byRole('link', { name: 'c quality profile' }), - listLinkNewCQualityProfile: byRole('link', { name: 'New c quality profile' }), - listLinkNewCQualityProfileFromCreateButton: byRole('link', { + headingNewCQualityProfile: byRole('heading', { name: 'New c quality profile' }), + headingNewCQualityProfileFromCreateButton: byRole('heading', { name: 'New c quality profile from create', }), listLinkJavaQualityProfile: byRole('link', { name: 'java quality profile' }), @@ -167,15 +180,15 @@ describe('Create', () => { serviceMock.setAdmin(); renderQualityProfiles(); - await user.click(await ui.profileActions('c quality profile', 'C').find()); + await user.click(await ui.listProfileActions('c quality profile', 'C').find()); await user.click(ui.extendButton.get()); await user.clear(ui.namePropupInput.get()); await user.type(ui.namePropupInput.get(), ui.newCQualityProfileName); await act(async () => { - await user.click(ui.extendButton.get()); + await user.click(ui.modalExtendButton.get()); }); - expect(await ui.listLinkNewCQualityProfile.find()).toBeInTheDocument(); + expect(await ui.headingNewCQualityProfile.find()).toBeInTheDocument(); await user.click(ui.returnToList.get()); await user.click(ui.createButton.get()); @@ -186,7 +199,7 @@ describe('Create', () => { await user.click(ui.createButton.get(ui.popup.get())); }); - expect(await ui.listLinkNewCQualityProfileFromCreateButton.find()).toBeInTheDocument(); + expect(await ui.headingNewCQualityProfileFromCreateButton.find()).toBeInTheDocument(); }); it('should be able to copy an existing Quality Profile', async () => { @@ -194,15 +207,15 @@ describe('Create', () => { serviceMock.setAdmin(); renderQualityProfiles(); - await user.click(await ui.profileActions('c quality profile', 'C').find()); + await user.click(await ui.listProfileActions('c quality profile', 'C').find()); await user.click(ui.copyButton.get()); await user.clear(ui.namePropupInput.get()); await user.type(ui.namePropupInput.get(), ui.newCQualityProfileName); await act(async () => { - await user.click(ui.copyButton.get(ui.popup.get())); + await user.click(ui.modalCopyButton.get(ui.popup.get())); }); - expect(await ui.listLinkNewCQualityProfile.find()).toBeInTheDocument(); + expect(await ui.headingNewCQualityProfile.find()).toBeInTheDocument(); await user.click(ui.returnToList.get()); await user.click(ui.createButton.get()); @@ -214,7 +227,7 @@ describe('Create', () => { await user.click(ui.createButton.get(ui.popup.get())); }); - expect(await ui.listLinkNewCQualityProfileFromCreateButton.find()).toBeInTheDocument(); + expect(await ui.headingNewCQualityProfileFromCreateButton.find()).toBeInTheDocument(); }); it('should be able to create blank Quality Profile', async () => { @@ -229,7 +242,7 @@ describe('Create', () => { await user.click(ui.createButton.get(ui.popup.get())); }); - expect(await ui.listLinkNewCQualityProfile.find()).toBeInTheDocument(); + expect(await ui.headingNewCQualityProfile.find()).toBeInTheDocument(); }); }); @@ -263,10 +276,10 @@ it('should be able to compare profiles', async () => { renderQualityProfiles(); // For language with 1 profle we should not see compare action - await user.click(await ui.profileActions('c quality profile', 'C').find()); + await user.click(await ui.listProfileActions('c quality profile', 'C').find()); expect(ui.compareButton.query()).not.toBeInTheDocument(); - await user.click(ui.profileActions('java quality profile', 'Java').get()); + await user.click(ui.listProfileActions('java quality profile', 'Java').get()); expect(ui.compareButton.get()).toBeInTheDocument(); await user.click(ui.compareButton.get()); expect(ui.compareDropdown.get()).toBeInTheDocument(); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/BuiltInQualityProfileBadge.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/BuiltInQualityProfileBadge.tsx index 035038aa579..9dfbc9e583a 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/BuiltInQualityProfileBadge.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/BuiltInQualityProfileBadge.tsx @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import classNames from 'classnames'; +import { Badge } from 'design-system'; import * as React from 'react'; import Tooltip from '../../../components/controls/Tooltip'; import { translate } from '../../../helpers/l10n'; @@ -29,9 +29,9 @@ interface Props { export default function BuiltInQualityProfileBadge({ className, tooltip = true }: Props) { const badge = ( - <div className={classNames('badge badge-info', className)}> + <Badge variant="default" className={className}> {translate('quality_profiles.built_in')} - </div> + </Badge> ); if (tooltip) { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx index 3f6553d3f49..cba17ac635a 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx @@ -17,7 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import classNames from 'classnames'; +import { + ActionsDropdown, + ItemButton, + ItemDangerButton, + ItemDivider, + ItemDownload, + ItemLink, + PopupPlacement, + Tooltip, +} from 'design-system'; import { some } from 'lodash'; import * as React from 'react'; import { @@ -28,11 +37,6 @@ import { renameProfile, setDefaultProfile, } from '../../../api/quality-profiles'; -import ActionsDropdown, { - ActionsDropdownDivider, - ActionsDropdownItem, -} from '../../../components/controls/ActionsDropdown'; -import Tooltip from '../../../components/controls/Tooltip'; import { Router, withRouter } from '../../../components/hoc/withRouter'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getBaseUrl } from '../../../helpers/system'; @@ -44,7 +48,6 @@ import DeleteProfileForm from './DeleteProfileForm'; import ProfileModalForm from './ProfileModalForm'; interface Props { - className?: string; profile: Profile; router: Router; isComparable: boolean; @@ -199,110 +202,111 @@ class ProfileActions extends React.PureComponent<Props, State> { const hasNoActiveRules = profile.activeRuleCount === 0; const hasAnyAction = some([...Object.values(actions), !profile.isBuiltIn, isComparable]); + if (!hasAnyAction) { + return null; + } + return ( <> <ActionsDropdown - className={classNames(this.props.className, { invisible: !hasAnyAction })} - label={translateWithParameters( + allowResizing + id={`quality-profile-actions-${profile.key}`} + className="it__quality-profiles__actions-dropdown" + toggleClassName="it__quality-profiles__actions-dropdown-toggle" + ariaLabel={translateWithParameters( 'quality_profiles.actions', profile.name, profile.languageName, )} - disabled={!hasAnyAction} + isPortal > {actions.edit && ( - <ActionsDropdownItem - className="it__quality-profiles__activate-more-rules" - to={activateMoreUrl} - > + <ItemLink className="it__quality-profiles__activate-more-rules" to={activateMoreUrl}> {translate('quality_profiles.activate_more_rules')} - </ActionsDropdownItem> + </ItemLink> )} {!profile.isBuiltIn && ( - <ActionsDropdownItem - className="it__quality-profiles__backup" + <ItemDownload download={`${profile.key}.xml`} - to={backupUrl} + href={backupUrl} + className="it__quality-profiles__backup" > {translate('backup_verb')} - </ActionsDropdownItem> + </ItemDownload> )} {isComparable && ( - <ActionsDropdownItem + <ItemLink className="it__quality-profiles__compare" to={getProfileComparePath(profile.name, profile.language)} > {translate('compare')} - </ActionsDropdownItem> + </ItemLink> )} {actions.copy && ( <> - <ActionsDropdownItem - tooltipPlacement="left" - tooltipOverlay={translateWithParameters( - 'quality_profiles.extend_help', - profile.name, - )} - className="it__quality-profiles__extend" - onClick={this.handleExtendClick} + <Tooltip + overlay={translateWithParameters('quality_profiles.extend_help', profile.name)} + placement={PopupPlacement.Left} > - {translate('extend')} - </ActionsDropdownItem> - - <ActionsDropdownItem - tooltipPlacement="left" - tooltipOverlay={translateWithParameters('quality_profiles.copy_help', profile.name)} - className="it__quality-profiles__copy" - onClick={this.handleCopyClick} + <ItemButton + className="it__quality-profiles__extend" + onClick={this.handleExtendClick} + > + {translate('extend')} + </ItemButton> + </Tooltip> + + <Tooltip + overlay={translateWithParameters('quality_profiles.copy_help', profile.name)} + placement={PopupPlacement.Left} > - {translate('copy')} - </ActionsDropdownItem> + <ItemButton className="it__quality-profiles__copy" onClick={this.handleCopyClick}> + {translate('copy')} + </ItemButton> + </Tooltip> </> )} {actions.edit && ( - <ActionsDropdownItem - className="it__quality-profiles__rename" - onClick={this.handleRenameClick} - > + <ItemButton className="it__quality-profiles__rename" onClick={this.handleRenameClick}> {translate('rename')} - </ActionsDropdownItem> + </ItemButton> )} {actions.setAsDefault && (hasNoActiveRules ? ( <li> <Tooltip - placement="left" + placement={PopupPlacement.Left} overlay={translate('quality_profiles.cannot_set_default_no_rules')} > - <span className="it__quality-profiles__set-as-default text-muted-2"> + <span className="it__quality-profiles__set-as-default"> {translate('set_as_default')} </span> </Tooltip> </li> ) : ( - <ActionsDropdownItem + <ItemButton className="it__quality-profiles__set-as-default" onClick={this.handleSetDefaultClick} > {translate('set_as_default')} - </ActionsDropdownItem> + </ItemButton> ))} - {actions.delete && <ActionsDropdownDivider />} - {actions.delete && ( - <ActionsDropdownItem - className="it__quality-profiles__delete" - destructive - onClick={this.handleDeleteClick} - > - {translate('delete')} - </ActionsDropdownItem> + <> + <ItemDivider /> + <ItemDangerButton + className="it__quality-profiles__delete" + onClick={this.handleDeleteClick} + > + {translate('delete')} + </ItemDangerButton> + </> )} </ActionsDropdown> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.tsx index 12ded3bcae8..72f7a2bc9e9 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.tsx @@ -17,8 +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 { StandoutLink } from 'design-system'; import * as React from 'react'; -import { NavLink } from 'react-router-dom'; import { getProfilePath } from '../utils'; interface Props { @@ -30,12 +30,8 @@ interface Props { export default function ProfileLink({ name, language, children, ...other }: Props) { return ( - <NavLink - className={({ isActive }) => (isActive ? 'link-no-underline' : '')} - to={getProfilePath(name, language)} - {...other} - > + <StandoutLink to={getProfilePath(name, language)} {...other}> {children} - </NavLink> + </StandoutLink> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/QualityProfilesApp.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/QualityProfilesApp.tsx index 3f7d208399b..761d73fea28 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/QualityProfilesApp.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/QualityProfilesApp.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 { LargeCenteredLayout, Spinner } from 'design-system'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import { Outlet } from 'react-router-dom'; @@ -91,15 +92,15 @@ export class QualityProfilesApp extends React.PureComponent<Props, State> { const { actions, loading, profiles, exporters } = this.state; if (loading) { - return <i className="spinner" />; + return <Spinner />; } const finalLanguages = Object.values(this.props.languages); const context: QualityProfilesContextProps = { - actions: actions || {}, - profiles: profiles || [], + actions: actions ?? {}, + profiles: profiles ?? [], languages: finalLanguages, - exporters: exporters || [], + exporters: exporters ?? [], updateProfiles: this.updateProfiles, }; @@ -108,12 +109,12 @@ export class QualityProfilesApp extends React.PureComponent<Props, State> { render() { return ( - <div className="page page-limited"> + <LargeCenteredLayout className="sw-my-8"> <Suggestions suggestions="quality_profiles" /> <Helmet defer={false} title={translate('quality_profiles.page')} /> {this.renderChild()} - </div> + </LargeCenteredLayout> ); } } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx index eb1143a57e3..2db4d407bd1 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx @@ -22,6 +22,7 @@ import * as React from 'react'; import { HelmetProvider } from 'react-helmet-async'; import { MemoryRouter, Outlet, Route, Routes } from 'react-router-dom'; import { mockQualityProfile } from '../../../../helpers/testMocks'; +import { IntlWrapper } from '../../../../helpers/testReactTestingUtils'; import { QualityProfilesContextProps, withQualityProfilesContext, @@ -85,13 +86,15 @@ function renderProfileContainer(path: string, overrides: Partial<QualityProfiles return render( <HelmetProvider context={{}}> <MemoryRouter initialEntries={[path]}> - <Routes> - <Route element={<ProfileOutlet {...overrides} />}> - <Route element={<ProfileContainer />}> - <Route path="*" element={<WrappedChild />} /> + <IntlWrapper> + <Routes> + <Route element={<ProfileOutlet {...overrides} />}> + <Route element={<ProfileContainer />}> + <Route path="*" element={<WrappedChild />} /> + </Route> </Route> - </Route> - </Routes> + </Routes> + </IntlWrapper> </MemoryRouter> </HelmetProvider>, ); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx index 29032b3b215..14f69388436 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx @@ -17,8 +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 styled from '@emotion/styled'; +import { FlagMessage, themeColor } from 'design-system'; import * as React from 'react'; -import { Alert } from '../../../components/ui/Alert'; import { translate } from '../../../helpers/l10n'; import { withQualityProfilesContext } from '../qualityProfilesContext'; import { Exporter, Profile } from '../types'; @@ -39,23 +40,18 @@ function ProfileDetails(props: ProfileDetailsProps) { const { profile, profiles, exporters } = props; return ( - <div> - <div className="quality-profile-grid"> - <div className="quality-profile-grid-left"> - <ProfileRules profile={profile} /> - <ProfileExporters exporters={exporters} profile={profile} /> - {profile.actions?.edit && !profile.isBuiltIn && <ProfilePermissions profile={profile} />} - </div> - <div className="quality-profile-grid-right"> + <ContentWrapper> + <div className="sw-grid sw-grid-cols-3 sw-gap-12"> + <div className="sw-col-span-2 sw-flex sw-flex-col sw-gap-12"> {profile.activeRuleCount === 0 && (profile.projectCount || profile.isDefault) && ( - <Alert className="big-spacer-bottom" variant="warning"> + <FlagMessage variant="warning"> {profile.projectCount !== undefined && profile.projectCount > 0 && translate('quality_profiles.warning.used_by_projects_no_rules')} {!profile.projectCount && profile.isDefault && translate('quality_profiles.warning.is_default_no_rules')} - </Alert> + </FlagMessage> )} <ProfileInheritance @@ -64,10 +60,19 @@ function ProfileDetails(props: ProfileDetailsProps) { updateProfiles={props.updateProfiles} /> <ProfileProjects profile={profile} /> + {profile.actions?.edit && !profile.isBuiltIn && <ProfilePermissions profile={profile} />} + </div> + <div className="sw-flex sw-flex-col sw-gap-12"> + <ProfileRules profile={profile} /> + <ProfileExporters exporters={exporters} profile={profile} /> </div> </div> - </div> + </ContentWrapper> ); } +const ContentWrapper = styled.div` + color: ${themeColor('pageContent')}; +`; + export default withQualityProfilesContext(ProfileDetails); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx index baf9b2c1f66..876da408600 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx @@ -17,10 +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 { Link, SubTitle } from 'design-system'; import * as React from 'react'; import { getQualityProfileExporterUrl } from '../../../api/quality-profiles'; -import Link from '../../../components/common/Link'; -import { Alert } from '../../../components/ui/Alert'; import { translate } from '../../../helpers/l10n'; import { Exporter, Profile } from '../types'; @@ -37,29 +36,17 @@ export default function ProfileExporters({ exporters, profile }: Props) { } return ( - <section - aria-label={translate('quality_profiles.exporters')} - className="boxed-group quality-profile-exporters" - > - <h2>{translate('quality_profiles.exporters')}</h2> - <div className="boxed-group-inner"> - <Alert className="big-spacer-bottom" variant="warning"> - {translate('quality_profiles.exporters.deprecated')} - </Alert> - <ul> - {exportersForLanguage.map((exporter, index) => ( - <li - className={index > 0 ? 'spacer-top' : undefined} - data-key={exporter.key} - key={exporter.key} - > - <Link to={getQualityProfileExporterUrl(exporter, profile)} target="_blank"> - {exporter.name} - </Link> - </li> - ))} - </ul> + <section aria-label={translate('quality_profiles.exporters')}> + <div> + <SubTitle>{translate('quality_profiles.exporters')}</SubTitle> </div> + <ul className="sw-flex sw-flex-col sw-gap-2"> + {exportersForLanguage.map((exporter) => ( + <li data-key={exporter.key} key={exporter.key}> + <Link to={getQualityProfileExporterUrl(exporter, profile)}>{exporter.name}</Link> + </li> + ))} + </ul> </section> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx index d622f6ce9de..421fc552beb 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx @@ -17,28 +17,20 @@ * 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, Breadcrumbs, HoverLink, Link } from 'design-system'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { FormattedMessage } from 'react-intl'; -import { NavLink } from 'react-router-dom'; -import Link from '../../../components/common/Link'; -import HelpTooltip from '../../../components/controls/HelpTooltip'; -import Tooltip from '../../../components/controls/Tooltip'; import { useLocation } from '../../../components/hoc/withRouter'; import DateFromNow from '../../../components/intl/DateFromNow'; +import { AdminPageHeader } from '../../../components/ui/AdminPageHeader'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getQualityProfileUrl } from '../../../helpers/urls'; import BuiltInQualityProfileBadge from '../components/BuiltInQualityProfileBadge'; import ProfileActions from '../components/ProfileActions'; -import ProfileLink from '../components/ProfileLink'; import { PROFILE_PATH } from '../constants'; import { QualityProfilePath } from '../routes'; import { Profile } from '../types'; -import { - getProfileChangelogPath, - getProfilesForLanguagePath, - isProfileComparePath, -} from '../utils'; +import { getProfileChangelogPath, isProfileComparePath } from '../utils'; interface Props { profile: Profile; @@ -53,7 +45,7 @@ export default function ProfileHeader(props: Props) { const isChangeLogPage = location.pathname.endsWith(`/${QualityProfilePath.CHANGELOG}`); return ( - <header className="page-header quality-profile-header"> + <div className="it__quality-profiles__header"> {(isComparePage || isChangeLogPage) && ( <Helmet defer={false} @@ -65,87 +57,58 @@ export default function ProfileHeader(props: Props) { )} /> )} - <nav className="note spacer-bottom" aria-label={translate('breadcrumbs')}> - <ul className="list-breadcrumbs"> - <li> - <NavLink end to={PROFILE_PATH}> - {translate('quality_profiles.page')} - </NavLink> - </li> - <li> - <Link to={getProfilesForLanguagePath(profile.language)}>{profile.languageName}</Link> - </li> - </ul> - </nav> - <h1 className="page-title"> - <ProfileLink language={profile.language} name={profile.name}> - <span>{profile.name}</span> - </ProfileLink> - {profile.isDefault && ( - <Tooltip overlay={translate('quality_profiles.list.default.help')}> - <span className=" spacer-left badge">{translate('default')}</span> - </Tooltip> - )} - {profile.isBuiltIn && ( - <BuiltInQualityProfileBadge className="spacer-left" tooltip={false} /> - )} - </h1> - {!isProfileComparePath(location.pathname) && ( - <div className="pull-right"> - <ul className="list-inline" style={{ lineHeight: '24px' }}> - <li className="small spacer-right"> - {translate('quality_profiles.updated_')} <DateFromNow date={profile.rulesUpdatedAt} /> - </li> - <li className="small big-spacer-right"> - {translate('quality_profiles.used_')} <DateFromNow date={profile.lastUsed} /> - </li> - <li> - <Link className="button" to={getProfileChangelogPath(profile.name, profile.language)}> - {translate('changelog')} - </Link> - </li> + <Breadcrumbs className="sw-mb-6"> + <HoverLink to={PROFILE_PATH}>{translate('quality_profiles.page')}</HoverLink> + <HoverLink to={getQualityProfileUrl(profile.name, profile.language)}> + {profile.languageName} + </HoverLink> + </Breadcrumbs> - <li> - <ProfileActions - className="pull-left" - profile={profile} - isComparable={isComparable} - updateProfiles={updateProfiles} - /> - </li> - </ul> - </div> - )} + <AdminPageHeader + description={profile.isBuiltIn && translate('quality_profiles.built_in.description')} + title={ + <span className="sw-inline-flex sw-items-center sw-gap-2"> + {profile.name} + {profile.isBuiltIn && <BuiltInQualityProfileBadge tooltip={false} />} + {profile.isDefault && <Badge>{translate('default')}</Badge>} + </span> + } + > + <div className="sw-flex sw-items-center sw-gap-3 sw-self-start"> + {!isProfileComparePath(location.pathname) && ( + <> + <div> + <strong className="sw-body-sm-highlight"> + {translate('quality_profiles.updated_')} + </strong>{' '} + <DateFromNow date={profile.rulesUpdatedAt} /> + </div> + <div> + <strong className="sw-body-sm-highlight"> + {translate('quality_profiles.used_')} + </strong>{' '} + <DateFromNow date={profile.lastUsed} /> + </div> - {profile.isBuiltIn && ( - <div className="page-description">{translate('quality_profiles.built_in.description')}</div> - )} + <div> + <Link + className="it__quality-profiles__changelog" + to={getProfileChangelogPath(profile.name, profile.language)} + > + {translate('see_changelog')} + </Link> + </div> + </> + )} - {profile.parentKey && profile.parentName && ( - <div className="page-description"> - <FormattedMessage - defaultMessage={translate('quality_profiles.extend_description')} - id="quality_profiles.extend_description" - values={{ - link: ( - <> - <Link to={getQualityProfileUrl(profile.parentName, profile.language)}> - {profile.parentName} - </Link> - <HelpTooltip - className="little-spacer-left" - overlay={translateWithParameters( - 'quality_profiles.extend_description_help', - profile.parentName, - )} - /> - </> - ), - }} + <ProfileActions + profile={profile} + isComparable={isComparable} + updateProfiles={updateProfiles} /> </div> - )} - </header> + </AdminPageHeader> + </div> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx index d3f8f987abd..f620b519ce3 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx @@ -18,9 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import classNames from 'classnames'; +import { ButtonSecondary, Spinner, SubTitle, Table } from 'design-system'; import * as React from 'react'; import { getProfileInheritance } from '../../../api/quality-profiles'; -import { Button } from '../../../components/controls/buttons'; import { translate } from '../../../helpers/l10n'; import { ProfileInheritanceDetails } from '../../../types/types'; import { Profile } from '../types'; @@ -107,7 +107,7 @@ export default class ProfileInheritance extends React.PureComponent<Props, State render() { const { profile, profiles } = this.props; - const { ancestors } = this.state; + const { ancestors, loading, formOpen, children } = this.state; const highlightCurrent = !this.state.loading && @@ -115,71 +115,65 @@ export default class ProfileInheritance extends React.PureComponent<Props, State this.state.children != null && (ancestors.length > 0 || this.state.children.length > 0); - const extendsBuiltIn = ancestors != null && ancestors.some((profile) => profile.isBuiltIn); + const extendsBuiltIn = ancestors?.some((profile) => profile.isBuiltIn); return ( <section aria-label={translate('quality_profiles.profile_inheritance')} - className="boxed-group quality-profile-inheritance" + className="it__quality-profiles__inheritance" > - {profile.actions && profile.actions.edit && !profile.isBuiltIn && ( - <div className="boxed-group-actions"> - <Button className="pull-right js-change-parent" onClick={this.handleChangeParentClick}> + <div className="sw-flex sw-items-center sw-gap-3 sw-mb-6"> + <SubTitle className="sw-mb-0"> + {translate('quality_profiles.profile_inheritance')} + </SubTitle> + {profile.actions?.edit && !profile.isBuiltIn && ( + <ButtonSecondary + className="it__quality-profiles__change-parent" + onClick={this.handleChangeParentClick} + > {translate('quality_profiles.change_parent')} - </Button> - </div> - )} - - <div className="boxed-group-header"> - <h2>{translate('quality_profiles.profile_inheritance')}</h2> - </div> - - <div className="boxed-group-inner"> - {this.state.loading ? ( - <i className="spinner" /> - ) : ( - <table className="data zebra"> - <tbody> - {ancestors != null && - ancestors.map((ancestor, index) => ( - <ProfileInheritanceBox - depth={index} - key={ancestor.key} - language={profile.language} - profile={ancestor} - type="ancestor" - /> - ))} - - {this.state.profile != null && ( - <ProfileInheritanceBox - className={classNames({ - selected: highlightCurrent, - })} - depth={ancestors ? ancestors.length : 0} - displayLink={false} - extendsBuiltIn={extendsBuiltIn} - language={profile.language} - profile={this.state.profile} - /> - )} - - {this.state.children != null && - this.state.children.map((child) => ( - <ProfileInheritanceBox - depth={ancestors ? ancestors.length + 1 : 0} - key={child.key} - language={profile.language} - profile={child} - type="child" - /> - ))} - </tbody> - </table> + </ButtonSecondary> )} </div> - {this.state.formOpen && ( + <Spinner loading={loading}> + <Table columnCount={3} noSidePadding> + {ancestors?.map((ancestor, index) => ( + <ProfileInheritanceBox + depth={index} + key={ancestor.key} + language={profile.language} + profile={ancestor} + type="ancestor" + /> + ))} + + {this.state.profile && ( + <ProfileInheritanceBox + className={classNames({ + selected: highlightCurrent, + })} + depth={ancestors ? ancestors.length : 0} + displayLink={false} + extendsBuiltIn={extendsBuiltIn} + language={profile.language} + profile={this.state.profile} + /> + )} + + {children?.map((child) => ( + <ProfileInheritanceBox + depth={ancestors ? ancestors.length + 1 : 0} + key={child.key} + language={profile.language} + profile={child} + type="child" + /> + ))} + </Table> + </Spinner> + + {formOpen && ( <ChangeParentForm onChange={this.handleParentChange} onClose={this.closeForm} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.tsx index 877b9106f5e..29774e6e34e 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import classNames from 'classnames'; +import { ContentCell, HelperHintIcon, TableRow } from 'design-system'; import * as React from 'react'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import { translate, translateWithParameters } from '../../../helpers/l10n'; @@ -48,29 +49,30 @@ export default function ProfileInheritanceBox(props: Props) { const offset = 25 * depth; return ( - <tr className={classNames(`it__quality-profiles__inheritance-${type}`, className)}> - <td> - <div style={{ paddingLeft: offset }}> + <TableRow className={classNames(`it__quality-profiles__inheritance-${type}`, className)}> + <ContentCell> + <div className="sw-flex sw-items-center sw-gap-2" style={{ paddingLeft: offset }}> {displayLink ? ( - <ProfileLink className="text-middle" language={language} name={profile.name}> + <ProfileLink language={language} name={profile.name}> {profile.name} </ProfileLink> ) : ( - <span className="text-middle">{profile.name}</span> + <span>{profile.name}</span> )} - {profile.isBuiltIn && <BuiltInQualityProfileBadge className="spacer-left" />} + {profile.isBuiltIn && <BuiltInQualityProfileBadge />} {extendsBuiltIn && ( - <HelpTooltip - className="spacer-left" - overlay={translate('quality_profiles.extends_built_in')} - /> + <HelpTooltip overlay={translate('quality_profiles.extends_built_in')}> + <HelperHintIcon aria-label="help-tooltip" /> + </HelpTooltip> )} </div> - </td> + </ContentCell> - <td>{translateWithParameters('quality_profile.x_active_rules', profile.activeRuleCount)}</td> + <ContentCell> + {translateWithParameters('quality_profile.x_active_rules', profile.activeRuleCount)} + </ContentCell> - <td> + <ContentCell> {profile.overridingRuleCount != null && ( <p> {translateWithParameters( @@ -79,7 +81,7 @@ export default function ProfileInheritanceBox(props: Props) { )} </p> )} - </td> - </tr> + </ContentCell> + </TableRow> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissions.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissions.tsx index 527b0bc9b04..f9cadf77ec0 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissions.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 { ButtonSecondary, Note, Spinner, SubTitle } from 'design-system'; import { sortBy, uniqBy } from 'lodash'; import * as React from 'react'; import { @@ -24,7 +25,6 @@ import { searchGroups, searchUsers, } from '../../../api/quality-profiles'; -import { Button } from '../../../components/controls/buttons'; import { translate } from '../../../helpers/l10n'; import { UserSelected } from '../../../types/types'; import { Profile } from '../types'; @@ -137,45 +137,43 @@ export default class ProfilePermissions extends React.PureComponent<Props, State }; render() { + const { loading } = this.state; + return ( - <section aria-label={translate('permissions.page')} className="boxed-group"> - <h2>{translate('permissions.page')}</h2> - <div className="boxed-group-inner"> - <p className="note">{translate('quality_profiles.default_permissions')}</p> - - {this.state.loading ? ( - <div className="big-spacer-top"> - <i className="spinner" /> - </div> - ) : ( - <div className="big-spacer-top"> - {this.state.users && - sortBy(this.state.users, 'name').map((user) => ( - <ProfilePermissionsUser - key={user.login} - onDelete={this.handleUserDelete} - profile={this.props.profile} - user={user} - /> - ))} - {this.state.groups && - sortBy(this.state.groups, 'name').map((group) => ( - <ProfilePermissionsGroup - group={group} - key={group.name} - onDelete={this.handleGroupDelete} - profile={this.props.profile} - /> - ))} - <div className="text-right"> - <Button onClick={this.handleAddUserButtonClick}> - {translate('quality_profiles.grant_permissions_to_more_users')} - </Button> - </div> - </div> - )} + <section aria-label={translate('permissions.page')}> + <div className="sw-mb-6"> + <SubTitle className="sw-mb-0">{translate('permissions.page')}</SubTitle> + <Note as="p">{translate('quality_profiles.default_permissions')}</Note> </div> + <Spinner loading={loading}> + <ul className="sw-flex sw-flex-col sw-gap-4 sw-max-w-[238px]"> + {this.state.users && + sortBy(this.state.users, 'name').map((user) => ( + <ProfilePermissionsUser + key={user.login} + onDelete={this.handleUserDelete} + profile={this.props.profile} + user={user} + /> + ))} + {this.state.groups && + sortBy(this.state.groups, 'name').map((group) => ( + <ProfilePermissionsGroup + group={group} + key={group.name} + onDelete={this.handleGroupDelete} + profile={this.props.profile} + /> + ))} + </ul> + <div className="sw-mt-6"> + <ButtonSecondary onClick={this.handleAddUserButtonClick}> + {translate('quality_profiles.grant_permissions_to_more_users')} + </ButtonSecondary> + </div> + </Spinner> + {this.state.addUserForm && ( <ProfilePermissionsForm onClose={this.handleAddUserFormClose} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsGroup.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsGroup.tsx index 9f8f8c70966..96e4b46b076 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsGroup.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsGroup.tsx @@ -17,12 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { DestructiveIcon, GenericAvatar, TrashIcon, UserGroupIcon } from 'design-system'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { removeGroup } from '../../../api/quality-profiles'; import SimpleModal, { ChildrenProps } from '../../../components/controls/SimpleModal'; -import { Button, DeleteButton, ResetButtonLink } from '../../../components/controls/buttons'; -import GroupIcon from '../../../components/icons/GroupIcon'; +import { Button, ResetButtonLink } from '../../../components/controls/buttons'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { Group } from './ProfilePermissions'; @@ -101,19 +101,23 @@ export default class ProfilePermissionsGroup extends React.PureComponent<Props, const { group } = this.props; return ( - <div className="clearfix big-spacer-bottom"> - <DeleteButton + <li className="sw-flex sw-items-center sw-justify-between sw-mb-4"> + <div className="sw-flex sw-items-center sw-truncate"> + <GenericAvatar + Icon={UserGroupIcon} + className="sw-mr-3 sw-grow-0 sw-shrink-0" + name={group.name} + /> + <strong className="sw-body-sm-highlight sw-truncate fs-mask">{group.name}</strong> + </div> + <DestructiveIcon + Icon={TrashIcon} aria-label={translateWithParameters( 'quality_profiles.permissions.remove.group_x', group.name, )} - className="pull-right spacer-top spacer-left spacer-right button-small" onClick={this.handleDeleteClick} /> - <GroupIcon className="pull-left spacer-right" size={32} /> - <div className="overflow-hidden" style={{ lineHeight: '32px' }}> - <strong>{group.name}</strong> - </div> {this.state.deleteModal && ( <SimpleModal @@ -124,7 +128,7 @@ export default class ProfilePermissionsGroup extends React.PureComponent<Props, {this.renderDeleteModal} </SimpleModal> )} - </div> + </li> ); } } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx index c3fd75e9137..7b351b4a786 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx @@ -17,12 +17,12 @@ * 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, DestructiveIcon, Note, TrashIcon } from 'design-system'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { removeUser } from '../../../api/quality-profiles'; import SimpleModal, { ChildrenProps } from '../../../components/controls/SimpleModal'; -import { DeleteButton, ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; -import LegacyAvatar from '../../../components/ui/LegacyAvatar'; +import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { UserSelected } from '../../../types/types'; @@ -105,25 +105,22 @@ export default class ProfilePermissionsUser extends React.PureComponent<Props, S const { user } = this.props; return ( - <div className="clearfix big-spacer-bottom"> - <DeleteButton + <li className="sw-flex sw-items-center sw-justify-between sw-mb-4"> + <div className="sw-flex sw-items-center sw-truncate"> + <Avatar className="sw-mr-3 sw-grow-0 sw-shrink-0" hash={user.avatar} name={user.name} /> + <div className="sw-truncate fs-mask"> + <strong className="sw-body-sm-highlight">{user.name}</strong> + <Note className="sw-block">{user.login}</Note> + </div> + </div> + <DestructiveIcon + Icon={TrashIcon} aria-label={translateWithParameters( 'quality_profiles.permissions.remove.user_x', user.name, )} - className="pull-right spacer-top spacer-left spacer-right button-small" onClick={this.handleDeleteClick} /> - <LegacyAvatar - className="pull-left spacer-right" - hash={user.avatar} - name={user.name} - size={32} - /> - <div className="overflow-hidden"> - <strong>{user.name}</strong> - <div className="note">{user.login}</div> - </div> {this.state.deleteModal && ( <SimpleModal @@ -134,7 +131,7 @@ export default class ProfilePermissionsUser extends React.PureComponent<Props, S {this.renderDeleteModal} </SimpleModal> )} - </div> + </li> ); } } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx index a1447c0d1da..63781b84a06 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx @@ -17,13 +17,20 @@ * 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, + ButtonSecondary, + ContentCell, + Link, + Spinner, + SubTitle, + Table, + TableRow, + Tooltip, +} from 'design-system'; import * as React from 'react'; import { getProfileProjects } from '../../../api/quality-profiles'; -import Link from '../../../components/common/Link'; import ListFooter from '../../../components/controls/ListFooter'; -import Tooltip from '../../../components/controls/Tooltip'; -import { Button } from '../../../components/controls/buttons'; -import QualifierIcon from '../../../components/icons/QualifierIcon'; import { translate } from '../../../helpers/l10n'; import { getProjectUrl } from '../../../helpers/urls'; import { Profile } from '../types'; @@ -121,46 +128,54 @@ export default class ProfileProjects extends React.PureComponent<Props, State> { renderDefault() { return ( - <div> - <span className="badge spacer-right">{translate('default')}</span> + <> + <Badge className="sw-mr-2">{translate('default')}</Badge> {translate('quality_profiles.projects_for_default')} - </div> + </> ); } renderProjects() { if (this.state.loading) { - return <i className="spinner" />; + return <Spinner />; } const { projects } = this.state; const { profile } = this.props; if (profile.activeRuleCount === 0 && projects.length === 0) { - return <div>{translate('quality_profiles.cannot_associate_projects_no_rules')}</div>; + return translate('quality_profiles.cannot_associate_projects_no_rules'); } if (projects.length === 0) { - return <div>{translate('quality_profiles.no_projects_associated_to_profile')}</div>; + return translate('quality_profiles.no_projects_associated_to_profile'); } return ( <> - <ul> + <Table columnCount={1} noSidePadding> {projects.map((project) => ( - <li className="spacer-top js-profile-project" data-key={project.key} key={project.key}> - <Link to={getProjectUrl(project.key)}> - <QualifierIcon qualifier="TRK" /> <span>{project.name}</span> - </Link> - </li> + <TableRow key={project.key}> + <ContentCell> + <Link + className="it__quality-profiles__project fs-mask" + to={getProjectUrl(project.key)} + > + {project.name} + </Link> + </ContentCell> + </TableRow> ))} - </ul> - <ListFooter - count={projects.length} - loadMore={this.loadMore} - ready={!this.state.loadingMore} - total={this.state.total} - /> + </Table> + {projects.length > 0 && ( + <ListFooter + useMIUIButtons + count={projects.length} + loadMore={this.loadMore} + loading={this.state.loadingMore} + total={this.state.total} + /> + )} </> ); } @@ -169,9 +184,14 @@ export default class ProfileProjects extends React.PureComponent<Props, State> { const { profile } = this.props; const hasNoActiveRules = profile.activeRuleCount === 0; return ( - <section className="boxed-group quality-profile-projects" aria-label={translate('projects')}> - {profile.actions && profile.actions.associateProjects && ( - <div className="boxed-group-actions"> + // eslint-disable-next-line local-rules/use-metrickey-enum + <section className="it__quality-profiles__projects" aria-label={translate('projects')}> + <div className="sw-flex sw-items-center sw-gap-3 sw-mb-6"> + { + // eslint-disable-next-line local-rules/use-metrickey-enum + <SubTitle className="sw-mb-0">{translate('projects')}</SubTitle> + } + {profile.actions?.associateProjects && ( <Tooltip overlay={ hasNoActiveRules @@ -179,25 +199,19 @@ export default class ProfileProjects extends React.PureComponent<Props, State> { : null } > - <Button - className="js-change-projects" + <ButtonSecondary + className="it__quality-profiles__change-projects" onClick={this.handleChangeClick} disabled={hasNoActiveRules} > {translate('quality_profiles.change_projects')} - </Button> + </ButtonSecondary> </Tooltip> - </div> - )} - - <header className="boxed-group-header"> - <h2>{translate('projects')}</h2> - </header> - - <div className="boxed-group-inner"> - {profile.isDefault ? this.renderDefault() : this.renderProjects()} + )} </div> + {profile.isDefault ? this.renderDefault() : this.renderProjects()} + {this.state.formOpen && <ChangeProjectsForm onClose={this.closeForm} profile={profile} />} </section> ); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx index 4e8b31886f2..e6a9d7f104c 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx @@ -17,21 +17,27 @@ * 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, + ContentCell, + NumericalCell, + SubTitle, + Table, + TableRow, +} from 'design-system/lib'; import { keyBy } from 'lodash'; import * as React from 'react'; import { getQualityProfile } from '../../../api/quality-profiles'; import { searchRules } from '../../../api/rules'; -import Link from '../../../components/common/Link'; -import Tooltip from '../../../components/controls/Tooltip'; -import { Button } from '../../../components/controls/buttons'; +import DocumentationTooltip from '../../../components/common/DocumentationTooltip'; import { translate } from '../../../helpers/l10n'; +import { isDefined } from '../../../helpers/types'; import { getRulesUrl } from '../../../helpers/urls'; import { SearchRulesResponse } from '../../../types/coding-rules'; import { Dict } from '../../../types/types'; import { Profile } from '../types'; import ProfileRulesDeprecatedWarning from './ProfileRulesDeprecatedWarning'; -import ProfileRulesRowOfType from './ProfileRulesRowOfType'; -import ProfileRulesRowTotal from './ProfileRulesRowTotal'; +import ProfileRulesRow from './ProfileRulesRow'; import ProfileRulesSonarWayComparison from './ProfileRulesSonarWayComparison'; const TYPES = ['BUG', 'VULNERABILITY', 'CODE_SMELL', 'SECURITY_HOTSPOT']; @@ -153,71 +159,74 @@ export default class ProfileRules extends React.PureComponent<Props, State> { const { actions = {} } = profile; return ( - <section aria-label={translate('rules')} className="boxed-group quality-profile-rules"> - <div className="quality-profile-rules-distribution"> - <table className="data condensed"> - <thead> - <tr> - <th> - <h2>{translate('rules')}</h2> - </th> - <th>{translate('active')}</th> - <th>{translate('inactive')}</th> - </tr> - </thead> - <tbody> - <ProfileRulesRowTotal - count={this.state.activatedTotal} - qprofile={profile.key} - total={this.state.total} - /> - {TYPES.map((type) => ( - <ProfileRulesRowOfType - count={this.getRulesCountForType(type)} - key={type} - qprofile={profile.key} - total={this.getRulesTotalForType(type)} - type={type} - /> - ))} - </tbody> - </table> + <section aria-label={translate('rules')} className="it__quality-profiles__rules"> + <Table + columnCount={3} + columnWidths={['50%', '25%', '25%']} + header={ + <TableRow> + <ContentCell> + <SubTitle className="sw-mb-0">{translate('rules')}</SubTitle> + </ContentCell> + <NumericalCell>{translate('active')}</NumericalCell> + <NumericalCell>{translate('inactive')}</NumericalCell> + </TableRow> + } + noHeaderTopBorder + noSidePadding + > + <ProfileRulesRow + count={this.state.activatedTotal} + qprofile={profile.key} + total={this.state.total} + /> + {TYPES.map((type) => ( + <ProfileRulesRow + count={this.state.activatedByType[type]?.count} + key={type} + qprofile={profile.key} + total={this.state.allByType[type]?.count} + type={type} + /> + ))} + </Table> + + <div className="sw-mt-6 sw-flex sw-flex-col sw-gap-4 sw-items-start"> + {profile.activeDeprecatedRuleCount > 0 && ( + <ProfileRulesDeprecatedWarning + activeDeprecatedRules={profile.activeDeprecatedRuleCount} + profile={profile.key} + /> + )} + + {isDefined(compareToSonarWay) && compareToSonarWay.missingRuleCount > 0 && ( + <ProfileRulesSonarWayComparison + language={profile.language} + profile={profile.key} + sonarWayMissingRules={compareToSonarWay.missingRuleCount} + sonarway={compareToSonarWay.profile} + /> + )} {actions.edit && !profile.isBuiltIn && ( - <div className="text-right big-spacer-top"> - <Link className="button js-activate-rules" to={activateMoreUrl}> - {translate('quality_profiles.activate_more')} - </Link> - </div> + <ButtonPrimary className="it__quality-profiles__activate-rules" to={activateMoreUrl}> + {translate('quality_profiles.activate_more')} + </ButtonPrimary> )} {/* if a user is allowed to `copy` a profile if they are a global admin */} - {/* this user could potentially active more rules if the profile was not built-in */} + {/* this user could potentially activate more rules if the profile was not built-in */} {/* in such cases it's better to show the button but disable it with a tooltip */} {actions.copy && profile.isBuiltIn && ( - <div className="text-right big-spacer-top"> - <Tooltip overlay={translate('quality_profiles.activate_more.help.built_in')}> - <Button className="disabled js-activate-rules"> - {translate('quality_profiles.activate_more')} - </Button> - </Tooltip> - </div> + <DocumentationTooltip + content={translate('quality_profiles.activate_more.help.built_in')} + > + <ButtonPrimary className="it__quality-profiles__activate-rules" disabled> + {translate('quality_profiles.activate_more')} + </ButtonPrimary> + </DocumentationTooltip> )} </div> - {profile.activeDeprecatedRuleCount > 0 && ( - <ProfileRulesDeprecatedWarning - activeDeprecatedRules={profile.activeDeprecatedRuleCount} - profile={profile.key} - /> - )} - {compareToSonarWay != null && compareToSonarWay.missingRuleCount > 0 && ( - <ProfileRulesSonarWayComparison - language={profile.language} - profile={profile.key} - sonarWayMissingRules={compareToSonarWay.missingRuleCount} - sonarway={compareToSonarWay.profile} - /> - )} </section> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesDeprecatedWarning.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesDeprecatedWarning.tsx index a0cc0e4c90c..914c8305b11 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesDeprecatedWarning.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesDeprecatedWarning.tsx @@ -17,8 +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, HelperHintIcon, Link } from 'design-system'; import * as React from 'react'; -import Link from '../../../components/common/Link'; +import { FormattedMessage } from 'react-intl'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import { translate } from '../../../helpers/l10n'; import { getDeprecatedActiveRulesUrl } from '../../../helpers/urls'; @@ -30,17 +31,24 @@ interface Props { export default function ProfileRulesDeprecatedWarning(props: Props) { return ( - <div className="quality-profile-rules-deprecated clearfix"> - <span className="pull-left"> - <span className="text-middle">{translate('quality_profiles.deprecated_rules')}</span> - <HelpTooltip - className="spacer-left" - overlay={translate('quality_profiles.deprecated_rules_description')} + <FlagMessage variant="warning"> + <div className="sw-flex sw-gap-1"> + <FormattedMessage + defaultMessage={translate('quality_profiles.x_deprecated_rules')} + id="quality_profiles.x_deprecated_rules" + values={{ + count: props.activeDeprecatedRules, + linkCount: ( + <Link to={getDeprecatedActiveRulesUrl({ qprofile: props.profile })}> + {props.activeDeprecatedRules} + </Link> + ), + }} /> - </span> - <Link className="pull-right" to={getDeprecatedActiveRulesUrl({ qprofile: props.profile })}> - {props.activeDeprecatedRules} - </Link> - </div> + <HelpTooltip overlay={translate('quality_profiles.deprecated_rules_description')}> + <HelperHintIcon aria-label="help-tooltip" /> + </HelpTooltip> + </div> + </FlagMessage> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRowOfType.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.tsx index a52e3644763..311dda5c90f 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRowOfType.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.tsx @@ -17,18 +17,20 @@ * 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, Link, Note, NumericalCell, TableRow } from 'design-system'; import * as React from 'react'; -import Link from '../../../components/common/Link'; import IssueTypeIcon from '../../../components/icons/IssueTypeIcon'; import { translate } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; +import { isDefined } from '../../../helpers/types'; import { getRulesUrl } from '../../../helpers/urls'; +import { MetricType } from '../../../types/metrics'; interface Props { count: number | null; qprofile: string; total: number | null; - type: string; + type?: string; } export default function ProfileRulesRowOfType(props: Props) { @@ -48,28 +50,32 @@ export default function ProfileRulesRowOfType(props: Props) { } return ( - <tr> - <td> - <span> - <IssueTypeIcon className="little-spacer-right" query={props.type} /> - {translate('issue.type', props.type, 'plural')} - </span> - </td> - <td className="thin nowrap text-right"> - {props.count != null && ( - <Link to={activeRulesUrl}>{formatMeasure(props.count, 'SHORT_INT', null)}</Link> + <TableRow> + <ContentCell> + {props.type ? ( + <> + <IssueTypeIcon className="sw-mr-1" query={props.type} /> + {translate('issue.type', props.type, 'plural')} + </> + ) : ( + translate('total') )} - </td> - <td className="thin nowrap text-right"> - {inactiveCount != null && + </ContentCell> + <NumericalCell> + {isDefined(props.count) && ( + <Link to={activeRulesUrl}>{formatMeasure(props.count, MetricType.ShortInteger)}</Link> + )} + </NumericalCell> + <NumericalCell> + {isDefined(inactiveCount) && (inactiveCount > 0 ? ( - <Link className="small" to={inactiveRulesUrl}> - {formatMeasure(inactiveCount, 'SHORT_INT', null)} + <Link to={inactiveRulesUrl}> + {formatMeasure(inactiveCount, MetricType.ShortInteger)} </Link> ) : ( - <span className="note">0</span> + <Note>0</Note> ))} - </td> - </tr> + </NumericalCell> + </TableRow> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRowTotal.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRowTotal.tsx deleted file mode 100644 index 37fc1b51bb9..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRowTotal.tsx +++ /dev/null @@ -1,64 +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. - */ -import * as React from 'react'; -import Link from '../../../components/common/Link'; -import { translate } from '../../../helpers/l10n'; -import { formatMeasure } from '../../../helpers/measures'; -import { getRulesUrl } from '../../../helpers/urls'; - -interface Props { - count: number | null; - qprofile: string; - total: number | null; -} - -export default function ProfileRulesRowTotal(props: Props) { - const activeRulesUrl = getRulesUrl({ qprofile: props.qprofile, activation: 'true' }); - const inactiveRulesUrl = getRulesUrl({ qprofile: props.qprofile, activation: 'false' }); - let inactiveCount = null; - if (props.count != null && props.total != null) { - inactiveCount = props.total - props.count; - } - - return ( - <tr> - <td> - <strong>{translate('total')}</strong> - </td> - <td className="thin nowrap text-right"> - {props.count != null && ( - <Link to={activeRulesUrl}> - <strong>{formatMeasure(props.count, 'SHORT_INT', null)}</strong> - </Link> - )} - </td> - <td className="thin nowrap text-right"> - {inactiveCount != null && - (inactiveCount > 0 ? ( - <Link className="small" to={inactiveRulesUrl}> - <strong>{formatMeasure(inactiveCount, 'SHORT_INT', null)}</strong> - </Link> - ) : ( - <span className="note">0</span> - ))} - </td> - </tr> - ); -} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesSonarWayComparison.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesSonarWayComparison.tsx index ade00d68673..279a43620a7 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesSonarWayComparison.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesSonarWayComparison.tsx @@ -17,8 +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, Link } from 'design-system'; import * as React from 'react'; -import Link from '../../../components/common/Link'; +import { FormattedMessage } from 'react-intl'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import { translate } from '../../../helpers/l10n'; import { getRulesUrl } from '../../../helpers/urls'; @@ -39,17 +40,21 @@ export default function ProfileRulesSonarWayComparison(props: Props) { }); return ( - <div className="quality-profile-rules-sonarway-missing clearfix"> - <span className="pull-left"> - <span className="text-middle">{translate('quality_profiles.sonarway_missing_rules')}</span> + <FlagMessage variant="warning"> + <div className="sw-flex sw-items-center sw-gap-1"> + <FormattedMessage + defaultMessage={translate('quality_profiles.x_sonarway_missing_rules')} + id="quality_profiles.x_sonarway_missing_rules" + values={{ + count: props.sonarWayMissingRules, + linkCount: <Link to={url}>{props.sonarWayMissingRules}</Link>, + }} + /> <HelpTooltip - className="spacer-left" + className="sw-ml-2" overlay={translate('quality_profiles.sonarway_missing_rules_description')} /> - </span> - <Link className="pull-right" data-test="rules" to={url}> - {props.sonarWayMissingRules} - </Link> - </div> + </div> + </FlagMessage> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/styles.css b/server/sonar-web/src/main/js/apps/quality-profiles/styles.css index b3636067441..31b8aa0e061 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/styles.css +++ b/server/sonar-web/src/main/js/apps/quality-profiles/styles.css @@ -65,12 +65,6 @@ background-color: var(--alertBackgroundError); } -.quality-profile-rules-sonarway-missing { - margin-top: 20px; - padding: 15px 20px; - background-color: var(--alertBackgroundWarning); -} - .quality-profile-not-found { padding-top: 100px; text-align: center; diff --git a/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx b/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx index 6795a8e06a6..37a73752b57 100644 --- a/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx +++ b/server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx @@ -24,9 +24,9 @@ import Link from '../common/Link'; import DropdownIcon from '../icons/DropdownIcon'; import SettingsIcon from '../icons/SettingsIcon'; import { PopupPlacement } from '../ui/popups'; -import { Button, ButtonPlain } from './buttons'; import Dropdown from './Dropdown'; import Tooltip, { Placement } from './Tooltip'; +import { Button, ButtonPlain } from './buttons'; export interface ActionsDropdownProps { className?: string; diff --git a/server/sonar-web/src/main/js/components/ui/AdminPageHeader.tsx b/server/sonar-web/src/main/js/components/ui/AdminPageHeader.tsx new file mode 100644 index 00000000000..566738cc58a --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/AdminPageHeader.tsx @@ -0,0 +1,52 @@ +/* + * 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. + */ +import { withTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import classNames from 'classnames'; +import { themeColor } from 'design-system'; +import React, { ReactNode } from 'react'; + +interface Props { + children?: ReactNode; + className?: string; + description?: ReactNode; + title: ReactNode; +} + +export function AdminPageHeader({ children, className, description, title }: Props) { + return ( + <div className={classNames('sw-flex sw-justify-between', className)}> + <header className="sw-flex-1"> + <AdminPageTitle className="sw-heading-lg sw-pb-4">{title}</AdminPageTitle> + <AdminPageDescription className="sw-body-sm sw-pb-12 sw-max-w-9/12"> + {description} + </AdminPageDescription> + </header> + {children && <div className="sw-flex sw-gap-2">{children}</div>} + </div> + ); +} +export const AdminPageTitle = withTheme(styled.h1` + color: ${themeColor('pageTitle')}; +`); + +export const AdminPageDescription = withTheme(styled.div` + color: ${themeColor('pageContent')}; +`); diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/AdminPageHeader-test.tsx b/server/sonar-web/src/main/js/components/ui/__tests__/AdminPageHeader-test.tsx new file mode 100644 index 00000000000..411e001642b --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/__tests__/AdminPageHeader-test.tsx @@ -0,0 +1,39 @@ +/* + * 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. + */ +import { screen } from '@testing-library/react'; +import React from 'react'; +import { renderComponent } from '../../../helpers/testReactTestingUtils'; +import { AdminPageHeader } from '../AdminPageHeader'; + +it('render correctly', () => { + renderAdminPageHeader(); + + expect(screen.getByRole('heading', { name: 'Page title' })).toBeInTheDocument(); + expect(screen.getByText('Page description')).toBeInTheDocument(); + expect(screen.getByText('Actions')).toBeInTheDocument(); +}); + +function renderAdminPageHeader() { + return renderComponent( + <AdminPageHeader description="Page description" title="Page title"> + Actions + </AdminPageHeader>, + ); +} |