Skip to content
Snippets Groups Projects
Commit 3eab2de8 authored by Jannik Lehmann's avatar Jannik Lehmann :baby:
Browse files

Merge branch '416008-lazy-load-data' into 'master'

Improve Value stream dashboard loading state

See merge request !127240



Merged-by: default avatarJannik Lehmann <jlehmann@gitlab.com>
Approved-by: Deepika Guliani's avatarDeepika Guliani <dguliani@gitlab.com>
Approved-by: default avatarJannik Lehmann <jlehmann@gitlab.com>
Reviewed-by: Deepika Guliani's avatarDeepika Guliani <dguliani@gitlab.com>
Reviewed-by: default avatarAlex Pennells <apennells@gitlab.com>
Co-authored-by: default avatarAlex Pennells <apennells@gitlab.com>
parents 0dcc57b8 93e2b4e8
No related branches found
No related tags found
1 merge request!127240Improve Value stream dashboard loading state
Pipeline #950943370 passed
<script>
import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
import { joinPaths } from '~/lib/utils/url_utility';
import { createAlert } from '~/alert';
import { toYmd } from '~/analytics/shared/utils';
......@@ -12,7 +11,7 @@ import ProjectFlowMetricsQuery from '../graphql/project_flow_metrics.query.graph
import GroupDoraMetricsQuery from '../graphql/group_dora_metrics.query.graphql';
import ProjectDoraMetricsQuery from '../graphql/project_dora_metrics.query.graphql';
import { BUCKETING_INTERVAL_ALL, MERGE_REQUESTS_STATE_MERGED } from '../graphql/constants';
import { DASHBOARD_LOADING_FAILURE, DASHBOARD_NO_DATA, CHART_LOADING_FAILURE } from '../constants';
import { DASHBOARD_LOADING_FAILURE, CHART_LOADING_FAILURE } from '../constants';
import {
fetchMetricsForTimePeriods,
extractGraphqlVulnerabilitiesData,
......@@ -21,10 +20,10 @@ import {
extractGraphqlMergeRequestsData,
} from '../api';
import {
hasDoraMetricValues,
generateDoraTimePeriodComparisonTable,
generateSkeletonTableData,
generateMetricComparisons,
generateSparklineCharts,
mergeSparklineCharts,
mergeTableData,
generateDateRanges,
generateChartTimePeriods,
generateValueStreamDashboardStartDate,
......@@ -46,8 +45,6 @@ const extractQueryResponseFromNamespace = ({ result, resultKey }) => {
export default {
name: 'ComparisonChart',
components: {
GlAlert,
GlSkeletonLoader,
ComparisonTable,
},
props: {
......@@ -72,25 +69,19 @@ export default {
},
data() {
return {
tableData: [],
tableData: {},
chartData: {},
loadingTable: false,
};
},
computed: {
hasData() {
return Boolean(this.allData.length);
skeletonData() {
return generateSkeletonTableData(this.excludeMetrics);
},
hasTableData() {
return Boolean(this.tableData.length);
},
hasChartData() {
return Boolean(Object.keys(this.chartData).length);
},
allData() {
return this.hasChartData
? mergeSparklineCharts(this.tableData, this.chartData)
: this.tableData;
combinedData() {
let data = this.skeletonData;
data = mergeTableData(data, this.tableData);
data = mergeTableData(data, this.chartData);
return data;
},
namespaceRequestPath() {
return this.isProject ? this.requestPath : joinPaths('groups', this.requestPath);
......@@ -103,15 +94,8 @@ export default {
},
},
async mounted() {
this.loadingTable = true;
try {
await this.fetchTableMetrics();
if (this.hasTableData) {
await this.fetchSparklineMetrics();
}
} finally {
this.loadingTable = false;
}
await this.fetchTableMetrics();
await this.fetchSparklineMetrics();
},
methods: {
async fetchFlowMetricsQuery({ isProject, ...variables }) {
......@@ -213,12 +197,7 @@ export default {
this.fetchGraphqlData,
);
this.tableData = hasDoraMetricValues(timePeriods)
? generateDoraTimePeriodComparisonTable({
timePeriods,
excludeMetrics: this.excludeMetrics,
})
: [];
this.tableData = generateMetricComparisons(timePeriods);
} catch (error) {
createAlert({ message: DASHBOARD_LOADING_FAILURE, error, captureError: true });
}
......@@ -230,30 +209,20 @@ export default {
this.fetchGraphqlData,
);
this.chartData = hasDoraMetricValues(chartData) ? generateSparklineCharts(chartData) : {};
this.chartData = generateSparklineCharts(chartData);
} catch (error) {
createAlert({ message: CHART_LOADING_FAILURE, error, captureError: true });
}
},
},
i18n: {
noData: DASHBOARD_NO_DATA,
},
now,
};
</script>
<template>
<div>
<gl-skeleton-loader v-if="loadingTable" />
<gl-alert v-else-if="!hasData" variant="info" :dismissible="false">{{
$options.i18n.noData
}}</gl-alert>
<comparison-table
v-else
:table-data="allData"
:request-path="namespaceRequestPath"
:is-project="isProject"
:now="$options.now"
/>
</div>
<comparison-table
:table-data="combinedData"
:request-path="namespaceRequestPath"
:is-project="isProject"
:now="$options.now"
/>
</template>
......@@ -66,26 +66,31 @@ export default {
</template>
<template #cell()="{ value: { value, change, valueLimitMessage }, item: { invertTrendColor } }">
{{ value }}
<gl-icon
v-if="valueLimitMessage"
v-gl-tooltip.hover
class="gl-text-blue-600"
name="information-o"
:title="valueLimitMessage"
data-testid="metric_max_value_info_icon"
/>
<trend-indicator
v-else-if="change"
:change="change"
:invert-color="invertTrendColor"
data-testid="metric_trend_indicator"
/>
<span v-if="value === undefined" data-testid="metric-comparison-skeleton">
<gl-skeleton-loader :lines="1" :width="100" />
</span>
<template v-else>
{{ value }}
<gl-icon
v-if="valueLimitMessage"
v-gl-tooltip.hover
class="gl-text-blue-600"
name="information-o"
:title="valueLimitMessage"
data-testid="metric-max-value-info-icon"
/>
<trend-indicator
v-else-if="change"
:change="change"
:invert-color="invertTrendColor"
data-testid="metric-trend-indicator"
/>
</template>
</template>
<template #cell(metric)="{ value: { identifier } }">
<metric-table-cell
:data-testid="`${identifier}_metric_cell`"
:data-testid="`${identifier}-metric-cell`"
:identifier="identifier"
:request-path="requestPath"
:is-project="isProject"
......@@ -101,9 +106,9 @@ export default {
:data="data"
:smooth="0.2"
:gradient="chartGradient(invertTrendColor)"
data-testid="metric_chart"
data-testid="metric-chart"
/>
<div v-else class="gl-py-4" data-testid="metric_chart_skeleton">
<div v-else class="gl-py-4" data-testid="metric-chart-skeleton">
<gl-skeleton-loader :lines="1" :width="100" />
</div>
</template>
......
......@@ -103,7 +103,6 @@ export const DASHBOARD_DESCRIPTION_GROUP = s__('DORA4Metrics|Metrics comparison
export const DASHBOARD_DESCRIPTION_PROJECT = s__(
'DORA4Metrics|Metrics comparison for %{name} project',
);
export const DASHBOARD_NO_DATA = __('No data available');
export const DASHBOARD_LOADING_FAILURE = __('Failed to load');
export const DASHBOARD_NAMESPACE_LOAD_ERROR = s__(
'DORA4Metrics|Failed to load comparison chart for Namespace: %{fullPath}',
......
import { s__, __ } from '~/locale';
import { isNumeric } from '~/lib/utils/number_utils';
import {
formatDate,
getStartOfDay,
......@@ -88,51 +87,49 @@ export const percentChange = ({ current, previous }) =>
previous > 0 && current > 0 ? (current - previous) / previous : 0;
/**
* Takes an array of timePeriod objects containing DORA metrics, and returns
* true if any of the timePeriods contain metric values > 0.
* Creates the table rows filled with blank data for the comparison table. Once the data
* has loaded, it can be filled into the returned skeleton using `mergeTableData`.
*
* @param {Array} timePeriods - array of objects containing DORA metric values
* @returns {Boolean} true if there is any metric data, otherwise false.
* @param {Array} excludeMetrics - Array of DORA metric identifiers to remove from the table
* @returns {Array} array of data-less table rows
*/
export const hasDoraMetricValues = (timePeriods) =>
timePeriods.some((timePeriod) => {
// timePeriod may contain more attributes than just the DORA metrics,
// so filter out non-metrics before making a list of the raw values
const metricValues = Object.entries(timePeriod)
.filter(([k]) => Object.keys(TABLE_METRICS).includes(k))
.map(([, v]) => v.value);
export const generateSkeletonTableData = (excludeMetrics = []) =>
Object.entries(TABLE_METRICS)
.filter(([identifier]) => !excludeMetrics.includes(identifier))
.map(([identifier, { label, invertTrendColor, valueLimit }]) => ({
invertTrendColor,
metric: { identifier, value: label },
valueLimit,
}));
return metricValues.some((value) => isNumeric(value) && Number(value) > 0);
/**
* Fills the provided table rows with the matching metric data, returning a copy
* of the original table data.
*
* @param {Array} tableData - Table rows created by `generateSkeletonTableData`
* @param {Object} newData - New data to enter into the table rows. Object keys match the metric ID
* @returns {Array} A copy of `tableData` with the new data merged into each row
*/
export const mergeTableData = (tableData, newData) =>
tableData.map((row) => {
const data = newData[row.metric.identifier];
return data ? { ...row, ...data } : row;
});
/**
* Takes N time periods for a metric and generates the row for the comparison table.
*
* @param {String} identifier - ID of the metric to create a table row for.
* @param {String} label - User friendly name of the metric to show in the table row.
* @param {String} units - The type of units used for this metric (ex. days, /day, count)
* @param {Boolean} invertTrendColor - Inverts the color indicator used for metric trends.
* @param {Array} timePeriods - Array of the metrics for different time periods
* @param {Object} valueLimit - Object representing the maximum value of a metric, mask that replaces the value if the limit is reached and a description to be used in a tooltip.
* @returns {Object} The metric data formatted for the comparison table.
*/
const buildMetricComparisonTableRow = ({
identifier,
label,
units,
invertTrendColor,
timePeriods,
valueLimit,
}) => {
const data = {
invertTrendColor,
metric: { identifier, value: label },
valueLimit,
};
timePeriods.forEach((timePeriod, index) => {
const buildMetricComparisonTableRow = ({ identifier, units, timePeriods, valueLimit }) =>
timePeriods.reduce((acc, timePeriod, index) => {
// The last timePeriod is not rendered, we just use it
// to determine the % change for the 2nd last timePeriod
if (index === timePeriods.length - 1) return;
if (index === timePeriods.length - 1) return acc;
const current = timePeriod[identifier];
const previous = timePeriods[index + 1][identifier];
......@@ -149,36 +146,35 @@ const buildMetricComparisonTableRow = ({
const formattedMetric = hasCurrentValue ? formatMetric(current.value, units) : '-';
const value = valueLimitMessage ? valueLimit?.mask : formattedMetric;
data[timePeriod.key] = {
value,
change,
valueLimitMessage,
};
});
return data;
};
return Object.assign(acc, {
[timePeriod.key]: {
value,
change,
valueLimitMessage,
},
});
}, {});
/**
* Takes N time periods of DORA metrics and generates the data rows
* for the comparison table.
* Takes N time periods of DORA metrics and sorts the data into an
* object of metric comparisons, per metric.
*
* @param {Array} timePeriods - Array of the DORA metrics for different time periods
* @param {Array} excludeMetrics - Array of DORA metric identifiers to remove from the table
* @returns {Array} array comparing each DORA metric between the different time periods
* @returns {Object} object containing a comparisons of values for each metric
*/
export const generateDoraTimePeriodComparisonTable = ({ timePeriods, excludeMetrics = [] }) =>
Object.entries(TABLE_METRICS)
.filter(([identifier]) => !excludeMetrics.includes(identifier))
.map(([identifier, { label, units, invertTrendColor, valueLimit }]) =>
buildMetricComparisonTableRow({
identifier,
label,
units,
invertTrendColor,
timePeriods,
valueLimit,
export const generateMetricComparisons = (timePeriods) =>
Object.entries(TABLE_METRICS).reduce(
(acc, [identifier, { units, valueLimit }]) =>
Object.assign(acc, {
[identifier]: buildMetricComparisonTableRow({
identifier,
units,
timePeriods,
valueLimit,
}),
}),
);
{},
);
/**
* @param {Number|'-'|null|undefined} value
......@@ -206,30 +202,18 @@ export const generateSparklineCharts = (timePeriods) =>
(acc, [identifier, { units }]) =>
Object.assign(acc, {
[identifier]: {
tooltipLabel: CHART_TOOLTIP_UNITS[units],
data: timePeriods.map((timePeriod) => [
`${formatDate(timePeriod.start, 'mmm d')} - ${formatDate(timePeriod.end, 'mmm d')}`,
sanitizeSparklineData(timePeriod[identifier]?.value),
]),
chart: {
tooltipLabel: CHART_TOOLTIP_UNITS[units],
data: timePeriods.map((timePeriod) => [
`${formatDate(timePeriod.start, 'mmm d')} - ${formatDate(timePeriod.end, 'mmm d')}`,
sanitizeSparklineData(timePeriod[identifier]?.value),
]),
},
},
}),
{},
);
/**
* Merges the results of `generateDoraTimePeriodComparisonTable` and `generateSparklineCharts`
* into a new array for the comparison table.
*
* @param {Array} tableData - Table rows created by `generateDoraTimePeriodComparisonTable`
* @param {Object} chartData - Charts object created by `generateSparklineCharts`
* @returns {Array} A copy of tableData with `chart` added in each row
*/
export const mergeSparklineCharts = (tableData, chartData) =>
tableData.map((row) => {
const chart = chartData[row.metric.identifier];
return chart ? { ...row, chart } : row;
});
/**
* Generate the dashboard time periods
* this month - last month - 2 month ago - 3 month ago
......
......@@ -6,6 +6,7 @@ import {
DASHBOARD_LOADING_FAILURE,
CHART_LOADING_FAILURE,
} from 'ee/analytics/dashboards/constants';
import { generateSkeletonTableData } from 'ee/analytics/dashboards/utils';
import ComparisonChart from 'ee/analytics/dashboards/components/comparison_chart.vue';
import ComparisonTable from 'ee/analytics/dashboards/components/comparison_table.vue';
import GroupVulnerabilitiesQuery from 'ee/analytics/dashboards/graphql/group_vulnerabilities.query.graphql';
......@@ -39,10 +40,6 @@ import {
mockFlowMetricsResponseData,
mockMergeRequestsResponseData,
mockExcludeMetrics,
mockEmptyVulnerabilityResponse,
mockEmptyDoraResponse,
mockEmptyFlowMetricsResponse,
mockEmptyMergeRequestsResponse,
} from '../mock_data';
const mockTypePolicy = {
......@@ -165,6 +162,17 @@ describe('Comparison chart', () => {
createAlert.mockClear();
});
describe('loading table and chart data', () => {
beforeEach(() => {
setGraphqlQueryHandlerResponses();
createWrapper();
});
it('will pass skeleton data to the comparison table', () => {
expect(getTableData()).toEqual(generateSkeletonTableData());
});
});
describe('with table and chart data available', () => {
beforeEach(async () => {
setGraphqlQueryHandlerResponses();
......@@ -265,10 +273,6 @@ describe('Comparison chart', () => {
error: expect.anything(),
});
});
it('renders no data message', () => {
expect(wrapper.text()).toContain('No data available');
});
});
describe('failed chart request', () => {
......@@ -302,42 +306,6 @@ describe('Comparison chart', () => {
});
});
describe('no table data available', () => {
// When there is no table data available the chart data requests are skipped
beforeEach(async () => {
setGraphqlQueryHandlerResponses({
doraMetricsResponse: mockEmptyDoraResponse,
flowMetricsResponse: mockEmptyFlowMetricsResponse,
vulnerabilityResponse: mockEmptyVulnerabilityResponse,
mergeRequestsResponse: mockEmptyMergeRequestsResponse,
});
mockApolloProvider = createMockApolloProvider();
await createWrapper({ apolloProvider: mockApolloProvider });
});
it('will only request dora metrics for the table', () => {
expectDoraMetricsRequests(MOCK_TABLE_TIME_PERIODS);
});
it('will only request flow metrics for the table', () => {
expectFlowMetricsRequests(MOCK_TABLE_TIME_PERIODS);
});
it('will only request vulnerability metrics for the table', () => {
expectVulnerabilityRequests(MOCK_TABLE_TIME_PERIODS);
});
it('will only merge request metrics for the table', () => {
expectMergeRequestsRequests(MOCK_TABLE_TIME_PERIODS);
});
it('renders a message when theres no table data available', () => {
expect(wrapper.text()).toContain('No data available');
});
});
describe('isProject=true', () => {
const fakeProjectPath = 'fake/project/path';
......
......@@ -29,11 +29,12 @@ describe('Comparison table', () => {
});
};
const findMetricTableCell = (identifier) => wrapper.findByTestId(`${identifier}_metric_cell`);
const findChart = () => wrapper.findByTestId('metric_chart');
const findChartSkeleton = () => wrapper.findByTestId('metric_chart_skeleton');
const findTrendIndicator = () => wrapper.findByTestId('metric_trend_indicator');
const findValueLimitInfoIcon = () => wrapper.findByTestId('metric_max_value_info_icon');
const findMetricTableCell = (identifier) => wrapper.findByTestId(`${identifier}-metric-cell`);
const findMetricComparisonSkeletons = () => wrapper.findAllByTestId('metric-comparison-skeleton');
const findChart = () => wrapper.findByTestId('metric-chart');
const findChartSkeleton = () => wrapper.findByTestId('metric-chart-skeleton');
const findTrendIndicator = () => wrapper.findByTestId('metric-trend-indicator');
const findValueLimitInfoIcon = () => wrapper.findByTestId('metric-max-value-info-icon');
it.each(Object.keys(TABLE_METRICS))('renders table cell for %s metric', (identifier) => {
createWrapper();
......@@ -41,6 +42,11 @@ describe('Comparison table', () => {
expect(findMetricTableCell(identifier).props('identifier')).toBe(identifier);
});
it('shows loading skeletons for each metric comparison cell', () => {
createWrapper({ tableData: [{ metric: mockMetric }] });
expect(findMetricComparisonSkeletons().length).toBe(3);
});
describe('date range table cell', () => {
const valueLimit = {
max: 10001,
......
......@@ -416,6 +416,13 @@ export const mockComparativeTableData = [
},
];
export const mockGeneratedMetricComparisons = () =>
mockComparativeTableData.reduce(
(acc, { metric, lastMonth, thisMonth, twoMonthsAgo }) =>
Object.assign(acc, { [metric.identifier]: { lastMonth, thisMonth, twoMonthsAgo } }),
{},
);
const mockChartDataValues = (values) => values.map((v) => [expect.anything(), v]);
const mockChartDataWithSameValue = (count, value) =>
......@@ -423,91 +430,139 @@ const mockChartDataWithSameValue = (count, value) =>
export const mockSubsetChartData = {
change_failure_rate: {
data: mockChartDataWithSameValue(2, 0),
tooltipLabel: '%',
chart: {
data: mockChartDataWithSameValue(2, 0),
tooltipLabel: '%',
},
},
cycle_time: {
data: mockChartDataWithSameValue(2, 0),
tooltipLabel: 'days',
chart: {
data: mockChartDataWithSameValue(2, 0),
tooltipLabel: 'days',
},
},
deployment_frequency: {
data: mockChartDataWithSameValue(2, 0),
tooltipLabel: '/day',
chart: {
data: mockChartDataWithSameValue(2, 0),
tooltipLabel: '/day',
},
},
deploys: {
data: mockChartDataWithSameValue(2, 0),
chart: {
data: mockChartDataWithSameValue(2, 0),
},
},
issues: {
data: mockChartDataWithSameValue(2, 0),
chart: {
data: mockChartDataWithSameValue(2, 0),
},
},
issues_completed: {
data: mockChartDataWithSameValue(2, 0),
chart: {
data: mockChartDataWithSameValue(2, 0),
},
},
lead_time: {
data: mockChartDataWithSameValue(2, 0),
tooltipLabel: 'days',
chart: {
data: mockChartDataWithSameValue(2, 0),
tooltipLabel: 'days',
},
},
lead_time_for_changes: {
data: mockChartDataValues([1, 2]),
tooltipLabel: 'days',
chart: {
data: mockChartDataValues([1, 2]),
tooltipLabel: 'days',
},
},
time_to_restore_service: {
data: mockChartDataValues([100, 99]),
tooltipLabel: 'days',
chart: {
data: mockChartDataValues([100, 99]),
tooltipLabel: 'days',
},
},
vulnerability_critical: {
data: mockChartDataWithSameValue(2, 0),
chart: {
data: mockChartDataWithSameValue(2, 0),
},
},
vulnerability_high: {
data: mockChartDataWithSameValue(2, 0),
chart: {
data: mockChartDataWithSameValue(2, 0),
},
},
merge_request_throughput: {
data: mockChartDataWithSameValue(2, 0),
chart: {
data: mockChartDataWithSameValue(2, 0),
},
},
};
export const mockChartData = {
lead_time_for_changes: {
tooltipLabel: 'days',
data: mockChartDataWithSameValue(6, null),
chart: {
tooltipLabel: 'days',
data: mockChartDataWithSameValue(6, null),
},
},
time_to_restore_service: {
tooltipLabel: 'days',
data: mockChartDataWithSameValue(6, 0),
chart: {
tooltipLabel: 'days',
data: mockChartDataWithSameValue(6, 0),
},
},
change_failure_rate: {
tooltipLabel: '%',
data: mockChartDataWithSameValue(6, 0),
chart: {
tooltipLabel: '%',
data: mockChartDataWithSameValue(6, 0),
},
},
deployment_frequency: {
tooltipLabel: '/day',
data: mockChartDataValues([0, 1, 2, 3, 4, 5]),
chart: {
tooltipLabel: '/day',
data: mockChartDataValues([0, 1, 2, 3, 4, 5]),
},
},
lead_time: {
tooltipLabel: 'days',
data: mockChartDataValues([1, 2, 3, 4, 5, 6]),
chart: {
tooltipLabel: 'days',
data: mockChartDataValues([1, 2, 3, 4, 5, 6]),
},
},
cycle_time: {
tooltipLabel: 'days',
data: mockChartDataValues([0, 2, 4, 6, 8, 10]),
chart: {
tooltipLabel: 'days',
data: mockChartDataValues([0, 2, 4, 6, 8, 10]),
},
},
issues: {
data: mockChartDataValues([100, 98, 96, 94, 92, 90]),
chart: {
data: mockChartDataValues([100, 98, 96, 94, 92, 90]),
},
},
issues_completed: {
data: mockChartDataValues([200, 198, 196, 194, 192, 190]),
chart: {
data: mockChartDataValues([200, 198, 196, 194, 192, 190]),
},
},
deploys: {
data: mockChartDataValues([0, 1, 4, 9, 16, 25]),
chart: {
data: mockChartDataValues([0, 1, 4, 9, 16, 25]),
},
},
vulnerability_critical: {
data: mockChartDataValues([0, 1, 2, 3, 0, 1]),
chart: {
data: mockChartDataValues([0, 1, 2, 3, 0, 1]),
},
},
vulnerability_high: {
data: mockChartDataValues([0, 1, 0, 1, 0, 1]),
chart: {
data: mockChartDataValues([0, 1, 0, 1, 0, 1]),
},
},
merge_request_throughput: {
data: mockChartDataValues([0, 1, 2, 3, 4, 5]),
chart: {
data: mockChartDataValues([0, 1, 2, 3, 4, 5]),
},
},
};
......@@ -617,17 +672,6 @@ export const mockExcludeMetrics = [
DORA_METRICS.LEAD_TIME_FOR_CHANGES,
];
export const mockEmptyVulnerabilityResponse = [{ date: null, critical: null, high: null }];
export const mockEmptyDoraResponse = { metrics: [] };
export const mockEmptyMergeRequestsResponse = { mergeRequests: {} };
export const mockEmptyFlowMetricsResponse = {
issues: null,
issues_completed: null,
deploys: null,
cycle_time: null,
lead_time: null,
};
export const MOCK_LABELS = [
{ id: 1, title: 'one', color: '#FFFFFF' },
{ id: 2, title: 'two', color: '#000000' },
......
......@@ -5,10 +5,10 @@ import { useFakeDate } from 'helpers/fake_date';
import {
percentChange,
formatMetric,
hasDoraMetricValues,
generateDoraTimePeriodComparisonTable,
generateSkeletonTableData,
generateMetricComparisons,
generateSparklineCharts,
mergeSparklineCharts,
mergeTableData,
hasTrailingDecimalZero,
generateDateRanges,
generateChartTimePeriods,
......@@ -16,14 +16,13 @@ import {
generateValueStreamDashboardStartDate,
groupDoraPerformanceScoreCountsByCategory,
} from 'ee/analytics/dashboards/utils';
import { CHANGE_FAILURE_RATE, LEAD_TIME_FOR_CHANGES } from 'ee/api/dora_api';
import { LEAD_TIME_METRIC_TYPE, CYCLE_TIME_METRIC_TYPE } from '~/api/analytics_api';
import {
mockMonthToDateTimePeriod,
mockPreviousMonthTimePeriod,
mockTwoMonthsAgoTimePeriod,
mockThreeMonthsAgoTimePeriod,
mockComparativeTableData,
mockGeneratedMetricComparisons,
mockChartsTimePeriods,
mockChartData,
mockSubsetChartsTimePeriods,
......@@ -93,7 +92,24 @@ describe('Analytics Dashboards utils', () => {
});
});
describe('generateDoraTimePeriodComparisonTable', () => {
describe('generateSkeletonTableData', () => {
it('returns blank row data for each metric', () => {
const tableData = generateSkeletonTableData();
tableData.forEach((data) =>
expect(Object.keys(data)).toEqual(['invertTrendColor', 'metric', 'valueLimit']),
);
});
it('does not include metrics that were in excludeMetrics', () => {
const excludeMetrics = [LEAD_TIME_METRIC_TYPE, CYCLE_TIME_METRIC_TYPE];
const tableData = generateSkeletonTableData(excludeMetrics);
const metrics = tableData.map(({ metric }) => metric.identifier);
expect(metrics).not.toEqual(expect.arrayContaining(excludeMetrics));
});
});
describe('generateMetricComparisons', () => {
const timePeriods = [
mockMonthToDateTimePeriod,
mockPreviousMonthTimePeriod,
......@@ -102,30 +118,19 @@ describe('Analytics Dashboards utils', () => {
];
it('calculates the changes between the 2 time periods', () => {
const tableData = generateDoraTimePeriodComparisonTable({ timePeriods });
expect(tableData).toEqual(mockComparativeTableData);
const tableData = generateMetricComparisons(timePeriods);
expect(tableData).toEqual(mockGeneratedMetricComparisons());
});
it('returns the comparison table fields + metadata for each row', () => {
generateDoraTimePeriodComparisonTable({ timePeriods }).forEach((row) => {
expect(Object.keys(row)).toEqual([
'invertTrendColor',
'metric',
'valueLimit',
'thisMonth',
'lastMonth',
'twoMonthsAgo',
]);
Object.values(generateMetricComparisons(timePeriods)).forEach((row) => {
expect(row).toMatchObject({
thisMonth: expect.any(Object),
lastMonth: expect.any(Object),
twoMonthsAgo: expect.any(Object),
});
});
});
it('does not include metrics that were in excludeMetrics', () => {
const excludeMetrics = [LEAD_TIME_METRIC_TYPE, CYCLE_TIME_METRIC_TYPE];
const tableData = generateDoraTimePeriodComparisonTable({ timePeriods, excludeMetrics });
const metrics = tableData.map(({ metric }) => metric.identifier);
expect(metrics).not.toEqual(expect.arrayContaining(excludeMetrics));
});
});
describe('generateSparklineCharts', () => {
......@@ -150,46 +155,19 @@ describe('Analytics Dashboards utils', () => {
});
});
describe('mergeSparklineCharts', () => {
it('returns the table data with the additive chart data', () => {
const chart = { data: [1, 2, 3] };
const rowNoChart = { metric: { identifier: 'noChart' } };
const rowWithChart = { metric: { identifier: 'withChart' } };
describe('mergeTableData', () => {
it('correctly integrates existing and new data', () => {
const newData = { chart: { data: [1, 2, 3] }, lastMonth: { test: 'test' } };
const rowNoData = { metric: { identifier: 'noData' } };
const rowWithData = { metric: { identifier: 'withData' } };
expect(mergeSparklineCharts([rowNoChart, rowWithChart], { withChart: chart })).toEqual([
rowNoChart,
{ ...rowWithChart, chart },
expect(mergeTableData([rowNoData, rowWithData], { withData: newData })).toEqual([
rowNoData,
{ ...rowWithData, ...newData },
]);
});
});
describe('hasDoraMetricValues', () => {
it('returns false if only non-DORA metrics contain a value > 0', () => {
const timePeriods = [{ nonDoraMetric: { value: 100 } }];
expect(hasDoraMetricValues(timePeriods)).toBe(false);
});
it('returns false if all DORA metrics contain a non-numerical value', () => {
const timePeriods = [{ [LEAD_TIME_FOR_CHANGES]: { value: 'YEET' } }];
expect(hasDoraMetricValues(timePeriods)).toBe(false);
});
it('returns false if all DORA metrics contain a value == 0', () => {
const timePeriods = [{ [LEAD_TIME_FOR_CHANGES]: { value: 0 } }];
expect(hasDoraMetricValues(timePeriods)).toBe(false);
});
it('returns true if any DORA metrics contain a value > 0', () => {
const timePeriods = [
{
[LEAD_TIME_FOR_CHANGES]: { value: 0 },
[CHANGE_FAILURE_RATE]: { value: 100 },
},
];
expect(hasDoraMetricValues(timePeriods)).toBe(true);
});
});
describe('generateDateRanges', () => {
it('return correct value', () => {
const now = MOCK_TABLE_TIME_PERIODS[0].end;
......@@ -235,7 +213,7 @@ describe('Analytics Dashboards utils', () => {
useFakeDate(2020, 4, 4);
it('will return the correct day', () => {
expect(generateValueStreamDashboardStartDate().toISOString()).toEqual(
expect(generateValueStreamDashboardStartDate().toISOString()).toBe(
'2020-05-04T00:00:00.000Z',
);
});
......@@ -245,7 +223,7 @@ describe('Analytics Dashboards utils', () => {
useFakeDate(2023, 6, 1);
it('will return the previous day', () => {
expect(generateValueStreamDashboardStartDate().toISOString()).toEqual(
expect(generateValueStreamDashboardStartDate().toISOString()).toBe(
'2023-06-30T00:00:00.000Z',
);
});
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment