From f513e55f32cf00d7075a6d13aafef0cb602e7990 Mon Sep 17 00:00:00 2001 From: Viktor Vorona Date: Wed, 21 Aug 2024 17:34:53 +0200 Subject: [PATCH] SONAR-22716 Combine old ratings and new ones in one activity graph --- .../js/api/mocks/TimeMachineServiceMock.ts | 6 +- .../components/ProjectActivityApp.tsx | 61 +- .../components/ProjectActivityAppRenderer.tsx | 3 + .../components/ProjectActivityGraphs.tsx | 38 +- .../__tests__/ProjectActivityApp-it.tsx | 209 +++++- .../src/main/js/apps/projectActivity/utils.ts | 24 +- .../activity-graph/AddGraphMetric.tsx | 15 +- .../activity-graph/GraphHistory.tsx | 204 +++--- .../activity-graph/GraphsHistory.tsx | 5 +- .../js/components/charts/AdvancedTimeline.tsx | 60 +- .../main/js/components/charts/SplitLine.tsx | 47 ++ .../js/components/charts/SplitLinePopover.tsx | 63 ++ .../__tests__/AdvancedTimeline-test.tsx | 89 ++- .../AdvancedTimeline-test.tsx.snap | 692 ++++++++++++++++++ .../src/main/js/helpers/activity-graph.ts | 102 +++ .../src/main/js/helpers/mocks/metrics.ts | 203 ++++- .../src/main/js/queries/component.ts | 18 +- .../sonar-web/src/main/js/queries/measures.ts | 67 +- .../src/main/js/types/project-activity.ts | 1 + .../resources/org/sonar/l10n/core.properties | 4 + 20 files changed, 1649 insertions(+), 262 deletions(-) create mode 100644 server/sonar-web/src/main/js/components/charts/SplitLine.tsx create mode 100644 server/sonar-web/src/main/js/components/charts/SplitLinePopover.tsx create mode 100644 server/sonar-web/src/main/js/helpers/activity-graph.ts diff --git a/server/sonar-web/src/main/js/api/mocks/TimeMachineServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/TimeMachineServiceMock.ts index 53065863d8d..c292c70ebc4 100644 --- a/server/sonar-web/src/main/js/api/mocks/TimeMachineServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/TimeMachineServiceMock.ts @@ -57,6 +57,7 @@ const defaultMeasureHistory = [ export class TimeMachineServiceMock { #measureHistory: MeasureHistory[]; + toISO = false; constructor() { this.#measureHistory = cloneDeep(defaultMeasureHistory); @@ -109,7 +110,10 @@ export class TimeMachineServiceMock { map = (list: MeasureHistory[]) => { return list.map((item) => ({ ...item, - history: item.history.map((h) => ({ ...h, date: h.date.toDateString() })), + history: item.history.map((h) => ({ + ...h, + date: this.toISO ? h.date.toISOString() : h.date.toDateString(), + })), })); }; 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 0b0b0340620..f4cb3432bb3 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 @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { Spinner } from '@sonarsource/echoes-react'; import React from 'react'; import { useLocation, useRouter } from '~sonar-aligned/components/hoc/withRouter'; import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like'; @@ -33,11 +34,14 @@ import { getHistoryMetrics, isCustomGraph, } from '../../../components/activity-graph/utils'; +import { mergeRatingMeasureHistory } from '../../../helpers/activity-graph'; +import { SOFTWARE_QUALITY_RATING_METRICS } from '../../../helpers/constants'; import { parseDate } from '../../../helpers/dates'; import useApplicationLeakQuery from '../../../queries/applications'; import { useBranchesQuery } from '../../../queries/branch'; import { useAllMeasuresHistoryQuery } from '../../../queries/measures'; import { useAllProjectAnalysesQuery } from '../../../queries/project-analyses'; +import { useIsLegacyCCTMode } from '../../../queries/settings'; import { isApplication, isProject } from '../../../types/component'; import { MeasureHistory, ParsedAnalysis } from '../../../types/project-activity'; import { Query, parseQuery, serializeUrlQuery } from '../utils'; @@ -73,26 +77,22 @@ export function ProjectActivityApp() { ); const { data: analysesData, isLoading: isLoadingAnalyses } = useAllProjectAnalysesQuery(enabled); + const { data: isLegacy, isLoading: isLoadingLegacy } = useIsLegacyCCTMode(); const { data: historyData, isLoading: isLoadingHistory } = useAllMeasuresHistoryQuery( - componentKey, - getBranchLikeQuery(branchLike), - getHistoryMetrics(query.graph || DEFAULT_GRAPH, parsedQuery.customMetrics).join(','), - enabled, + { + component: componentKey, + branchParams: getBranchLikeQuery(branchLike), + metrics: getHistoryMetrics(query.graph || DEFAULT_GRAPH, parsedQuery.customMetrics).join(','), + }, + { enabled }, ); const analyses = React.useMemo(() => analysesData ?? [], [analysesData]); const measuresHistory = React.useMemo( - () => - historyData?.measures?.map((measure) => ({ - metric: measure.metric, - history: measure.history.map((historyItem) => ({ - date: parseDate(historyItem.date), - value: historyItem.value, - })), - })) ?? [], - [historyData], + () => (isLoadingLegacy ? [] : mergeRatingMeasureHistory(historyData, parseDate, isLegacy)), + [historyData, isLegacy, isLoadingLegacy], ); const leakPeriodDate = React.useMemo(() => { @@ -137,20 +137,31 @@ export function ProjectActivityApp() { }); }; + const firstSoftwareQualityRatingMetric = historyData?.measures.find((m) => + SOFTWARE_QUALITY_RATING_METRICS.includes(m.metric), + ); + return ( component && ( - + + h.value === undefined) + } + analysesLoading={isLoadingAnalyses} + graphLoading={isLoadingHistory} + leakPeriodDate={leakPeriodDate} + initializing={isLoadingAnalyses || isLoadingHistory} + measuresHistory={measuresHistory} + metrics={filteredMetrics} + project={component} + onUpdateQuery={handleUpdateQuery} + query={parsedQuery} + /> + ) ); } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppRenderer.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppRenderer.tsx index 6feb7c9302c..f52fbbd5b72 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppRenderer.tsx @@ -42,6 +42,7 @@ interface Props { analysesLoading: boolean; graphLoading: boolean; initializing: boolean; + isLegacy?: boolean; leakPeriodDate?: Date; measuresHistory: MeasureHistory[]; metrics: Metric[]; @@ -61,6 +62,7 @@ export default function ProjectActivityAppRenderer(props: Props) { graphLoading, metrics, project, + isLegacy, } = props; const { configuration, qualifier } = props.project; const canAdmin = @@ -101,6 +103,7 @@ export default function ProjectActivityAppRenderer(props: Props) { analyses={analyses} leakPeriodDate={leakPeriodDate} loading={graphLoading} + isLegacy={isLegacy} measuresHistory={measuresHistory} metrics={metrics} project={project.key} 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 a406dbd8d0e..80b1d177e92 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,9 +35,13 @@ import { splitSeriesInGraphs, } from '../../../components/activity-graph/utils'; import DocumentationLink from '../../../components/common/DocumentationLink'; -import { CCT_SOFTWARE_QUALITY_METRICS } from '../../../helpers/constants'; +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'; import { GraphType, MeasureHistory, @@ -51,6 +55,7 @@ import { PROJECT_ACTIVITY_GRAPH } from './ProjectActivityApp'; interface Props { analyses: ParsedAnalysis[]; + isLegacy?: boolean; leakPeriodDate?: Date; loading: boolean; measuresHistory: MeasureHistory[]; @@ -205,25 +210,28 @@ export default class ProjectActivityGraphs extends React.PureComponent { + const indexOfFirstMeasureWithValue = value?.history.findIndex((item) => item.value); + + return indexOfFirstMeasureWithValue === -1 + ? false + : value?.history.slice(indexOfFirstMeasureWithValue).some((item) => item.value === undefined); + }; + renderQualitiesMetricInfoMessage = () => { - const { measuresHistory } = this.props; + const { measuresHistory, isLegacy } = this.props; const qualityMeasuresHistory = measuresHistory.find((history) => CCT_SOFTWARE_QUALITY_METRICS.includes(history.metric), ); - - const indexOfFirstMeasureWithValue = qualityMeasuresHistory?.history.findIndex( - (item) => item.value, + const ratingQualityMeasuresHistory = measuresHistory.find((history) => + (Object.keys(SOFTWARE_QUALITY_RATING_METRICS_MAP) as MetricKey[]).includes(history.metric), ); - const hasGaps = - indexOfFirstMeasureWithValue === -1 - ? false - : qualityMeasuresHistory?.history - .slice(indexOfFirstMeasureWithValue) - .some((item) => item.value === undefined); - - if (hasGaps) { + if ( + this.hasGaps(qualityMeasuresHistory) || + (!isLegacy && this.hasGaps(ratingQualityMeasuresHistory)) + ) { return ( ({ ...jest.requireActual('../../../../helpers/storage'), @@ -67,6 +68,7 @@ jest.mock('../../../../api/branches', () => ({ const applicationHandler = new ApplicationServiceMock(); const projectActivityHandler = new ProjectActivityServiceMock(); const timeMachineHandler = new TimeMachineServiceMock(); +const settingsHandler = new SettingsServiceMock(); let isBranchReady = false; @@ -77,6 +79,7 @@ beforeEach(() => { applicationHandler.reset(); projectActivityHandler.reset(); timeMachineHandler.reset(); + settingsHandler.reset(); timeMachineHandler.setMeasureHistory( [ @@ -549,6 +552,204 @@ describe('graph interactions', () => { }); }); +describe('ratings', () => { + it('should combine old and new rating + gaps', async () => { + timeMachineHandler.setMeasureHistory([ + mockMeasureHistory({ + metric: MetricKey.reliability_rating, + history: [ + mockHistoryItem({ + value: '5', + date: new Date('2022-01-11'), + }), + mockHistoryItem({ + value: '2', + date: new Date('2022-01-12'), + }), + mockHistoryItem({ + value: '2', + date: new Date('2022-01-13'), + }), + mockHistoryItem({ + value: '2', + date: new Date('2022-01-14'), + }), + ], + }), + mockMeasureHistory({ + metric: MetricKey.software_quality_reliability_rating, + history: [ + mockHistoryItem({ + value: undefined, + date: new Date('2022-01-11'), + }), + mockHistoryItem({ + value: '3', + date: new Date('2022-01-12'), + }), + mockHistoryItem({ + value: undefined, + date: new Date('2022-01-13'), + }), + mockHistoryItem({ + value: '3', + date: new Date('2022-01-14'), + }), + ], + }), + ]); + const { ui } = getPageObject(); + renderProjectActivityAppContainer(); + + await ui.changeGraphType(GraphType.custom); + await ui.openMetricsDropdown(); + await ui.toggleMetric(MetricKey.reliability_rating); + await ui.closeMetricsDropdown(); + + expect(await ui.graphs.findAll()).toHaveLength(1); + expect(ui.metricChangedInfoBtn.get()).toBeInTheDocument(); + expect(ui.gapInfoMessage.get()).toBeInTheDocument(); + expect(byText('E').query()).not.toBeInTheDocument(); + }); + + it('should not show old rating if new one was always there', async () => { + timeMachineHandler.setMeasureHistory([ + mockMeasureHistory({ + metric: MetricKey.reliability_rating, + history: [ + mockHistoryItem({ + value: '5', + date: new Date('2022-01-11'), + }), + mockHistoryItem({ + value: '2', + date: new Date('2022-01-12'), + }), + ], + }), + mockMeasureHistory({ + metric: MetricKey.software_quality_reliability_rating, + history: [ + mockHistoryItem({ + value: '4', + date: new Date('2022-01-11'), + }), + mockHistoryItem({ + value: '3', + date: new Date('2022-01-12'), + }), + ], + }), + ]); + const { ui } = getPageObject(); + renderProjectActivityAppContainer(); + + await ui.changeGraphType(GraphType.custom); + await ui.openMetricsDropdown(); + await ui.toggleMetric(MetricKey.reliability_rating); + await ui.closeMetricsDropdown(); + + expect(await ui.graphs.findAll()).toHaveLength(1); + expect(ui.metricChangedInfoBtn.query()).not.toBeInTheDocument(); + expect(ui.gapInfoMessage.query()).not.toBeInTheDocument(); + expect(byText('E').query()).not.toBeInTheDocument(); + }); + + it('should show E if no new metrics', async () => { + timeMachineHandler.setMeasureHistory([ + mockMeasureHistory({ + metric: MetricKey.reliability_rating, + history: [ + mockHistoryItem({ + value: '5', + date: new Date('2022-01-11'), + }), + mockHistoryItem({ + value: '2', + date: new Date('2022-01-12'), + }), + mockHistoryItem({ + value: '2', + date: new Date('2022-01-13'), + }), + ], + }), + ]); + const { ui } = getPageObject(); + renderProjectActivityAppContainer(); + + await ui.changeGraphType(GraphType.custom); + await ui.openMetricsDropdown(); + await ui.toggleMetric(MetricKey.reliability_rating); + await ui.closeMetricsDropdown(); + + expect(await ui.graphs.findAll()).toHaveLength(1); + expect(ui.metricChangedInfoBtn.query()).not.toBeInTheDocument(); + expect(ui.gapInfoMessage.query()).not.toBeInTheDocument(); + expect(byText('E').get()).toBeInTheDocument(); + }); + + it('should not show gaps message and metric change button, but should show E in legacy mode', async () => { + settingsHandler.set(SettingsKey.LegacyMode, 'true'); + timeMachineHandler.setMeasureHistory([ + mockMeasureHistory({ + metric: MetricKey.reliability_rating, + history: [ + mockHistoryItem({ + value: '5', + date: new Date('2022-01-11'), + }), + mockHistoryItem({ + value: '2', + date: new Date('2022-01-12'), + }), + mockHistoryItem({ + value: '2', + date: new Date('2022-01-13'), + }), + mockHistoryItem({ + value: '2', + date: new Date('2022-01-14'), + }), + ], + }), + mockMeasureHistory({ + metric: MetricKey.software_quality_reliability_rating, + history: [ + mockHistoryItem({ + value: undefined, + date: new Date('2022-01-11'), + }), + mockHistoryItem({ + value: '4', + date: new Date('2022-01-12'), + }), + mockHistoryItem({ + value: undefined, + date: new Date('2022-01-13'), + }), + mockHistoryItem({ + value: '3', + date: new Date('2022-01-14'), + }), + ], + }), + ]); + const { ui } = getPageObject(); + renderProjectActivityAppContainer(); + + await ui.changeGraphType(GraphType.custom); + await ui.openMetricsDropdown(); + await ui.toggleMetric(MetricKey.reliability_rating); + await ui.closeMetricsDropdown(); + + expect(await ui.graphs.findAll()).toHaveLength(1); + expect(ui.metricChangedInfoBtn.query()).not.toBeInTheDocument(); + expect(ui.gapInfoMessage.query()).not.toBeInTheDocument(); + expect(byText('E').get()).toBeInTheDocument(); + }); +}); + function getPageObject() { const user = userEvent.setup(); @@ -562,6 +763,9 @@ function getPageObject() { graphs: byLabelText('project_activity.graphs.explanation_x', { exact: false }), noDataText: byText('project_activity.graphs.custom.no_history'), gapInfoMessage: byText('project_activity.graphs.data_table.data_gap', { exact: false }), + metricChangedInfoBtn: byRole('button', { + name: 'project_activity.graphs.rating_split.info_icon', + }), // Add metrics. addMetricBtn: byRole('button', { name: 'project_activity.graphs.custom.add' }), @@ -623,7 +827,7 @@ function getPageObject() { }, async changeGraphType(type: GraphType) { - await user.click(ui.graphTypeSelect.get()); + await user.click(await ui.graphTypeSelect.find()); const optionForType = await screen.findByText(`project_activity.graphs.${type}`); await user.click(optionForType); }, @@ -759,6 +963,7 @@ function renderProjectActivityAppContainer( 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 }), ], '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 93e6703bfc2..95bde09ef29 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/utils.ts +++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.ts @@ -22,6 +22,7 @@ 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 { @@ -113,7 +114,21 @@ export function getAnalysesByVersionByDay( export function parseQuery(urlQuery: RawQuery): Query { const parsedMetrics = parseAsArray(urlQuery['custom_metrics'], parseAsString); - const customMetrics = uniq(parsedMetrics.map((metric) => MEASURES_REDIRECTION[metric] ?? metric)); + 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) => reversedMetricMap[metric] ?? metric)) + .map((metric) => + SOFTWARE_QUALITY_RATING_METRICS_MAP[metric] + ? [metric, SOFTWARE_QUALITY_RATING_METRICS_MAP[metric]] + : metric, + ) + .flat(); return { category: parseAsString(urlQuery['category']), @@ -136,7 +151,12 @@ export function serializeQuery(query: Query): RawQuery { export function serializeUrlQuery(query: Query): RawQuery { return cleanQuery({ category: serializeString(query.category), - custom_metrics: serializeStringArray(query.customMetrics), + custom_metrics: serializeStringArray( + query.customMetrics.filter( + (metric) => + !Object.values(SOFTWARE_QUALITY_RATING_METRICS_MAP).includes(metric as MetricKey), + ), + ), from: serializeDate(query.from), graph: serializeGraph(query.graph), id: serializeString(query.project), 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 d8dda386b12..b79add8c0f4 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,7 +23,11 @@ import { Dropdown, TextMuted } from 'design-system'; import { sortBy } from 'lodash'; import * as React from 'react'; import { MetricKey, MetricType } from '~sonar-aligned/types/metrics'; -import { CCT_SOFTWARE_QUALITY_METRICS, HIDDEN_METRICS } from '../../helpers/constants'; +import { + CCT_SOFTWARE_QUALITY_METRICS, + HIDDEN_METRICS, + SOFTWARE_QUALITY_RATING_METRICS_MAP, +} from '../../helpers/constants'; import { getLocalizedMetricName, translate } from '../../helpers/l10n'; import { isDiffMetric } from '../../helpers/measures'; import { Metric } from '../../types/types'; @@ -77,6 +81,9 @@ export default class AddGraphMetric extends React.PureComponent { if (HIDDEN_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()) @@ -93,7 +100,11 @@ export default class AddGraphMetric extends React.PureComponent { getSelectedMetricsElements = (metrics: Metric[], selectedMetrics: string[]) => { return metrics - .filter((metric) => selectedMetrics.includes(metric.key)) + .filter( + (metric) => + selectedMetrics.includes(metric.key) && + !Object.values(SOFTWARE_QUALITY_RATING_METRICS_MAP).includes(metric.key as MetricKey), + ) .map((metric) => metric.key); }; diff --git a/server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx index dd5510c9501..028c1ccd46e 100644 --- a/server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx @@ -41,6 +41,7 @@ interface Props { graphEndDate?: Date; graphStartDate?: Date; isCustom?: boolean; + isLegacy?: boolean; leakPeriodDate?: Date; measuresHistory: MeasureHistory[]; metricsType: string; @@ -53,120 +54,117 @@ interface Props { updateTooltip: (selectedDate?: Date) => void; } -interface State { - tooltipIdx?: number; - tooltipXPos?: number; -} - -export default class GraphHistory extends React.PureComponent { - state: State = {}; +export default function GraphHistory(props: Readonly) { + const { + analyses, + canShowDataAsTable = true, + graph, + graphEndDate, + graphStartDate, + isCustom, + leakPeriodDate, + measuresHistory, + metricsType, + selectedDate, + series, + showAreas, + graphDescription, + isLegacy, + } = props; + const [tooltipIdx, setTooltipIdx] = React.useState(undefined); + const [tooltipXPos, setTooltipXPos] = React.useState(undefined); - formatValue = (tick: string | number) => { - return formatMeasure(tick, getShortType(this.props.metricsType)); + const formatValue = (tick: string | number) => { + return formatMeasure(tick, getShortType(metricsType)); }; - formatTooltipValue = (tick: string | number) => { - return formatMeasure(tick, this.props.metricsType); + const formatTooltipValue = (tick: string | number) => { + return formatMeasure(tick, metricsType); }; - updateTooltip = (selectedDate?: Date, tooltipXPos?: number, tooltipIdx?: number) => { - this.props.updateTooltip(selectedDate); - this.setState({ tooltipXPos, tooltipIdx }); + const updateTooltip = (selectedDate?: Date, tooltipXPos?: number, tooltipIdx?: number) => { + props.updateTooltip(selectedDate); + setTooltipIdx(tooltipIdx); + setTooltipXPos(tooltipXPos); }; - render() { - const { - analyses, - canShowDataAsTable = true, - graph, - graphEndDate, - graphStartDate, - isCustom, - leakPeriodDate, - measuresHistory, - metricsType, - selectedDate, - series, - showAreas, - graphDescription, - } = this.props; + const modalProp = ({ onClose }: { onClose: () => void }) => ( + + ); - const modalProp = ({ onClose }: { onClose: () => void }) => ( - - ); + const events = getAnalysisEventsForDate(analyses, selectedDate); - const { tooltipIdx, tooltipXPos } = this.state; - const events = getAnalysisEventsForDate(analyses, selectedDate); + return ( + + {isCustom && props.removeCustomMetric ? ( + + ) : ( + + )} - return ( - - {isCustom && this.props.removeCustomMetric ? ( - - ) : ( - - )} +
+ + {({ height, width }) => ( +
+ m.splitPointDate)?.splitPointDate} + metricType={metricsType} + selectedDate={selectedDate} + isLegacy={isLegacy} + series={series} + showAreas={showAreas} + startDate={graphStartDate} + graphDescription={graphDescription} + updateSelectedDate={props.updateSelectedDate} + updateTooltip={updateTooltip} + updateZoom={props.updateGraphZoom} + width={width} + /> -
- - {({ height, width }) => ( -
- - {selectedDate !== undefined && - tooltipIdx !== undefined && - tooltipXPos !== undefined && ( - - )} -
- )} -
-
- {canShowDataAsTable && ( - - {({ onClick }) => ( - - {translate('project_activity.graphs.open_in_table')} - - )} - - )} - - ); - } + {selectedDate !== undefined && + tooltipIdx !== undefined && + tooltipXPos !== undefined && ( + + )} +
+ )} +
+
+ {canShowDataAsTable && ( + + {({ onClick }) => ( + + {translate('project_activity.graphs.open_in_table')} + + )} + + )} +
+ ); } const StyledGraphContainer = styled.div` diff --git a/server/sonar-web/src/main/js/components/activity-graph/GraphsHistory.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsHistory.tsx index 5bf2137e1d8..9be16362121 100644 --- a/server/sonar-web/src/main/js/components/activity-graph/GraphsHistory.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsHistory.tsx @@ -33,6 +33,7 @@ interface Props { graphEndDate?: Date; graphStartDate?: Date; graphs: Serie[][]; + isLegacy?: boolean; leakPeriodDate?: Date; loading: boolean; measuresHistory: MeasureHistory[]; @@ -66,7 +67,8 @@ export default class GraphsHistory extends React.PureComponent { }; render() { - const { analyses, graph, loading, series, ariaLabel, canShowDataAsTable } = this.props; + const { analyses, graph, loading, series, ariaLabel, canShowDataAsTable, isLegacy } = + this.props; const isCustom = isCustomGraph(graph); if (loading) { @@ -105,6 +107,7 @@ export default class GraphsHistory extends React.PureComponent { graphStartDate={this.props.graphStartDate} isCustom={isCustom} key={idx} + isLegacy={isLegacy} leakPeriodDate={this.props.leakPeriodDate} measuresHistory={this.props.measuresHistory} metricsType={getSeriesMetricType(graphSeries)} diff --git a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.tsx b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.tsx index d53df833e7b..42d786fd4db 100644 --- a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.tsx +++ b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.tsx @@ -39,6 +39,8 @@ import { Chart } from '../../types/types'; import { LINE_CHART_DASHES } from '../activity-graph/utils'; import './AdvancedTimeline.css'; import './LineChart.css'; +import SplitLine from './SplitLine'; +import SplitLinePopover from './SplitLinePopover'; export interface PropsWithoutTheme { basisCurve?: boolean; @@ -49,6 +51,7 @@ export interface PropsWithoutTheme { height: number; hideGrid?: boolean; hideXAxis?: boolean; + isLegacy?: boolean; leakPeriodDate?: Date; // used to avoid same y ticks labels maxYTicksCount?: number; @@ -57,6 +60,7 @@ export interface PropsWithoutTheme { selectedDate?: Date; series: Chart.Serie[]; showAreas?: boolean; + splitPointDate?: Date; startDate?: Date; updateSelectedDate?: (selectedDate?: Date) => void; updateTooltip?: (selectedDate?: Date, tooltipXPos?: number, tooltipIdx?: number) => void; @@ -141,7 +145,10 @@ export class AdvancedTimelineClass extends React.PureComponent { } getRatingScale = (availableHeight: number) => { - return scalePoint().domain([5, 4, 3, 2, 1]).range([availableHeight, 0]); + const { isLegacy } = this.props; + return scalePoint() + .domain(isLegacy ? [5, 4, 3, 2, 1] : [4, 3, 2, 1]) + .range([availableHeight, 0]); }; getLevelScale = (availableHeight: number) => { @@ -627,7 +634,10 @@ export class AdvancedTimelineClass extends React.PureComponent { hideXAxis, showAreas, graphDescription, + metricType, + splitPointDate, } = this.props as PropsWithDefaults; + const { xScale, yScale } = this.state; if (!width || !height) { return
; @@ -637,24 +647,36 @@ export class AdvancedTimelineClass extends React.PureComponent { const isZoomed = Boolean(startDate ?? endDate); return ( - - {zoomEnabled && this.renderClipPath()} - - {leakPeriodDate != null && this.renderLeak()} - {!hideGrid && this.renderHorizontalGrid()} - {!hideXAxis && this.renderXAxisTicks()} - {showAreas && this.renderAreas()} - {this.renderLines()} - {this.renderDots()} - {this.renderSelectedDate()} - {this.renderMouseEventsOverlay(zoomEnabled)} - - +
+ + {zoomEnabled && this.renderClipPath()} + + {leakPeriodDate != null && this.renderLeak()} + {!hideGrid && this.renderHorizontalGrid()} + {!hideXAxis && this.renderXAxisTicks()} + {showAreas && this.renderAreas()} + {this.renderLines()} + {this.renderDots()} + {this.renderSelectedDate()} + {this.renderMouseEventsOverlay(zoomEnabled)} + {metricType === MetricType.Rating && ( + + )} + + + {metricType === MetricType.Rating && ( + + )} +
); } } diff --git a/server/sonar-web/src/main/js/components/charts/SplitLine.tsx b/server/sonar-web/src/main/js/components/charts/SplitLine.tsx new file mode 100644 index 00000000000..cc65690d2de --- /dev/null +++ b/server/sonar-web/src/main/js/components/charts/SplitLine.tsx @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { ScaleLinear, ScalePoint, ScaleTime } from 'd3-scale'; +import * as React from 'react'; +import { shouldShowSplitLine } from '../../helpers/activity-graph'; + +interface Props { + splitPointDate?: Date; + xScale: ScaleTime; + yScale: ScaleLinear | ScalePoint; +} + +export default function SplitLine({ splitPointDate, xScale, yScale }: Readonly) { + const showSplitLine = shouldShowSplitLine(splitPointDate, xScale); + + if (!showSplitLine) { + return null; + } + + return ( + + ); +} diff --git a/server/sonar-web/src/main/js/components/charts/SplitLinePopover.tsx b/server/sonar-web/src/main/js/components/charts/SplitLinePopover.tsx new file mode 100644 index 00000000000..3d29dc59f22 --- /dev/null +++ b/server/sonar-web/src/main/js/components/charts/SplitLinePopover.tsx @@ -0,0 +1,63 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { ButtonIcon, IconInfo, Popover } from '@sonarsource/echoes-react'; +import { ScaleTime } from 'd3-scale'; +import * as React from 'react'; +import { shouldShowSplitLine } from '../../helpers/activity-graph'; +import { DocLink } from '../../helpers/doc-links'; +import { translate } from '../../helpers/l10n'; +import DocumentationLink from '../common/DocumentationLink'; + +interface Props { + paddingLeft: number; + splitPointDate?: Date; + xScale: ScaleTime; +} + +export default function SplitLinePopover({ paddingLeft, splitPointDate, xScale }: Readonly) { + const [popoverOpen, setPopoverOpen] = React.useState(false); + const showSplitLine = shouldShowSplitLine(splitPointDate, xScale); + + if (!showSplitLine) { + return null; + } + + return ( + + {translate('learn_more')} + + } + > + setPopoverOpen(!popoverOpen)} + /> + + ); +} diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/AdvancedTimeline-test.tsx b/server/sonar-web/src/main/js/components/charts/__tests__/AdvancedTimeline-test.tsx index 857ea926894..fbf0cad0c04 100644 --- a/server/sonar-web/src/main/js/components/charts/__tests__/AdvancedTimeline-test.tsx +++ b/server/sonar-web/src/main/js/components/charts/__tests__/AdvancedTimeline-test.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { TooltipProvider } from '@sonarsource/echoes-react'; import { render } from '@testing-library/react'; import * as React from 'react'; import { MetricType } from '~sonar-aligned/types/metrics'; @@ -60,45 +61,61 @@ it('should render correctly', () => { checkSnapShot({ zoomSpeed: 2 }, 'zoomSpeed'); checkSnapShot({ leakPeriodDate: new Date('2019-10-02T00:00:00.000Z') }, 'leakPeriodDate'); checkSnapShot({ basisCurve: true }, 'basisCurve'); + checkSnapShot({ isLegacy: false }, 'not legacy'); + checkSnapShot( + { isLegacy: false, splitPointDate: new Date('2019-10-02T00:00:00.000Z') }, + 'not legacy + split point, but not Rating', + ); + checkSnapShot( + { + isLegacy: false, + splitPointDate: new Date('2019-10-02T00:00:00.000Z'), + metricType: MetricType.Rating, + }, + 'not legacy + split point', + ); }); function renderComponent(props?: Partial) { return render( - , + + + , ); } diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/AdvancedTimeline-test.tsx.snap b/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/AdvancedTimeline-test.tsx.snap index 65b69b04b6c..1d357bc97b8 100644 --- a/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/AdvancedTimeline-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/AdvancedTimeline-test.tsx.snap @@ -1913,6 +1913,698 @@ exports[`should render correctly: no height 1`] = `null`; exports[`should render correctly: no width 1`] = `null`; +exports[`should render correctly: not legacy + split point 1`] = ` + + + + + + + + + + + + + + + + + + + October + + + 06 AM + + + 12 PM + + + 06 PM + + + Wed 02 + + + 06 AM + + + 12 PM + + + 06 PM + + + + + + + + + + + + + +`; + +exports[`should render correctly: not legacy + split point, but not Rating 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + October + + + 06 AM + + + 12 PM + + + 06 PM + + + Wed 02 + + + 06 AM + + + 12 PM + + + 06 PM + + + + + + + + + + + + +`; + +exports[`should render correctly: not legacy 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + October + + + 06 AM + + + 12 PM + + + 06 PM + + + Wed 02 + + + 06 AM + + + 12 PM + + + 06 PM + + + + + + + + + + + + +`; + exports[`should render correctly: rating metric 1`] = ` Date, + isLegacy: boolean = false, +) => { + const softwareQualityMeasures = Object.values(SOFTWARE_QUALITY_RATING_METRICS_MAP); + const softwareQualityMeasuresMap = new Map< + string, + { history: { date: string; value?: string }[]; index: number; splitDate?: Date } + >(); + if (isLegacy) { + 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, + })), + })) ?? [] + ); + } + + const historyDataFiltered = + historyData?.measures?.filter((measure) => { + if (softwareQualityMeasures.includes(measure.metric)) { + const splitPointIndex = measure.history.findIndex( + (historyItem) => historyItem.value != null, + ); + softwareQualityMeasuresMap.set(measure.metric, { + history: measure.history, + index: measure.history.findIndex((historyItem) => historyItem.value != null), + splitDate: + // Don't show splitPoint if it's the first history item + splitPointIndex !== -1 && splitPointIndex !== 0 + ? parseDateFn(measure.history[splitPointIndex].date) + : undefined, + }); + return false; + } + return true; + }) ?? []; + + const historyMapper = (historyItem: { date: string; value?: string }) => ({ + date: parseDateFn(historyItem.date), + value: + softwareQualityMeasuresMap.size > 0 && historyItem.value === '5.0' + ? '4.0' + : historyItem.value, + }); + + return historyDataFiltered.map((measure) => { + const softwareQualityMetric = softwareQualityMeasuresMap.get( + SOFTWARE_QUALITY_RATING_METRICS_MAP[measure.metric], + ); + 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), + }; + }); +}; + +export const shouldShowSplitLine = ( + splitPointDate: Date | undefined, + xScale: ScaleTime, +): splitPointDate is Date => + splitPointDate !== undefined && + xScale(splitPointDate) >= xScale.range()[0] && + xScale(splitPointDate) <= xScale.range()[1]; diff --git a/server/sonar-web/src/main/js/helpers/mocks/metrics.ts b/server/sonar-web/src/main/js/helpers/mocks/metrics.ts index 4bb0a7f82b6..44b98806ac6 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/metrics.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/metrics.ts @@ -618,6 +618,56 @@ export const DEFAULT_METRICS: Dict = { qualitative: false, hidden: true, }, + last_change_on_software_quality_maintainability_rating: { + id: 'f82cff1f-a70a-497a-bca8-33d8abb20e2e', + key: 'last_change_on_software_quality_maintainability_rating', + type: 'DATA', + name: 'Last Change on Software Quality Maintainability Rating', + domain: 'Maintainability', + direction: 0, + qualitative: false, + hidden: true, + }, + last_change_on_software_quality_releasability_rating: { + id: 'a2aebcc4-366d-49b3-852b-c7b36f43e1c7', + key: 'last_change_on_software_quality_releasability_rating', + type: 'DATA', + name: 'Last Change on Software Quality Releasability Rating', + domain: 'Releasability', + direction: 0, + qualitative: false, + hidden: true, + }, + last_change_on_software_quality_reliability_rating: { + id: '42889539-14b7-45a5-a383-8c4d4a5e48a5', + key: 'last_change_on_software_quality_reliability_rating', + type: 'DATA', + name: 'Last Change on Software Quality Reliability Rating', + domain: 'Reliability', + direction: 0, + qualitative: false, + hidden: true, + }, + last_change_on_software_quality_security_rating: { + id: 'd236e941-90e8-4c35-b995-47d05637b6a4', + key: 'last_change_on_software_quality_security_rating', + type: 'DATA', + name: 'Last Change on Software Quality Security Rating', + domain: 'Security', + direction: 0, + qualitative: false, + hidden: true, + }, + last_change_on_software_quality_security_review_rating: { + id: '0f4f143d-b76b-40c9-8769-7f959f4a49ea', + key: 'last_change_on_software_quality_security_review_rating', + type: 'DATA', + name: 'Last Change on SoftwareQuality Security Review Rating', + domain: 'Security', + direction: 0, + qualitative: false, + hidden: true, + }, line_coverage: { id: 'AXJMbIl_PAOIsUIE3gtl', key: 'line_coverage', @@ -1126,7 +1176,7 @@ export const DEFAULT_METRICS: Dict = { key: 'reliability_rating_distribution', type: 'DATA', name: 'Reliability Rating Distribution', - description: 'Maintainability rating distribution', + description: 'Reliability rating distribution', domain: 'Reliability', direction: -1, qualitative: true, @@ -1137,7 +1187,7 @@ export const DEFAULT_METRICS: Dict = { key: 'new_reliability_rating_distribution', type: 'DATA', name: 'Reliability Rating Distribution on New Code', - description: 'Maintainability rating distribution on new code', + description: 'Reliability rating distribution on new code', domain: 'Reliability', direction: -1, qualitative: true, @@ -1420,6 +1470,38 @@ export const DEFAULT_METRICS: Dict = { qualitative: true, hidden: false, }, + software_quality_maintainability_rating_distribution: { + id: 'b39b797b-216d-4800-810e-2277012ee096', + key: 'software_quality_maintainability_rating_distribution', + type: 'DATA', + name: 'Software Quality Maintainability Rating Distribution', + description: 'Software Quality Maintainability rating distribution', + domain: 'Maintainability', + direction: -1, + qualitative: true, + hidden: true, + }, + new_software_quality_maintainability_rating_distribution: { + id: '21d0f133-de6d-4b2e-8302-99169720f8c6', + key: 'new_software_quality_maintainability_rating_distribution', + type: 'DATA', + name: 'Software Quality Maintainability Rating Distribution on New Code', + description: 'Software Quality Maintainability rating distribution on new code', + domain: 'Maintainability', + direction: -1, + qualitative: true, + hidden: true, + }, + software_quality_maintainability_rating_effort: { + id: '0a25e15c-10c9-4d66-8dc0-41446319405d', + key: 'software_quality_maintainability_rating_effort', + type: 'DATA', + name: 'Software Quality Maintainability Rating Effort', + domain: 'Maintainability', + direction: 0, + qualitative: false, + hidden: true, + }, new_software_quality_maintainability_rating: { id: 'c5d12cc4-e712-4701-a395-c9113ce13c3e', key: 'new_software_quality_maintainability_rating', @@ -1455,6 +1537,27 @@ export const DEFAULT_METRICS: Dict = { qualitative: true, hidden: false, }, + software_quality_releasability_rating: { + id: '1fb38855-84b8-41b2-88a0-50c3dceda102', + key: 'software_quality_releasability_rating', + type: 'RATING', + name: 'Software Quality Releasability rating', + domain: 'Releasability', + direction: -1, + qualitative: true, + hidden: false, + }, + software_quality_releasability_rating_distribution: { + id: 'a34a08a2-29b5-4efb-bd2a-eebe3dc10dab', + key: 'software_quality_releasability_rating_distribution', + type: 'DATA', + name: 'Software Quality Releasability Rating Distribution', + description: 'Software Quality Releasability rating distribution', + domain: 'Releasability', + direction: -1, + qualitative: true, + hidden: true, + }, software_quality_reliability_rating: { id: '6548ffa4-8a5e-4445-a28d-e2fd9fdbba78', key: 'software_quality_reliability_rating', @@ -1466,6 +1569,38 @@ export const DEFAULT_METRICS: Dict = { qualitative: true, hidden: false, }, + software_quality_reliability_rating_distribution: { + id: '571de2d7-d1ef-460b-8f99-e29e0aa6218c', + key: 'software_quality_reliability_rating_distribution', + type: 'DATA', + name: 'Software Quality Reliability Rating Distribution', + description: 'Software Quality Reliability rating distribution', + domain: 'Reliability', + direction: -1, + qualitative: true, + hidden: true, + }, + new_software_quality_reliability_rating_distribution: { + id: '77693e0a-fc61-465f-8fe0-a5fe77f4d507', + key: 'new_software_quality_reliability_rating_distribution', + type: 'DATA', + name: 'Software Quality Reliability Rating Distribution on New Code', + description: 'Software Quality Reliability rating distribution on new code', + domain: 'Reliability', + direction: -1, + qualitative: true, + hidden: true, + }, + software_quality_reliability_rating_effort: { + id: '38f61088-42f3-437f-b2b5-65a8fa4c7448', + key: 'software_quality_reliability_rating_effort', + type: 'DATA', + name: 'Software Quality Reliability Rating Effort', + domain: 'Reliability', + direction: 0, + qualitative: false, + hidden: true, + }, new_software_quality_reliability_rating: { id: 'ab82dcac-cf81-4780-965d-1384ce9e8983', key: 'new_software_quality_reliability_rating', @@ -1510,6 +1645,38 @@ export const DEFAULT_METRICS: Dict = { qualitative: true, hidden: false, }, + software_quality_security_rating_distribution: { + id: 'f9a76abe-7663-47b3-a27c-1dea7e6b4861', + key: 'software_quality_security_rating_distribution', + type: 'DATA', + name: 'Software Quality Security Rating Distribution', + description: 'Software Quality Security rating distribution', + domain: 'Security', + direction: -1, + qualitative: true, + hidden: true, + }, + new_software_quality_security_rating_distribution: { + id: '2f1155cb-3802-463a-95d3-dd352bafdc0c', + key: 'new_software_quality_security_rating_distribution', + type: 'DATA', + name: 'Software Quality Security Rating Distribution on New Code', + description: 'Software Quality Security rating distribution on new code', + domain: 'Security', + direction: -1, + qualitative: true, + hidden: true, + }, + software_quality_security_rating_effort: { + id: 'be673f93-1b72-418c-b134-b097dae65048', + key: 'software_quality_security_rating_effort', + type: 'DATA', + name: 'Software Quality Security Rating Effort', + domain: 'Security', + direction: 0, + qualitative: false, + hidden: true, + }, new_software_quality_security_rating: { id: '228b9a04-09a2-418e-9ea4-3584a57a95ba', key: 'new_software_quality_security_rating', @@ -1554,6 +1721,38 @@ export const DEFAULT_METRICS: Dict = { qualitative: true, hidden: false, }, + software_quality_security_review_rating_distribution: { + id: '3b9d046c-a319-4cbf-99c4-15c206e71401', + key: 'software_quality_security_review_rating_distribution', + type: 'DATA', + name: 'software Quality Security Review Rating Distribution', + description: 'Software Quality Security review rating distribution', + domain: 'Security', + direction: -1, + qualitative: true, + hidden: true, + }, + new_software_quality_security_review_rating_distribution: { + id: 'b6565b9b-3676-405f-8149-a37cb0bd78b9', + key: 'new_software_quality_security_review_rating_distribution', + type: 'DATA', + name: 'Software Quality Security Review Rating Distribution on New Code', + description: 'Software Quality Security review rating distribution on new code', + domain: 'Security', + direction: -1, + qualitative: true, + hidden: true, + }, + software_quality_security_review_rating_effort: { + id: '8d6d023e-6764-42ee-9fb7-bc1f17bda205', + key: 'software_quality_security_review_rating_effort', + type: 'DATA', + name: 'Software Quality Security Review Rating Effort', + domain: 'Security', + direction: 0, + qualitative: false, + hidden: true, + }, new_software_quality_security_review_rating: { id: '4d4b1d18-da7e-403c-a3f0-99951c58b050', key: 'new_software_quality_security_review_rating', diff --git a/server/sonar-web/src/main/js/queries/component.ts b/server/sonar-web/src/main/js/queries/component.ts index 0034d72d883..642564b73d4 100644 --- a/server/sonar-web/src/main/js/queries/component.ts +++ b/server/sonar-web/src/main/js/queries/component.ts @@ -22,22 +22,9 @@ import { groupBy, omit } from 'lodash'; import { BranchParameters } from '~sonar-aligned/types/branch-like'; import { getTasksForComponent } from '../api/ce'; import { getBreadcrumbs, getComponent, getComponentData } from '../api/components'; -import { MetricKey } from '../sonar-aligned/types/metrics'; import { Component, Measure } from '../types/types'; import { StaleTime, createQueryHook } from './common'; -const NEW_METRICS = [ - MetricKey.software_quality_maintainability_rating, - MetricKey.software_quality_security_rating, - MetricKey.software_quality_reliability_rating, - MetricKey.software_quality_security_review_rating, - MetricKey.software_quality_releasability_rating, - MetricKey.new_software_quality_security_rating, - MetricKey.new_software_quality_reliability_rating, - MetricKey.new_software_quality_maintainability_rating, - MetricKey.new_software_quality_security_review_rating, -]; - const TASK_RETRY = 10_000; type QueryKeyData = { @@ -70,10 +57,7 @@ export const useComponentQuery = createQueryHook( queryFn: async () => { const result = await getComponent({ component, - metricKeys: metricKeys - .split(',') - .filter((m) => !NEW_METRICS.includes(m as MetricKey)) - .join(), + metricKeys, ...params, }); const measuresMapByMetricKey = groupBy(result.component.measures, 'metric'); diff --git a/server/sonar-web/src/main/js/queries/measures.ts b/server/sonar-web/src/main/js/queries/measures.ts index 5b5194bf1e0..38ebd2c0c82 100644 --- a/server/sonar-web/src/main/js/queries/measures.ts +++ b/server/sonar-web/src/main/js/queries/measures.ts @@ -18,12 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { - infiniteQueryOptions, - queryOptions, - useQuery, - useQueryClient, -} from '@tanstack/react-query'; +import { infiniteQueryOptions, queryOptions, useQueryClient } from '@tanstack/react-query'; import { groupBy, isUndefined, omitBy } from 'lodash'; import { BranchParameters } from '~sonar-aligned/types/branch-like'; import { getComponentTree } from '../api/components'; @@ -40,26 +35,28 @@ import { BranchLike } from '../types/branch-like'; import { Measure } from '../types/types'; import { createInfiniteQueryHook, createQueryHook, StaleTime } from './common'; -export function useAllMeasuresHistoryQuery( - component: string | undefined, - branchParams: BranchParameters, - metrics: string, - enabled = true, -) { - return useQuery({ - queryKey: ['measures', 'history', component, branchParams, metrics], - queryFn: () => { - if (metrics.length <= 0) { - return Promise.resolve({ - measures: [], - paging: { pageIndex: 1, pageSize: 1, total: 0 }, - }); - } - return getAllTimeMachineData({ component, metrics, ...branchParams, p: 1 }); - }, - enabled, - }); -} +export const useAllMeasuresHistoryQuery = createQueryHook( + ({ + component, + branchParams, + metrics, + }: Omit[0], 'to' | 'from' | 'p'> & { + branchParams?: BranchParameters; + }) => { + return queryOptions({ + queryKey: ['measures', 'history', component, branchParams, metrics], + queryFn: () => { + if (metrics.length <= 0) { + return Promise.resolve({ + measures: [], + paging: { pageIndex: 1, pageSize: 1, total: 0 }, + }); + } + return getAllTimeMachineData({ component, metrics, ...branchParams, p: 1 }); + }, + }); + }, +); export const useMeasuresComponentQuery = createQueryHook( ({ @@ -79,9 +76,7 @@ export const useMeasuresComponentQuery = createQueryHook( queryFn: async () => { const data = await getMeasuresWithPeriodAndMetrics( componentKey, - metricKeys.filter( - (m) => ![MetricKey.software_quality_releasability_rating].includes(m as MetricKey), - ), + metricKeys, branchLikeQuery, ); metricKeys.forEach((metricKey) => { @@ -123,14 +118,11 @@ export const useComponentTreeQuery = createInfiniteQueryHook( return infiniteQueryOptions({ queryKey: ['component', component, 'tree', strategy, { metrics, additionalData }], queryFn: async ({ pageParam }) => { - const result = await getComponentTree( - strategy, - component, - metrics?.filter( - (m) => ![MetricKey.software_quality_releasability_rating].includes(m as MetricKey), - ), - { ...additionalData, p: pageParam, ...branchLikeQuery }, - ); + const result = await getComponentTree(strategy, component, metrics, { + ...additionalData, + p: pageParam, + ...branchLikeQuery, + }); if (result.baseComponent.measures && result.baseComponent.measures.length > 0) { const measuresMapByMetricKeyForBaseComponent = groupBy( @@ -270,7 +262,6 @@ export const useMeasureQuery = createQueryHook( ); const PORTFOLIO_OVERVIEW_METRIC_KEYS = [ - MetricKey.software_quality_releasability_rating, MetricKey.software_quality_releasability_rating_distribution, MetricKey.software_quality_security_rating_distribution, MetricKey.software_quality_security_review_rating_distribution, diff --git a/server/sonar-web/src/main/js/types/project-activity.ts b/server/sonar-web/src/main/js/types/project-activity.ts index 16611694225..cedb36929ea 100644 --- a/server/sonar-web/src/main/js/types/project-activity.ts +++ b/server/sonar-web/src/main/js/types/project-activity.ts @@ -99,6 +99,7 @@ export interface HistoryItem { export interface MeasureHistory { history: HistoryItem[]; metric: MetricKey; + splitPointDate?: Date; } export interface Serie { 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 41ac785abeb..2389ea590ce 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2033,6 +2033,10 @@ 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.graphs.data_table.data_gap=The chart history for issues related to software qualities may contain gaps while information is not available for one or more projects. {learn_more} +project_activity.graphs.rating_split.title=Metrics calculation changed +project_activity.graphs.rating_split.description=The way we calculate ratings has changed and it might have affected your ratings. +project_activity.graphs.rating_split.info_icon=Metrics calculation change information + project_activity.custom_metric.covered_lines=Covered Lines project_activity.custom_metric.deprecated.severity=Old severities and the corresponding filters are deprecated. -- 2.39.5