export class TimeMachineServiceMock {
#measureHistory: MeasureHistory[];
+ toISO = false;
constructor() {
this.#measureHistory = cloneDeep(defaultMeasureHistory);
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(),
+ })),
}));
};
* 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';
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';
);
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(() => {
});
};
+ const firstSoftwareQualityRatingMetric = historyData?.measures.find((m) =>
+ SOFTWARE_QUALITY_RATING_METRICS.includes(m.metric),
+ );
+
return (
component && (
- <ProjectActivityAppRenderer
- analyses={analyses}
- analysesLoading={isLoadingAnalyses}
- graphLoading={isLoadingHistory}
- leakPeriodDate={leakPeriodDate}
- initializing={isLoadingAnalyses || isLoadingHistory}
- measuresHistory={measuresHistory}
- metrics={filteredMetrics}
- project={component}
- onUpdateQuery={handleUpdateQuery}
- query={parsedQuery}
- />
+ <Spinner isLoading={isLoadingLegacy}>
+ <ProjectActivityAppRenderer
+ analyses={analyses}
+ isLegacy={
+ isLegacy ||
+ !firstSoftwareQualityRatingMetric ||
+ firstSoftwareQualityRatingMetric.history.every((h) => h.value === undefined)
+ }
+ analysesLoading={isLoadingAnalyses}
+ graphLoading={isLoadingHistory}
+ leakPeriodDate={leakPeriodDate}
+ initializing={isLoadingAnalyses || isLoadingHistory}
+ measuresHistory={measuresHistory}
+ metrics={filteredMetrics}
+ project={component}
+ onUpdateQuery={handleUpdateQuery}
+ query={parsedQuery}
+ />
+ </Spinner>
)
);
}
analysesLoading: boolean;
graphLoading: boolean;
initializing: boolean;
+ isLegacy?: boolean;
leakPeriodDate?: Date;
measuresHistory: MeasureHistory[];
metrics: Metric[];
graphLoading,
metrics,
project,
+ isLegacy,
} = props;
const { configuration, qualifier } = props.project;
const canAdmin =
analyses={analyses}
leakPeriodDate={leakPeriodDate}
loading={graphLoading}
+ isLegacy={isLegacy}
measuresHistory={measuresHistory}
metrics={metrics}
project={project.key}
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,
interface Props {
analyses: ParsedAnalysis[];
+ isLegacy?: boolean;
leakPeriodDate?: Date;
loading: boolean;
measuresHistory: MeasureHistory[];
}
};
+ hasGaps = (value?: MeasureHistory) => {
+ 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 (
<FlagMessage variant="info">
<FormattedMessage
};
render() {
- const { analyses, leakPeriodDate, loading, measuresHistory, metrics, query } = this.props;
+ const { analyses, leakPeriodDate, loading, measuresHistory, metrics, query, isLegacy } =
+ this.props;
const { graphEndDate, graphStartDate, series } = this.state;
return (
graphs={this.state.graphs}
leakPeriodDate={leakPeriodDate}
loading={loading}
+ isLegacy={isLegacy}
measuresHistory={measuresHistory}
removeCustomMetric={this.handleRemoveCustomMetric}
selectedDate={query.selectedDate}
import { MetricKey, MetricType } from '~sonar-aligned/types/metrics';
import ApplicationServiceMock from '../../../../api/mocks/ApplicationServiceMock';
import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock';
+import SettingsServiceMock from '../../../../api/mocks/SettingsServiceMock';
import { TimeMachineServiceMock } from '../../../../api/mocks/TimeMachineServiceMock';
import { mockBranchList } from '../../../../api/mocks/data/branches';
import { DEPRECATED_ACTIVITY_METRICS } from '../../../../helpers/constants';
GraphType,
ProjectAnalysisEventCategory,
} from '../../../../types/project-activity';
+import { SettingsKey } from '../../../../types/settings';
import ProjectActivityAppContainer from '../ProjectActivityApp';
jest.mock('../../../../api/projectActivity');
-jest.mock('../../../../api/time-machine');
jest.mock('../../../../helpers/storage', () => ({
...jest.requireActual('../../../../helpers/storage'),
const applicationHandler = new ApplicationServiceMock();
const projectActivityHandler = new ProjectActivityServiceMock();
const timeMachineHandler = new TimeMachineServiceMock();
+const settingsHandler = new SettingsServiceMock();
let isBranchReady = false;
applicationHandler.reset();
projectActivityHandler.reset();
timeMachineHandler.reset();
+ settingsHandler.reset();
timeMachineHandler.setMeasureHistory(
[
});
});
+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();
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' }),
},
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);
},
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',
),
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 {
export function parseQuery(urlQuery: RawQuery): Query {
const parsedMetrics = parseAsArray(urlQuery['custom_metrics'], parseAsString<MetricKey>);
- 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']),
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),
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';
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())
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);
};
graphEndDate?: Date;
graphStartDate?: Date;
isCustom?: boolean;
+ isLegacy?: boolean;
leakPeriodDate?: Date;
measuresHistory: MeasureHistory[];
metricsType: string;
updateTooltip: (selectedDate?: Date) => void;
}
-interface State {
- tooltipIdx?: number;
- tooltipXPos?: number;
-}
-
-export default class GraphHistory extends React.PureComponent<Props, State> {
- state: State = {};
+export default function GraphHistory(props: Readonly<Props>) {
+ const {
+ analyses,
+ canShowDataAsTable = true,
+ graph,
+ graphEndDate,
+ graphStartDate,
+ isCustom,
+ leakPeriodDate,
+ measuresHistory,
+ metricsType,
+ selectedDate,
+ series,
+ showAreas,
+ graphDescription,
+ isLegacy,
+ } = props;
+ const [tooltipIdx, setTooltipIdx] = React.useState<number | undefined>(undefined);
+ const [tooltipXPos, setTooltipXPos] = React.useState<number | undefined>(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 }) => (
+ <DataTableModal
+ analyses={analyses}
+ graphEndDate={graphEndDate}
+ graphStartDate={graphStartDate}
+ series={series}
+ onClose={onClose}
+ />
+ );
- const modalProp = ({ onClose }: { onClose: () => void }) => (
- <DataTableModal
- analyses={analyses}
- graphEndDate={graphEndDate}
- graphStartDate={graphStartDate}
- series={series}
- onClose={onClose}
- />
- );
+ const events = getAnalysisEventsForDate(analyses, selectedDate);
- const { tooltipIdx, tooltipXPos } = this.state;
- const events = getAnalysisEventsForDate(analyses, selectedDate);
+ return (
+ <StyledGraphContainer className="sw-flex sw-flex-col sw-justify-center sw-items-stretch sw-grow sw-py-2">
+ {isCustom && props.removeCustomMetric ? (
+ <GraphsLegendCustom
+ leakPeriodDate={leakPeriodDate}
+ removeMetric={props.removeCustomMetric}
+ series={series}
+ />
+ ) : (
+ <GraphsLegendStatic leakPeriodDate={leakPeriodDate} series={series} />
+ )}
- return (
- <StyledGraphContainer className="sw-flex sw-flex-col sw-justify-center sw-items-stretch sw-grow sw-py-2">
- {isCustom && this.props.removeCustomMetric ? (
- <GraphsLegendCustom
- leakPeriodDate={leakPeriodDate}
- removeMetric={this.props.removeCustomMetric}
- series={series}
- />
- ) : (
- <GraphsLegendStatic leakPeriodDate={leakPeriodDate} series={series} />
- )}
+ <div className="sw-flex-1">
+ <AutoSizer>
+ {({ height, width }) => (
+ <div>
+ <AdvancedTimeline
+ endDate={graphEndDate}
+ formatYTick={formatValue}
+ height={height}
+ leakPeriodDate={leakPeriodDate}
+ splitPointDate={measuresHistory.find((m) => 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}
+ />
- <div className="sw-flex-1">
- <AutoSizer>
- {({ height, width }) => (
- <div>
- <AdvancedTimeline
- endDate={graphEndDate}
- formatYTick={this.formatValue}
- height={height}
- leakPeriodDate={leakPeriodDate}
- metricType={metricsType}
- selectedDate={selectedDate}
- series={series}
- showAreas={showAreas}
- startDate={graphStartDate}
- graphDescription={graphDescription}
- updateSelectedDate={this.props.updateSelectedDate}
- updateTooltip={this.updateTooltip}
- updateZoom={this.props.updateGraphZoom}
- width={width}
- />
- {selectedDate !== undefined &&
- tooltipIdx !== undefined &&
- tooltipXPos !== undefined && (
- <GraphsTooltips
- events={events}
- formatValue={this.formatTooltipValue}
- graph={graph}
- graphWidth={width}
- measuresHistory={measuresHistory}
- selectedDate={selectedDate}
- series={series}
- tooltipIdx={tooltipIdx}
- tooltipPos={tooltipXPos}
- />
- )}
- </div>
- )}
- </AutoSizer>
- </div>
- {canShowDataAsTable && (
- <ModalButton modal={modalProp}>
- {({ onClick }) => (
- <ButtonSecondary className="sw-sr-only" onClick={onClick}>
- {translate('project_activity.graphs.open_in_table')}
- </ButtonSecondary>
- )}
- </ModalButton>
- )}
- </StyledGraphContainer>
- );
- }
+ {selectedDate !== undefined &&
+ tooltipIdx !== undefined &&
+ tooltipXPos !== undefined && (
+ <GraphsTooltips
+ events={events}
+ formatValue={formatTooltipValue}
+ graph={graph}
+ graphWidth={width}
+ measuresHistory={measuresHistory}
+ selectedDate={selectedDate}
+ series={series}
+ tooltipIdx={tooltipIdx}
+ tooltipPos={tooltipXPos}
+ />
+ )}
+ </div>
+ )}
+ </AutoSizer>
+ </div>
+ {canShowDataAsTable && (
+ <ModalButton modal={modalProp}>
+ {({ onClick }) => (
+ <ButtonSecondary className="sw-sr-only" onClick={onClick}>
+ {translate('project_activity.graphs.open_in_table')}
+ </ButtonSecondary>
+ )}
+ </ModalButton>
+ )}
+ </StyledGraphContainer>
+ );
}
const StyledGraphContainer = styled.div`
graphEndDate?: Date;
graphStartDate?: Date;
graphs: Serie[][];
+ isLegacy?: boolean;
leakPeriodDate?: Date;
loading: boolean;
measuresHistory: MeasureHistory[];
};
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) {
graphStartDate={this.props.graphStartDate}
isCustom={isCustom}
key={idx}
+ isLegacy={isLegacy}
leakPeriodDate={this.props.leakPeriodDate}
measuresHistory={this.props.measuresHistory}
metricsType={getSeriesMetricType(graphSeries)}
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;
height: number;
hideGrid?: boolean;
hideXAxis?: boolean;
+ isLegacy?: boolean;
leakPeriodDate?: Date;
// used to avoid same y ticks labels
maxYTicksCount?: number;
selectedDate?: Date;
series: Chart.Serie[];
showAreas?: boolean;
+ splitPointDate?: Date;
startDate?: Date;
updateSelectedDate?: (selectedDate?: Date) => void;
updateTooltip?: (selectedDate?: Date, tooltipXPos?: number, tooltipIdx?: number) => void;
}
getRatingScale = (availableHeight: number) => {
- return scalePoint<number>().domain([5, 4, 3, 2, 1]).range([availableHeight, 0]);
+ const { isLegacy } = this.props;
+ return scalePoint<number>()
+ .domain(isLegacy ? [5, 4, 3, 2, 1] : [4, 3, 2, 1])
+ .range([availableHeight, 0]);
};
getLevelScale = (availableHeight: number) => {
hideXAxis,
showAreas,
graphDescription,
+ metricType,
+ splitPointDate,
} = this.props as PropsWithDefaults;
+ const { xScale, yScale } = this.state;
if (!width || !height) {
return <div />;
const isZoomed = Boolean(startDate ?? endDate);
return (
- <svg
- aria-label={graphDescription}
- className={classNames('line-chart', { 'chart-zoomed': isZoomed })}
- height={height}
- width={width}
- >
- {zoomEnabled && this.renderClipPath()}
- <g transform={`translate(${padding[3]}, ${padding[0]})`}>
- {leakPeriodDate != null && this.renderLeak()}
- {!hideGrid && this.renderHorizontalGrid()}
- {!hideXAxis && this.renderXAxisTicks()}
- {showAreas && this.renderAreas()}
- {this.renderLines()}
- {this.renderDots()}
- {this.renderSelectedDate()}
- {this.renderMouseEventsOverlay(zoomEnabled)}
- </g>
- </svg>
+ <div className="sw-relative">
+ <svg
+ aria-label={graphDescription}
+ className={classNames('line-chart', { 'chart-zoomed': isZoomed })}
+ height={height}
+ width={width}
+ >
+ {zoomEnabled && this.renderClipPath()}
+ <g transform={`translate(${padding[3]}, ${padding[0]})`}>
+ {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 && (
+ <SplitLine splitPointDate={splitPointDate} xScale={xScale} yScale={yScale} />
+ )}
+ </g>
+ </svg>
+ {metricType === MetricType.Rating && (
+ <SplitLinePopover
+ paddingLeft={padding[3]}
+ splitPointDate={splitPointDate}
+ xScale={xScale}
+ />
+ )}
+ </div>
);
}
}
--- /dev/null
+/*
+ * 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<number, number>;
+ yScale: ScaleLinear<number, number> | ScalePoint<number | string>;
+}
+
+export default function SplitLine({ splitPointDate, xScale, yScale }: Readonly<Props>) {
+ const showSplitLine = shouldShowSplitLine(splitPointDate, xScale);
+
+ if (!showSplitLine) {
+ return null;
+ }
+
+ return (
+ <line
+ className="line-tooltip"
+ strokeDasharray="2"
+ x1={xScale(splitPointDate)}
+ x2={xScale(splitPointDate)}
+ y1={yScale.range()[0]}
+ y2={yScale.range()[1] - 10}
+ />
+ );
+}
--- /dev/null
+/*
+ * 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<number, number>;
+}
+
+export default function SplitLinePopover({ paddingLeft, splitPointDate, xScale }: Readonly<Props>) {
+ const [popoverOpen, setPopoverOpen] = React.useState(false);
+ const showSplitLine = shouldShowSplitLine(splitPointDate, xScale);
+
+ if (!showSplitLine) {
+ return null;
+ }
+
+ return (
+ <Popover
+ isOpen={popoverOpen}
+ title={translate('project_activity.graphs.rating_split.title')}
+ description={translate('project_activity.graphs.rating_split.description')}
+ footer={
+ <DocumentationLink to={DocLink.MetricDefinitions}>
+ {translate('learn_more')}
+ </DocumentationLink>
+ }
+ >
+ <ButtonIcon
+ isIconFilled
+ style={{ left: `${Math.round(xScale(splitPointDate)) + paddingLeft}px` }}
+ className="sw-border-none sw-absolute sw-bg-transparent sw--top-3 sw--translate-x-2/4"
+ ariaLabel={translate('project_activity.graphs.rating_split.info_icon')}
+ Icon={IconInfo}
+ onClick={() => setPopoverOpen(!popoverOpen)}
+ />
+ </Popover>
+ );
+}
* 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';
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<PropsWithoutTheme>) {
return render(
- <AdvancedTimeline
- height={100}
- maxYTicksCount={10}
- metricType="TEST_METRIC"
- series={[
- {
- name: 'test-1',
- type: 'test-type-1',
- translatedName: '',
- data: [
- {
- x: new Date('2019-10-01T00:00:00.000Z'),
- y: 1,
- },
- {
- x: new Date('2019-10-02T00:00:00.000Z'),
- y: 2,
- },
- ],
- },
- {
- name: 'test-2',
- type: 'test-type-2',
- translatedName: '',
- data: [
- {
- x: new Date('2019-10-03T00:00:00.000Z'),
- y: 3,
- },
- ],
- },
- ]}
- width={100}
- zoomSpeed={1}
- {...props}
- />,
+ <TooltipProvider>
+ <AdvancedTimeline
+ height={100}
+ maxYTicksCount={10}
+ metricType="TEST_METRIC"
+ series={[
+ {
+ name: 'test-1',
+ type: 'test-type-1',
+ translatedName: '',
+ data: [
+ {
+ x: new Date('2019-10-01T00:00:00.000Z'),
+ y: 1,
+ },
+ {
+ x: new Date('2019-10-02T00:00:00.000Z'),
+ y: 2,
+ },
+ ],
+ },
+ {
+ name: 'test-2',
+ type: 'test-type-2',
+ translatedName: '',
+ data: [
+ {
+ x: new Date('2019-10-03T00:00:00.000Z'),
+ y: 3,
+ },
+ ],
+ },
+ ]}
+ width={100}
+ zoomSpeed={1}
+ isLegacy
+ {...props}
+ />
+ </TooltipProvider>,
);
}
exports[`should render correctly: no width 1`] = `null`;
+exports[`should render correctly: not legacy + split point 1`] = `
+<svg
+ class="line-chart"
+ height="100"
+ width="100"
+>
+ <g
+ transform="translate(50, 26)"
+ >
+ <g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="24"
+ y2="24"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="16"
+ y2="16"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="8"
+ y2="8"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="0"
+ y2="0"
+ />
+ </g>
+ </g>
+ <g
+ transform="translate(0, 20)"
+ >
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 15, 24)"
+ x="15"
+ y="24"
+ >
+ October
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 20, 24)"
+ x="20"
+ y="24"
+ >
+ 06 AM
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 25, 24)"
+ x="25"
+ y="24"
+ >
+ 12 PM
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 30, 24)"
+ x="30"
+ y="24"
+ >
+ 06 PM
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 35, 24)"
+ x="35"
+ y="24"
+ >
+ Wed 02
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 40, 24)"
+ x="40"
+ y="24"
+ >
+ 06 AM
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 45, 24)"
+ x="45"
+ y="24"
+ >
+ 12 PM
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 50, 24)"
+ x="50"
+ y="24"
+ >
+ 06 PM
+ </text>
+ </g>
+ <g>
+ <path
+ class="line-chart-path line-chart-path-0"
+ d="M0,0L20,8"
+ stroke="rgb(85,170,223)"
+ stroke-dasharray="0"
+ />
+ <path
+ class="line-chart-path line-chart-path-1"
+ d="M40,16Z"
+ stroke="rgb(58,127,173)"
+ stroke-dasharray="3"
+ />
+ </g>
+ <g>
+ <circle
+ cx="40"
+ cy="16"
+ fill="rgb(58,127,173)"
+ r="2"
+ stroke="white"
+ stroke-width="1"
+ />
+ </g>
+ <rect
+ class="chart-mouse-events-overlay"
+ height="24"
+ width="40"
+ />
+ <line
+ class="line-tooltip"
+ stroke-dasharray="2"
+ x1="20"
+ x2="20"
+ y1="24"
+ y2="-10"
+ />
+ </g>
+</svg>
+`;
+
+exports[`should render correctly: not legacy + split point, but not Rating 1`] = `
+<svg
+ class="line-chart"
+ height="100"
+ width="100"
+>
+ <g
+ transform="translate(50, 26)"
+ >
+ <g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="24"
+ y2="24"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="22.4"
+ y2="22.4"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="20.8"
+ y2="20.8"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="19.200000000000003"
+ y2="19.200000000000003"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="17.6"
+ y2="17.6"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="16"
+ y2="16"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="14.400000000000002"
+ y2="14.400000000000002"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="12.800000000000002"
+ y2="12.800000000000002"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="11.2"
+ y2="11.2"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="9.600000000000001"
+ y2="9.600000000000001"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="8"
+ y2="8"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="6.399999999999999"
+ y2="6.399999999999999"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="4.800000000000002"
+ y2="4.800000000000002"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="3.1999999999999993"
+ y2="3.1999999999999993"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="1.6000000000000023"
+ y2="1.6000000000000023"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="0"
+ y2="0"
+ />
+ </g>
+ </g>
+ <g
+ transform="translate(0, 20)"
+ >
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 15, 24)"
+ x="15"
+ y="24"
+ >
+ October
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 20, 24)"
+ x="20"
+ y="24"
+ >
+ 06 AM
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 25, 24)"
+ x="25"
+ y="24"
+ >
+ 12 PM
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 30, 24)"
+ x="30"
+ y="24"
+ >
+ 06 PM
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 35, 24)"
+ x="35"
+ y="24"
+ >
+ Wed 02
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 40, 24)"
+ x="40"
+ y="24"
+ >
+ 06 AM
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 45, 24)"
+ x="45"
+ y="24"
+ >
+ 12 PM
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 50, 24)"
+ x="50"
+ y="24"
+ >
+ 06 PM
+ </text>
+ </g>
+ <g>
+ <path
+ class="line-chart-path line-chart-path-0"
+ d="M0,16L20,8"
+ stroke="rgb(85,170,223)"
+ stroke-dasharray="0"
+ />
+ <path
+ class="line-chart-path line-chart-path-1"
+ d="M40,0Z"
+ stroke="rgb(58,127,173)"
+ stroke-dasharray="3"
+ />
+ </g>
+ <g>
+ <circle
+ cx="40"
+ cy="0"
+ fill="rgb(58,127,173)"
+ r="2"
+ stroke="white"
+ stroke-width="1"
+ />
+ </g>
+ <rect
+ class="chart-mouse-events-overlay"
+ height="24"
+ width="40"
+ />
+ </g>
+</svg>
+`;
+
+exports[`should render correctly: not legacy 1`] = `
+<svg
+ class="line-chart"
+ height="100"
+ width="100"
+>
+ <g
+ transform="translate(50, 26)"
+ >
+ <g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="24"
+ y2="24"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="22.4"
+ y2="22.4"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="20.8"
+ y2="20.8"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="19.200000000000003"
+ y2="19.200000000000003"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="17.6"
+ y2="17.6"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="16"
+ y2="16"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="14.400000000000002"
+ y2="14.400000000000002"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="12.800000000000002"
+ y2="12.800000000000002"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="11.2"
+ y2="11.2"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="9.600000000000001"
+ y2="9.600000000000001"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="8"
+ y2="8"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="6.399999999999999"
+ y2="6.399999999999999"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="4.800000000000002"
+ y2="4.800000000000002"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="3.1999999999999993"
+ y2="3.1999999999999993"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="1.6000000000000023"
+ y2="1.6000000000000023"
+ />
+ </g>
+ <g>
+ <line
+ class="line-chart-grid"
+ x1="0"
+ x2="40"
+ y1="0"
+ y2="0"
+ />
+ </g>
+ </g>
+ <g
+ transform="translate(0, 20)"
+ >
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 15, 24)"
+ x="15"
+ y="24"
+ >
+ October
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 20, 24)"
+ x="20"
+ y="24"
+ >
+ 06 AM
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 25, 24)"
+ x="25"
+ y="24"
+ >
+ 12 PM
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 30, 24)"
+ x="30"
+ y="24"
+ >
+ 06 PM
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 35, 24)"
+ x="35"
+ y="24"
+ >
+ Wed 02
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 40, 24)"
+ x="40"
+ y="24"
+ >
+ 06 AM
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 45, 24)"
+ x="45"
+ y="24"
+ >
+ 12 PM
+ </text>
+ <text
+ class="line-chart-tick sw-body-sm"
+ text-anchor="end"
+ transform="rotate(-35, 50, 24)"
+ x="50"
+ y="24"
+ >
+ 06 PM
+ </text>
+ </g>
+ <g>
+ <path
+ class="line-chart-path line-chart-path-0"
+ d="M0,16L20,8"
+ stroke="rgb(85,170,223)"
+ stroke-dasharray="0"
+ />
+ <path
+ class="line-chart-path line-chart-path-1"
+ d="M40,0Z"
+ stroke="rgb(58,127,173)"
+ stroke-dasharray="3"
+ />
+ </g>
+ <g>
+ <circle
+ cx="40"
+ cy="0"
+ fill="rgb(58,127,173)"
+ r="2"
+ stroke="white"
+ stroke-width="1"
+ />
+ </g>
+ <rect
+ class="chart-mouse-events-overlay"
+ height="24"
+ width="40"
+ />
+ </g>
+</svg>
+`;
+
exports[`should render correctly: rating metric 1`] = `
<svg
class="line-chart"
--- /dev/null
+/*
+ * 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 { ScaleTime } from 'd3-scale';
+import { TimeMachineResponse } from '../api/time-machine';
+import { SOFTWARE_QUALITY_RATING_METRICS_MAP } from './constants';
+
+export const mergeRatingMeasureHistory = (
+ historyData: TimeMachineResponse | undefined,
+ parseDateFn: (date: string) => 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<number, number>,
+): splitPointDate is Date =>
+ splitPointDate !== undefined &&
+ xScale(splitPointDate) >= xScale.range()[0] &&
+ xScale(splitPointDate) <= xScale.range()[1];
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',
key: 'reliability_rating_distribution',
type: 'DATA',
name: 'Reliability Rating Distribution',
- description: 'Maintainability rating distribution',
+ description: 'Reliability rating distribution',
domain: 'Reliability',
direction: -1,
qualitative: true,
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,
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',
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',
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',
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',
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',
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 = {
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');
* 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';
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<Parameters<typeof getAllTimeMachineData>[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(
({
queryFn: async () => {
const data = await getMeasuresWithPeriodAndMetrics(
componentKey,
- metricKeys.filter(
- (m) => ![MetricKey.software_quality_releasability_rating].includes(m as MetricKey),
- ),
+ metricKeys,
branchLikeQuery,
);
metricKeys.forEach((metricKey) => {
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(
);
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,
export interface MeasureHistory {
history: HistoryItem[];
metric: MetricKey;
+ splitPointDate?: Date;
}
export interface Serie {
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.