]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20366 Migrate profile compare page
authorstanislavh <stanislav.honcharov@sonarsource.com>
Wed, 27 Sep 2023 14:19:08 +0000 (16:19 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 29 Sep 2023 20:02:47 +0000 (20:02 +0000)
Co-authored-by: Benjamin Raymond <31401273+7PH@users.noreply.github.com>
13 files changed:
server/sonar-web/__mocks__/react-intl.tsx
server/sonar-web/design-system/src/components/Table.tsx
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/app/components/global-search/__tests__/GlobalSearch-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/ComparisonContainer.tsx
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonForm.tsx
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResultActivation.tsx
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.tsx
server/sonar-web/src/main/js/components/ui/AdminPageHeader.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index dbd784e1301ed3a8e44d892f86c9c4a461774e27..aa20e55fbda6bc5cd930259b978b1bc73be6dc39 100644 (file)
@@ -21,6 +21,9 @@ import * as React from 'react';
 
 module.exports = {
   ...jest.requireActual('react-intl'),
+  useIntl: () => ({
+    formatMessage: ({ id }, values = {}) => [id, ...Object.values(values)].join('.'),
+  }),
   FormattedMessage: ({ id, values }: { id: string; values?: { [x: string]: React.ReactNode } }) => {
     return (
       <>
index b44ce861b8bb4ce7c6eaf064ce4a4572615e4bca..0e24be3e3240f0f657beaaa96cf01c2be8c911a3 100644 (file)
@@ -26,6 +26,7 @@ import { themeBorder, themeColor } from '../helpers';
 import { FCProps } from '../types/misc';
 
 export interface TableProps extends ComponentProps<'table'> {
+  caption?: ReactNode;
   columnCount: number;
   columnWidths?: Array<number | string>;
   header?: ReactNode;
@@ -39,6 +40,7 @@ export function Table(props: TableProps) {
     columnCount,
     columnWidths = [],
     header,
+    caption,
     children,
     noHeaderTopBorder,
     noSidePadding,
@@ -49,7 +51,7 @@ export function Table(props: TableProps) {
     <StyledTable
       className={classNames(
         { 'no-header-top-border': noHeaderTopBorder, 'no-side-padding': noSidePadding },
-        className
+        className,
       )}
       {...rest}
     >
@@ -58,11 +60,21 @@ export function Table(props: TableProps) {
           <col key={i} width={columnWidths[i] ?? 'auto'} />
         ))}
       </colgroup>
+
+      {caption && (
+        <caption>
+          <div className="sw-py-4 sw-text-middle sw-flex sw-justify-center sw-body-sm-highlight">
+            {caption}
+          </div>
+        </caption>
+      )}
+
       {header && (
         <thead>
           <CellTypeContext.Provider value="th">{header}</CellTypeContext.Provider>
         </thead>
       )}
+
       <tbody>{children}</tbody>
     </StyledTable>
   );
index d6f0fe887d63e9ff23d05700227d97067de17c87..ecbd8e59d554e8f886d2c82450ca076deefdd007 100644 (file)
@@ -48,12 +48,13 @@ export interface SearchSelectDropdownProps<
   V,
   Option extends LabelValueSelectOption<V>,
   IsMulti extends boolean = false,
-  Group extends GroupBase<Option> = GroupBase<Option>
+  Group extends GroupBase<Option> = GroupBase<Option>,
 > extends SelectProps<V, Option, IsMulti, Group>,
     AsyncProps<Option, IsMulti, Group> {
   className?: string;
   controlAriaLabel?: string;
   controlLabel?: React.ReactNode | string;
+  controlPlaceholder?: string;
   controlSize?: InputSizeKeys;
   isDiscreet?: boolean;
   zLevel?: PopupZLevel;
@@ -63,7 +64,7 @@ export function SearchSelectDropdown<
   V,
   Option extends LabelValueSelectOption<V>,
   IsMulti extends boolean = false,
-  Group extends GroupBase<Option> = GroupBase<Option>
+  Group extends GroupBase<Option> = GroupBase<Option>,
 >(props: SearchSelectDropdownProps<V, Option, IsMulti, Group>) {
   const {
     className,
@@ -71,6 +72,7 @@ export function SearchSelectDropdown<
     value,
     loadOptions,
     controlLabel,
+    controlPlaceholder,
     controlSize,
     isDisabled,
     minLength,
@@ -96,7 +98,7 @@ export function SearchSelectDropdown<
     (value?: boolean) => {
       setOpen(value === undefined ? !open : value);
     },
-    [open]
+    [open],
   );
 
   const handleChange = React.useCallback(
@@ -104,14 +106,14 @@ export function SearchSelectDropdown<
       toggleDropdown(false);
       onChange?.(newValue, actionMeta);
     },
-    [toggleDropdown, onChange]
+    [toggleDropdown, onChange],
   );
 
   const handleLoadOptions = React.useCallback(
     (query: string, callback: (options: OptionsOrGroups<Option, Group>) => void) => {
       return query.length >= (minLength ?? 0) ? loadOptions?.(query, callback) : undefined;
     },
-    [minLength, loadOptions]
+    [minLength, loadOptions],
   );
   const debouncedLoadOptions = React.useRef(debounce(handleLoadOptions, DEBOUNCE_DELAY));
 
@@ -126,7 +128,7 @@ export function SearchSelectDropdown<
       onInputChange?.(newValue, actionMeta);
       return newValue;
     },
-    [onInputChange]
+    [onInputChange],
   );
 
   React.useEffect(() => {
@@ -179,6 +181,7 @@ export function SearchSelectDropdown<
         onClick={() => {
           toggleDropdown(true);
         }}
+        placeholder={controlPlaceholder}
         size={controlSize}
       />
     </DropdownToggler>
index 8f6bbf78e0e5e0746233d888834e4e9db840cdcc..17f36ac498fa9c6f102d053cd8883fa9cfd5d90b 100644 (file)
@@ -33,11 +33,21 @@ interface SearchSelectDropdownControlProps {
   isDiscreet?: boolean;
   label?: React.ReactNode | string;
   onClick: VoidFunction;
+  placeholder?: string;
   size?: InputSizeKeys;
 }
 
 export function SearchSelectDropdownControl(props: SearchSelectDropdownControlProps) {
-  const { className, disabled, label, isDiscreet, onClick, size = 'full', ariaLabel = '' } = props;
+  const {
+    className,
+    disabled,
+    placeholder,
+    label,
+    isDiscreet,
+    onClick,
+    size = 'full',
+    ariaLabel = '',
+  } = props;
   return (
     <StyledControl
       aria-label={ariaLabel}
@@ -62,10 +72,10 @@ export function SearchSelectDropdownControl(props: SearchSelectDropdownControlPr
           {
             'is-disabled': disabled,
             'is-placeholder': !label,
-          }
+          },
         )}
       >
-        <span className="sw-truncate">{label}</span>
+        <span className="sw-truncate">{label ?? placeholder}</span>
         <ChevronDownIcon className="sw-ml-1" />
       </InputValue>
     </StyledControl>
index 733b53be1ed5ebed9a48bc6f605f80aa6b0691ca..31d888d3c1db086267aaad2f9c990690ef1873cd 100644 (file)
@@ -46,6 +46,7 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND = [
   '/project/activity',
   '/code',
   '/profiles/show',
+  '/profiles/compare',
   '/project/extension/securityreport/securityreport',
   '/projects',
   '/project/information',
index 92885b62591b744d215937e67ef3d78461fde119..ae3197eb2a7c21eecdede7a2aff5a2eb6a229a3d 100644 (file)
@@ -60,7 +60,7 @@ const ui = {
   searchItemListWrapper: byRole('menu'),
   searchItem: byRole('menuitem'),
   showMoreButton: byRole('menuitem', { name: 'show_more' }),
-  tooShortWarning: byText('select2.tooShort'),
+  tooShortWarning: byText('select2.tooShort.2'),
   noResultTextABCD: byText('no_results_for_x.abcd'),
 };
 
index 2be89439d938833f5e09abb2513d12b4eebf19f0..9afe310a39b56f7a20537265b586aaf1b1eea7cd 100644 (file)
@@ -103,7 +103,7 @@ const ui = {
   nameCreatePopupInput: byRole('textbox', { name: 'name field_required' }),
   comparisonDiffTableHeading: (rulesQuantity: number, profileName: string) =>
     byRole('columnheader', {
-      name: `quality_profiles.x_rules_only_in.${rulesQuantity} ${profileName}`,
+      name: `quality_profiles.x_rules_only_in.${rulesQuantity}.${profileName}`,
     }),
   comparisonModifiedTableHeading: (rulesQuantity: number) =>
     byRole('table', {
index 77103a126f4143390e47e6e740fcff9f752cb816..260ea4a98eafca3f06adc9170fb2652ee9dd1dc7 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 { Spinner } from 'design-system';
 import * as React from 'react';
 import { compareProfiles, CompareResponse } from '../../../api/quality-profiles';
 import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
@@ -95,29 +96,29 @@ class ComparisonContainer extends React.PureComponent<Props, State> {
     const { withKey } = location.query;
 
     return (
-      <div className="boxed-group boxed-group-inner js-profile-comparison">
-        <ComparisonForm
-          onCompare={this.handleCompare}
-          profile={profile}
-          profiles={profiles}
-          withKey={withKey}
-        />
+      <div className="sw-body-sm">
+        <div className="sw-flex sw-items-center">
+          <ComparisonForm
+            onCompare={this.handleCompare}
+            profile={profile}
+            profiles={profiles}
+            withKey={withKey}
+          />
 
-        {this.state.loading && <i className="spinner spacer-left" />}
+          <Spinner className="sw-ml-2" loading={this.state.loading} />
+        </div>
 
         {this.hasResults(this.state) && (
-          <div className="spacer-top">
-            <ComparisonResults
-              inLeft={this.state.inLeft}
-              inRight={this.state.inRight}
-              left={this.state.left}
-              leftProfile={profile}
-              modified={this.state.modified}
-              refresh={this.loadResults}
-              right={this.state.right}
-              rightProfile={profiles.find((p) => p.key === withKey)}
-            />
-          </div>
+          <ComparisonResults
+            inLeft={this.state.inLeft}
+            inRight={this.state.inRight}
+            left={this.state.left}
+            leftProfile={profile}
+            modified={this.state.modified}
+            refresh={this.loadResults}
+            right={this.state.right}
+            rightProfile={profiles.find((p) => p.key === withKey)}
+          />
         )}
       </div>
     );
index 38b80eec7ac48314e6b2d8d3f20ab24fcf4b9029..ea5c66a1e522eda56f333389c0c53871b7589da1 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, SearchSelectDropdown } from 'design-system';
 import * as React from 'react';
-import { components, OptionProps, SingleValueProps } from 'react-select';
-import Select from '../../../components/controls/Select';
+import { useIntl } from 'react-intl';
+import { OptionProps, Options, components } from 'react-select';
 import Tooltip from '../../../components/controls/Tooltip';
-import { translate } from '../../../helpers/l10n';
 import { Profile } from '../types';
 
 interface Props {
@@ -36,69 +36,61 @@ interface Option {
   label: string;
   isDefault: boolean | undefined;
 }
-export default class ComparisonForm extends React.PureComponent<Props> {
-  handleChange = (option: { value: string }) => {
-    this.props.onCompare(option.value);
-  };
 
-  optionRenderer(
-    options: Option[],
-    props: OptionProps<Omit<Option, 'label' | 'isDefault'>, false>,
-  ) {
-    const { data } = props;
-    return <components.Option {...props}>{renderValue(data, options)}</components.Option>;
-  }
+export default function ComparisonForm(props: Readonly<Props>) {
+  const { profile, profiles, withKey } = props;
+  const intl = useIntl();
 
-  singleValueRenderer = (
-    options: Option[],
-    props: SingleValueProps<Omit<Option, 'label' | 'isDefault'>, false>,
-  ) => (
-    <components.SingleValue {...props}>{renderValue(props.data, options)}</components.SingleValue>
-  );
+  const options = profiles
+    .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);
 
-  render() {
-    const { profile, profiles, withKey } = this.props;
-    const options = profiles
-      .filter((p) => p.language === profile.language && p !== profile)
-      .map((p) => ({ value: p.key, label: p.name, isDefault: p.isDefault }));
+  const handleProfilesSearch = React.useCallback(
+    (query: string, cb: (options: Options<Option>) => void) => {
+      cb(options.filter((option) => option.label.toLowerCase().includes(query.toLowerCase())));
+    },
+    [options],
+  );
 
-    return (
-      <div>
-        <label htmlFor="quality-profiles-comparison-input" className="spacer-right">
-          {translate('quality_profiles.compare_with')}
-        </label>
-        <Select
-          className="input-super-large"
-          autoFocus
-          isClearable={false}
-          id="quality-profiles-comparision"
-          inputId="quality-profiles-comparison-input"
-          onChange={this.handleChange}
-          options={options}
-          isSearchable
-          components={{
-            Option: this.optionRenderer.bind(this, options),
-            SingleValue: this.singleValueRenderer.bind(null, options),
-          }}
-          value={options.filter((o) => o.value === withKey)}
-        />
-      </div>
-    );
-  }
+  return (
+    <>
+      <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)}
+        defaultOptions={options}
+        loadOptions={handleProfilesSearch}
+        components={{
+          Option: OptionRenderer,
+        }}
+        autoFocus
+        controlSize="medium"
+        value={options.find((o) => o.value === withKey)}
+      />
+    </>
+  );
 }
 
-function renderValue(p: Omit<Option, 'label' | 'isDefault'>, options: Option[]) {
-  const selectedOption = options.find((o) => o.value === p.value);
-  if (selectedOption !== undefined) {
-    return (
-      <>
-        <span>{selectedOption.label}</span>
-        {selectedOption.isDefault && (
-          <Tooltip overlay={translate('quality_profiles.list.default.help')}>
-            <span className="spacer-left badge">{translate('default')}</span>
-          </Tooltip>
-        )}
-      </>
-    );
-  }
+function OptionRenderer(props: Readonly<OptionProps<Option, false>>) {
+  const { isDefault, label } = props.data;
+  const intl = useIntl();
+
+  return (
+    <components.Option {...props}>
+      <span>{label}</span>
+      {isDefault && (
+        <Tooltip overlay={intl.formatMessage({ id: 'quality_profiles.list.default.help' })}>
+          <span>
+            <Badge className="sw-ml-1">{intl.formatMessage({ id: 'default' })}</Badge>
+          </span>
+        </Tooltip>
+      )}
+    </components.Option>
+  );
 }
index f7c559ba0e0f9e735f2ca71f0b0aa1478e62ddaa..ccb10cda61ec8ceb0edb2a516249899e4932832e 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 { ButtonSecondary, Spinner } from 'design-system';
 import * as React from 'react';
+import { useIntl } from 'react-intl';
 import { Profile } from '../../../api/quality-profiles';
 import { getRuleDetails } from '../../../api/rules';
 import Tooltip from '../../../components/controls/Tooltip';
-import { Button } from '../../../components/controls/buttons';
-import Spinner from '../../../components/ui/Spinner';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { RuleDetails } from '../../../types/types';
 import ActivationFormModal from '../../coding-rules/components/ActivationFormModal';
 
@@ -33,81 +32,55 @@ interface Props {
   ruleKey: string;
 }
 
-interface State {
-  rule?: RuleDetails;
-  state: 'closed' | 'opening' | 'open';
-}
-
-export default class ComparisonResultActivation extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { state: 'closed' };
+export default function ComparisonResultActivation(props: React.PropsWithChildren<Props>) {
+  const { profile, ruleKey } = props;
+  const [state, setState] = React.useState<'closed' | 'opening' | 'open'>('closed');
+  const [rule, setRule] = React.useState<RuleDetails>();
+  const intl = useIntl();
 
-  componentDidMount() {
-    this.mounted = true;
-  }
+  const isOpen = state === 'open' && rule;
 
-  componentWillUnmount() {
-    this.mounted = false;
-  }
+  const activateRuleMsg = intl.formatMessage(
+    { id: 'quality_profiles.comparison.activate_rule' },
+    { profile: profile.name },
+  );
 
-  handleButtonClick = () => {
-    this.setState({ state: 'opening' });
-    getRuleDetails({ key: this.props.ruleKey }).then(
+  const handleButtonClick = () => {
+    setState('opening');
+    getRuleDetails({ key: ruleKey }).then(
       ({ rule }) => {
-        if (this.mounted) {
-          this.setState({ rule, state: 'open' });
-        }
+        setState('open');
+        setRule(rule);
       },
       () => {
-        if (this.mounted) {
-          this.setState({ state: 'closed' });
-        }
+        setState('closed');
       },
     );
   };
 
-  handleCloseModal = () => {
-    this.setState({ state: 'closed' });
-  };
-
-  isOpen(state: State): state is { state: 'open'; rule: RuleDetails } {
-    return state.state === 'open';
-  }
-
-  render() {
-    const { profile } = this.props;
-
-    return (
-      <Spinner loading={this.state.state === 'opening'}>
-        <Tooltip
-          placement="bottom"
-          overlay={translateWithParameters(
-            'quality_profiles.comparison.activate_rule',
-            profile.name,
-          )}
+  return (
+    <Spinner loading={state === 'opening'}>
+      <Tooltip placement="bottom" overlay={activateRuleMsg}>
+        <ButtonSecondary
+          disabled={state !== 'closed'}
+          aria-label={activateRuleMsg}
+          onClick={handleButtonClick}
         >
-          <Button
-            disabled={this.state.state !== 'closed'}
-            aria-label={translateWithParameters(
-              'quality_profiles.comparison.activate_rule',
-              profile.name,
-            )}
-            onClick={this.handleButtonClick}
-          >
-            {this.props.children}
-          </Button>
-        </Tooltip>
+          {intl.formatMessage({ id: 'activate' })}
+        </ButtonSecondary>
+      </Tooltip>
 
-        {this.isOpen(this.state) && (
-          <ActivationFormModal
-            modalHeader={translate('coding_rules.activate_in_quality_profile')}
-            onClose={this.handleCloseModal}
-            onDone={this.props.onDone}
-            profiles={[profile]}
-            rule={this.state.rule}
-          />
-        )}
-      </Spinner>
-    );
-  }
+      {isOpen && (
+        <ActivationFormModal
+          modalHeader={intl.formatMessage({ id: 'coding_rules.activate_in_quality_profile' })}
+          onClose={() => {
+            setState('closed');
+          }}
+          onDone={props.onDone}
+          profiles={[profile]}
+          rule={rule}
+        />
+      )}
+    </Spinner>
+  );
 }
index 47cf7d8e741136aa2d818b4639ca0e5461e54b64..6f643b5e8c7c31304eb292764563b3ec9f25a0c8 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 { ActionCell, ContentCell, Link, Table, TableRowInteractive } from 'design-system';
 import * as React from 'react';
+import { useIntl } from 'react-intl';
 import { CompareResponse, Profile } from '../../../api/quality-profiles';
-import Link from '../../../components/common/Link';
-import ChevronLeftIcon from '../../../components/icons/ChevronLeftIcon';
-import ChevronRightIcon from '../../../components/icons/ChevronRightIcon';
-import SeverityIcon from '../../../components/icons/SeverityIcon';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { getRulesUrl } from '../../../helpers/urls';
 import { Dict } from '../../../types/types';
 import ComparisonResultActivation from './ComparisonResultActivation';
@@ -37,29 +33,35 @@ interface Props extends CompareResponse {
   rightProfile?: Profile;
 }
 
-export default class ComparisonResults extends React.PureComponent<Props> {
-  canActivate(profile: Profile) {
-    return !profile.isBuiltIn && profile.actions && profile.actions.edit;
-  }
+export default function ComparisonResults(props: Readonly<Props>) {
+  const { leftProfile, rightProfile, inLeft, left, right, inRight, modified } = props;
 
-  renderRule(rule: { key: string; name: string }, severity: string) {
+  const intl = useIntl();
+
+  const emptyComparison = !inLeft.length && !inRight.length && !modified.length;
+
+  const canActivate = (profile: Profile) =>
+    !profile.isBuiltIn && profile.actions && profile.actions.edit;
+
+  const renderRule = React.useCallback((rule: { key: string; name: string }) => {
     return (
       <div>
-        <SeverityIcon severity={severity} />{' '}
-        <Link to={getRulesUrl({ rule_key: rule.key, open: rule.key })}>{rule.name}</Link>
+        <Link className="sw-ml-1" to={getRulesUrl({ rule_key: rule.key, open: rule.key })}>
+          {rule.name}
+        </Link>
       </div>
     );
-  }
+  }, []);
 
-  renderParameters(params: Params) {
+  const renderParameters = React.useCallback((params: Params) => {
     if (!params) {
       return null;
     }
     return (
       <ul>
         {Object.keys(params).map((key) => (
-          <li className="spacer-top break-word" key={key}>
-            <code>
+          <li className="sw-mt-2 sw-break-all" key={key}>
+            <code className="sw-code">
               {key}
               {': '}
               {params[key]}
@@ -68,147 +70,158 @@ export default class ComparisonResults extends React.PureComponent<Props> {
         ))}
       </ul>
     );
-  }
+  }, []);
 
-  renderLeft() {
-    if (this.props.inLeft.length === 0) {
+  const renderLeft = () => {
+    if (inLeft.length === 0) {
       return null;
     }
 
-    const renderSecondColumn = this.props.rightProfile && this.canActivate(this.props.rightProfile);
+    const renderSecondColumn = rightProfile && canActivate(rightProfile);
 
     return (
-      <table className="data fixed zebra">
-        <thead>
-          <tr>
-            <th>
-              {translateWithParameters(
-                'quality_profiles.x_rules_only_in',
-                this.props.inLeft.length,
-              )}{' '}
-              {this.props.left.name}
-            </th>
-            {renderSecondColumn && <th aria-label={translate('actions')}>&nbsp;</th>}
-          </tr>
-        </thead>
-        <tbody>
-          {this.props.inLeft.map((rule) => (
-            <tr key={`left-${rule.key}`}>
-              <td>{this.renderRule(rule, rule.severity)}</td>
-              {renderSecondColumn && (
-                <td>
-                  <ComparisonResultActivation
-                    key={rule.key}
-                    onDone={this.props.refresh}
-                    profile={this.props.rightProfile as Profile}
-                    ruleKey={rule.key}
-                  >
-                    <ChevronRightIcon />
-                  </ComparisonResultActivation>
-                </td>
+      <Table
+        columnCount={2}
+        columnWidths={['50%', 'auto']}
+        noSidePadding
+        header={
+          <TableRowInteractive>
+            <ContentCell>
+              {intl.formatMessage(
+                {
+                  id: 'quality_profiles.x_rules_only_in',
+                },
+                { count: inLeft.length, profile: left.name },
               )}
-            </tr>
-          ))}
-        </tbody>
-      </table>
+            </ContentCell>
+            {renderSecondColumn && (
+              <ContentCell aria-label={intl.formatMessage({ id: 'actions' })}>&nbsp;</ContentCell>
+            )}
+          </TableRowInteractive>
+        }
+      >
+        {inLeft.map((rule) => (
+          <TableRowInteractive key={`left-${rule.key}`}>
+            <ContentCell>{renderRule(rule)}</ContentCell>
+            {renderSecondColumn && (
+              <ContentCell className="sw-px-0">
+                <ComparisonResultActivation
+                  key={rule.key}
+                  onDone={props.refresh}
+                  profile={rightProfile}
+                  ruleKey={rule.key}
+                />
+              </ContentCell>
+            )}
+          </TableRowInteractive>
+        ))}
+      </Table>
     );
-  }
+  };
 
-  renderRight() {
-    if (this.props.inRight.length === 0) {
+  const renderRight = () => {
+    if (inRight.length === 0) {
       return null;
     }
 
-    const renderFirstColumn = this.props.leftProfile && this.canActivate(this.props.leftProfile);
+    const renderFirstColumn = leftProfile && canActivate(leftProfile);
 
     return (
-      <table
-        className={classNames('data fixed zebra quality-profile-compare-right-table', {
-          'has-first-column': renderFirstColumn,
-        })}
-      >
-        <thead>
-          <tr>
-            {renderFirstColumn && <th aria-label={translate('actions')}>&nbsp;</th>}
-            <th>
-              {translateWithParameters(
-                'quality_profiles.x_rules_only_in',
-                this.props.inRight.length,
-              )}{' '}
-              {this.props.right.name}
-            </th>
-          </tr>
-        </thead>
-        <tbody>
-          {this.props.inRight.map((rule) => (
-            <tr key={`right-${rule.key}`}>
-              {renderFirstColumn && (
-                <td className="text-right">
-                  <ComparisonResultActivation
-                    key={rule.key}
-                    onDone={this.props.refresh}
-                    profile={this.props.leftProfile}
-                    ruleKey={rule.key}
-                  >
-                    <ChevronLeftIcon />
-                  </ComparisonResultActivation>
-                </td>
+      <Table
+        columnCount={2}
+        columnWidths={['50%', 'auto']}
+        noSidePadding
+        header={
+          <TableRowInteractive>
+            {renderFirstColumn && (
+              <ContentCell aria-label={intl.formatMessage({ id: 'actions' })}>&nbsp;</ContentCell>
+            )}
+            <ContentCell className="sw-pl-4">
+              {intl.formatMessage(
+                {
+                  id: 'quality_profiles.x_rules_only_in',
+                },
+                { count: inRight.length, profile: right.name },
               )}
-              <td>{this.renderRule(rule, rule.severity)}</td>
-            </tr>
-          ))}
-        </tbody>
-      </table>
+            </ContentCell>
+          </TableRowInteractive>
+        }
+      >
+        {inRight.map((rule) => (
+          <TableRowInteractive key={`right-${rule.key}`}>
+            {renderFirstColumn && (
+              <ActionCell className="sw-px-0">
+                <ComparisonResultActivation
+                  key={rule.key}
+                  onDone={props.refresh}
+                  profile={leftProfile}
+                  ruleKey={rule.key}
+                />
+              </ActionCell>
+            )}
+            <ContentCell className="sw-pl-4">{renderRule(rule)}</ContentCell>
+          </TableRowInteractive>
+        ))}
+      </Table>
     );
-  }
+  };
 
-  renderModified() {
-    if (this.props.modified.length === 0) {
+  const renderModified = () => {
+    if (modified.length === 0) {
       return null;
     }
-    return (
-      <table className="data fixed zebra zebra-inversed">
-        <caption>
-          {translateWithParameters(
-            'quality_profiles.x_rules_have_different_configuration',
-            this.props.modified.length,
-          )}
-        </caption>
-        <thead>
-          <tr>
-            <th>{this.props.left.name}</th>
-            <th>{this.props.right.name}</th>
-          </tr>
-        </thead>
-        <tbody>
-          {this.props.modified.map((rule) => (
-            <tr key={`modified-${rule.key}`}>
-              <td>
-                {this.renderRule(rule, rule.left.severity)}
-                {this.renderParameters(rule.left.params)}
-              </td>
-              <td>
-                {this.renderRule(rule, rule.right.severity)}
-                {this.renderParameters(rule.right.params)}
-              </td>
-            </tr>
-          ))}
-        </tbody>
-      </table>
-    );
-  }
-
-  render() {
-    if (!this.props.inLeft.length && !this.props.inRight.length && !this.props.modified.length) {
-      return <div className="big-spacer-top">{translate('quality_profile.empty_comparison')}</div>;
-    }
 
     return (
-      <>
-        {this.renderLeft()}
-        {this.renderRight()}
-        {this.renderModified()}
-      </>
+      <Table
+        columnCount={2}
+        columnWidths={['50%', 'auto']}
+        noSidePadding
+        header={
+          <TableRowInteractive>
+            <ContentCell>{left.name}</ContentCell>
+            <ContentCell className="sw-pl-4">{right.name}</ContentCell>
+          </TableRowInteractive>
+        }
+        caption={
+          <>
+            {intl.formatMessage(
+              { id: 'quality_profiles.x_rules_have_different_configuration' },
+              { count: modified.length },
+            )}
+          </>
+        }
+      >
+        {modified.map((rule) => (
+          <TableRowInteractive key={`modified-${rule.key}`}>
+            <ContentCell>
+              <div>
+                {renderRule(rule)}
+                {renderParameters(rule.left.params)}
+              </div>
+            </ContentCell>
+            <ContentCell className="sw-pl-4">
+              <div>
+                {renderRule(rule)}
+                {renderParameters(rule.right.params)}
+              </div>
+            </ContentCell>
+          </TableRowInteractive>
+        ))}
+      </Table>
     );
-  }
+  };
+
+  return (
+    <div className="sw-mt-4">
+      {emptyComparison ? (
+        intl.formatMessage({ id: 'quality_profile.empty_comparison' })
+      ) : (
+        <>
+          {renderLeft()}
+          {renderRight()}
+          {renderModified()}
+        </>
+      )}
+    </div>
+  );
 }
index 94a6f3554f1c27893e758dafbb90324bcf7fd4a1..222f14dc6b23dceace427fa7faa3e6477931119e 100644 (file)
@@ -35,9 +35,11 @@ export function AdminPageHeader({ children, className, description, title }: Rea
     <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>
+        {description && (
+          <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>
index 8c409b5a0da6a340eba83cf23b40ae3b72299d8a..47fddcc4e356e3ef50576aa9e4d9fe36984207f0 100644 (file)
@@ -1994,8 +1994,8 @@ quality_profiles.warning.is_default_no_rules=The current profile is the default
 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
-quality_profiles.x_rules_have_different_configuration={0} rules have a different configuration
+quality_profiles.x_rules_only_in={count} rules only in {profile}
+quality_profiles.x_rules_have_different_configuration={count} rules have a different configuration
 quality_profiles.copy_x_title=Copy Profile "{0}" - {1}
 quality_profiles.extend_x_title=Extend Profile "{0}" - {1}
 quality_profiles.rename_x_title=Rename Profile {0} - {1}
@@ -2021,7 +2021,7 @@ quality_profile.empty_comparison=The quality profiles are equal.
 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 "{0}"
+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.list.projects=Projects