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

allowNewElements?: boolean; allowNewElements?: boolean;
allowSearch?: boolean; allowSearch?: boolean;
createElementLabel: string; createElementLabel: string;
disableMessage?: string;
elements: string[]; elements: string[];
headerLabel: string; headerLabel: string;
listSize?: number; listSize?: number;
onSearch?: (query: string) => Promise<void>; onSearch?: (query: string) => Promise<void>;
onSelect: (item: string) => void; onSelect: (item: string) => void;
onUnselect: (item: string) => void; onUnselect: (item: string) => void;
renderTooltip?: (item: string, disabled: boolean) => React.ReactNode;
searchInputAriaLabel: string; searchInputAriaLabel: string;
selectedElements: string[]; selectedElements: string[];
selectedElementsDisabled?: string[]; selectedElementsDisabled?: string[];
const { const {
allowNewElements, allowNewElements,
createElementLabel, createElementLabel,
disableMessage,
selectedElementsDisabled, selectedElementsDisabled,
headerLabel, headerLabel,
noResultsLabel, noResultsLabel,
selectedElements, selectedElements,
elements, elements,
allowSearch = true, allowSearch = true,
renderTooltip,
listSize = LIST_SIZE, listSize = LIST_SIZE,
} = props; } = props;


allowNewElements={allowNewElements} allowNewElements={allowNewElements}
allowSearch={allowSearch} allowSearch={allowSearch}
createElementLabel={createElementLabel} createElementLabel={createElementLabel}
disableMessage={disableMessage}
elements={elements} elements={elements}
headerNode={<div className="sw-mt-4 sw-font-semibold">{headerLabel}</div>} headerNode={<div className="sw-mt-4 sw-font-semibold">{headerLabel}</div>}
listSize={listSize} listSize={listSize}
onSelect={props.onSelect} onSelect={props.onSelect}
onUnselect={props.onUnselect} onUnselect={props.onUnselect}
placeholder={searchInputAriaLabel} placeholder={searchInputAriaLabel}
renderTooltip={renderTooltip}
searchInputAriaLabel={searchInputAriaLabel} searchInputAriaLabel={searchInputAriaLabel}
selectedElements={selectedElements} selectedElements={selectedElements}
selectedElementsDisabled={selectedElementsDisabled} selectedElementsDisabled={selectedElementsDisabled}

+ 5
- 3
server/sonar-web/design-system/src/components/input/MultiSelectMenu.tsx View File

allowSearch?: boolean; allowSearch?: boolean;
allowSelection?: boolean; allowSelection?: boolean;
createElementLabel: string; createElementLabel: string;
disableMessage?: string;
elements: string[]; elements: string[];
elementsDisabled?: string[]; elementsDisabled?: string[];
footerNode?: React.ReactNode; footerNode?: React.ReactNode;
onSelect: (item: string) => void; onSelect: (item: string) => void;
onUnselect: (item: string) => void; onUnselect: (item: string) => void;
placeholder: string; placeholder: string;
renderTooltip?: (element: string, disabled: boolean) => React.ReactNode;
searchInputAriaLabel: string; searchInputAriaLabel: string;
selectedElements: string[]; selectedElements: string[];
selectedElementsDisabled?: string[]; selectedElementsDisabled?: string[];
allowSelection = true, allowSelection = true,
allowNewElements = true, allowNewElements = true,
createElementLabel, createElementLabel,
disableMessage,
selectedElementsDisabled = [], selectedElementsDisabled = [],
headerNode = '', headerNode = '',
footerNode = '', footerNode = '',
noResultsLabel, noResultsLabel,
searchInputAriaLabel, searchInputAriaLabel,
elementsDisabled, elementsDisabled,
renderTooltip,
} = this.props; } = this.props;
const { renderLabel } = this.props as PropsWithDefault; const { renderLabel } = this.props as PropsWithDefault;


<MultiSelectMenuOption <MultiSelectMenuOption
active={activeElement === element} active={activeElement === element}
createElementLabel={createElementLabel} createElementLabel={createElementLabel}
disableMessage={disableMessage}
disabled={selectedElementsDisabled.includes(element)} disabled={selectedElementsDisabled.includes(element)}
element={element} element={element}
key={element} key={element}
onHover={this.handleElementHover} onHover={this.handleElementHover}
onSelectChange={this.handleSelectChange} onSelectChange={this.handleSelectChange}
renderLabel={renderLabel} renderLabel={renderLabel}
renderTooltip={renderTooltip}
selected selected
/> />
))} ))}
onHover={this.handleElementHover} onHover={this.handleElementHover}
onSelectChange={this.handleSelectChange} onSelectChange={this.handleSelectChange}
renderLabel={renderLabel} renderLabel={renderLabel}
renderTooltip={renderTooltip}
/> />
))} ))}
{elementsDisabled?.map((element) => ( {elementsDisabled?.map((element) => (
onHover={this.handleElementHover} onHover={this.handleElementHover}
onSelectChange={this.handleSelectChange} onSelectChange={this.handleSelectChange}
renderLabel={renderLabel} renderLabel={renderLabel}
renderTooltip={renderTooltip}
/> />
))} ))}
{showNewElement && ( {showNewElement && (

+ 4
- 4
server/sonar-web/design-system/src/components/input/MultiSelectMenuOption.tsx View File

active?: boolean; active?: boolean;
createElementLabel: string; createElementLabel: string;
custom?: boolean; custom?: boolean;
disableMessage?: string;
disabled?: boolean; disabled?: boolean;
element: string; element: string;
onHover: (element: string) => void; onHover: (element: string) => void;
onSelectChange: (selected: boolean, element: string) => void; onSelectChange: (selected: boolean, element: string) => void;
renderLabel?: (element: string) => React.ReactNode; renderLabel?: (element: string) => React.ReactNode;
renderTooltip?: (element: string, disabled: boolean) => React.ReactNode;
selected?: boolean; selected?: boolean;
} }


active, active,
createElementLabel, createElementLabel,
custom, custom,
disabled,
disableMessage,
disabled = false,
element, element,
onSelectChange, onSelectChange,
selected, selected,
renderLabel = identity, renderLabel = identity,
renderTooltip,
} = props; } = props;


