Commit 9e54019b authored by Brandon Labuschagne's avatar Brandon Labuschagne

Merge branch...

Merge branch '343729-ci-cd-analytics-add-vsa-deployment-tiles-to-deployment-frequency-tab' into 'master'

CI/CD Analytics: Add VSA deployment tiles to deployment frequency tab

See merge request gitlab-org/gitlab!78655
parents bc9ccdcf 9124f81f
<script> <script>
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { flatten } from 'lodash'; import { flatten, isEqual } from 'lodash';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { sprintf, s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
import { METRICS_POPOVER_CONTENT } from '../constants'; import { METRICS_POPOVER_CONTENT } from '../constants';
import { removeFlash, prepareTimeMetricsData } from '../utils'; import { removeFlash, prepareTimeMetricsData } from '../utils';
import MetricTile from './metric_tile.vue'; import MetricTile from './metric_tile.vue';
...@@ -48,6 +47,11 @@ export default { ...@@ -48,6 +47,11 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
filterFn: {
type: Function,
required: false,
default: null,
},
}, },
data() { data() {
return { return {
...@@ -56,8 +60,10 @@ export default { ...@@ -56,8 +60,10 @@ export default {
}; };
}, },
watch: { watch: {
requestParams() { requestParams(newVal, oldVal) {
this.fetchData(); if (!isEqual(newVal, oldVal)) {
this.fetchData();
}
}, },
}, },
mounted() { mounted() {
...@@ -69,25 +75,13 @@ export default { ...@@ -69,25 +75,13 @@ export default {
this.isLoading = true; this.isLoading = true;
return fetchMetricsData(this.requests, this.requestPath, this.requestParams) return fetchMetricsData(this.requests, this.requestPath, this.requestParams)
.then((data) => { .then((data) => {
this.metrics = data; this.metrics = this.filterFn ? this.filterFn(data) : data;
this.isLoading = false; this.isLoading = false;
}) })
.catch(() => { .catch(() => {
this.isLoading = false; this.isLoading = false;
}); });
}, },
hasLinks(links) {
return links?.length && links[0].url;
},
clickHandler({ links }) {
if (this.hasLinks(links)) {
redirectTo(links[0].url);
}
},
getDecimalPlaces(value) {
const parsedFloat = parseFloat(value);
return Number.isNaN(parsedFloat) || Number.isInteger(parsedFloat) ? 0 : 1;
},
}, },
}; };
</script> </script>
......
...@@ -45,7 +45,8 @@ export default { ...@@ -45,7 +45,8 @@ export default {
:chart-data="chart.data" :chart-data="chart.data"
:area-chart-options="chartOptions" :area-chart-options="chartOptions"
> >
{{ dateRange }} <p>{{ dateRange }}</p>
<slot name="metrics" :selected-chart="selectedChart"></slot>
<template #tooltip-title> <template #tooltip-title>
<slot name="tooltip-title"></slot> <slot name="tooltip-title"></slot>
</template> </template>
......
<script> <script>
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import * as DoraApi from 'ee/api/dora_api'; import * as DoraApi from 'ee/api/dora_api';
import { toYmd } from '~/analytics/shared/utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
import { SUMMARY_METRICS_REQUEST } from '~/cycle_analytics/constants';
import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue'; import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue';
import DoraChartHeader from './dora_chart_header.vue'; import DoraChartHeader from './dora_chart_header.vue';
import { import {
...@@ -19,11 +22,16 @@ import { ...@@ -19,11 +22,16 @@ import {
} from './static_data/deployment_frequency'; } from './static_data/deployment_frequency';
import { apiDataToChartSeries, seriesToAverageSeries } from './util'; import { apiDataToChartSeries, seriesToAverageSeries } from './util';
const VISIBLE_METRICS = ['deploys', 'deployment-frequency', 'deployment_frequency'];
const filterFn = (data) =>
data.filter((d) => VISIBLE_METRICS.includes(d.identifier)).map(({ links, ...rest }) => rest);
export default { export default {
name: 'DeploymentFrequencyCharts', name: 'DeploymentFrequencyCharts',
components: { components: {
CiCdAnalyticsCharts, CiCdAnalyticsCharts,
DoraChartHeader, DoraChartHeader,
ValueStreamMetrics,
}, },
inject: { inject: {
projectPath: { projectPath: {
...@@ -56,6 +64,9 @@ export default { ...@@ -56,6 +64,9 @@ export default {
data: this.chartData[chart.id], data: this.chartData[chart.id],
})); }));
}, },
metricsRequestPath() {
return this.projectPath ? this.projectPath : `groups/${this.groupPath}`;
},
}, },
async mounted() { async mounted() {
const results = await Promise.allSettled( const results = await Promise.allSettled(
...@@ -114,9 +125,23 @@ export default { ...@@ -114,9 +125,23 @@ export default {
); );
} }
}, },
methods: {
getMetricsRequestParams(selectedChart) {
const {
requestParams: { start_date },
} = allChartDefinitions[selectedChart];
return {
created_after: toYmd(start_date),
};
},
},
areaChartOptions, areaChartOptions,
chartDescriptionText, chartDescriptionText,
chartDocumentationHref, chartDocumentationHref,
metricsRequest: SUMMARY_METRICS_REQUEST,
filterFn,
}; };
</script> </script>
<template> <template>
...@@ -126,6 +151,15 @@ export default { ...@@ -126,6 +151,15 @@ export default {
:chart-description-text="$options.chartDescriptionText" :chart-description-text="$options.chartDescriptionText"
:chart-documentation-href="$options.chartDocumentationHref" :chart-documentation-href="$options.chartDocumentationHref"
/> />
<ci-cd-analytics-charts :charts="charts" :chart-options="$options.areaChartOptions" /> <ci-cd-analytics-charts :charts="charts" :chart-options="$options.areaChartOptions">
<template #metrics="{ selectedChart }">
<value-stream-metrics
:request-path="metricsRequestPath"
:requests="$options.metricsRequest"
:request-params="getMetricsRequestParams(selectedChart)"
:filter-fn="$options.filterFn"
/>
</template>
</ci-cd-analytics-charts>
</div> </div>
</template> </template>
...@@ -5,6 +5,8 @@ import lastWeekData from 'test_fixtures/api/dora/metrics/daily_deployment_freque ...@@ -5,6 +5,8 @@ import lastWeekData from 'test_fixtures/api/dora/metrics/daily_deployment_freque
import lastMonthData from 'test_fixtures/api/dora/metrics/daily_deployment_frequency_for_last_month.json'; import lastMonthData from 'test_fixtures/api/dora/metrics/daily_deployment_frequency_for_last_month.json';
import last90DaysData from 'test_fixtures/api/dora/metrics/daily_deployment_frequency_for_last_90_days.json'; import last90DaysData from 'test_fixtures/api/dora/metrics/daily_deployment_frequency_for_last_90_days.json';
import { useFixturesFakeDate } from 'helpers/fake_date'; import { useFixturesFakeDate } from 'helpers/fake_date';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
import createFlash from '~/flash'; import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
...@@ -12,6 +14,14 @@ import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_a ...@@ -12,6 +14,14 @@ import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_a
jest.mock('~/flash'); jest.mock('~/flash');
const makeMockCiCdAnalyticsCharts = ({ selectedChart = 0 } = {}) => ({
render() {
return this.$scopedSlots.metrics({
selectedChart,
});
},
});
describe('deployment_frequency_charts.vue', () => { describe('deployment_frequency_charts.vue', () => {
useFixturesFakeDate(); useFixturesFakeDate();
...@@ -36,7 +46,7 @@ describe('deployment_frequency_charts.vue', () => { ...@@ -36,7 +46,7 @@ describe('deployment_frequency_charts.vue', () => {
}; };
const createComponent = (mountOptions = defaultMountOptions) => { const createComponent = (mountOptions = defaultMountOptions) => {
wrapper = shallowMount(DeploymentFrequencyCharts, mountOptions); wrapper = extendedWrapper(shallowMount(DeploymentFrequencyCharts, mountOptions));
}; };
// Initializes the mock endpoint to return a specific set of deployment // Initializes the mock endpoint to return a specific set of deployment
...@@ -55,6 +65,8 @@ describe('deployment_frequency_charts.vue', () => { ...@@ -55,6 +65,8 @@ describe('deployment_frequency_charts.vue', () => {
.replyOnce(httpStatus.OK, data); .replyOnce(httpStatus.OK, data);
}; };
const findValueStreamMetrics = () => wrapper.findComponent(ValueStreamMetrics);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
...@@ -99,6 +111,31 @@ describe('deployment_frequency_charts.vue', () => { ...@@ -99,6 +111,31 @@ describe('deployment_frequency_charts.vue', () => {
it('renders a header', () => { it('renders a header', () => {
expect(wrapper.findComponent(DoraChartHeader).exists()).toBe(true); expect(wrapper.findComponent(DoraChartHeader).exists()).toBe(true);
}); });
describe('value stream metrics', () => {
beforeEach(() => {
createComponent({
...defaultMountOptions,
stubs: {
CiCdAnalyticsCharts: makeMockCiCdAnalyticsCharts({
selectedChart: 1,
}),
},
});
});
it('renders the value stream metrics component', () => {
const metricsComponent = findValueStreamMetrics();
expect(metricsComponent.exists()).toBe(true);
});
it('passes the selectedChart correctly and computes the requestParams', () => {
const metricsComponent = findValueStreamMetrics();
expect(metricsComponent.props('requestParams')).toMatchObject({
created_after: '2015-06-04',
});
});
});
}); });
describe('when there are network errors', () => { describe('when there are network errors', () => {
......
...@@ -5,6 +5,8 @@ import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics ...@@ -5,6 +5,8 @@ import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api'; import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api';
import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue'; import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
import { METRICS_POPOVER_CONTENT } from '~/cycle_analytics/constants';
import { prepareTimeMetricsData } from '~/cycle_analytics/utils';
import MetricTile from '~/cycle_analytics/components/metric_tile.vue'; import MetricTile from '~/cycle_analytics/components/metric_tile.vue';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { group } from './mock_data'; import { group } from './mock_data';
...@@ -14,6 +16,7 @@ jest.mock('~/flash'); ...@@ -14,6 +16,7 @@ jest.mock('~/flash');
describe('ValueStreamMetrics', () => { describe('ValueStreamMetrics', () => {
let wrapper; let wrapper;
let mockGetValueStreamSummaryMetrics; let mockGetValueStreamSummaryMetrics;
let mockFilterFn;
const { full_path: requestPath } = group; const { full_path: requestPath } = group;
const fakeReqName = 'Mock metrics'; const fakeReqName = 'Mock metrics';
...@@ -23,12 +26,13 @@ describe('ValueStreamMetrics', () => { ...@@ -23,12 +26,13 @@ describe('ValueStreamMetrics', () => {
name: fakeReqName, name: fakeReqName,
}); });
const createComponent = ({ requestParams = {} } = {}) => { const createComponent = (props = {}) => {
return shallowMount(ValueStreamMetrics, { return shallowMount(ValueStreamMetrics, {
propsData: { propsData: {
requestPath, requestPath,
requestParams, requestParams: {},
requests: [metricsRequestFactory()], requests: [metricsRequestFactory()],
...props,
}, },
}); });
}; };
...@@ -104,6 +108,35 @@ describe('ValueStreamMetrics', () => { ...@@ -104,6 +108,35 @@ describe('ValueStreamMetrics', () => {
expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false); expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
}); });
describe('filterFn', () => {
const transferedMetricsData = prepareTimeMetricsData(metricsData, METRICS_POPOVER_CONTENT);
it('with a filter function, will call the function with the metrics data', async () => {
const filteredData = [
{ identifier: 'issues', value: '3', title: 'New Issues', description: 'foo' },
];
mockFilterFn = jest.fn(() => filteredData);
wrapper = createComponent({
filterFn: mockFilterFn,
});
await waitForPromises();
expect(mockFilterFn).toHaveBeenCalledWith(transferedMetricsData);
expect(wrapper.vm.metrics).toEqual(filteredData);
});
it('without a filter function, it will only update the metrics', async () => {
wrapper = createComponent();
await waitForPromises();
expect(mockFilterFn).not.toHaveBeenCalled();
expect(wrapper.vm.metrics).toEqual(transferedMetricsData);
});
});
describe('with additional params', () => { describe('with additional params', () => {
beforeEach(async () => { beforeEach(async () => {
wrapper = createComponent({ wrapper = createComponent({
......
import { GlSegmentedControl } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { GlSegmentedControl } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CiCdAnalyticsAreaChart from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue'; import CiCdAnalyticsAreaChart from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue';
import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue'; import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue';
import { transformedAreaChartData, chartOptions } from '../mock_data'; import { transformedAreaChartData, chartOptions } from '../mock_data';
...@@ -29,12 +29,15 @@ const DEFAULT_PROPS = { ...@@ -29,12 +29,15 @@ const DEFAULT_PROPS = {
describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', () => { describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', () => {
let wrapper; let wrapper;
const createWrapper = (props = {}) => const createWrapper = (props = {}, slots = {}) =>
shallowMount(CiCdAnalyticsCharts, { shallowMountExtended(CiCdAnalyticsCharts, {
propsData: { propsData: {
...DEFAULT_PROPS, ...DEFAULT_PROPS,
...props, ...props,
}, },
scopedSlots: {
...slots,
},
}); });
afterEach(() => { afterEach(() => {
...@@ -44,20 +47,20 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', ( ...@@ -44,20 +47,20 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
} }
}); });
describe('segmented control', () => { const findMetricsSlot = () => wrapper.findByTestId('metrics-slot');
let segmentedControl; const findSegmentedControl = () => wrapper.findComponent(GlSegmentedControl);
describe('segmented control', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createWrapper(); wrapper = createWrapper();
segmentedControl = wrapper.find(GlSegmentedControl);
}); });
it('should default to the first chart', () => { it('should default to the first chart', () => {
expect(segmentedControl.props('checked')).toBe(0); expect(findSegmentedControl().props('checked')).toBe(0);
}); });
it('should use the title and index as values', () => { it('should use the title and index as values', () => {
const options = segmentedControl.props('options'); const options = findSegmentedControl().props('options');
expect(options).toHaveLength(3); expect(options).toHaveLength(3);
expect(options).toEqual([ expect(options).toEqual([
{ {
...@@ -76,7 +79,7 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', ( ...@@ -76,7 +79,7 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
}); });
it('should select a different chart on change', async () => { it('should select a different chart on change', async () => {
segmentedControl.vm.$emit('input', 1); findSegmentedControl().vm.$emit('input', 1);
const chart = wrapper.find(CiCdAnalyticsAreaChart); const chart = wrapper.find(CiCdAnalyticsAreaChart);
...@@ -91,4 +94,24 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', ( ...@@ -91,4 +94,24 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
wrapper = createWrapper({ charts: [] }); wrapper = createWrapper({ charts: [] });
expect(wrapper.find(CiCdAnalyticsAreaChart).exists()).toBe(false); expect(wrapper.find(CiCdAnalyticsAreaChart).exists()).toBe(false);
}); });
describe('slots', () => {
beforeEach(() => {
wrapper = createWrapper(
{},
{
metrics: '<div data-testid="metrics-slot">selected chart: {{props.selectedChart}}</div>',
},
);
});
it('renders a metrics slot', async () => {
const selectedChart = 1;
findSegmentedControl().vm.$emit('input', selectedChart);
await nextTick();
expect(findMetricsSlot().text()).toBe(`selected chart: ${selectedChart}`);
});
});
}); });
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