aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
author7PH <benjamin.raymond@sonarsource.com>2023-09-11 04:44:27 +0200
committersonartech <sonartech@sonarsource.com>2023-09-12 20:02:41 +0000
commit172333c558e538ceb6a45392a91d06a348c6eb64 (patch)
treeb8bcc2295a90ea191455681e560f1d294709a73c /server
parent8df0d85e3c80052d7826c47005c1a83bf6025920 (diff)
downloadsonarqube-172333c558e538ceb6a45392a91d06a348c6eb64.tar.gz
sonarqube-172333c558e538ceb6a45392a91d06a348c6eb64.zip
SONAR-20366 Migrate quality profile individual QP page to new UI
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/design-system/src/components/Dropdown.tsx4
-rw-r--r--server/sonar-web/design-system/src/components/icons/UserGroupIcon.tsx23
-rw-r--r--server/sonar-web/design-system/src/components/icons/index.ts1
-rw-r--r--server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts4
-rw-r--r--server/sonar-web/src/main/js/app/components/GlobalContainer.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx30
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx47
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/BuiltInQualityProfileBadge.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx118
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/QualityProfilesApp.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx29
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx35
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx139
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx110
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.tsx32
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissions.tsx72
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsGroup.tsx24
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx29
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx90
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx131
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesDeprecatedWarning.tsx32
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.tsx (renamed from server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRowOfType.tsx)46
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRowTotal.tsx64
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesSonarWayComparison.tsx25
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/styles.css6
-rw-r--r--server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/ui/AdminPageHeader.tsx52
-rw-r--r--server/sonar-web/src/main/js/components/ui/__tests__/AdminPageHeader-test.tsx39
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>,
+ );
+}