const onHover = () => { const onHover = () => {
const label = renderLabel(element); const label = renderLabel(element);


return ( return (
<Tooltip overlay={disabled && disableMessage} placement={PopupPlacement.Right}>
<Tooltip overlay={renderTooltip?.(element, disabled)} placement={PopupPlacement.Right}>
<ItemCheckbox <ItemCheckbox
checked={Boolean(selected)} checked={Boolean(selected)}
className={classNames('sw-flex sw-py-2 sw-px-4', { active })} 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

}; };


render() { 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 ( return (
<MultiSelector <MultiSelector
createElementLabel={translate('coding_rules.create_tag')} createElementLabel={translate('coding_rules.create_tag')}
disableMessage={translate('coding_rules.system_tags_tooltip')}
headerLabel={translate('tags')} headerLabel={translate('tags')}
searchInputAriaLabel={translate('search.search_for_tags')} searchInputAriaLabel={translate('search.search_for_tags')}
noResultsLabel={translate('no_results')} noResultsLabel={translate('no_results')}
onSelect={this.onSelect} onSelect={this.onSelect}
onUnselect={this.onUnselect} onUnselect={this.onUnselect}
selectedElements={selectedTags} selectedElements={selectedTags}
selectedElementsDisabled={this.props.sysTags}
selectedElementsDisabled={sysTags}
elements={availableTags} 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

} from '../../../components/activity-graph/utils'; } from '../../../components/activity-graph/utils';
import { useLocation, useRouter } from '../../../components/hoc/withRouter'; import { useLocation, useRouter } from '../../../components/hoc/withRouter';
import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { HIDDEN_METRICS } from '../../../helpers/constants';
import { parseDate } from '../../../helpers/dates'; import { parseDate } from '../../../helpers/dates';
import useApplicationLeakQuery from '../../../queries/applications'; import useApplicationLeakQuery from '../../../queries/applications';
import { useBranchesQuery } from '../../../queries/branch'; import { useBranchesQuery } from '../../../queries/branch';
}, [appLeaks, component?.leakPeriodDate, component?.qualifier]); }, [appLeaks, component?.leakPeriodDate, component?.qualifier]);


const filteredMetrics = React.useMemo(() => { 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]); }, [component?.qualifier, metrics]);


