]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20366 Migrate profiles list page
authorstanislavh <stanislav.honcharov@sonarsource.com>
Tue, 3 Oct 2023 12:46:42 +0000 (14:46 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 4 Oct 2023 20:03:19 +0000 (20:03 +0000)
23 files changed:
server/sonar-web/design-system/src/components/input/SearchSelectDropdown.tsx
server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/apps/issues/components/AssigneeSelect.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/MetricSelect.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.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/compare/ComparisonForm.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileNotFound.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/Evolution.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionDeprecated.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/LanguageSelect.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx [deleted file]
server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.tsx
server/sonar-web/src/main/js/apps/quality-profiles/styles.css
server/sonar-web/src/main/js/components/hoc/withRouter.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index ecbd8e59d554e8f886d2c82450ca076deefdd007..355b232f9a330247353c69c6c96a42a448819d0c 100644 (file)
@@ -80,7 +80,9 @@ export function SearchSelectDropdown<
     menuIsOpen,
     onChange,
     onInputChange,
+    isClearable,
     zLevel = PopupZLevel.Global,
+    placeholder = '',
     ...rest
   } = props;
   const [open, setOpen] = React.useState(false);
@@ -94,9 +96,11 @@ export function SearchSelectDropdown<
 
   const ref = React.useRef<Select<Option, IsMulti, Group>>(null);
 
+  const computedControlLabel = controlLabel ?? (value as Option | undefined)?.label ?? null;
+
   const toggleDropdown = React.useCallback(
     (value?: boolean) => {
-      setOpen(value === undefined ? !open : value);
+      setOpen(value ?? !open);
     },
     [open],
   );
@@ -131,6 +135,13 @@ export function SearchSelectDropdown<
     [onInputChange],
   );
 
