Browse Source

SONAR-21797 Show deprecation badges for metrics on activity page

tags/10.5.0.89998
stanislavh 1 month ago
parent
commit
d4518aca0c

+ 3
- 3
server/sonar-web/design-system/src/components/MultiSelector.tsx View 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}

+ 5
- 3
server/sonar-web/design-system/src/components/input/MultiSelectMenu.tsx View 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 && (

+ 4
- 4
server/sonar-web/design-system/src/components/input/MultiSelectMenuOption.tsx View 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 })}

+ 11
- 4
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx View 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;
}}
/>
);
}

+ 12
- 10
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx View 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) => {

+ 14
- 5
server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx View File

@@ -20,9 +20,10 @@
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}
/>
}

+ 48
- 4
server/sonar-web/src/main/js/components/activity-graph/AddGraphMetricPopup.tsx View File

@@ -17,9 +17,13 @@
* 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}
/>

+ 31
- 0
server/sonar-web/src/main/js/components/activity-graph/GraphsLegendItem.tsx View 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}

+ 4
- 0
server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx View 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 }),

+ 17
- 1
server/sonar-web/src/main/js/helpers/constants.ts View 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;


+ 1
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View 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}

#------------------------------------------------------------------------------
#

Loading…
Cancel
Save