const handleUpdateQuery = (newQuery: Query) => { const handleUpdateQuery = (newQuery: Query) => {

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

import { ButtonSecondary, ChevronDownIcon, Dropdown, TextMuted } from 'design-system'; import { ButtonSecondary, ChevronDownIcon, Dropdown, TextMuted } from 'design-system';
import { sortBy } from 'lodash'; import { sortBy } from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { HIDDEN_METRICS } from '../../helpers/constants';
import { getLocalizedMetricName, translate } from '../../helpers/l10n'; import { getLocalizedMetricName, translate } from '../../helpers/l10n';
import { isDiffMetric } from '../../helpers/measures'; import { isDiffMetric } from '../../helpers/measures';
import { MetricType } from '../../types/metrics';
import { MetricKey, MetricType } from '../../types/metrics';
import { Metric } from '../../types/types'; import { Metric } from '../../types/types';
import AddGraphMetricPopup from './AddGraphMetricPopup'; import AddGraphMetricPopup from './AddGraphMetricPopup';


) => { ) => {
return metrics return metrics
.filter((metric) => { .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 ( if (
metric.hidden ||
isDiffMetric(metric.key) ||
[MetricType.Data, MetricType.Distribution].includes(metric.type as MetricType) ||
selectedMetrics.includes(metric.key) || selectedMetrics.includes(metric.key) ||
!getLocalizedMetricName(metric).toLowerCase().includes(query.toLowerCase()) !getLocalizedMetricName(metric).toLowerCase().includes(query.toLowerCase())
) { ) {
onSearch={this.onSearch} onSearch={this.onSearch}
onSelect={this.onSelect} onSelect={this.onSelect}
onUnselect={this.onUnselect} onUnselect={this.onUnselect}
renderLabel={(element) => this.getLocalizedMetricNameFromKey(element)}
selectedElements={selectedMetrics} selectedElements={selectedMetrics}
/> />
} }

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

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * 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 * 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 { export interface AddGraphMetricPopupProps {
elements: string[]; elements: string[];
onSelect: (item: string) => void; onSelect: (item: string) => void;
onUnselect: (item: string) => void; onUnselect: (item: string) => void;
popupPosition?: any; popupPosition?: any;
renderLabel: (element: string) => React.ReactNode;
selectedElements: string[]; selectedElements: string[];
} }


metricsTypeFilter, metricsTypeFilter,
...props ...props
}: AddGraphMetricPopupProps) { }: AddGraphMetricPopupProps) {
const intl = useIntl();
let footerNode: React.ReactNode = ''; let footerNode: React.ReactNode = '';


if (props.selectedElements.length >= 6) { if (props.selectedElements.length >= 6) {
); );
} }


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 ( return (
<MultiSelectMenu <MultiSelectMenu
createElementLabel="" createElementLabel=""
onSelect={(item: string) => elements.includes(item) && props.onSelect(item)} onSelect={(item: string) => elements.includes(item) && props.onSelect(item)}
onUnselect={props.onUnselect} onUnselect={props.onUnselect}
placeholder={translate('search.search_for_metrics')} placeholder={translate('search.search_for_metrics')}
renderLabel={props.renderLabel}
renderLabel={renderLabel}
renderTooltip={renderTooltip}
selectedElements={props.selectedElements} selectedElements={props.selectedElements}
listSize={0} listSize={0}
/> />

+ 31
- 0
server/sonar-web/src/main/js/components/activity-graph/GraphsLegendItem.tsx View File

import styled from '@emotion/styled'; import styled from '@emotion/styled';
import classNames from 'classnames'; import classNames from 'classnames';
import { import {
Badge,
CloseIcon, CloseIcon,
FlagWarningIcon, FlagWarningIcon,
InteractiveIcon, InteractiveIcon,
themeColor, themeColor,
} from 'design-system'; } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { DEPRECATED_ACTIVITY_METRICS } from '../../helpers/constants';
import { translateWithParameters } from '../../helpers/l10n'; import { translateWithParameters } from '../../helpers/l10n';
import { MetricKey } from '../../types/metrics';
import DocumentationLink from '../common/DocumentationLink';
import Tooltip from '../controls/Tooltip';
import { ChartLegend } from './ChartLegend'; import { ChartLegend } from './ChartLegend';


interface Props { interface Props {
removeMetric, removeMetric,
showWarning, showWarning,
}: Props) { }: Props) {
const intl = useIntl();
const theme = useTheme() as Theme; const theme = useTheme() as Theme;


const isActionable = removeMetric !== undefined; const isActionable = removeMetric !== undefined;
const isDeprecated = DEPRECATED_ACTIVITY_METRICS.includes(metric as MetricKey);


return ( return (
<StyledLegendItem <StyledLegendItem
<span className="sw-body-sm" style={{ color: themeColor('graphCursorLineColor')({ theme }) }}> <span className="sw-body-sm" style={{ color: themeColor('graphCursorLineColor')({ theme }) }}>
{name} {name}
</span> </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 && ( {isActionable && (
<InteractiveIcon <InteractiveIcon
Icon={CloseIcon} Icon={CloseIcon}

+ 4
- 0
server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx View File

// We should see 2 graphs, correctly labelled. // We should see 2 graphs, correctly labelled.
expect(ui.graphs.getAll()).toHaveLength(2); 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. // We cannot select anymore Int types. It should hide options, and show an alert.
expect(ui.vulnerabilityCheckbox.query()).not.toBeInTheDocument(); expect(ui.vulnerabilityCheckbox.query()).not.toBeInTheDocument();
expect(ui.hiddenOptionsAlert.get()).toBeInTheDocument(); expect(ui.hiddenOptionsAlert.get()).toBeInTheDocument();


// Add/remove metrics. // Add/remove metrics.
addMetricBtn: byRole('button', { name: 'project_activity.graphs.custom.add' }), addMetricBtn: byRole('button', { name: 'project_activity.graphs.custom.add' }),
deprecatedBadge: byText('deprecated'),
bugsCheckbox: byRole('checkbox', { name: MetricKey.bugs }), bugsCheckbox: byRole('checkbox', { name: MetricKey.bugs }),
newBugsCheckbox: byRole('checkbox', { name: MetricKey.new_bugs }), newBugsCheckbox: byRole('checkbox', { name: MetricKey.new_bugs }),
burnedBudgetCheckbox: byRole('checkbox', { name: MetricKey.burned_budget }), burnedBudgetCheckbox: byRole('checkbox', { name: MetricKey.burned_budget }),

+ 17
- 1
server/sonar-web/src/main/js/helpers/constants.ts View File

{ fill: colors.error400, fillTransparent: colors.error400a20, stroke: colors.error700 }, { 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; export const PROJECT_KEY_MAX_LEN = 400;



+ 1
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

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.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.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