Commit f16769f1 authored by Brandon Labuschagne's avatar Brandon Labuschagne Committed by David O'Regan

Add median to CI/CD lead time chart

This commit adds a median line to the lead time
chart in the CI/CD feature.

Changelog: added
EE: true
parent e5435114
......@@ -2,12 +2,14 @@
import * as DoraApi from 'ee/api/dora_api';
import createFlash from '~/flash';
import { humanizeTimeInterval } from '~/lib/utils/datetime_utility';
import { s__ } from '~/locale';
import { s__, sprintf } from '~/locale';
import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue';
import DoraChartHeader from './dora_chart_header.vue';
import {
allChartDefinitions,
areaChartOptions,
averageSeriesOptions,
medianSeriesTitle,
chartDescriptionText,
chartDocumentationHref,
LAST_WEEK,
......@@ -15,7 +17,11 @@ import {
LAST_90_DAYS,
CHART_TITLE,
} from './static_data/lead_time';
import { buildNullSeriesForLeadTimeChart, apiDataToChartSeries } from './util';
import {
buildNullSeriesForLeadTimeChart,
apiDataToChartSeries,
seriesToMedianSeries,
} from './util';
export default {
name: 'LeadTimeCharts',
......@@ -33,6 +39,11 @@ export default {
default: '',
},
},
chartInDays: {
[LAST_WEEK]: 7,
[LAST_MONTH]: 30,
[LAST_90_DAYS]: 90,
},
data() {
return {
chartData: {
......@@ -71,9 +82,21 @@ export default {
requestParams,
);
this.chartData[id] = buildNullSeriesForLeadTimeChart(
apiDataToChartSeries(apiData, startDate, endDate, CHART_TITLE, null),
);
const seriesData = apiDataToChartSeries(apiData, startDate, endDate, CHART_TITLE, null);
const nullSeries = buildNullSeriesForLeadTimeChart(seriesData);
const { data } = seriesData[0];
const medianSeries = {
...averageSeriesOptions,
...seriesToMedianSeries(
data,
sprintf(medianSeriesTitle, { days: this.$options.chartInDays[id] }),
),
};
// TODO: Refactor buildNullSeriesForLeadTimeChart into 2 separate utils to clean this up
// https://gitlab.com/gitlab-org/gitlab/-/issues/351318
this.chartData[id] = [nullSeries[1], medianSeries, nullSeries[0]];
}),
);
......@@ -92,9 +115,29 @@ export default {
methods: {
formatTooltipText(params) {
this.tooltipTitle = params.value;
const seconds = params.seriesData[1].data[1];
this.tooltipValue = seconds != null ? humanizeTimeInterval(seconds) : null;
const leadTimeSeries = params.seriesData[0];
if (leadTimeSeries.data?.length) {
const leadTimeValue = leadTimeSeries.data[1];
const medianSeries = params.seriesData[1];
const { seriesName: medianSeriesName } = medianSeries;
const medianSeriesValue = medianSeries.data[1];
this.tooltipValue = [
{
title: this.$options.i18n.medianLeadTime,
value: humanizeTimeInterval(leadTimeValue),
},
{
title: medianSeriesName,
value: humanizeTimeInterval(medianSeriesValue),
},
];
} else {
this.tooltipValue = null;
}
},
/**
* Validates that exactly one of [this.projectPath, this.groupPath] has been
......@@ -132,8 +175,8 @@ export default {
chartDocumentationHref,
i18n: {
flashMessage: s__('DORA4Metrics|Something went wrong while getting lead time data.'),
chartHeaderText: s__('DORA4Metrics|Lead time'),
medianLeadTime: s__('DORA4Metrics|Median lead time'),
chartHeaderText: CHART_TITLE,
medianLeadTime: CHART_TITLE,
noMergeRequestsDeployed: s__('DORA4Metrics|No merge requests were deployed during this period'),
},
};
......@@ -160,9 +203,15 @@ export default {
<template v-if="tooltipValue === null">
{{ $options.i18n.noMergeRequestsDeployed }}
</template>
<div v-else class="gl-display-flex gl-align-items-flex-end">
<div class="gl-mr-5">{{ $options.i18n.medianLeadTime }}</div>
<div class="gl-font-weight-bold" data-testid="tooltip-value">{{ tooltipValue }}</div>
<div v-else class="gl-display-flex gl-flex-direction-column">
<div
v-for="metric in tooltipValue"
:key="metric.title"
class="gl-display-flex gl-justify-content-space-between"
>
<div class="gl-mr-5">{{ metric.title }}</div>
<div class="gl-font-weight-bold" data-testid="tooltip-value">{{ metric.value }}</div>
</div>
</div>
</template>
</ci-cd-analytics-charts>
......
......@@ -3,7 +3,9 @@ import { s__ } from '~/locale';
export * from './shared';
export const CHART_TITLE = s__('DORA4Metrics|Lead time');
export const CHART_TITLE = s__('DORA4Metrics|Lead time for changes');
export const medianSeriesTitle = s__('DORA4Metrics|Median (last %{days}d)');
export const areaChartOptions = {
xAxis: {
......
import { dataVizBlue500, dataVizOrange600 } from '@gitlab/ui/scss_to_js/scss_variables';
import { dataVizBlue500, gray300 } from '@gitlab/ui/scss_to_js/scss_variables';
import dateFormat from 'dateformat';
import { merge, cloneDeep } from 'lodash';
import { getDatesInRange, nDaysBefore, getStartOfDay } from '~/lib/utils/datetime_utility';
import { median } from '~/lib/utils/number_utils';
import { s__ } from '~/locale';
/**
......@@ -124,6 +125,7 @@ export const buildNullSeriesForLeadTimeChart = (seriesData) => {
},
areaStyle: {
color: dataVizBlue500,
opacity: 0,
},
itemStyle: {
color: dataVizBlue500,
......@@ -135,13 +137,13 @@ export const buildNullSeriesForLeadTimeChart = (seriesData) => {
data: nullSeriesData,
lineStyle: {
type: 'dashed',
color: dataVizOrange600,
color: gray300,
},
areaStyle: {
color: 'none',
},
itemStyle: {
color: dataVizOrange600,
color: gray300,
},
};
......@@ -168,3 +170,21 @@ export const seriesToAverageSeries = (chartSeriesData, seriesName) => {
data: chartSeriesData.map((day) => [day[0], average]),
};
};
/**
* Converts a data series into a formatted median series
*
* @param {Array} chartSeriesData Correctly formatted chart series data
*
* @returns {Object} An object containing the series name and an array of original data keys with the median of the dataset as each value.
*/
export const seriesToMedianSeries = (chartSeriesData, seriesName) => {
if (!chartSeriesData) return {};
const medianValue = median(chartSeriesData.filter((day) => day[1] !== null).map((day) => day[1]));
return {
name: seriesName,
data: chartSeriesData.map((day) => [day[0], medianValue]),
};
};
......@@ -37,10 +37,10 @@ Array [
],
],
"itemStyle": Object {
"color": "#b24800",
"color": "#999",
},
"lineStyle": Object {
"color": "#b24800",
"color": "#999",
"type": "dashed",
},
"name": "No merge requests were deployed during this period",
......@@ -48,6 +48,7 @@ Array [
Object {
"areaStyle": Object {
"color": "#5772ff",
"opacity": 0,
},
"data": Array [
Array [
......
......@@ -59,7 +59,7 @@ describe('lead_time_charts.vue', () => {
mock.restore();
});
const getTooltipValue = () => wrapper.find('[data-testid="tooltip-value"]').text();
const getTooltipValues = () => wrapper.findAll('[data-testid="tooltip-value"]');
const findCiCdAnalyticsCharts = () => wrapper.findComponent(CiCdAnalyticsCharts);
describe('when there are no network errors', () => {
......@@ -103,7 +103,7 @@ describe('lead_time_charts.vue', () => {
await axios.waitForAll();
const params = { seriesData: [{}, { data: ['Apr 7', 5328] }] };
const params = { seriesData: [{ data: ['Apr 7', 5328] }, { data: ['Apr 7', 4000] }, {}] };
// Simulate the child CiCdAnalyticsCharts component calling the
// function bound to the `format-tooltip-text`.
......@@ -112,7 +112,10 @@ describe('lead_time_charts.vue', () => {
await nextTick();
expect(getTooltipValue()).toBe('1.5 hours');
const toolTipValues = getTooltipValues();
expect(toolTipValues.at(0).text()).toBe('1.5 hours');
expect(toolTipValues.at(1).text()).toBe('1.1 hours');
});
});
});
......
......@@ -3,6 +3,7 @@ import {
apiDataToChartSeries,
buildNullSeriesForLeadTimeChart,
seriesToAverageSeries,
seriesToMedianSeries,
} from 'ee/dora/components/util';
describe('ee/dora/components/util.js', () => {
......@@ -76,6 +77,7 @@ describe('ee/dora/components/util.js', () => {
},
areaStyle: {
color: expect.any(String),
opacity: 0,
},
itemStyle: {
color: expect.any(String),
......@@ -249,4 +251,40 @@ describe('ee/dora/components/util.js', () => {
});
});
});
describe('seriesToMedianSeries', () => {
const seriesName = 'Median';
it('returns an empty object if chart data is undefined', () => {
const data = seriesToMedianSeries(undefined, seriesName);
expect(data).toStrictEqual({});
});
it('returns an empty object if chart data is blank', () => {
const data = seriesToMedianSeries(null, seriesName);
expect(data).toStrictEqual({});
});
it('returns the correct median values', () => {
const data = seriesToMedianSeries(
[
['Jul 1', 1],
['Jul 2', 3],
['Jul 3', 10],
],
seriesName,
);
expect(data).toStrictEqual({
name: seriesName,
data: [
['Jul 1', 3],
['Jul 2', 3],
['Jul 3', 3],
],
});
});
});
});
......@@ -10856,10 +10856,10 @@ msgstr ""
msgid "DORA4Metrics|Deployment frequency"
msgstr ""
msgid "DORA4Metrics|Lead time"
msgid "DORA4Metrics|Lead time for changes"
msgstr ""
msgid "DORA4Metrics|Median lead time"
msgid "DORA4Metrics|Median (last %{days}d)"
msgstr ""
msgid "DORA4Metrics|No merge requests were deployed during this period"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment