]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20366 Migrate quality profile individual QP page to new UI
author7PH <benjamin.raymond@sonarsource.com>
Mon, 11 Sep 2023 02:44:27 +0000 (04:44 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 12 Sep 2023 20:02:41 +0000 (20:02 +0000)
32 files changed:
server/sonar-web/design-system/src/components/Dropdown.tsx
server/sonar-web/design-system/src/components/icons/UserGroupIcon.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/icons/index.ts
server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx
server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/BuiltInQualityProfileBadge.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/QualityProfilesApp.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissions.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsGroup.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesDeprecatedWarning.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRowOfType.tsx [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRowTotal.tsx [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesSonarWayComparison.tsx
server/sonar-web/src/main/js/apps/quality-profiles/styles.css
server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx
server/sonar-web/src/main/js/components/ui/AdminPageHeader.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/__tests__/AdminPageHeader-test.tsx [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 6fb33b5853bb0fc8fa708f3213e9d6c3d4f965e8..1e2f47de5975d4ea4d472252e6af81e44ddcaa20 100644 (file)
@@ -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 (file)
index 0000000..9b6cb6d
--- /dev/null
@@ -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');
index a10b249dd7ab01b87de6723229b06ac6c09c12a4..9cbd5c2611899d087769b1f86f93f744b5b6e247 100644 (file)
@@ -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';
index 237dabd2da8f07022c364d006cfa56c12d5ba8b7..712447b19c87020b39549e6454b39bf37460a65a 100644 (file)
@@ -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() {
index abcf9c142c4e421572c593b7014745c7bd7c03f2..97aa3eaae2e1bc389afa99ab725a3ec1e1d5ea9a 100644 (file)
@@ -45,6 +45,7 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND = [
   '/project/issues',
   '/project/activity',
   '/code',
+  '/profiles/show',
   '/project/extension/securityreport/securityreport',
   '/projects',
   '/project/information',
index 39f2ab60075e30ac7d0e428ff2454d59edf67471..0eab39edea89023f8e78a8aa33afb4520da6e078 100644 (file)
@@ -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',
index 8c5a30d4cd97f230a1e3e30fd3627815fd8eb2a3..ec48942dc25f9b92ec9a137eac7af19c705cceba 100644 (file)
@@ -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();
index 035038aa57911f263c9e3377bd3c1573fa8b8418..9dfbc9e583a3916c785fdefb952894396d6f9e54 100644 (file)
@@ -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) {
index 3f6553d3f49295d30168d6fee99ac132f35acf18..cba17ac635a6c1fc3c0847f7f621ac8ed5c9cb6c 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import 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>
 
index 12ded3bcae8f88749e2b88cf94c0e78dac9b71cf..72f7a2bc9e94b41b5596e982587bfb3ce6be9046 100644 (file)
@@ -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>
   );
 }
index 3f7d208399b6e39d45b9e956bcbc5adbf6b94bac..761d73fea286ad6cc07a9625b1d99d8729e34783 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { LargeCenteredLayout, 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>
     );
   }
 }
index eb1143a57e3b4bb163e3d4e0ffa34a86211264bd..2db4d407bd1c66186057d148b8adf6c880e9b012 100644 (file)
@@ -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>,
   );
index 29032b3b21538415765c9de261889bf83c576467..14f69388436cd00cd20212b5a39ec96e4352fed7 100644 (file)
@@ -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);
index baf9b2c1f66fad4e65ebfb386df570370ca6f5fd..876da408600595d50966d785dc09896fc4f4f2d3 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Link, 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>
   );
 }
index d622f6ce9de064fc381230fc2ea0918689b58030..421fc552beb36bd4cc30e2eb4e031fdd22e4e3e2 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { 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>
   );
 }
index d3f8f987abd53b7ab3dfc625b157f7c1abe5e6f2..f620b519ce3bb78692f77f1e79de5a42600c1137 100644 (file)
@@ -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}
index 877b9106f5e8ec6ed731e426262f71430f64052d..29774e6e34e102a41fcab6b8c2d8c2b0c6df3b10 100644 (file)
@@ -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>
   );
 }
index 527b0bc9b04921fd0d6979d6c1073d4520a81433..f9cadf77ec0e616bafcaabde827bbf7ae3045561 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { 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}
index 9f8f8c709661f6523046a46071dd8c6dfdbcc512..96e4b46b076b429cbefdf72eda52bf648bdfb8e5 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { 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>
     );
   }
 }
index c3fd75e91378d80f34783998448004ae05ebad15..7b351b4a7868407b04b26b56342294a97a80b5d4 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Avatar, 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>
     );
   }
 }
index a1447c0d1da1e04b45152fa4e1585fcae53ef4cd..63781b84a0690aad6158996dad9a2abce4dff20a 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import {
+  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>
     );
index 4e8b31886f2de6be977d4298a6d2d0b4e56720ab..e6a9d7f104cf7f4f274ede7e9d28d50dfd930d9a 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import {
+  ButtonPrimary,
+  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>
     );
   }