+  const handleClear = () => {
+    onChange?.(null as OnChangeValue<Option, IsMulti>, {
+      action: 'clear',
+      removedValues: [],
+    });
+  };
+
   React.useEffect(() => {
     if (open) {
       ref.current?.inputRef?.select();
@@ -164,7 +175,9 @@ export function SearchSelectDropdown<
               minLength={minLength}
               onChange={handleChange}
               onInputChange={handleInputChange}
+              placeholder={placeholder}
               selectRef={ref}
+              value={value}
             />
           </StyledSearchSelectWrapper>
         </SearchHighlighterContext.Provider>
@@ -176,8 +189,10 @@ export function SearchSelectDropdown<
         ariaLabel={controlAriaLabel}
         className={className}
         disabled={isDisabled}
+        isClearable={isClearable && Boolean(value)}
         isDiscreet={isDiscreet}
-        label={controlLabel}
+        label={computedControlLabel}
+        onClear={handleClear}
         onClick={() => {
           toggleDropdown(true);
         }}
index 17f36ac498fa9c6f102d053cd8883fa9cfd5d90b..bba4a3623a75d8d5aa68133cc0ba85a4f8c8e9b8 100644 (file)
 
 import styled from '@emotion/styled';
 import classNames from 'classnames';
+import { useIntl } from 'react-intl';
 import tw from 'twin.macro';
 import { INPUT_SIZES, themeBorder, themeColor, themeContrast } from '../../helpers';
 import { Key } from '../../helpers/keyboard';
 import { InputSizeKeys } from '../../types/theme';
-import { ChevronDownIcon } from '../icons';
+import { InteractiveIcon } from '../InteractiveIcon';
+import { ChevronDownIcon, CloseIcon } from '../icons';
 
 interface SearchSelectDropdownControlProps {
   ariaLabel?: string;
   className?: string;
   disabled?: boolean;
+  isClearable?: boolean;
   isDiscreet?: boolean;
   label?: React.ReactNode | string;
+  onClear: VoidFunction;
   onClick: VoidFunction;
   placeholder?: string;
   size?: InputSizeKeys;
@@ -43,11 +47,16 @@ export function SearchSelectDropdownControl(props: SearchSelectDropdownControlPr
     disabled,
     placeholder,
     label,
+    isClearable,
     isDiscreet,
+    onClear,
     onClick,
     size = 'full',
     ariaLabel = '',
   } = props;
+
+  const intl = useIntl();
+
   return (
     <StyledControl
       aria-label={ariaLabel}
@@ -75,8 +84,21 @@ export function SearchSelectDropdownControl(props: SearchSelectDropdownControlPr
           },
         )}
       >
-        <span className="sw-truncate">{label ?? placeholder}</span>
-        <ChevronDownIcon className="sw-ml-1" />
+        <span className="sw-flex-1 sw-truncate">{label ?? placeholder}</span>
+        <div className="sw-flex sw-items-center">
+          {isClearable && (
+            <InteractiveIcon
+              Icon={CloseIcon}
+              aria-label={intl.formatMessage({ id: 'clear' })}
+              currentColor
+              onClick={() => {
+                onClear();
+              }}
+              size="small"
+            />
+          )}
+          <ChevronDownIcon />
+        </div>
       </InputValue>
     </StyledControl>
   );
@@ -91,7 +113,7 @@ const StyledControl = styled.div`
   ${tw`sw-flex sw-justify-between sw-items-center`};
   ${tw`sw-rounded-2`};
   ${tw`sw-box-border`};
-  ${tw`sw-px-3 sw-py-2`};
+  ${tw`sw-px-3`};
   ${tw`sw-body-sm`};
   ${tw`sw-h-control`};
   ${tw`sw-leading-4`};
@@ -128,6 +150,7 @@ const StyledControl = styled.div`
 `;
 
 const InputValue = styled.span`
+  height: 100%;
   width: 100%;
   color: ${themeContrast('inputBackground')};
 
index 31d888d3c1db086267aaad2f9c990690ef1873cd..375edebed5015e85357deec8fb4c67e6ce123afe 100644 (file)
@@ -45,8 +45,7 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND = [
   '/project/issues',
   '/project/activity',
   '/code',
-  '/profiles/show',
-  '/profiles/compare',
+  '/profiles',
   '/project/extension/securityreport/securityreport',
   '/projects',
   '/project/information',
index 2f371153229466a7aa00dcadb15aa344196bee71..56df9ef1fe2c677c1732501f1902f1d9d9f0b2c0 100644 (file)
@@ -91,7 +91,6 @@ export default function AssigneeSelect(props: AssigneeSelectProps) {
       size="full"
       controlSize="full"
       inputId={inputId}
-      isClearable
       defaultOptions={defaultOptions}
       loadOptions={handleAssigneeSearch}
       onChange={props.onAssigneeSelect}
index cc4f2cc0a2cf93d89ef074582b7710b5a352b108..7790b657c466f8ef4c9f71e91450eac5e5465bd4 100644 (file)
@@ -85,7 +85,6 @@ export function MetricSelect({ metric, metricsArray, metrics, onMetricChange }:
       size="large"
       controlSize="full"
       inputId="condition-metric"
-      isClearable
       defaultOptions={optionsWithDomains}
       loadOptions={handleAssigneeSearch}
       onChange={handleChange}
index 42c028346480abcee2ac92503c47e8c9209020ee..bf4200dd188a2318df22f91cd05916f1d91a1c1c 100644 (file)
@@ -73,8 +73,6 @@ export default function QualityGatePermissionsAddModalRenderer(
               controlAriaLabel={translate('quality_gates.permissions.search')}
               inputId={USER_SELECT_INPUT_ID}
               autoFocus
-              isClearable={false}
-              placeholder=""
               defaultOptions
               noOptionsMessage={() => translate('no_results')}
               onChange={props.onSelection}
index d1ddc9772cf9e3539c0ce3c5d7cb02cfb84c400c..88bbd3f7bc95f5467a54fb77ecb2d65ed63ba8c6 100644 (file)
@@ -71,7 +71,7 @@ const ui = {
   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' }),
+  qualityProfilePageLink: byRole('link', { name: 'quality_profiles.back_to_list' }),
   rulesTotalRow: byRole('row', { name: /total/ }),
   rulesBugsRow: byRole('row', { name: /issue.type.BUG.plural/ }),
   rulesVulnerabilitiesRow: byRole('row', { name: /issue.type.VULNERABILITY/ }),
@@ -515,7 +515,9 @@ describe('Every Users', () => {
     renderQualityProfile('i-dont-exist');
     await ui.waitForDataLoaded();
 
-    expect(await screen.findByText('quality_profiles.not_found')).toBeInTheDocument();
+    expect(
+      await screen.findByRole('heading', { name: 'quality_profiles.not_found' }),
+    ).toBeInTheDocument();
     expect(ui.qualityProfilePageLink.get()).toBeInTheDocument();
   });
 
index 9afe310a39b56f7a20537265b586aaf1b1eea7cd..cf51ccf8c44077dea94be7c341dce5a10c5002a2 100644 (file)
@@ -85,7 +85,7 @@ const ui = {
     }),
   activateConfirmButton: byRole('button', { name: 'coding_rules.activate' }),
   namePropupInput: byRole('textbox', { name: 'quality_profiles.new_name required' }),
-  filterByLang: byRole('combobox', { name: 'quality_profiles.filter_by:' }),
+  filterByLang: byRole('combobox', { name: 'quality_profiles.select_lang' }),
   listLinkCQualityProfile: byRole('link', { name: 'c quality profile' }),
   headingNewCQualityProfile: byRole('heading', { name: 'New c quality profile' }),
   headingNewCQualityProfileFromCreateButton: byRole('heading', {
@@ -113,7 +113,7 @@ const ui = {
   stagnantProfilesRegion: byRole('region', { name: 'quality_profiles.stagnant_profiles' }),
   recentlyAddedRulesRegion: byRole('region', { name: 'quality_profiles.latest_new_rules' }),
   newRuleLink: byRole('link', { name: 'Recently Added Rule' }),
-  seeAllNewRulesLink: byRole('link', { name: 'see_all 20 quality_profiles.latest_new_rules' }),
+  seeAllNewRulesLink: byRole('link', { name: 'quality_profiles.latest_new_rules.see_all_x.20' }),
 };
 
 it('should list Quality Profiles and filter by language', async () => {
index ea5c66a1e522eda56f333389c0c53871b7589da1..61380ed4cbf072eb1e39fae907eef1423208feaf 100644 (file)
@@ -45,8 +45,6 @@ export default function ComparisonForm(props: Readonly<Props>) {
     .filter((p) => p.language === profile.language && p !== profile)
     .map((p) => ({ value: p.key, label: p.name, isDefault: p.isDefault }));
 
-  const value = options.find((o) => o.value === withKey);
-
   const handleProfilesSearch = React.useCallback(
     (query: string, cb: (options: Options<Option>) => void) => {
       cb(options.filter((option) => option.label.toLowerCase().includes(query.toLowerCase())));
@@ -58,9 +56,7 @@ export default function ComparisonForm(props: Readonly<Props>) {
     <>
       <span className="sw-mr-2">{intl.formatMessage({ id: 'quality_profiles.compare_with' })}</span>
       <SearchSelectDropdown
-        placeholder=""
         controlPlaceholder={intl.formatMessage({ id: 'select_verb' })}
-        controlLabel={value?.label}
         controlAriaLabel={intl.formatMessage({ id: 'quality_profiles.compare_with' })}
         options={options}
         onChange={(option: Option) => props.onCompare(option.value)}
index 93941beea46bb3b66789627d93f82386c34fe751..bbc277da4a675fe8e9957538544cdf9af5995be9 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 } from 'design-system';
 import * as React from 'react';
-import { NavLink } from 'react-router-dom';
-import { translate } from '../../../helpers/l10n';
+import { useIntl } from 'react-intl';
 import { PROFILE_PATH } from '../constants';
 
 export default function ProfileNotFound() {
-  return (
-    <div className="quality-profile-not-found">
-      <div className="note spacer-bottom">
-        <NavLink end to={PROFILE_PATH}>
-          {translate('quality_profiles.page')}
-        </NavLink>
-      </div>
+  const intl = useIntl();
 
-      <div>{translate('quality_profiles.not_found')}</div>
+  return (
+    <div className="sw-text-center sw-mt-4">
+      <h1 className="sw-body-md-highlight sw-mb-4">
+        {intl.formatMessage({ id: 'quality_profiles.not_found' })}
+      </h1>
+      <Link className="sw-body-sm-highlight" to={PROFILE_PATH}>
+        {intl.formatMessage({ id: 'quality_profiles.back_to_list' })}
+      </Link>
     </div>
   );
 }
index 46445afc1f17d3d2892925dcb9cc317074400346..0990cd13fa6e7e476cc7fd18b98de2fe12a69201 100644 (file)
@@ -29,7 +29,7 @@ export interface EvolutionProps {
 
 export default function Evolution({ profiles }: EvolutionProps) {
   return (
-    <div className="quality-profiles-evolution">
+    <div className="sw-flex sw-flex-col sw-gap-12">
       <EvolutionDeprecated profiles={profiles} />
       <EvolutionStagnant profiles={profiles} />
       <EvolutionRules />
index 5f03e1365d804df9b4c6e9bcbcb6e0130b2de3b9..09b5aedfda7b04aec40d0255933fcee85b655db4 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 { DiscreetLink, FlagMessage, Note } from 'design-system';
 import { sortBy } from 'lodash';
 import * as React from 'react';
-import Link from '../../../components/common/Link';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { useIntl } from 'react-intl';
 import { getDeprecatedActiveRulesUrl } from '../../../helpers/urls';
-import ProfileLink from '../components/ProfileLink';
 import { Profile } from '../types';
+import { getProfilePath } from '../utils';
 
 interface Props {
   profiles: Profile[];
@@ -34,120 +34,131 @@ interface InheritedRulesInfo {
   from: Profile;
 }
 
-export default class EvolutionDeprecated extends React.PureComponent<Props> {
-  getDeprecatedRulesInheritanceChain = (profile: Profile, profilesWithDeprecations: Profile[]) => {
-    let rules: InheritedRulesInfo[] = [];
-    let count = profile.activeDeprecatedRuleCount;
+export default function EvolutionDeprecated({ profiles }: Readonly<Props>) {
+  const intl = useIntl();
 
-    if (count === 0) {
-      return rules;
-    }
+  const profilesWithDeprecations = profiles.filter(
+    (profile) => profile.activeDeprecatedRuleCount > 0,
+  );
 
-    if (profile.parentKey) {
-      const parentProfile = profilesWithDeprecations.find((p) => p.key === profile.parentKey);
-      if (parentProfile) {
-        const parentRules = this.getDeprecatedRulesInheritanceChain(
-          parentProfile,
-          profilesWithDeprecations,
-        );
-        if (parentRules.length) {
-          count -= parentRules.reduce((n, rule) => n + rule.count, 0);
-          rules = rules.concat(parentRules);
-        }
-      }
-    }
+  if (profilesWithDeprecations.length === 0) {
+    return null;
+  }
 
-    if (count > 0) {
-      rules.push({
-        count,
-        from: profile,
-      });
-    }
+  const sortedProfiles = sortBy(profilesWithDeprecations, (p) => -p.activeDeprecatedRuleCount);
 
-    return rules;
-  };
-
-  renderInheritedInfo = (profile: Profile, profilesWithDeprecations: Profile[]) => {
-    const rules = this.getDeprecatedRulesInheritanceChain(profile, profilesWithDeprecations);
-    if (rules.length) {
-      return (
-        <>
-          {rules.map((rule) => {
-            if (rule.from.key === profile.key) {
-              return null;
-            }
-
-            return (
-              <div key={rule.from.key}>
-                {' '}
-                {translateWithParameters(
-                  'coding_rules.filters.inheritance.x_inherited_from_y',
-                  rule.count,
-                  rule.from.name,
+  return (
+    <section aria-label={intl.formatMessage({ id: 'quality_profiles.deprecated_rules' })}>
+      <h2 className="sw-heading-md sw-mb-6">
+        {intl.formatMessage({ id: 'quality_profiles.deprecated_rules' })}
+      </h2>
+      <FlagMessage variant="error" className="sw-mb-3">
+        {intl.formatMessage(
+          { id: 'quality_profiles.deprecated_rules_are_still_activated' },
+          { count: profilesWithDeprecations.length },
+        )}
+      </FlagMessage>
+
+      <ul className="sw-flex sw-flex-col sw-gap-4 sw-body-sm">
+        {sortedProfiles.map((profile) => (
+          <li className="sw-flex sw-flex-col sw-gap-1" key={profile.key}>
+            <div className="sw-truncate">
+              <DiscreetLink to={getProfilePath(profile.name, profile.language)}>
+                {profile.name}
+              </DiscreetLink>
+            </div>
+
+            <Note>
+              {profile.languageName}
+              {', '}
+              <DiscreetLink
+                className="link-no-underline"
+                to={getDeprecatedActiveRulesUrl({ qprofile: profile.key })}
+                aria-label={intl.formatMessage(
+                  { id: 'quality_profile.lang_deprecated_x_rules' },
+                  { count: profile.activeDeprecatedRuleCount, name: profile.languageName },
                 )}
-              </div>
-            );
-          })}
-        </>
-      );
-    }
-    return null;
-  };
+              >
+                {intl.formatMessage(
+                  { id: 'quality_profile.x_rules' },
+                  { count: profile.activeDeprecatedRuleCount },
+                )}
+              </DiscreetLink>
+            </Note>
+            <EvolutionDeprecatedInherited
+              profile={profile}
+              profilesWithDeprecations={profilesWithDeprecations}
+            />
+          </li>
+        ))}
+      </ul>
+    </section>
+  );
+}
 
-  render() {
-    const profilesWithDeprecations = this.props.profiles.filter(
-      (profile) => profile.activeDeprecatedRuleCount > 0,
+function EvolutionDeprecatedInherited(
+  props: Readonly<{
+    profile: Profile;
+    profilesWithDeprecations: Profile[];
+  }>,
+) {
+  const { profile, profilesWithDeprecations } = props;
+  const intl = useIntl();
+  const rules = React.useMemo(
+    () => getDeprecatedRulesInheritanceChain(profile, profilesWithDeprecations),
+    [profile, profilesWithDeprecations],
+  );
+  if (rules.length) {
+    return (
+      <>
+        {rules.map((rule) => {
+          if (rule.from.key === profile.key) {
+            return null;
+          }
+
+          return (
+            <Note key={rule.from.key}>
+              {intl.formatMessage(
+                { id: 'coding_rules.filters.inheritance.x_inherited_from_y' },
+                { count: rule.count, name: rule.from.name },
+              )}
+            </Note>
+          );
+        })}
+      </>
     );
+  }
+  return null;
+}
 
-    if (profilesWithDeprecations.length === 0) {
-      return null;
-    }
+function getDeprecatedRulesInheritanceChain(profile: Profile, profilesWithDeprecations: Profile[]) {
+  let rules: InheritedRulesInfo[] = [];
+  let count = profile.activeDeprecatedRuleCount;
 
-    const sortedProfiles = sortBy(profilesWithDeprecations, (p) => -p.activeDeprecatedRuleCount);
+  if (count === 0) {
+    return rules;
+  }
 
-    return (
-      <section
-        className="boxed-group boxed-group-inner quality-profiles-evolution-deprecated"
-        aria-label={translate('quality_profiles.deprecated_rules')}
-      >
-        <h2 className="h4 spacer-bottom">{translate('quality_profiles.deprecated_rules')}</h2>
-        <div className="spacer-bottom">
-          {translateWithParameters(
-            'quality_profiles.deprecated_rules_are_still_activated',
-            profilesWithDeprecations.length,
-          )}
-        </div>
-        <ul>
-          {sortedProfiles.map((profile) => (
-            <li className="spacer-top" key={profile.key}>
-              <div className="text-ellipsis little-spacer-bottom">
-                <ProfileLink language={profile.language} name={profile.name}>
-                  {profile.name}
-                </ProfileLink>
-              </div>
-              <div className="note">
-                {profile.languageName}
-                {', '}
-                <Link
-                  className="link-no-underline"
-                  to={getDeprecatedActiveRulesUrl({ qprofile: profile.key })}
-                  aria-label={translateWithParameters(
-                    'quality_profile.lang_deprecated_x_rules',
-                    profile.languageName,
-                    profile.activeDeprecatedRuleCount,
-                  )}
-                >
-                  {translateWithParameters(
-                    'quality_profile.x_rules',
-                    profile.activeDeprecatedRuleCount,
-                  )}
-                </Link>
-                {this.renderInheritedInfo(profile, profilesWithDeprecations)}
-              </div>
-            </li>
-          ))}
-        </ul>
-      </section>
-    );
+  if (profile.parentKey) {
+    const parentProfile = profilesWithDeprecations.find((p) => p.key === profile.parentKey);
+    if (parentProfile) {
+      const parentRules = getDeprecatedRulesInheritanceChain(
+        parentProfile,
+        profilesWithDeprecations,
+      );
+      if (parentRules.length) {
+        count -= parentRules.reduce((n, rule) => n + rule.count, 0);
+        rules = rules.concat(parentRules);
+      }
+    }
   }
+
+  if (count > 0) {
+    rules.push({
+      count,
+      from: profile,
+    });
+  }
+
+  return rules;
 }
index abe159f89592781a384de7c5ffb955fc3600655b..6cce1debf5cae17fc1bd308132542d5be5ee05d8 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 { sortBy } from 'lodash';
+import { DiscreetLink, Link, Note } from 'design-system';
+import { noop, sortBy } from 'lodash';
 import * as React from 'react';
+import { useIntl } from 'react-intl';
 import { searchRules } from '../../../api/rules';
-import Link from '../../../components/common/Link';
 import { toShortISO8601String } from '../../../helpers/dates';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { translateWithParameters } from '../../../helpers/l10n';
 import { formatMeasure } from '../../../helpers/measures';
 import { getRulesUrl } from '../../../helpers/urls';
 import { MetricType } from '../../../types/metrics';
-import { Dict, Rule, RuleActivation } from '../../../types/types';
+import { Rule, RuleActivation } from '../../../types/types';
 
 const RULES_LIMIT = 10;
 
-function parseRules(rules: Rule[], actives?: Dict<RuleActivation[]>): ExtendedRule[] {
-  return rules.map((rule) => {
-    const activations = actives?.[rule.key];
-    return { ...rule, activations: activations ? activations.length : 0 };
-  });
-}
-
 interface ExtendedRule extends Rule {
   activations: number;
 }
 
-interface State {
-  latestRules?: ExtendedRule[];
-  latestRulesTotal?: number;
-}
-
-export default class EvolutionRules extends React.PureComponent<{}, State> {
-  periodStartDate: string;
-  mounted = false;
-
-  constructor(props: {}) {
-    super(props);
-    this.state = {};
+export default function EvolutionRules() {
+  const intl = useIntl();
+  const [latestRules, setLatestRules] = React.useState<ExtendedRule[]>();
+  const [latestRulesTotal, setLatestRulesTotal] = React.useState<number>();
+  const periodStartDate = React.useMemo(() => {
     const startDate = new Date();
     startDate.setFullYear(startDate.getFullYear() - 1);
-    this.periodStartDate = toShortISO8601String(startDate);
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-    this.loadLatestRules();
-  }
+    return toShortISO8601String(startDate);
+  }, []);
 
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  loadLatestRules() {
+  React.useEffect(() => {
     const data = {
       asc: false,
-      available_since: this.periodStartDate,
+      available_since: periodStartDate,
       f: 'name,langName,actives',
       ps: RULES_LIMIT,
       s: 'createdAt',
     };
 
-    searchRules(data).then(
-      ({ actives, rules, paging: { total } }) => {
-        if (this.mounted) {
-          this.setState({
-            latestRules: sortBy(parseRules(rules, actives), 'langName'),
-            latestRulesTotal: total,
-          });
-        }
-      },
-      () => {
-        /*noop*/
-      },
-    );
-  }
+    searchRules(data).then(({ actives, rules, paging: { total } }) => {
+      setLatestRules(sortBy(parseRules(rules, actives), 'langName'));
+      setLatestRulesTotal(total);
+    }, noop);
+  }, [periodStartDate]);
 
-  render() {
-    const { latestRulesTotal, latestRules } = this.state;
-
-    if (!latestRulesTotal || !latestRules) {
-      return null;
-    }
+  if (!latestRulesTotal || !latestRules) {
+    return null;
+  }
 
-    const newRulesTitle = translate('quality_profiles.latest_new_rules');
-    const newRulesUrl = getRulesUrl({ available_since: this.periodStartDate });
-    const seeAllRulesText = `${translate('see_all')} ${formatMeasure(
-      latestRulesTotal,
-      MetricType.ShortInteger,
-    )}`;
+  return (
+    <section aria-label={intl.formatMessage({ id: 'quality_profiles.latest_new_rules' })}>
+      <h2 className="sw-heading-md sw-mb-6">
+        {intl.formatMessage({ id: 'quality_profiles.latest_new_rules' })}
+      </h2>
+      <ul className="sw-flex sw-flex-col sw-gap-4 sw-body-sm">
+        {latestRules.map((rule) => (
+          <li className="sw-flex sw-flex-col sw-gap-1" key={rule.key}>
+            <div className="sw-truncate">
+              <DiscreetLink to={getRulesUrl({ rule_key: rule.key })}>{rule.name}</DiscreetLink>
+            </div>
+            <Note className="sw-truncate">
+              {rule.activations
+                ? translateWithParameters(
+                    'quality_profiles.latest_new_rules.activated',
+                    rule.langName!,
+                    rule.activations,
+                  )
+                : translateWithParameters(
+                    'quality_profiles.latest_new_rules.not_activated',
+                    rule.langName!,
+                  )}
+            </Note>
+          </li>
+        ))}
+      </ul>
+      {latestRulesTotal > RULES_LIMIT && (
+        <div className="sw-mt-6 sw-body-sm-highlight">
+          <Link to={getRulesUrl({ available_since: periodStartDate })}>
+            {intl.formatMessage(
+              { id: 'quality_profiles.latest_new_rules.see_all_x' },
+              { count: formatMeasure(latestRulesTotal, MetricType.ShortInteger) },
+            )}
+          </Link>
+        </div>
+      )}
+    </section>
+  );
+}
 
-    return (
-      <section
-        className="boxed-group boxed-group-inner quality-profiles-evolution-rules"
-        aria-label={newRulesTitle}
-      >
-        <h2 className="h4 spacer-bottom">{newRulesTitle}</h2>
-        <ul>
-          {latestRules.map((rule) => (
-            <li className="spacer-top" key={rule.key}>
-              <div className="text-ellipsis">
-                <Link className="link-no-underline" to={getRulesUrl({ rule_key: rule.key })}>
-                  {' '}
-                  {rule.name}
-                </Link>
-                <div className="note">
-                  {rule.activations
-                    ? translateWithParameters(
-                        'quality_profiles.latest_new_rules.activated',
-                        rule.langName!,
-                        rule.activations,
-                      )
-                    : translateWithParameters(
-                        'quality_profiles.latest_new_rules.not_activated',
-                        rule.langName!,
-                      )}
-                </div>
-              </div>
-            </li>
-          ))}
-        </ul>
-        {latestRulesTotal > RULES_LIMIT && (
-          <div className="spacer-top">
-            <Link
-              className="small"
-              to={newRulesUrl}
-              aria-label={`${seeAllRulesText} ${newRulesTitle}`}
-            >
-              {seeAllRulesText}
-            </Link>
-          </div>
-        )}
-      </section>
-    );
-  }
+function parseRules(rules: Rule[], actives?: Record<string, RuleActivation[]>): ExtendedRule[] {
+  return rules.map((rule) => {
+    const activations = actives?.[rule.key]?.length ?? 0;
+    return { ...rule, activations };
+  });
 }
index bb94d2c456a4292ca0c9ea55fbdf4308947453a6..40598fe7de922922e569785e457497c5d523c8f6 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 { DiscreetLink, FlagMessage, Note } from 'design-system';
 import * as React from 'react';
+import { useIntl } from 'react-intl';
 import DateFormatter from '../../../components/intl/DateFormatter';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import ProfileLink from '../components/ProfileLink';
 import { Profile } from '../types';
-import { isStagnant } from '../utils';
+import { getProfilePath, isStagnant } from '../utils';
 
 interface Props {
   profiles: Profile[];
 }
 
 export default function EvolutionStagnant(props: Props) {
+  const intl = useIntl();
   const outdated = props.profiles.filter((profile) => !profile.isBuiltIn && isStagnant(profile));
 
   if (outdated.length === 0) {
@@ -36,38 +37,33 @@ export default function EvolutionStagnant(props: Props) {
   }
 
   return (
-    <section
-      className="boxed-group boxed-group-inner quality-profiles-evolution-stagnant"
-      aria-label={translate('quality_profiles.stagnant_profiles')}
-    >
-      <h2 className="h4 spacer-bottom">{translate('quality_profiles.stagnant_profiles')}</h2>
-      <div className="spacer-bottom">
-        {translate('quality_profiles.not_updated_more_than_year')}
-      </div>
-      <ul>
+    <section aria-label={intl.formatMessage({ id: 'quality_profiles.stagnant_profiles' })}>
+      <h2 className="sw-heading-md sw-mb-6">
+        {intl.formatMessage({ id: 'quality_profiles.stagnant_profiles' })}
+      </h2>
+
+      <FlagMessage variant="warning" className="sw-mb-3">
+        {intl.formatMessage({ id: 'quality_profiles.not_updated_more_than_year' })}
+      </FlagMessage>
+      <ul className="sw-flex sw-flex-col sw-gap-4 sw-body-sm">
         {outdated.map((profile) => (
-          <li className="spacer-top" key={profile.key}>
-            <div className="text-ellipsis">
-              <ProfileLink
-                className="link-no-underline"
-                language={profile.language}
-                name={profile.name}
-              >
+          <li className="sw-flex sw-flex-col sw-gap-1" key={profile.key}>
+            <div className="sw-truncate">
+              <DiscreetLink to={getProfilePath(profile.name, profile.language)}>
                 {profile.name}
-              </ProfileLink>
+              </DiscreetLink>
             </div>
             {profile.rulesUpdatedAt && (
-              <DateFormatter date={profile.rulesUpdatedAt} long>
-                {(formattedDate) => (
-                  <div className="note">
-                    {translateWithParameters(
-                      'quality_profiles.x_updated_on_y',
-                      profile.languageName,
-                      formattedDate,
-                    )}
-                  </div>
-                )}
-              </DateFormatter>
+              <Note>
+                <DateFormatter date={profile.rulesUpdatedAt} long>
+                  {(formattedDate) =>
+                    intl.formatMessage(
+                      { id: 'quality_profiles.x_updated_on_y' },
+                      { name: profile.languageName, date: formattedDate },
+                    )
+                  }
+                </DateFormatter>
+              </Note>
             )}
           </li>
         ))}
index e06487ef75b23bb836bce54f219557a66b0d94e5..af756706f2d9a186bc3ade3f7a92d778da1f7c6b 100644 (file)
@@ -21,6 +21,7 @@ import * as React from 'react';
 import { useOutletContext, useSearchParams } from 'react-router-dom';
 import { QualityProfilesContextProps } from '../qualityProfilesContext';
 import Evolution from './Evolution';
+import LanguageSelect from './LanguageSelect';
 import PageHeader from './PageHeader';
 import ProfilesList from './ProfilesList';
 
@@ -34,11 +35,12 @@ export default function HomeContainer() {
     <div>
       <PageHeader {...context} />
 
-      <div className="page-with-sidebar">
-        <main className="page-main">
+      <div className="sw-grid sw-grid-cols-3 sw-gap-12 sw-mt-12">
+        <main className="sw-col-span-2">
+          <LanguageSelect currentFilter={selectedLanguage} languages={context.languages} />
           <ProfilesList {...context} language={selectedLanguage} />
         </main>
-        <aside className="page-sidebar">
+        <aside>
           <Evolution {...context} />
         </aside>
       </div>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/LanguageSelect.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/LanguageSelect.tsx
new file mode 100644 (file)
index 0000000..974accd
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * 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 { LabelValueSelectOption, SearchSelectDropdown } from 'design-system';
+import * as React from 'react';
+import { useIntl } from 'react-intl';
+import { useRouter } from '../../../components/hoc/withRouter';
+import { PROFILE_PATH } from '../constants';
+import { getProfilesForLanguagePath } from '../utils';
+
+const MIN_LANGUAGES = 2;
+
+interface Props {
+  currentFilter?: string;
+  languages: Array<{ key: string; name: string }>;
+}
+
+export default function LanguageSelect(props: Readonly<Props>) {
+  const { currentFilter, languages } = props;
+  const intl = useIntl();
+  const router = useRouter();
+
+  const options = languages.map((language) => ({
+    label: language.name,
+    value: language.key,
+  }));
+
+  const handleLanguagesSearch = React.useCallback(
+    (query: string, cb: (options: LabelValueSelectOption<string>[]) => void) => {
+      cb(options.filter((option) => option.label.toLowerCase().includes(query.toLowerCase())));
+    },
+    [options],
+  );
+
+  if (languages.length < MIN_LANGUAGES) {
+    return null;
+  }
+
+  return (
+    <div className="sw-mb-4">
+      <span className="sw-mr-2 sw-body-sm-highlight">
+        {intl.formatMessage({ id: 'quality_profiles.filter_by' })}
+      </span>
+      <SearchSelectDropdown
+        className="sw-inline-block"
+        controlPlaceholder={intl.formatMessage({ id: 'quality_profiles.select_lang' })}
+        controlAriaLabel={intl.formatMessage({ id: 'quality_profiles.select_lang' })}
+        options={options}
+        onChange={(option: LabelValueSelectOption<string>) =>
+          router.replace(!option ? PROFILE_PATH : getProfilesForLanguagePath(option.value))
+        }
+        defaultOptions={options}
+        loadOptions={handleLanguagesSearch}
+        autoFocus
+        isClearable
+        controlSize="medium"
+        value={options.find((o) => o.value === currentFilter)}
+      />
+    </div>
+  );
+}
index 6fe50c91189e25cfe9db59faea864fb9e3e97af4..649e5a02f39a3e117701e8eecfdffff5cf7d8895 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, ButtonSecondary, FlagMessage, Link } from 'design-system';
 import * as React from 'react';
+import { useIntl } from 'react-intl';
 import { Actions } from '../../../api/quality-profiles';
-import DocLink from '../../../components/common/DocLink';
-import { Button } from '../../../components/controls/buttons';
-import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
-import { Alert } from '../../../components/ui/Alert';
+import { useLocation, useRouter } from '../../../components/hoc/withRouter';
+import { useDocUrl } from '../../../helpers/docs';
 import { translate } from '../../../helpers/l10n';
 import { Profile } from '../types';
 import { getProfilePath } from '../utils';
@@ -32,106 +32,81 @@ import RestoreProfileForm from './RestoreProfileForm';
 interface Props {
   actions: Actions;
   languages: Array<{ key: string; name: string }>;
-  location: Location;
   profiles: Profile[];
-  router: Router;
   updateProfiles: () => Promise<void>;
 }
 
-interface State {
-  createFormOpen: boolean;
-  restoreFormOpen: boolean;
-}
-
-export class PageHeader extends React.PureComponent<Props, State> {
-  state: State = {
-    createFormOpen: false,
-    restoreFormOpen: false,
-  };
+export default function PageHeader(props: Readonly<Props>) {
+  const { actions, languages, profiles } = props;
+  const intl = useIntl();
+  const location = useLocation();
+  const router = useRouter();
+  const docUrl = useDocUrl();
 
-  handleCreateClick = () => {
-    this.setState({ createFormOpen: true });
-  };
+  const [modal, setModal] = React.useState<'' | 'createProfile' | 'restoreProfile'>('');
 
-  handleCreate = (profile: Profile) => {
-    this.props.updateProfiles().then(
+  const handleCreate = (profile: Profile) => {
+    props.updateProfiles().then(
       () => {
-        this.props.router.push(getProfilePath(profile.name, profile.language));
+        router.push(getProfilePath(profile.name, profile.language));
       },
       () => {},
     );
   };
 
-  closeCreateForm = () => {
-    this.setState({ createFormOpen: false });
-  };
+  const closeModal = () => setModal('');
 
-  handleRestoreClick = () => {
-    this.setState({ restoreFormOpen: true });
-  };
+  return (
+    <header className="sw-grid sw-grid-cols-3 sw-gap-12">
+      <div className="sw-col-span-2">
+        <h1 className="sw-heading-lg sw-mb-4">{translate('quality_profiles.page')}</h1>
+        <div className="sw-body-sm">
+          {intl.formatMessage({ id: 'quality_profiles.intro' })}
 
-  closeRestoreForm = () => {
-    this.setState({ restoreFormOpen: false });
-  };
-
-  render() {
-    const { actions, languages, location, profiles } = this.props;
-    return (
-      <header className="page-header">
-        <h1 className="page-title">{translate('quality_profiles.page')}</h1>
-
-        {actions.create && (
-          <div className="page-actions">
-            <Button
+          <Link className="sw-ml-2" to={docUrl('/instance-administration/quality-profiles/')}>
+            {intl.formatMessage({ id: 'learn_more' })}
+          </Link>
+        </div>
+      </div>
+      {actions.create && (
+        <div className="sw-flex sw-flex-col sw-items-end">
+          <div>
+            <ButtonPrimary
               disabled={languages.length === 0}
               id="quality-profiles-create"
-              onClick={this.handleCreateClick}
+              onClick={() => setModal('createProfile')}
             >
-              {translate('create')}
-            </Button>
-            <Button
-              className="little-spacer-left"
+              {intl.formatMessage({ id: 'create' })}
+            </ButtonPrimary>
+            <ButtonSecondary
+              className="sw-ml-2"
               id="quality-profiles-restore"
-              onClick={this.handleRestoreClick}
+              onClick={() => setModal('restoreProfile')}
             >
-              {translate('restore')}
-            </Button>
-            {languages.length === 0 && (
-              <Alert className="spacer-top" variant="warning">
-                {translate('quality_profiles.no_languages_available')}
-              </Alert>
-            )}
+              {intl.formatMessage({ id: 'restore' })}
+            </ButtonSecondary>
           </div>
-        )}
-
-        <div className="page-description markdown">
-          {translate('quality_profiles.intro1')}
-          <br />
-          {translate('quality_profiles.intro2')}
-          <DocLink className="spacer-left" to="/instance-administration/quality-profiles/">
-            {translate('learn_more')}
-          </DocLink>
+          {languages.length === 0 && (
+            <FlagMessage className="sw-mt-2" variant="warning">
+              {intl.formatMessage({ id: 'quality_profiles.no_languages_available' })}
+            </FlagMessage>
+          )}
         </div>
+      )}
 
-        {this.state.restoreFormOpen && (
-          <RestoreProfileForm
-            onClose={this.closeRestoreForm}
-            onRestore={this.props.updateProfiles}
-          />
-        )}
+      {modal === 'restoreProfile' && (
+        <RestoreProfileForm onClose={closeModal} onRestore={props.updateProfiles} />
+      )}
 
-        {this.state.createFormOpen && (
-          <CreateProfileForm
-            languages={languages}
-            location={location}
-            onClose={this.closeCreateForm}
-            onCreate={this.handleCreate}
-            profiles={profiles}
-          />
-        )}
-      </header>
-    );
-  }
+      {modal === 'createProfile' && (
+        <CreateProfileForm
+          languages={languages}
+          location={location}
+          onClose={closeModal}
+          onCreate={handleCreate}
+          profiles={profiles}
+        />
+      )}
+    </header>
+  );
 }
-
-export default withRouter(PageHeader);
index 2fc20a70c5c8651827b76f8db18103b1b975e0af..37e9269aea3e8ce2e9083767041c4cf16cc81070 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 { ContentCell, FlagMessage, HelperHintIcon, Table, TableRow } from 'design-system';
 import { groupBy, pick, sortBy } from 'lodash';
 import * as React from 'react';
+import { useIntl } from 'react-intl';
 import HelpTooltip from '../../../components/controls/HelpTooltip';
-import { Alert } from '../../../components/ui/Alert';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { Language } from '../../../types/languages';
 import { Dict } from '../../../types/types';
 import { Profile } from '../types';
-import ProfilesListHeader from './ProfilesListHeader';
 import ProfilesListRow from './ProfilesListRow';
 
 interface Props {
@@ -35,95 +34,79 @@ interface Props {
   updateProfiles: () => Promise<void>;
 }
 
-export default class ProfilesList extends React.PureComponent<Props> {
-  renderProfiles(profiles: Profile[]) {
-    return profiles.map((profile) => (
-      <ProfilesListRow
-        key={profile.key}
-        profile={profile}
-        updateProfiles={this.props.updateProfiles}
-        isComparable={profiles.length > 1}
-      />
-    ));
-  }
+export default function ProfilesList(props: Readonly<Props>) {
+  const { profiles, languages, language } = props;
+  const intl = useIntl();
 
-  renderHeader(languageKey: string, profilesCount: number) {
-    const language = this.props.languages.find((l) => l.key === languageKey);
+  const profilesIndex: Dict<Profile[]> = groupBy<Profile>(profiles, (profile) => profile.language);
+  const profilesToShow = language ? pick(profilesIndex, language) : profilesIndex;
 
-    if (!language) {
-      return null;
-    }
+  let languagesToShow = sortBy(languages, ({ name }) => name).map(({ key }) => key);
 
-    return (
-      <thead>
-        <tr>
-          <th>
-            {language.name}
-            {', '}
-            {translateWithParameters('quality_profiles.x_profiles', profilesCount)}
-          </th>
-          <th className="text-right nowrap">
-            {translate('quality_profiles.list.projects')}
-            <HelpTooltip
-              className="table-cell-doc"
-              overlay={
-                <div className="big-padded-top big-padded-bottom">
-                  {translate('quality_profiles.list.projects.help')}
-                </div>
-              }
-            />
-          </th>
-          <th className="text-right nowrap">{translate('quality_profiles.list.rules')}</th>
-          <th className="text-right nowrap">{translate('quality_profiles.list.updated')}</th>
-          <th className="text-right nowrap">{translate('quality_profiles.list.used')}</th>
-          <th>&nbsp;</th>
-        </tr>
-      </thead>
-    );
+  if (language) {
+    languagesToShow = languagesToShow.find((key) => key === language) ? [language] : [];
   }
 
-  renderLanguage = (languageKey: string, profiles: Profile[] | undefined) => {
-    return (
-      <div className="boxed-group boxed-group-inner quality-profiles-table" key={languageKey}>
-        <table className="data zebra zebra-hover" data-language={languageKey}>
-          {profiles !== undefined && this.renderHeader(languageKey, profiles.length)}
-          <tbody>{profiles !== undefined && this.renderProfiles(profiles)}</tbody>
-        </table>
-      </div>
-    );
-  };
-
-  render() {
-    const { profiles, languages, language } = this.props;
-
-    const profilesIndex: Dict<Profile[]> = groupBy<Profile>(
-      profiles,
-      (profile) => profile.language,
-    );
+  const renderHeader = React.useCallback(
+    (languageKey: string, count: number) => {
+      const language = languages.find((l) => l.key === languageKey);
 
-    const profilesToShow = language ? pick(profilesIndex, language) : profilesIndex;
-
-    let languagesToShow: string[];
-    if (language) {
-      languagesToShow = languages.find(({ key }) => key === language) ? [language] : [];
-    } else {
-      languagesToShow = sortBy(languages, ({ name }) => name).map(({ key }) => key);
-    }
-
-    return (
-      <div>
-        <ProfilesListHeader currentFilter={language} languages={languages} />
+      return (
+        <TableRow>
+          <ContentCell>
+            {intl.formatMessage(
+              { id: 'quality_profiles.x_profiles' },
+              { count, name: language?.name },
+            )}
+          </ContentCell>
+          <ContentCell>
+            {intl.formatMessage({ id: 'quality_profiles.list.projects' })}
+            <HelpTooltip
+              className="sw-ml-1"
+              overlay={intl.formatMessage({ id: 'quality_profiles.list.projects.help' })}
+            >
+              <HelperHintIcon />
+            </HelpTooltip>
+          </ContentCell>
+          <ContentCell>{intl.formatMessage({ id: 'quality_profiles.list.rules' })}</ContentCell>
+          <ContentCell>{intl.formatMessage({ id: 'quality_profiles.list.updated' })}</ContentCell>
+          <ContentCell>{intl.formatMessage({ id: 'quality_profiles.list.used' })}</ContentCell>
+          <ContentCell>&nbsp;</ContentCell>
+        </TableRow>
+      );
+    },
+    [languages, intl],
+  );
 
-        {Object.keys(profilesToShow).length === 0 && (
-          <Alert className="spacer-top" variant="warning">
-            {translate('no_results')}
-          </Alert>
-        )}
+  return (
+    <div>
+      {Object.keys(profilesToShow).length === 0 && (
+        <FlagMessage className="sw-mt-4 sw-w-full" variant="warning">
+          {intl.formatMessage({ id: 'no_results' })}
+        </FlagMessage>
+      )}
 
-        {languagesToShow.map((languageKey) =>
-          this.renderLanguage(languageKey, profilesToShow[languageKey]),
-        )}
-      </div>
-    );
-  }
+      {languagesToShow.map((languageKey) => (
+        <Table
+          className="sw-mb-12"
+          noSidePadding
+          noHeaderTopBorder
+          key={languageKey}
+          columnCount={6}
+          columnWidths={['43%', '14%', '14%', '14%', '14%', '1%']}
+          header={renderHeader(languageKey, profilesToShow[languageKey].length)}
+          data-language={languageKey}
+        >
+          {profilesToShow[languageKey].map((profile) => (
+            <ProfilesListRow
+              key={profile.key}
+              profile={profile}
+              updateProfiles={props.updateProfiles}
+              isComparable={profilesToShow[languageKey].length > 1}
+            />
+          ))}
+        </Table>
+      ))}
+    </div>
+  );
 }
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx
deleted file mode 100644 (file)
index b0ec8ba..0000000
+++ /dev/null
@@ -1,72 +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 Select from '../../../components/controls/Select';
-import { Router, withRouter } from '../../../components/hoc/withRouter';
-import { translate } from '../../../helpers/l10n';
-import { PROFILE_PATH } from '../constants';
-import { getProfilesForLanguagePath } from '../utils';
-
-interface Props {
-  currentFilter?: string;
-  languages: Array<{ key: string; name: string }>;
-  router: Router;
-}
-
-export class ProfilesListHeader extends React.PureComponent<Props> {
-  handleChange = (option: { value: string } | null) => {
-    const { router } = this.props;
-
-    router.replace(!option ? PROFILE_PATH : getProfilesForLanguagePath(option.value));
-  };
-
-  render() {
-    const { currentFilter, languages } = this.props;
-    if (languages.length < 2) {
-      return null;
-    }
-
-    const options = languages.map((language) => ({
-      label: language.name,
-      value: language.key,
-    }));
-
-    return (
-      <div className="quality-profiles-list-header clearfix">
-        <label htmlFor="quality-profiles-filter-input" className="spacer-right">
-          {translate('quality_profiles.filter_by')}:
-        </label>
-        <Select
-          className="input-medium"
-          autoFocus
-          id="quality-profiles-filter"
-          inputId="quality-profiles-filter-input"
-          isClearable
-          onChange={this.handleChange}
-          options={options}
-          isSearchable
-          value={options.filter((o) => o.value === currentFilter)}
-        />
-      </div>
-    );
-  }
-}
-
-export default withRouter(ProfilesListHeader);
index 40a19f0bfed743c6b3a20931f350d448b8c9945e..1e87c2231c543de52e68368559a25d09dee81f69 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 { ActionCell, Badge, BaseLink, ContentCell, Link, Note, TableRow } from 'design-system';
 import * as React from 'react';
-import Link from '../../../components/common/Link';
+import { useIntl } from 'react-intl';
 import Tooltip from '../../../components/controls/Tooltip';
 import DateFromNow from '../../../components/intl/DateFromNow';
-import { translate } from '../../../helpers/l10n';
 import { getRulesUrl } from '../../../helpers/urls';
 import BuiltInQualityProfileBadge from '../components/BuiltInQualityProfileBadge';
 import ProfileActions from '../components/ProfileActions';
@@ -34,10 +34,11 @@ export interface ProfilesListRowProps {
   isComparable: boolean;
 }
 
-export function ProfilesListRow(props: ProfilesListRowProps) {
+export function ProfilesListRow(props: Readonly<ProfilesListRowProps>) {
   const { profile, isComparable } = props;
+  const intl = useIntl();
 
-  const offset = 25 * (profile.depth - 1);
+  const offset = 24 * (profile.depth - 1);
   const activeRulesUrl = getRulesUrl({
     qprofile: profile.key,
     activation: 'true',
@@ -49,64 +50,66 @@ export function ProfilesListRow(props: ProfilesListRowProps) {
   });
 
   return (
-    <tr
-      className="quality-profiles-table-row text-middle"
+    <TableRow
+      className="quality-profiles-table-row"
       data-key={profile.key}
       data-name={profile.name}
     >
-      <td className="quality-profiles-table-name text-middle">
-        <div className="display-flex-center" style={{ paddingLeft: offset }}>
-          <div>
-            <ProfileLink language={profile.language} name={profile.name}>
-              {profile.name}
-            </ProfileLink>
-          </div>
-          {profile.isBuiltIn && <BuiltInQualityProfileBadge className="spacer-left" />}
+      <ContentCell>
+        <div className="sw-flex sw-items-center" style={{ paddingLeft: offset }}>
+          <ProfileLink language={profile.language} name={profile.name}>
+            {profile.name}
+          </ProfileLink>
+          {profile.isBuiltIn && <BuiltInQualityProfileBadge className="sw-ml-2" />}
         </div>
-      </td>
+      </ContentCell>
 
-      <td className="quality-profiles-table-projects thin nowrap text-middle text-right">
+      <ContentCell>
         {profile.isDefault ? (
-          <Tooltip overlay={translate('quality_profiles.list.default.help')}>
-            <span className="badge">{translate('default')}</span>
+          <Tooltip overlay={intl.formatMessage({ id: 'quality_profiles.list.default.help' })}>
+            <Badge>{intl.formatMessage({ id: 'default' })}</Badge>
           </Tooltip>
         ) : (
-          <span>{profile.projectCount}</span>
+          <Note>{profile.projectCount}</Note>
         )}
-      </td>
+      </ContentCell>
 
-      <td className="quality-profiles-table-rules thin nowrap text-middle text-right">
+      <ContentCell>
         <div>
+          <Link to={activeRulesUrl}>{profile.activeRuleCount}</Link>
+
           {profile.activeDeprecatedRuleCount > 0 && (
-            <span className="spacer-right">
-              <Tooltip overlay={translate('quality_profiles.deprecated_rules')}>
-                <Link className="badge badge-error" to={deprecatedRulesUrl}>
-                  {profile.activeDeprecatedRuleCount}
-                </Link>
+            <span className="sw-ml-2">
+              <Tooltip overlay={intl.formatMessage({ id: 'quality_profiles.deprecated_rules' })}>
+                <BaseLink to={deprecatedRulesUrl} className="sw-border-0">
+                  <Badge variant="deleted">{profile.activeDeprecatedRuleCount}</Badge>
+                </BaseLink>
               </Tooltip>
             </span>
           )}
-
-          <Link to={activeRulesUrl}>{profile.activeRuleCount}</Link>
         </div>
-      </td>
+      </ContentCell>
 
-      <td className="quality-profiles-table-date thin nowrap text-middle text-right">
-        <DateFromNow date={profile.rulesUpdatedAt} />
-      </td>
+      <ContentCell>
+        <Note>
+          <DateFromNow date={profile.rulesUpdatedAt} />
+        </Note>
+      </ContentCell>
 
-      <td className="quality-profiles-table-date thin nowrap text-middle text-right">
-        <DateFromNow date={profile.lastUsed} />
-      </td>
+      <ContentCell>
+        <Note>
+          <DateFromNow date={profile.lastUsed} />
+        </Note>
+      </ContentCell>
 
-      <td className="quality-profiles-table-actions thin nowrap text-middle text-right">
+      <ActionCell>
         <ProfileActions
           isComparable={isComparable}
           profile={profile}
           updateProfiles={props.updateProfiles}
         />
-      </td>
-    </tr>
+      </ActionCell>
+    </TableRow>
   );
 }
 
index 31b8aa0e061d1327f3e0393906256b6b51bf45ba..291324828b80d1e7392d9abcb9bb94a6790fa73f 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.
  */
-.quality-profiles-table {
-  padding-top: 7px;
-}
-
-.quality-profiles-table-inheritance {
-  width: 280px;
-}
-
-.quality-profiles-table-projects,
-.quality-profiles-table-rules,
-.quality-profiles-table-date {
-  min-width: 80px;
-}
-
-.quality-profiles-list-header {
-  line-height: var(--controlHeight);
-  margin-bottom: 20px;
-  padding: 5px 10px;
-  border-bottom: 1px solid var(--barBorderColor);
-}
-
-.quality-profile-grid {
-  display: flex;
-  justify-content: space-between;
-  align-items: flex-start;
-}
-
-.quality-profile-grid-left {
-  width: 340px;
-  flex-shrink: 0;
-}
-
-.quality-profile-grid-right {
-  flex-grow: 1;
-  margin-left: 20px;
-}
-
-.quality-profile-rules-distribution {
-  margin-bottom: 15px;
-  padding: 7px 20px 0;
-}
-
-.quality-profile-rules-deprecated {
-  margin-top: 20px;
-  padding: 15px 20px;
-  background-color: var(--alertBackgroundError);
-}
-
-.quality-profile-not-found {
-  padding-top: 100px;
-  text-align: center;
-}
-
-.quality-profiles-evolution {
-  padding-top: 55px;
-}
-
-.quality-profiles-evolution-deprecated {
-  border-color: var(--alertBorderError);
-  background-color: var(--alertBackgroundError);
-}
-
-.quality-profiles-evolution-stagnant {
-  border-color: var(--alertBorderWarning);
-  background-color: var(--alertBackgroundWarning);
-}
-
-.quality-profiles-evolution-deprecated h2,
-.quality-profiles-evolution-stagnant h2,
-.quality-profiles-evolution-rules h2 {
-  padding: 0;
-}
 
 .quality-profile-changelog-rule-cell {
   line-height: 1.5;
   word-break: break-word;
 }
 
-.quality-profile-compare-right-table:not(.has-first-column) td,
-.quality-profile-compare-right-table:not(.has-first-column) th {
-  /* Aligns the first column with the second one (50%) and add usual cell padding */
-  padding-left: calc(50% + 10px);
-}
-
 #create-profile-form .radio-card {
   width: 244px;
   background-color: var(--neutral50);
index 90b9a5fdfdbd17412421ca82919156783fb63550..329c601e0eca6a7f6efb77e6505333c2bbaa5120 100644 (file)
@@ -24,7 +24,6 @@ import {
   useLocation as useLocationRouter,
   useNavigate,
   useParams,
-  useSearchParams,
 } from 'react-router-dom';
 import { queryToSearch, searchParamsToQuery } from '../../helpers/urls';
 import { RawQuery } from '../../types/types';
@@ -49,33 +48,9 @@ export function withRouter<P extends Partial<WithRouterProps>>(
   WrappedComponent: React.ComponentType<P>,
 ): React.ComponentType<Omit<P, keyof WithRouterProps>> {
   function ComponentWithRouterProp(props: P) {
-    const locationRouter = useLocationRouter();
-    const navigate = useNavigate();
+    const router = useRouter();
     const params = useParams();
-    const [searchParams] = useSearchParams();
-
-    const router = React.useMemo(
-      () => ({
-        replace: (path: string | Partial<Location>) => {
-          if ((path as Location).query) {
-            path.search = queryToSearch((path as Location).query);
-          }
-          navigate(path, { replace: true });
-        },
-        push: (path: string | Partial<Location>) => {
-          if ((path as Location).query) {
-            path.search = queryToSearch((path as Location).query);
-          }
-          navigate(path);
-        },
-      }),
-      [navigate],
-    );
-
-    const location = {
-      ...locationRouter,
-      query: searchParamsToQuery(searchParams),
-    };
+    const location = useLocation();
 
     return <WrappedComponent {...props} location={location} params={params} router={router} />;
   }
@@ -88,6 +63,30 @@ export function withRouter<P extends Partial<WithRouterProps>>(
   return ComponentWithRouterProp;
 }
 
+export function useRouter() {
+  const navigate = useNavigate();
+
+  const router = React.useMemo(
+    () => ({
+      replace: (path: string | Partial<Location>) => {
+        if ((path as Location).query) {
+          path.search = queryToSearch((path as Location).query);
+        }
+        navigate(path, { replace: true });
+      },
+      push: (path: string | Partial<Location>) => {
+        if ((path as Location).query) {
+          path.search = queryToSearch((path as Location).query);
+        }
+        navigate(path);
+      },
+    }),
+    [navigate],
+  );
+
+  return router;
+}
+
 export function useLocation() {
   const location = useLocationRouter();
 
index 5ea2c62b47bc95129e054aac770b253b0cc7c680..cc85b2a30a4079616d55c7000940bdb2d47bfe48 100644 (file)
@@ -1979,7 +1979,8 @@ quality_profiles.page_title_changelog_x={0} Changelog
 quality_profiles.page_title_compare_x={0} Comparison
 quality_profiles.new_profile=New Quality Profile
 quality_profiles.compare_with=Compare with
-quality_profiles.filter_by=Filter profiles by
+quality_profiles.filter_by=Filter by
+quality_profiles.select_lang=Select language
 quality_profiles.restore_profile=Restore Profile
 quality_profiles.restore_profile.success={1} rule(s) restored in profile "{0}"
 quality_profiles.restore_profile.warning={1} rule(s) restored, {2} rule(s) ignored in profile "{0}"
@@ -2012,14 +2013,14 @@ quality_profiles.changelog.UPDATED=Updated
 quality_profiles.changelog.parameter_reset_to_default_value=Parameter {0} reset to default value
 quality_profiles.deleted_profile=The profile {0} doesn't exist anymore
 quality_profiles.projects_for_default=Every project not specifically associated with a quality profile will be associated to this one by default.
-quality_profile.x_rules={0} rule(s)
-quality_profile.lang_deprecated_x_rules={0}, {1} deprecated rule(s)
+quality_profile.x_rules={count} rule(s)
+quality_profile.lang_deprecated_x_rules={name}, {count} deprecated rule(s)
 quality_profile.x_active_rules={0} active rules
 quality_profiles.x_overridden_rules={0} overridden rules
 quality_profiles.change_parent=Change Parent
 quality_profiles.change_parent_warning=By changing the parent of this profile, any information on inherited rules that were manually disabled will be lost. This means some previously disabled rules might be re-enabled.
 quality_profiles.all_profiles=All Profiles
-quality_profiles.x_profiles={0} profile(s)
+quality_profiles.x_profiles={name}, {count} profile(s)
 quality_profiles.projects.select_hint=Click to associate this project with the quality profile
 quality_profiles.projects.deselect_hint=Click to remove association between this project and the quality profile
 quality_profile.empty_comparison=The quality profiles are equal.
@@ -2027,24 +2028,25 @@ quality_profiles.activate_more=Activate More
 quality_profiles.activate_more.help.built_in=This quality profile is built in, and cannot be updated manually. If you want to activate more rules, create a new profile that inherits from this one and add rules there.
 quality_profiles.activate_more_rules=Activate More Rules
 quality_profiles.comparison.activate_rule=Activate rule for profile "{profile}"
-quality_profiles.intro1=Quality profiles are collections of rules to apply during an analysis.
-quality_profiles.intro2=For each language there is a default profile. All projects not explicitly assigned to some other profile will be analyzed with the default. Ideally, all projects will use the same profile for a language.
+quality_profiles.intro=Quality profiles are collections of rules to apply during an analysis. For each language there is a default profile. All projects not explicitly assigned to some other profile will be analyzed with the default. Ideally, all projects will use the same profile for a language.
 quality_profiles.list.projects=Projects
 quality_profiles.list.projects.help=Projects assigned to a profile will always be analyzed with it for that language, regardless of which profile is the default. Quality profile administrators may assign projects to a non-default profile, or always make it follow the system default. Project administrators may choose any profile for each language.
 quality_profiles.list.rules=Rules
 quality_profiles.list.updated=Updated
 quality_profiles.list.used=Used
 quality_profiles.list.default.help=For each language there is a default profile. All projects not explicitly assigned to some other profile will be analyzed with the default.
-quality_profiles.x_updated_on_y={0}, updated on {1}
+quality_profiles.x_updated_on_y={name}, updated on {date}
 quality_profiles.change_projects=Change Projects
 quality_profiles.not_found=The requested quality profile was not found.
+quality_profiles.back_to_list=Go back to the list of Quality Profiles
 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.latest_new_rules.see_all_x=See all {count} recently added rules
 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.deprecated_rules_are_still_activated=Deprecated rules are still activated on {count} 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
@@ -2327,7 +2329,7 @@ coding_rules.filters.inheritance=Inheritance
 coding_rules.filters.inheritance.inactive=Inheritance criterion is available when an inherited Quality Profile is selected
 coding_rules.filters.inheritance.none=Not Inherited
 coding_rules.filters.inheritance.inherited=Inherited
-coding_rules.filters.inheritance.x_inherited_from_y={0} inherited from "{1}"
+coding_rules.filters.inheritance.x_inherited_from_y={count} inherited from "{name}"
 coding_rules.filters.inheritance.overrides=Overridden
 coding_rules.filters.key=Key
 coding_rules.filters.language=Language