diff options
11 files changed, 150 insertions, 35 deletions
diff --git a/server/sonar-web/design-system/src/components/MultiSelector.tsx b/server/sonar-web/design-system/src/components/MultiSelector.tsx index 7a7c066c59c..ebbc3c06830 100644 --- a/server/sonar-web/design-system/src/components/MultiSelector.tsx +++ b/server/sonar-web/design-system/src/components/MultiSelector.tsx @@ -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} diff --git a/server/sonar-web/design-system/src/components/input/MultiSelectMenu.tsx b/server/sonar-web/design-system/src/components/input/MultiSelectMenu.tsx index 97121244228..86da7e4b5bd 100644 --- a/server/sonar-web/design-system/src/components/input/MultiSelectMenu.tsx +++ b/server/sonar-web/design-system/src/components/input/MultiSelectMenu.tsx @@ -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 && ( diff --git a/server/sonar-web/design-system/src/components/input/MultiSelectMenuOption.tsx b/server/sonar-web/design-system/src/components/input/MultiSelectMenuOption.tsx index 7a5d2eec6a7..2df004a3c6a 100644 --- a/server/sonar-web/design-system/src/components/input/MultiSelectMenuOption.tsx +++ b/server/sonar-web/design-system/src/components/input/MultiSelectMenuOption.tsx @@ -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 })} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx index c0be592eabb..188cde32c99 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx @@ -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; + }} /> ); } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx index b8f83ead011..b0460c1475f 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx @@ -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) => { diff --git a/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx b/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx index 2ee99609452..7a224c9d3f7 100644 --- a/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx @@ -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} /> } diff --git a/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetricPopup.tsx b/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetricPopup.tsx index 5acd46b0190..1498b661f0b 100644 --- a/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetricPopup.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetricPopup.tsx @@ -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} /> diff --git a/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendItem.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendItem.tsx index 58767ea47ed..f8655c20a29 100644 --- a/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendItem.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendItem.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx index c2c0943c804..88131755a68 100644 --- a/server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx @@ -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 }), diff --git a/server/sonar-web/src/main/js/helpers/constants.ts b/server/sonar-web/src/main/js/helpers/constants.ts index 63de008d849..f5b42b3adce 100644 --- a/server/sonar-web/src/main/js/helpers/constants.ts +++ b/server/sonar-web/src/main/js/helpers/constants.ts @@ -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; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 520c4ec9605..7d302471139 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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} #------------------------------------------------------------------------------ # |