index a0cc0e4c90c6a0ad20ca8360e183bf9499dbec26..914c8305b118aeaa73a042b4c99b4ab5184d73c6 100644 (file)
@@ -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/ProfileRulesRow.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.tsx
new file mode 100644 (file)
index 0000000..311dda5
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * 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 { ContentCell, Link, Note, NumericalCell, TableRow } from 'design-system';
+import * as React from 'react';
+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;
+}
+
+export default function ProfileRulesRowOfType(props: Props) {
+  const activeRulesUrl = getRulesUrl({
+    qprofile: props.qprofile,
+    activation: 'true',
+    types: props.type,
+  });
+  const inactiveRulesUrl = getRulesUrl({
+    qprofile: props.qprofile,
+    activation: 'false',
+    types: props.type,
+  });
+  let inactiveCount = null;
+  if (props.count != null && props.total != null) {
+    inactiveCount = props.total - props.count;
+  }
+
+  return (
+    <TableRow>
+      <ContentCell>
+        {props.type ? (
+          <>
+            <IssueTypeIcon className="sw-mr-1" query={props.type} />
+            {translate('issue.type', props.type, 'plural')}
+          </>
+        ) : (
+          translate('total')
+        )}
+      </ContentCell>
+      <NumericalCell>
+        {isDefined(props.count) && (
+          <Link to={activeRulesUrl}>{formatMeasure(props.count, MetricType.ShortInteger)}</Link>
+        )}
+      </NumericalCell>
+      <NumericalCell>
+        {isDefined(inactiveCount) &&
+          (inactiveCount > 0 ? (
+            <Link to={inactiveRulesUrl}>
+              {formatMeasure(inactiveCount, MetricType.ShortInteger)}
+            </Link>
+          ) : (
+            <Note>0</Note>
+          ))}
+      </NumericalCell>
+    </TableRow>
+  );
+}
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/ProfileRulesRowOfType.tsx
deleted file mode 100644 (file)
index a52e364..0000000
+++ /dev/null
@@ -1,75 +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 IssueTypeIcon from '../../../components/icons/IssueTypeIcon';
-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;
-  type: string;
-}
-
-export default function ProfileRulesRowOfType(props: Props) {
-  const activeRulesUrl = getRulesUrl({
-    qprofile: props.qprofile,
-    activation: 'true',
-    types: props.type,
-  });
-  const inactiveRulesUrl = getRulesUrl({
-    qprofile: props.qprofile,
-    activation: 'false',
-    types: props.type,
-  });
-  let inactiveCount = null;
-  if (props.count != null && props.total != null) {
-    inactiveCount = props.total - props.count;
-  }
-
-  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>
-        )}
-      </td>
-      <td className="thin nowrap text-right">
-        {inactiveCount != null &&
-          (inactiveCount > 0 ? (
-            <Link className="small" to={inactiveRulesUrl}>
-              {formatMeasure(inactiveCount, 'SHORT_INT', null)}
-            </Link>
-          ) : (
-            <span className="note">0</span>
-          ))}
-      </td>
-    </tr>
-  );
-}
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 (file)
index 37fc1b5..0000000
+++ /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>
-  );
-}
index ade00d686732d3cc4f03b4adee7fdd9a128d5ae5..279a43620a725b534805419207003d11d5273cb6 100644 (file)
@@ -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>
   );
 }
index b36360674412fe3e2ae05a454fc5986622b2e5bf..31b8aa0e061d1327f3e0393906256b6b51bf45ba 100644 (file)
   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;
index 6795a8e06a64f02b18df54e99eb0e996b184688d..37a73752b57c23bc12c2e52e90b3449ff161ae1c 100644 (file)
@@ -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 (file)
index 0000000..566738c
--- /dev/null
@@ -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 (file)
index 0000000..411e001
--- /dev/null
@@ -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>,
+  );
+}
index b1fa53d3b8289c114e45d4414a993f0760a10d11..e8eb048d6da724f52324b8a0a6695c15ec823e8c 100644 (file)
@@ -36,6 +36,7 @@ by_=by
 calendar=Calendar
 cancel=Cancel
 category=Category
+see_changelog=See Changelog
 changelog=Changelog
 change_verb=Change
 check_all=Check all
@@ -1974,6 +1975,7 @@ quality_profiles.cannot_associate_projects_no_rules=You must activate at least 1
 quality_profiles.cannot_set_default_no_rules=You must activate at least 1 rule before you can make this profile the default profile.
 quality_profiles.warning.used_by_projects_no_rules=The current profile is used on several projects, but it has no active rules. Please activate at least 1 rule for this profile.
 quality_profiles.warning.is_default_no_rules=The current profile is the default profile, but it has no active rules. Please activate at least 1 rule for this profile.
+quality_profiles.x_sonarway_missing_rules={linkCount} Sonar way {count, plural, one {rule} other {rules}} not included
 quality_profiles.parent=Parent:
 quality_profiles.parameter_set_to=Parameter {0} set to {1}
 quality_profiles.x_rules_only_in={0} rules only in
@@ -2018,10 +2020,12 @@ quality_profiles.latest_new_rules=Recently Added Rules
 quality_profiles.latest_new_rules.activated={0}, activated on {1} profile(s)
 quality_profiles.latest_new_rules.not_activated={0}, not yet activated
 quality_profiles.deprecated_rules=Deprecated Rules
+quality_profiles.x_deprecated_rules={linkCount} deprecated {count, plural, one {rule} other {rules}}
 quality_profiles.deprecated_rules_description=These deprecated rules will eventually disappear. You should proactively investigate replacing them.
 quality_profiles.deprecated_rules_are_still_activated=Deprecated rules are still activated on {0} quality profile(s):
 quality_profiles.sonarway_missing_rules=Sonar way rules not included
 quality_profiles.sonarway_missing_rules_description=Recommended rules are missing from your profile
+quality_profiles.x_sonarway_missing_rules={linkCount} Sonar way {count, plural, one {rule} other {rules}} not included
 quality_profiles.stagnant_profiles=Stagnant Profiles
 quality_profiles.not_updated_more_than_year=The following profiles haven't been updated for more than 1 year:
 quality_profiles.exporters=Exporters