diff options
Diffstat (limited to 'server/sonar-web')
14 files changed, 208 insertions, 233 deletions
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 9ab33dd9f2a..1cb21f29650 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 @@ -32,11 +32,11 @@ import { getHistoryMetrics, isCustomGraph, } from '../../../components/activity-graph/utils'; -import { mergeRatingMeasureHistory } from '../../../helpers/activity-graph'; -import { SOFTWARE_QUALITY_RATING_METRICS } from '../../../helpers/constants'; +import { mergeMeasureHistory } from '../../../helpers/activity-graph'; import { parseDate } from '../../../helpers/dates'; -import useApplicationLeakQuery from '../../../queries/applications'; +import { useApplicationLeakQuery } from '../../../queries/applications'; import { useCurrentBranchQuery } from '../../../queries/branch'; +import { StaleTime } from '../../../queries/common'; import { useAllMeasuresHistoryQuery } from '../../../queries/measures'; import { useAllProjectAnalysesQuery } from '../../../queries/project-analyses'; import { useStandardExperienceMode } from '../../../queries/settings'; @@ -59,22 +59,27 @@ export const PROJECT_ACTIVITY_GRAPH = 'sonar_project_activity.graph'; export function ProjectActivityApp() { const { query, pathname } = useLocation(); - const parsedQuery = parseQuery(query); + const { data: isStandardMode, isLoading: isLoadingStandardMode } = useStandardExperienceMode(); + const parsedQuery = parseQuery(query, isStandardMode); const router = useRouter(); const { component } = useComponent(); const metrics = useMetrics(); - const { data: branchLike, isFetching: isFetchingBranch } = useCurrentBranchQuery(component); + const { data: branchLike, isFetching: isFetchingBranch } = useCurrentBranchQuery( + component, + StaleTime.LONG, + ); const enabled = component?.key !== undefined && (isPortfolioLike(component?.qualifier) || (Boolean(branchLike) && !isFetchingBranch)); - const { data: appLeaks } = useApplicationLeakQuery( - component?.key ?? '', - isApplication(component?.qualifier), - ); + const { data: appLeaks } = useApplicationLeakQuery(component?.key ?? '', { + enabled: isApplication(component?.qualifier), + }); - const { data: analysesData, isLoading: isLoadingAnalyses } = useAllProjectAnalysesQuery(enabled); - const { data: isStandardMode, isLoading: isLoadingStandardMode } = useStandardExperienceMode(); + const { data: analysesData, isLoading: isLoadingAnalyses } = useAllProjectAnalysesQuery({ + enabled, + staleTime: StaleTime.LONG, + }); const { data: historyData, isLoading: isLoadingHistory } = useAllMeasuresHistoryQuery( { @@ -82,16 +87,14 @@ export function ProjectActivityApp() { branchParams: getBranchLikeQuery(branchLike), metrics: getHistoryMetrics(query.graph || DEFAULT_GRAPH, parsedQuery.customMetrics).join(','), }, - { enabled }, + { enabled, staleTime: StaleTime.LONG }, ); const analyses = React.useMemo(() => analysesData ?? [], [analysesData]); const measuresHistory = React.useMemo( () => - isLoadingStandardMode - ? [] - : mergeRatingMeasureHistory(historyData, parseDate, isStandardMode), + isLoadingStandardMode ? [] : mergeMeasureHistory(historyData, parseDate, isStandardMode), [historyData, isStandardMode, isLoadingStandardMode], ); @@ -122,10 +125,13 @@ export function ProjectActivityApp() { }, [component?.qualifier, metrics]); const handleUpdateQuery = (newQuery: Query) => { - const q = serializeUrlQuery({ - ...parsedQuery, - ...newQuery, - }); + const q = serializeUrlQuery( + { + ...parsedQuery, + ...newQuery, + }, + isStandardMode, + ); router.push({ pathname, @@ -137,20 +143,12 @@ export function ProjectActivityApp() { }); }; - const firstSoftwareQualityRatingMetric = historyData?.measures.find((m) => - SOFTWARE_QUALITY_RATING_METRICS.includes(m.metric), - ); - return ( component && ( <Spinner isLoading={isLoadingStandardMode}> <ProjectActivityAppRenderer analyses={analyses} - isStandardMode={ - isStandardMode || - !firstSoftwareQualityRatingMetric || - firstSoftwareQualityRatingMetric.history.every((h) => h.value === undefined) - } + isStandardMode={isStandardMode} analysesLoading={isLoadingAnalyses} graphLoading={isLoadingHistory} leakPeriodDate={leakPeriodDate} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx index ebf076627bb..10523c0eb92 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx @@ -35,10 +35,6 @@ import { splitSeriesInGraphs, } from '../../../components/activity-graph/utils'; import DocumentationLink from '../../../components/common/DocumentationLink'; -import { - CCT_SOFTWARE_QUALITY_METRICS, - SOFTWARE_QUALITY_RATING_METRICS_MAP, -} from '../../../helpers/constants'; import { DocLink } from '../../../helpers/doc-links'; import { translate } from '../../../helpers/l10n'; import { MetricKey } from '../../../sonar-aligned/types/metrics'; @@ -50,6 +46,7 @@ import { Serie, } from '../../../types/project-activity'; import { Metric } from '../../../types/types'; +import { MQR_CONDITIONS_MAP } from '../../quality-gates/utils'; import { Query, datesQueryChanged, historyQueryChanged } from '../utils'; import { PROJECT_ACTIVITY_GRAPH } from './ProjectActivityApp'; @@ -221,17 +218,11 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St renderQualitiesMetricInfoMessage = () => { const { measuresHistory, isStandardMode } = this.props; - const qualityMeasuresHistory = measuresHistory.find((history) => - CCT_SOFTWARE_QUALITY_METRICS.includes(history.metric), - ); - const ratingQualityMeasuresHistory = measuresHistory.find((history) => - (Object.keys(SOFTWARE_QUALITY_RATING_METRICS_MAP) as MetricKey[]).includes(history.metric), + const mqrMeasuresHistory = measuresHistory.find( + (history) => MQR_CONDITIONS_MAP[history.metric as MetricKey] !== undefined, ); - if ( - this.hasGaps(qualityMeasuresHistory) || - (!isStandardMode && this.hasGaps(ratingQualityMeasuresHistory)) - ) { + if (!isStandardMode && this.hasGaps(mqrMeasuresHistory)) { return ( <FlagMessage variant="info"> <FormattedMessage diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx index 63deee154e3..b74a5f34e96 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx @@ -253,7 +253,17 @@ describe('rendering', () => { it('should render graph gap info message', async () => { timeMachineHandler.setMeasureHistory([ mockMeasureHistory({ - metric: MetricKey.maintainability_issues, + metric: MetricKey.code_smells, + history: projectActivityHandler.getAnalysesList().map(({ date }) => + mockHistoryItem({ + // eslint-disable-next-line jest/no-conditional-in-test + value: '2', + date: parseDate(date), + }), + ), + }), + mockMeasureHistory({ + metric: MetricKey.software_quality_maintainability_issues, history: projectActivityHandler.getAnalysesList().map(({ date }, index) => mockHistoryItem({ // eslint-disable-next-line jest/no-conditional-in-test @@ -274,7 +284,7 @@ describe('rendering', () => { await ui.changeGraphType(GraphType.custom); await ui.openMetricsDropdown(); - await ui.toggleMetric(MetricKey.maintainability_issues); + await ui.toggleMetric(MetricKey.software_quality_maintainability_issues); expect(ui.gapInfoMessage.get()).toBeInTheDocument(); }); @@ -290,7 +300,7 @@ describe('rendering', () => { await ui.changeGraphType(GraphType.custom); await ui.openMetricsDropdown(); - await ui.toggleMetric(MetricKey.maintainability_issues); + await ui.toggleMetric(MetricKey.software_quality_maintainability_issues); expect(ui.gapInfoMessage.query()).not.toBeInTheDocument(); }); }); @@ -522,7 +532,7 @@ describe('graph interactions', () => { }); it.each([ - ['MQR', 'true', MetricKey.maintainability_issues], + ['MQR', 'true', MetricKey.software_quality_maintainability_issues], ['Standard', 'false', MetricKey.code_smells], ])('should correctly handle customizing the graph in %s mode', async (_, mode, metric) => { settingsHandler.set(SettingsKey.MQRMode, mode); @@ -607,7 +617,7 @@ describe('ratings', () => { await ui.changeGraphType(GraphType.custom); await ui.openMetricsDropdown(); - await ui.toggleMetric(MetricKey.reliability_rating); + await ui.toggleMetric(MetricKey.software_quality_reliability_rating); await ui.closeMetricsDropdown(); expect(await ui.graphs.findAll()).toHaveLength(1); @@ -649,7 +659,7 @@ describe('ratings', () => { await ui.changeGraphType(GraphType.custom); await ui.openMetricsDropdown(); - await ui.toggleMetric(MetricKey.reliability_rating); + await ui.toggleMetric(MetricKey.software_quality_reliability_rating); await ui.closeMetricsDropdown(); expect(await ui.graphs.findAll()).toHaveLength(1); @@ -682,7 +692,7 @@ describe('ratings', () => { await ui.changeGraphType(GraphType.custom); await ui.openMetricsDropdown(); - await ui.toggleMetric(MetricKey.reliability_rating); + await ui.toggleMetric(MetricKey.software_quality_reliability_rating); await ui.closeMetricsDropdown(); expect(await ui.graphs.findAll()).toHaveLength(1); @@ -956,12 +966,19 @@ function renderProjectActivityAppContainer( { metrics: keyBy( [ - mockMetric({ key: MetricKey.maintainability_issues, type: MetricType.Data }), + mockMetric({ + key: MetricKey.software_quality_maintainability_issues, + type: MetricType.Integer, + }), mockMetric({ key: MetricKey.bugs, type: MetricType.Integer }), mockMetric({ key: MetricKey.code_smells, type: MetricType.Integer }), mockMetric({ key: MetricKey.security_hotspots_reviewed }), mockMetric({ key: MetricKey.security_review_rating, type: MetricType.Rating }), mockMetric({ key: MetricKey.reliability_rating, type: MetricType.Rating }), + mockMetric({ + key: MetricKey.software_quality_reliability_rating, + type: MetricType.Rating, + }), ], 'key', ), diff --git a/server/sonar-web/src/main/js/apps/projectActivity/utils.ts b/server/sonar-web/src/main/js/apps/projectActivity/utils.ts index a93df4ef785..9ec0145af44 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/utils.ts +++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.ts @@ -23,7 +23,6 @@ import { isEqual, uniq } from 'lodash'; import { MetricKey } from '~sonar-aligned/types/metrics'; import { RawQuery } from '~sonar-aligned/types/router'; import { DEFAULT_GRAPH } from '../../components/activity-graph/utils'; -import { SOFTWARE_QUALITY_RATING_METRICS_MAP } from '../../helpers/constants'; import { parseDate } from '../../helpers/dates'; import { MEASURES_REDIRECTION } from '../../helpers/measures'; import { @@ -37,6 +36,7 @@ import { } from '../../helpers/query'; import { GraphType, ParsedAnalysis } from '../../types/project-activity'; import { Dict } from '../../types/types'; +import { MQR_CONDITIONS_MAP, STANDARD_CONDITIONS_MAP } from '../quality-gates/utils'; export interface Query { category: string; @@ -113,21 +113,17 @@ export function getAnalysesByVersionByDay( }, []); } -export function parseQuery(urlQuery: RawQuery): Query { +export function parseQuery(urlQuery: RawQuery, isStandardMode = false): Query { const parsedMetrics = parseAsArray(urlQuery['custom_metrics'], parseAsString<MetricKey>); let customMetrics = uniq(parsedMetrics.map((metric) => MEASURES_REDIRECTION[metric] ?? metric)); - const reversedMetricMap = Object.fromEntries( - Object.entries(SOFTWARE_QUALITY_RATING_METRICS_MAP).map( - ([k, v]) => [v, k] as [MetricKey, MetricKey], + customMetrics = uniq( + customMetrics.map((metric) => + !isStandardMode ? (STANDARD_CONDITIONS_MAP[metric] ?? metric) : metric, ), - ); - - customMetrics = uniq(customMetrics.map((metric) => reversedMetricMap[metric] ?? metric)) + ) .map((metric) => - SOFTWARE_QUALITY_RATING_METRICS_MAP[metric] - ? [metric, SOFTWARE_QUALITY_RATING_METRICS_MAP[metric]] - : metric, + !isStandardMode && MQR_CONDITIONS_MAP[metric] ? [metric, MQR_CONDITIONS_MAP[metric]] : metric, ) .flat(); @@ -149,13 +145,12 @@ export function serializeQuery(query: Query): RawQuery { }); } -export function serializeUrlQuery(query: Query): RawQuery { +export function serializeUrlQuery(query: Query, isStandardMode = false): RawQuery { return cleanQuery({ category: serializeString(query.category), custom_metrics: serializeStringArray( query.customMetrics.filter( - (metric) => - !Object.values(SOFTWARE_QUALITY_RATING_METRICS_MAP).includes(metric as MetricKey), + (metric) => isStandardMode || !STANDARD_CONDITIONS_MAP[metric as MetricKey], ), ), from: serializeDate(query.from), 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 1b4ef643205..ad331f5cfcf 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 @@ -23,24 +23,15 @@ import { sortBy } from 'lodash'; import * as React from 'react'; import { Dropdown } from '~design-system'; import { MetricKey, MetricType } from '~sonar-aligned/types/metrics'; -import { - CCT_SOFTWARE_QUALITY_METRICS, - DEPRECATED_ACTIVITY_METRICS, - HIDDEN_METRICS, - LEAK_CCT_SOFTWARE_QUALITY_METRICS, - LEAK_OLD_TAXONOMY_METRICS, - LEAK_OLD_TAXONOMY_RATINGS, - OLD_TAXONOMY_METRICS, - SOFTWARE_QUALITY_RATING_METRICS, - SOFTWARE_QUALITY_RATING_METRICS_MAP, -} from '../../helpers/constants'; +import { MQR_CONDITIONS_MAP, STANDARD_CONDITIONS_MAP } from '../../apps/quality-gates/utils'; +import { HIDDEN_METRICS } from '../../helpers/constants'; import { getLocalizedMetricName, translate } from '../../helpers/l10n'; import { isDiffMetric } from '../../helpers/measures'; +import { useStandardExperienceMode } from '../../queries/settings'; import { Metric } from '../../types/types'; import AddGraphMetricPopup from './AddGraphMetricPopup'; interface Props { - isStandardMode?: boolean; metrics: Metric[]; metricsTypeFilter?: string[]; onAddMetric: (metric: string) => void; @@ -48,27 +39,22 @@ interface Props { selectedMetrics: string[]; } -interface State { - metrics: string[]; - query: string; - selectedMetrics: string[]; -} +export default function AddGraphMetric(props: Readonly<Props>) { + const [metrics, setMetrics] = React.useState<string[]>([]); + const [query, setQuery] = React.useState<string>(''); + const [selectedMetrics, setSelectedMetrics] = React.useState<string[]>([]); -export default class AddGraphMetric extends React.PureComponent<Props, State> { - state: State = { - metrics: [], - query: '', - selectedMetrics: [], - }; + const { data: isStandardMode } = useStandardExperienceMode(); - filterSelected = (query: string, selectedElements: string[]) => { + const filterSelected = (query: string, selectedElements: string[]) => { return selectedElements.filter((element) => - this.getLocalizedMetricNameFromKey(element).toLowerCase().includes(query.toLowerCase()), + getLocalizedMetricNameFromKey(element).toLowerCase().includes(query.toLowerCase()), ); }; - filterMetricsElements = (query: string) => { - const { metrics, selectedMetrics, metricsTypeFilter, isStandardMode } = this.props; + const filterMetricsElements = () => { + const { metricsTypeFilter, metrics, selectedMetrics } = props; + return metrics .filter((metric) => { if (metric.hidden) { @@ -77,40 +63,20 @@ export default class AddGraphMetric extends React.PureComponent<Props, State> { if (isDiffMetric(metric.key)) { return false; } - if ( - [MetricType.Data, MetricType.Distribution].includes(metric.type as MetricType) && - !CCT_SOFTWARE_QUALITY_METRICS.includes(metric.key as MetricKey) - ) { + if ([MetricType.Data, MetricType.Distribution].includes(metric.type as MetricType)) { return false; } if (HIDDEN_METRICS.includes(metric.key as MetricKey)) { return false; } if ( - isStandardMode && - [ - ...LEAK_CCT_SOFTWARE_QUALITY_METRICS, - ...CCT_SOFTWARE_QUALITY_METRICS, - ...SOFTWARE_QUALITY_RATING_METRICS, - ].includes(metric.key as MetricKey) + isStandardMode + ? MQR_CONDITIONS_MAP[metric.key as MetricKey] + : STANDARD_CONDITIONS_MAP[metric.key as MetricKey] ) { return false; } if ( - !isStandardMode && - [ - ...LEAK_OLD_TAXONOMY_METRICS, - ...OLD_TAXONOMY_METRICS, - ...LEAK_OLD_TAXONOMY_RATINGS, - ...DEPRECATED_ACTIVITY_METRICS, - ].includes(metric.key as MetricKey) - ) { - return false; - } - if (Object.values(SOFTWARE_QUALITY_RATING_METRICS_MAP).includes(metric.key as MetricKey)) { - return false; - } - if ( selectedMetrics.includes(metric.key) || !getLocalizedMetricName(metric).toLowerCase().includes(query.toLowerCase()) ) { @@ -124,78 +90,64 @@ export default class AddGraphMetric extends React.PureComponent<Props, State> { .map((metric) => metric.key); }; - getSelectedMetricsElements = (metrics: Metric[], selectedMetrics: string[]) => { + const getSelectedMetricsElements = (metrics: Metric[], selectedMetrics: string[]) => { return metrics .filter( (metric) => selectedMetrics.includes(metric.key) && - !Object.values(SOFTWARE_QUALITY_RATING_METRICS_MAP).includes(metric.key as MetricKey), + (isStandardMode || !STANDARD_CONDITIONS_MAP[metric.key as MetricKey]), ) .map((metric) => metric.key); }; - getLocalizedMetricNameFromKey = (key: string) => { - const metric = this.props.metrics.find((m) => m.key === key); + const getLocalizedMetricNameFromKey = (key: string) => { + const metric = props.metrics.find((m) => m.key === key); return metric === undefined ? key : getLocalizedMetricName(metric); }; - onSearch = (query: string) => { - this.setState({ query }); + const onSearch = (query: string) => { + setQuery(query); return Promise.resolve(); }; - onSelect = (metric: string) => { - this.props.onAddMetric(metric); - this.setState((state) => { - return { - selectedMetrics: sortBy([...state.selectedMetrics, metric]), - metrics: this.filterMetricsElements(state.query), - }; - }); + const onSelect = (metric: string) => { + props.onAddMetric(metric); + setSelectedMetrics(sortBy([...selectedMetrics, metric])); + setMetrics(filterMetricsElements()); }; - onUnselect = (metric: string) => { - this.props.onRemoveMetric(metric); - this.setState((state) => { - return { - metrics: sortBy([...state.metrics, metric]), - selectedMetrics: state.selectedMetrics.filter((selected) => selected !== metric), - }; - }); + const onUnselect = (metric: string) => { + props.onRemoveMetric(metric); + setSelectedMetrics(selectedMetrics.filter((selected) => selected !== metric)); + setMetrics(sortBy(metrics, metric)); }; - render() { - const { query } = this.state; - const filteredMetrics = this.filterMetricsElements(query); - const selectedMetrics = this.getSelectedMetricsElements( - this.props.metrics, - this.props.selectedMetrics, - ); + const filteredMetrics = filterMetricsElements(); + const selectedElements = getSelectedMetricsElements(props.metrics, props.selectedMetrics); - return ( - <Dropdown - allowResizing - size="large" - closeOnClick={false} - id="activity-graph-custom-metric-selector" - overlay={ - <AddGraphMetricPopup - elements={filteredMetrics} - filterSelected={this.filterSelected} - metricsTypeFilter={this.props.metricsTypeFilter} - onSearch={this.onSearch} - onSelect={this.onSelect} - onUnselect={this.onUnselect} - selectedElements={selectedMetrics} - /> - } - > - <Button suffix={<IconChevronDown />}> - <span className="sw-typo-default sw-flex"> - {translate('project_activity.graphs.custom.add')} - </span> - </Button> - </Dropdown> - ); - } + return ( + <Dropdown + allowResizing + size="large" + closeOnClick={false} + id="activity-graph-custom-metric-selector" + overlay={ + <AddGraphMetricPopup + elements={filteredMetrics} + filterSelected={filterSelected} + metricsTypeFilter={props.metricsTypeFilter} + onSearch={onSearch} + onSelect={onSelect} + onUnselect={onUnselect} + selectedElements={selectedElements} + /> + } + > + <Button suffix={<IconChevronDown />}> + <span className="sw-typo-default sw-flex"> + {translate('project_activity.graphs.custom.add')} + </span> + </Button> + </Dropdown> + ); } 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 07d00062654..5cd379f1e2e 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 @@ -18,10 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { ReactNode, useCallback } from 'react'; -import { FlagMessage, MultiSelectMenu } from '~design-system'; +import { ReactNode } from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { Badge, FlagMessage, MultiSelectMenu } from '~design-system'; import { MetricKey } from '~sonar-aligned/types/metrics'; +import { DEPRECATED_ACTIVITY_METRICS } from '../../helpers/constants'; import { getLocalizedMetricName, translate, translateWithParameters } from '../../helpers/l10n'; +import { getDeprecatedTranslationKeyForTooltip } from './utils'; export interface AddGraphMetricPopupProps { elements: string[]; @@ -39,6 +42,7 @@ export default function AddGraphMetricPopup({ metricsTypeFilter, ...props }: Readonly<AddGraphMetricPopupProps>) { + const intl = useIntl(); let footerNode: ReactNode = ''; if (props.selectedElements.length >= 6) { @@ -61,10 +65,37 @@ export default function AddGraphMetricPopup({ ); } - const renderLabel = useCallback((key: MetricKey) => { - return getLocalizedMetricName({ key }); - }, []); + const renderAriaLabel = (key: MetricKey) => { + const metricName = getLocalizedMetricName({ key }); + const isDeprecated = DEPRECATED_ACTIVITY_METRICS.includes(key); + return isDeprecated + ? `${metricName} (${intl.formatMessage({ id: 'deprecated' })})` + : metricName; + }; + + const renderLabel = (key: MetricKey) => { + const metricName = getLocalizedMetricName({ key }); + const isDeprecated = DEPRECATED_ACTIVITY_METRICS.includes(key); + + return ( + <> + {metricName} + {isDeprecated && ( + <Badge className="sw-ml-1">{intl.formatMessage({ id: 'deprecated' })}</Badge> + )} + </> + ); + }; + + const renderTooltip = (key: MetricKey) => { + const isDeprecated = DEPRECATED_ACTIVITY_METRICS.includes(key); + if (isDeprecated) { + return <FormattedMessage id={getDeprecatedTranslationKeyForTooltip(key)} tagName="div" />; + } + + return null; + }; return ( <MultiSelectMenu createElementLabel="" @@ -79,7 +110,8 @@ export default function AddGraphMetricPopup({ onSelect={(item: string) => elements.includes(item) && props.onSelect(item)} onUnselect={props.onUnselect} placeholder={translate('search.search_for_metrics')} - renderAriaLabel={renderLabel} + renderAriaLabel={renderAriaLabel} + renderTooltip={renderTooltip} renderLabel={renderLabel} selectedElements={props.selectedElements} listSize={0} diff --git a/server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx b/server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx index 2604c4f5725..050fac489ad 100644 --- a/server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx @@ -23,6 +23,7 @@ import { Note } from '~design-system'; import { ComponentContext } from '../../app/components/componentContext/ComponentContext'; import { translate } from '../../helpers/l10n'; import { useCurrentBranchQuery } from '../../queries/branch'; +import { StaleTime } from '../../queries/common'; import { AnalysisEvent, ProjectAnalysisEventCategory } from '../../types/project-activity'; import Tooltip from '../controls/Tooltip'; import { DefinitionChangeEventInner, isDefinitionChangeEvent } from './DefinitionChangeEventInner'; @@ -40,7 +41,7 @@ export interface EventInnerProps { export default function EventInner({ event, readonly }: EventInnerProps) { const { component } = React.useContext(ComponentContext); - const { data: branchLike } = useCurrentBranchQuery(component); + const { data: branchLike } = useCurrentBranchQuery(component, StaleTime.LONG); if (isRichQualityGateEvent(event)) { return <RichQualityGateEventInner event={event} readonly={readonly} />; } else if (isDefinitionChangeEvent(event)) { diff --git a/server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx index 88785de65ce..35d1150b9cd 100644 --- a/server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx @@ -21,7 +21,6 @@ import { ButtonGroup, InputSize, Select } from '@sonarsource/echoes-react'; import * as React from 'react'; import { translate } from '../../helpers/l10n'; -import { useStandardExperienceMode } from '../../queries/settings'; import { GraphType } from '../../types/project-activity'; import { Metric } from '../../types/types'; import AddGraphMetric from './AddGraphMetric'; @@ -48,8 +47,6 @@ export default function GraphsHeader(props: Readonly<Props>) { selectedMetrics = [], } = props; - const { data: isStandardMode } = useStandardExperienceMode(); - const handleGraphChange = React.useCallback( (value: GraphType) => { if (value !== graph) { @@ -90,7 +87,6 @@ export default function GraphsHeader(props: Readonly<Props>) { metricsTypeFilter={metricsTypeFilter} onRemoveMetric={props.onRemoveCustomMetric} selectedMetrics={selectedMetrics} - isStandardMode={isStandardMode} /> )} </ButtonGroup> 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 dbd3729e914..d9ae9ae663b 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 @@ -50,7 +50,7 @@ export function GraphsLegendItem({ isActionable={isActionable} > {showWarning ? ( - <IconWarning className="sw-mr-2" /> + <IconWarning color="echoes-color-icon-warning" className="sw-mr-2" /> ) : ( <ChartLegend className="sw-mr-2" index={index} /> )} diff --git a/server/sonar-web/src/main/js/helpers/activity-graph.ts b/server/sonar-web/src/main/js/helpers/activity-graph.ts index d2273460103..ea0ccc4dd03 100644 --- a/server/sonar-web/src/main/js/helpers/activity-graph.ts +++ b/server/sonar-web/src/main/js/helpers/activity-graph.ts @@ -20,39 +20,36 @@ import { ScaleTime } from 'd3-scale'; import { TimeMachineResponse } from '../api/time-machine'; -import { SOFTWARE_QUALITY_RATING_METRICS_MAP } from './constants'; +import { MQR_CONDITIONS_MAP, STANDARD_CONDITIONS_MAP } from '../apps/quality-gates/utils'; -export const mergeRatingMeasureHistory = ( +export const mergeMeasureHistory = ( historyData: TimeMachineResponse | undefined, parseDateFn: (date: string) => Date, isStandardMode = false, ) => { - const softwareQualityMeasures = Object.values(SOFTWARE_QUALITY_RATING_METRICS_MAP); - const softwareQualityMeasuresMap = new Map< + const standardMeasuresMap = new Map< string, { history: { date: string; value?: string }[]; index: number; splitDate?: Date } >(); if (isStandardMode) { return ( - historyData?.measures - ?.filter((m) => !softwareQualityMeasures.includes(m.metric)) - .map((measure) => ({ - metric: measure.metric, - history: measure.history.map((historyItem) => ({ - date: parseDateFn(historyItem.date), - value: historyItem.value, - })), - })) ?? [] + historyData?.measures.map((measure) => ({ + metric: measure.metric, + history: measure.history.map((historyItem) => ({ + date: parseDateFn(historyItem.date), + value: historyItem.value, + })), + })) ?? [] ); } const historyDataFiltered = historyData?.measures?.filter((measure) => { - if (softwareQualityMeasures.includes(measure.metric)) { + if (MQR_CONDITIONS_MAP[measure.metric]) { const splitPointIndex = measure.history.findIndex( (historyItem) => historyItem.value != null, ); - softwareQualityMeasuresMap.set(measure.metric, { + standardMeasuresMap.set(measure.metric, { history: measure.history, index: measure.history.findIndex((historyItem) => historyItem.value != null), splitDate: @@ -72,20 +69,24 @@ export const mergeRatingMeasureHistory = ( }); return historyDataFiltered.map((measure) => { - const softwareQualityMetric = softwareQualityMeasuresMap.get( - SOFTWARE_QUALITY_RATING_METRICS_MAP[measure.metric], - ); + const metric = STANDARD_CONDITIONS_MAP[measure.metric]; + const softwareQualityMetric = standardMeasuresMap.get(metric as string); + if (softwareQualityMetric !== undefined && metric) { + return { + metric, + splitPointDate: softwareQualityMetric.splitDate, + history: measure.history + .slice(0, softwareQualityMetric.index) + .map(historyMapper) + .concat( + softwareQualityMetric.history.slice(softwareQualityMetric.index).map(historyMapper), + ), + }; + } + return { metric: measure.metric, - splitPointDate: softwareQualityMetric ? softwareQualityMetric.splitDate : undefined, - history: softwareQualityMetric - ? measure.history - .slice(0, softwareQualityMetric.index) - .map(historyMapper) - .concat( - softwareQualityMetric.history.slice(softwareQualityMetric.index).map(historyMapper), - ) - : measure.history.map(historyMapper), + history: measure.history.map(historyMapper), }; }); }; diff --git a/server/sonar-web/src/main/js/helpers/constants.ts b/server/sonar-web/src/main/js/helpers/constants.ts index cf169fbcf94..2afd6152403 100644 --- a/server/sonar-web/src/main/js/helpers/constants.ts +++ b/server/sonar-web/src/main/js/helpers/constants.ts @@ -199,14 +199,7 @@ export const HIDDEN_METRICS = [ 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.confirmed_issues, -]; +export const DEPRECATED_ACTIVITY_METRICS = [MetricKey.confirmed_issues]; export const SOFTWARE_QUALITY_RATING_METRICS_MAP: Record<string, MetricKey> = { [MetricKey.sqale_rating]: MetricKey.software_quality_maintainability_rating, @@ -267,11 +260,6 @@ export const SOFTWARE_QUALITY_RATING_METRICS = [ MetricKey.new_software_quality_reliability_remediation_effort, MetricKey.new_software_quality_security_remediation_effort, MetricKey.new_software_quality_maintainability_debt_ratio, - MetricKey.software_quality_blocker_issues, - MetricKey.software_quality_high_issues, - MetricKey.software_quality_medium_issues, - MetricKey.software_quality_low_issues, - MetricKey.software_quality_info_issues, ]; export const PROJECT_KEY_MAX_LEN = 400; diff --git a/server/sonar-web/src/main/js/queries/applications.ts b/server/sonar-web/src/main/js/queries/applications.ts index c08095b30e2..13b20809900 100644 --- a/server/sonar-web/src/main/js/queries/applications.ts +++ b/server/sonar-web/src/main/js/queries/applications.ts @@ -18,17 +18,17 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { queryOptions, useMutation, useQueryClient } from '@tanstack/react-query'; import { deleteApplication, getApplicationLeak } from '../api/application'; +import { createQueryHook } from './common'; import { invalidateMeasuresByComponentKey } from './measures'; -export default function useApplicationLeakQuery(application: string, enabled = true) { - return useQuery({ +export const useApplicationLeakQuery = createQueryHook((application: string) => { + return queryOptions({ queryKey: ['application', 'leak', application], queryFn: () => getApplicationLeak(application), - enabled, }); -} +}); export function useDeleteApplicationMutation() { const queryClient = useQueryClient(); diff --git a/server/sonar-web/src/main/js/queries/branch.tsx b/server/sonar-web/src/main/js/queries/branch.tsx index ada7254d613..8acc97b5e68 100644 --- a/server/sonar-web/src/main/js/queries/branch.tsx +++ b/server/sonar-web/src/main/js/queries/branch.tsx @@ -168,7 +168,10 @@ export function useBranchesQuery(component: LightComponent | undefined) { }); } -export function useCurrentBranchQuery(component: LightComponent | undefined) { +export function useCurrentBranchQuery( + component: LightComponent | undefined, + staleTime = StaleTime.LIVE, +) { const features = useContext(AvailableFeaturesContext); const { search } = useLocation(); @@ -196,7 +199,7 @@ export function useCurrentBranchQuery(component: LightComponent | undefined) { return useQuery({ ...branchesQuery(component, features.includes(Feature.BranchSupport)), select, - staleTime: StaleTime.LIVE, + staleTime, }); } diff --git a/server/sonar-web/src/main/js/queries/project-analyses.ts b/server/sonar-web/src/main/js/queries/project-analyses.ts index ee6fe2a0e73..c7edf3eb132 100644 --- a/server/sonar-web/src/main/js/queries/project-analyses.ts +++ b/server/sonar-web/src/main/js/queries/project-analyses.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { queryOptions, useMutation, useQueryClient } from '@tanstack/react-query'; import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like'; import { BranchParameters } from '~sonar-aligned/types/branch-like'; import { @@ -38,6 +38,7 @@ import { parseDate } from '../helpers/dates'; import { serializeStringArray } from '../helpers/query'; import { ParsedAnalysis } from '../types/project-activity'; import { useCurrentBranchQuery } from './branch'; +import { createQueryHook } from './common'; const ACTIVITY_PAGE_SIZE = 500; @@ -55,9 +56,10 @@ function useProjectActivityQueryKey() { ]; } -export function useAllProjectAnalysesQuery(enabled = true) { +export const useAllProjectAnalysesQuery = createQueryHook(() => { const queryKey = useProjectActivityQueryKey(); - return useQuery({ + + return queryOptions({ queryKey, queryFn: ({ queryKey: [_0, _1, project, branchParams] }) => getAllTimeProjectActivity({ @@ -75,9 +77,8 @@ export function useAllProjectAnalysesQuery(enabled = true) { date: parseDate(analysis.date), })), ), - enabled, }); -} +}); export function useDeleteAnalysisMutation(successCb?: () => void) { const queryClient = useQueryClient(); |