]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21797 Show deprecation badges for metrics on activity page
authorstanislavh <stanislav.honcharov@sonarsource.com>
Thu, 14 Mar 2024 16:30:18 +0000 (17:30 +0100)
committersonartech <sonartech@sonarsource.com>
Mon, 25 Mar 2024 20:02:42 +0000 (20:02 +0000)
server/sonar-web/design-system/src/components/MultiSelector.tsx
server/sonar-web/design-system/src/components/input/MultiSelectMenu.tsx
server/sonar-web/design-system/src/components/input/MultiSelectMenuOption.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx
server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx
server/sonar-web/src/main/js/components/activity-graph/AddGraphMetricPopup.tsx
server/sonar-web/src/main/js/components/activity-graph/GraphsLegendItem.tsx
server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx
server/sonar-web/src/main/js/helpers/constants.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 7a7c066c59cf50549d735a5166cd6c4380e6166d..ebbc3c0683019ff8023e1b28521a3ec91fbbee28 100644 (file)
@@ -23,7 +23,6 @@ interface Props {
   allowNewElements?: boolean;
   allowSearch?: boolean;
   createElementLabel: string;
-  disableMessage?: string;
   elements: string[];
   headerLabel: string;
   listSize?: number;
@@ -31,6 +30,7 @@ interface Props {
   onSearch?: (query: string) => Promise<void>;
   onSelect: (item: string) => void;
   onUnselect: (item: string) => void;
+  renderTooltip?: (item: string, disabled: boolean) => React.ReactNode;
   searchInputAriaLabel: string;
   selectedElements: string[];
   selectedElementsDisabled?: string[];
@@ -42,7 +42,6 @@ export function MultiSelector(props: Readonly<Props>) {
   const {
     allowNewElements,
     createElementLabel,
-    disableMessage,
     selectedElementsDisabled,
     headerLabel,
     noResultsLabel,
@@ -50,6 +49,7 @@ export function MultiSelector(props: Readonly<Props>) {
     selectedElements,
     elements,
     allowSearch = true,
+    renderTooltip,
     listSize = LIST_SIZE,
   } = props;
 
@@ -58,7 +58,6 @@ export function MultiSelector(props: Readonly<Props>) {
       allowNewElements={allowNewElements}
       allowSearch={allowSearch}
       createElementLabel={createElementLabel}
-      disableMessage={disableMessage}
       elements={elements}
       headerNode={<div className="sw-mt-4 sw-font-semibold">{headerLabel}</div>}
       listSize={listSize}
@@ -67,6 +66,7 @@ export function MultiSelector(props: Readonly<Props>) {
       onSelect={props.onSelect}
       onUnselect={props.onUnselect}
       placeholder={searchInputAriaLabel}
+      renderTooltip={renderTooltip}
       searchInputAriaLabel={searchInputAriaLabel}
       selectedElements={selectedElements}
       selectedElementsDisabled={selectedElementsDisabled}
index 97121244228c81dacfdcfc8100410936bc310042..86da7e4b5bdd97b84b359d9991f7eb70fae6e8be 100644 (file)
@@ -30,7 +30,6 @@ interface Props {
   allowSearch?: boolean;
   allowSelection?: boolean;
   createElementLabel: string;
-  disableMessage?: string;
   elements: string[];
   elementsDisabled?: string[];
   footerNode?: React.ReactNode;
@@ -42,6 +41,7 @@ interface Props {
   onSelect: (item: string) => void;
   onUnselect: (item: string) => void;
   placeholder: string;
+  renderTooltip?: (element: string, disabled: boolean) => React.ReactNode;
   searchInputAriaLabel: string;
   selectedElements: string[];
   selectedElementsDisabled?: string[];
@@ -265,7 +265,6 @@ export class MultiSelectMenu extends PureComponent<Props, State> {
       allowSelection = true,
       allowNewElements = true,
       createElementLabel,
-      disableMessage,
       selectedElementsDisabled = [],
       headerNode = '',
       footerNode = '',
@@ -273,6 +272,7 @@ export class MultiSelectMenu extends PureComponent<Props, State> {
       noResultsLabel,
       searchInputAriaLabel,
       elementsDisabled,
+      renderTooltip,
     } = this.props;
     const { renderLabel } = this.props as PropsWithDefault;
 
@@ -313,13 +313,13 @@ export class MultiSelectMenu extends PureComponent<Props, State> {
               <MultiSelectMenuOption
                 active={activeElement === element}
                 createElementLabel={createElementLabel}
-                disableMessage={disableMessage}
                 disabled={selectedElementsDisabled.includes(element)}
                 element={element}
                 key={element}
                 onHover={this.handleElementHover}
                 onSelectChange={this.handleSelectChange}
                 renderLabel={renderLabel}
+                renderTooltip={renderTooltip}
                 selected
               />
             ))}
@@ -334,6 +334,7 @@ export class MultiSelectMenu extends PureComponent<Props, State> {
                 onHover={this.handleElementHover}
                 onSelectChange={this.handleSelectChange}
                 renderLabel={renderLabel}
+                renderTooltip={renderTooltip}
               />
             ))}
           {elementsDisabled?.map((element) => (
@@ -346,6 +347,7 @@ export class MultiSelectMenu extends PureComponent<Props, State> {
               onHover={this.handleElementHover}
               onSelectChange={this.handleSelectChange}
               renderLabel={renderLabel}
+              renderTooltip={renderTooltip}
             />
           ))}
           {showNewElement && (
index 7a5d2eec6a7f595c7fb957ae0da21265457302b8..2df004a3c6afffe72c625af27d6d74c8371b1758 100644 (file)
@@ -27,12 +27,12 @@ export interface MultiSelectOptionProps {
   active?: boolean;
   createElementLabel: string;
   custom?: boolean;
-  disableMessage?: string;
   disabled?: boolean;
   element: string;
   onHover: (element: string) => void;
   onSelectChange: (selected: boolean, element: string) => void;
   renderLabel?: (element: string) => React.ReactNode;
+  renderTooltip?: (element: string, disabled: boolean) => React.ReactNode;
   selected?: boolean;
 }
 
@@ -41,12 +41,12 @@ export function MultiSelectMenuOption(props: MultiSelectOptionProps) {
     active,
     createElementLabel,
     custom,
-    disabled,
-    disableMessage,
+    disabled = false,
     element,
     onSelectChange,
     selected,
     renderLabel = identity,
+    renderTooltip,
   } = props;
 
   const onHover = () => {
@@ -56,7 +56,7 @@ export function MultiSelectMenuOption(props: MultiSelectOptionProps) {
   const label = renderLabel(element);
 
   return (
-    <Tooltip overlay={disabled && disableMessage} placement={PopupPlacement.Right}>
+    <Tooltip overlay={renderTooltip?.(element, disabled)} placement={PopupPlacement.Right}>
       <ItemCheckbox
         checked={Boolean(selected)}
         className={classNames('sw-flex sw-py-2 sw-px-4', { active })}
index c0be592eabb93535d939e041f9bb8d3bf9c379c9..188cde32c99e681ec2fdb86d77592636152bd7da 100644 (file)
@@ -70,12 +70,13 @@ export default class RuleDetailsTagsPopup extends React.PureComponent<Props, Sta
   };
 
   render() {
-    const availableTags = difference(this.state.searchResult, this.props.tags);
-    const selectedTags = [...this.props.sysTags, ...this.props.tags];
+    const { sysTags, tags } = this.props;
+    const { searchResult } = this.state;
+    const availableTags = difference(searchResult, tags);
+    const selectedTags = [...sysTags, ...tags];
     return (
       <MultiSelector
         createElementLabel={translate('coding_rules.create_tag')}
-        disableMessage={translate('coding_rules.system_tags_tooltip')}
         headerLabel={translate('tags')}
         searchInputAriaLabel={translate('search.search_for_tags')}
         noResultsLabel={translate('no_results')}
@@ -83,8 +84,14 @@ export default class RuleDetailsTagsPopup extends React.PureComponent<Props, Sta
         onSelect={this.onSelect}
         onUnselect={this.onUnselect}
         selectedElements={selectedTags}
-        selectedElementsDisabled={this.props.sysTags}
+        selectedElementsDisabled={sysTags}
         elements={availableTags}
+        renderTooltip={(element: string, disabled: boolean) => {
+          if (sysTags.includes(element) && disabled) {
+            return translate('coding_rules.system_tags_tooltip');
+          }
+          return null;
+        }}
       />
     );
   }
index b8f83ead011d6b9b9a89abd80a94f2d08704ab77..b0460c1475f96ecbf705e80f68be3cf2b29c0ea1 100644 (file)
@@ -31,7 +31,6 @@ import {
 } from '../../../components/activity-graph/utils';
 import { useLocation, useRouter } from '../../../components/hoc/withRouter';
 import { getBranchLikeQuery } from '../../../helpers/branch-like';
-import { HIDDEN_METRICS } from '../../../helpers/constants';
 import { parseDate } from '../../../helpers/dates';
 import useApplicationLeakQuery from '../../../queries/applications';
 import { useBranchesQuery } from '../../../queries/branch';
@@ -106,16 +105,19 @@ export function ProjectActivityApp() {
   }, [appLeaks, component?.leakPeriodDate, component?.qualifier]);
 
   const filteredMetrics = React.useMemo(() => {
-    if (isPortfolioLike(component?.qualifier)) {
-      return Object.values(metrics).filter(
-        (metric) => metric.key !== MetricKey.security_hotspots_reviewed,
-      );
-    }
+    return Object.values(metrics).filter((metric) => {
+      if (
+        isPortfolioLike(component?.qualifier) &&
+        metric.key === MetricKey.security_hotspots_reviewed
+      ) {
+        return false;
+      }
+      if (isProject(component?.qualifier) && metric.key === MetricKey.security_review_rating) {
+        return false;
+      }
 
-    return Object.values(metrics).filter(
-      (metric) =>
-        ![...HIDDEN_METRICS, MetricKey.security_review_rating].includes(metric.key as MetricKey),
-    );
+      return true;
+    });
   }, [component?.qualifier, metrics]);
 
   const handleUpdateQuery = (newQuery: Query) => {
index 2ee996094521844ee470d327c079da3ec9630667..7a224c9d3f7e9d1af9f6cbe6fadca300626041de 100644 (file)
 import { ButtonSecondary, ChevronDownIcon, Dropdown, TextMuted } from 'design-system';
 import { sortBy } from 'lodash';
 import * as React from 'react';
+import { HIDDEN_METRICS } from '../../helpers/constants';
 import { getLocalizedMetricName, translate } from '../../helpers/l10n';
 import { isDiffMetric } from '../../helpers/measures';
-import { MetricType } from '../../types/metrics';
+import { MetricKey, MetricType } from '../../types/metrics';
 import { Metric } from '../../types/types';
 import AddGraphMetricPopup from './AddGraphMetricPopup';
 
@@ -59,10 +60,19 @@ export default class AddGraphMetric extends React.PureComponent<Props, State> {
   ) => {
     return metrics
       .filter((metric) => {
+        if (metric.hidden) {
+          return false;
+        }
+        if (isDiffMetric(metric.key)) {
+          return false;
+        }
+        if ([MetricType.Data, MetricType.Distribution].includes(metric.type as MetricType)) {
+          return false;
+        }
+        if (HIDDEN_METRICS.includes(metric.key as MetricKey)) {
+          return false;
+        }
         if (
-          metric.hidden ||
-          isDiffMetric(metric.key) ||
-          [MetricType.Data, MetricType.Distribution].includes(metric.type as MetricType) ||
           selectedMetrics.includes(metric.key) ||
           !getLocalizedMetricName(metric).toLowerCase().includes(query.toLowerCase())
         ) {
@@ -134,7 +144,6 @@ export default class AddGraphMetric extends React.PureComponent<Props, State> {
             onSearch={this.onSearch}
             onSelect={this.onSelect}
             onUnselect={this.onUnselect}
-            renderLabel={(element) => this.getLocalizedMetricNameFromKey(element)}
             selectedElements={selectedMetrics}
           />
         }
index 5acd46b0190d04b759549d2d0e44a0b2ac4e57b4..1498b661f0bc870b7210366c01493052df189a27 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 { FlagMessage, MultiSelectMenu } from 'design-system';
+import { Badge, FlagMessage, MultiSelectMenu } from 'design-system';
 import * as React from 'react';
-import { translate, translateWithParameters } from '../../helpers/l10n';
+import { FormattedMessage, useIntl } from 'react-intl';
+import { DEPRECATED_ACTIVITY_METRICS } from '../../helpers/constants';
+import { getLocalizedMetricName, translate, translateWithParameters } from '../../helpers/l10n';
+import { MetricKey } from '../../types/metrics';
+import DocumentationLink from '../common/DocumentationLink';
 
 export interface AddGraphMetricPopupProps {
   elements: string[];
@@ -29,7 +33,6 @@ export interface AddGraphMetricPopupProps {
   onSelect: (item: string) => void;
   onUnselect: (item: string) => void;
   popupPosition?: any;
-  renderLabel: (element: string) => React.ReactNode;
   selectedElements: string[];
 }
 
@@ -38,6 +41,7 @@ export default function AddGraphMetricPopup({
   metricsTypeFilter,
   ...props
 }: AddGraphMetricPopupProps) {
+  const intl = useIntl();
   let footerNode: React.ReactNode = '';
 
   if (props.selectedElements.length >= 6) {
@@ -60,6 +64,45 @@ export default function AddGraphMetricPopup({
     );
   }
 
+  const renderLabel = (key: string) => {
+    const metricName = getLocalizedMetricName({ key });
+    const isDeprecated = DEPRECATED_ACTIVITY_METRICS.includes(key as MetricKey);
+
+    return (
+      <>
+        {metricName}
+        {isDeprecated && (
+          <Badge className="sw-ml-1">{intl.formatMessage({ id: 'deprecated' })}</Badge>
+        )}
+      </>
+    );
+  };
+
+  const renderTooltip = (key: string) => {
+    const isDeprecated = DEPRECATED_ACTIVITY_METRICS.includes(key as MetricKey);
+
+    if (isDeprecated) {
+      return (
+        <FormattedMessage
+          id="project_activity.custom_metric.deprecated"
+          tagName="div"
+          values={{
+            learn_more: (
+              <DocumentationLink
+                className="sw-ml-2 sw-whitespace-nowrap"
+                to="/user-guide/clean-code/code-analysis/"
+              >
+                {intl.formatMessage({ id: 'learn_more' })}
+              </DocumentationLink>
+            ),
+          }}
+        />
+      );
+    }
+
+    return null;
+  };
+
   return (
     <MultiSelectMenu
       createElementLabel=""
@@ -74,7 +117,8 @@ export default function AddGraphMetricPopup({
       onSelect={(item: string) => elements.includes(item) && props.onSelect(item)}
       onUnselect={props.onUnselect}
       placeholder={translate('search.search_for_metrics')}
-      renderLabel={props.renderLabel}
+      renderLabel={renderLabel}
+      renderTooltip={renderTooltip}
       selectedElements={props.selectedElements}
       listSize={0}
     />
index 58767ea47ed2b6efd6180c7d334f71568043d3cc..f8655c20a29064a019f0f71217889f76645f4018 100644 (file)
@@ -21,6 +21,7 @@ import { useTheme } from '@emotion/react';
 import styled from '@emotion/styled';
 import classNames from 'classnames';
 import {
+  Badge,
   CloseIcon,
   FlagWarningIcon,
   InteractiveIcon,
@@ -29,7 +30,12 @@ import {
   themeColor,
 } from 'design-system';
 import * as React from 'react';
+import { FormattedMessage, useIntl } from 'react-intl';
+import { DEPRECATED_ACTIVITY_METRICS } from '../../helpers/constants';
 import { translateWithParameters } from '../../helpers/l10n';
+import { MetricKey } from '../../types/metrics';
+import DocumentationLink from '../common/DocumentationLink';
+import Tooltip from '../controls/Tooltip';
 import { ChartLegend } from './ChartLegend';
 
 interface Props {
@@ -49,9 +55,11 @@ export function GraphsLegendItem({
   removeMetric,
   showWarning,
 }: Props) {
+  const intl = useIntl();
   const theme = useTheme() as Theme;
 
   const isActionable = removeMetric !== undefined;
+  const isDeprecated = DEPRECATED_ACTIVITY_METRICS.includes(metric as MetricKey);
 
   return (
     <StyledLegendItem
@@ -66,6 +74,29 @@ export function GraphsLegendItem({
       <span className="sw-body-sm" style={{ color: themeColor('graphCursorLineColor')({ theme }) }}>
         {name}
       </span>
+      {isDeprecated && (
+        <Tooltip
+          overlay={
+            <FormattedMessage
+              id="project_activity.custom_metric.deprecated"
+              values={{
+                learn_more: (
+                  <DocumentationLink
+                    className="sw-ml-2 sw-whitespace-nowrap"
+                    to="/user-guide/clean-code/code-analysis/"
+                  >
+                    {intl.formatMessage({ id: 'learn_more' })}
+                  </DocumentationLink>
+                ),
+              }}
+            />
+          }
+        >
+          <div>
+            <Badge className="sw-ml-1">{intl.formatMessage({ id: 'deprecated' })}</Badge>
+          </div>
+        </Tooltip>
+      )}
       {isActionable && (
         <InteractiveIcon
           Icon={CloseIcon}
index c2c0943c804625942f1eb170b4bc1026c7d62d19..88131755a6868b2bcc19ebf87b7d47ebbbee4092 100644 (file)
@@ -132,6 +132,9 @@ it('should correctly handle adding/removing custom metrics', async () => {
   // We should see 2 graphs, correctly labelled.
   expect(ui.graphs.getAll()).toHaveLength(2);
 
+  // old types and confirmed metrics should be deprecated and show a badge (both in dropdown and in legend)
+  expect(ui.deprecatedBadge.getAll()).toHaveLength(6);
+
   // We cannot select anymore Int types. It should hide options, and show an alert.
   expect(ui.vulnerabilityCheckbox.query()).not.toBeInTheDocument();
   expect(ui.hiddenOptionsAlert.get()).toBeInTheDocument();
@@ -172,6 +175,7 @@ function getPageObject() {
 
     // Add/remove metrics.
     addMetricBtn: byRole('button', { name: 'project_activity.graphs.custom.add' }),
+    deprecatedBadge: byText('deprecated'),
     bugsCheckbox: byRole('checkbox', { name: MetricKey.bugs }),
     newBugsCheckbox: byRole('checkbox', { name: MetricKey.new_bugs }),
     burnedBudgetCheckbox: byRole('checkbox', { name: MetricKey.burned_budget }),
index 63de008d849474cf8912e86bab9b27903a9212c6..f5b42b3adce344628b923583f51fe9911f178b47 100644 (file)
@@ -137,7 +137,23 @@ export const RATING_COLORS = [
   { fill: colors.error400, fillTransparent: colors.error400a20, stroke: colors.error700 },
 ];
 
-export const HIDDEN_METRICS = [MetricKey.open_issues, MetricKey.reopened_issues];
+export const HIDDEN_METRICS = [
+  MetricKey.open_issues,
+  MetricKey.reopened_issues,
+  MetricKey.high_impact_accepted_issues,
+];
+
+export const DEPRECATED_ACTIVITY_METRICS = [
+  MetricKey.blocker_violations,
+  MetricKey.critical_violations,
+  MetricKey.major_violations,
+  MetricKey.minor_violations,
+  MetricKey.info_violations,
+  MetricKey.code_smells,
+  MetricKey.bugs,
+  MetricKey.vulnerabilities,
+  MetricKey.confirmed_issues,
+];
 
 export const PROJECT_KEY_MAX_LEN = 400;
 
index 520c4ec9605d1ed44a49210ba016fb1200ba0714..7d30247113949811a78e4b1f856e4bce707b86b3 100644 (file)
@@ -1952,7 +1952,7 @@ project_activity.graphs.data_table.no_data_warning_check_dates_y=There is no dat
 project_activity.graphs.data_table.no_data_warning_check_dates_x_y=There is no data for the selected date range ({start} to {end}). Try modifying the date filters on the main page.
 
 project_activity.custom_metric.covered_lines=Covered Lines
-
+project_activity.custom_metric.deprecated=We are deprecating those filters to align the Activity page with our definition of Clean Code. They will be removed in a future release. {learn_more}
 
 #------------------------------------------------------------------------------
 #