Commit aaeb74a5 authored by Andrew Fontaine's avatar Andrew Fontaine Committed by Nathan Friend

Only Display One Chart at a Time

This adds segmented controls so only one CI/CD analytics chart is
displayed at a time. The user selects the date range that they wish to
view. It also reduces the number of `echarts` instances required.

This affects both pipeline charts and deployment charts.
parent 86d12bd1
<script>
import { GlSegmentedControl } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
export default {
components: {
GlSegmentedControl,
CiCdAnalyticsAreaChart,
},
props: {
charts: {
required: true,
type: Array,
},
chartOptions: {
required: true,
type: Object,
},
},
data() {
return {
selectedChart: 0,
};
},
computed: {
chartRanges() {
return this.charts.map(({ title }, index) => ({ text: title, value: index }));
},
chart() {
return this.charts[this.selectedChart];
},
dateRange() {
return sprintf(s__('CiCdAnalytics|Date range: %{range}'), { range: this.chart.range });
},
},
};
</script>
<template>
<div>
<gl-segmented-control v-model="selectedChart" :options="chartRanges" class="gl-mb-4" />
<ci-cd-analytics-area-chart
v-if="chart"
:chart-data="chart.data"
:area-chart-options="chartOptions"
>
{{ dateRange }}
</ci-cd-analytics-area-chart>
</div>
</template>
......@@ -13,6 +13,7 @@ import {
INNER_CHART_HEIGHT,
ONE_WEEK_AGO_DAYS,
ONE_MONTH_AGO_DAYS,
ONE_YEAR_AGO_DAYS,
X_AXIS_LABEL_ROTATION,
X_AXIS_TITLE_OFFSET,
PARSE_FAILURE,
......@@ -21,7 +22,7 @@ import {
UNSUPPORTED_DATA,
} from '../constants';
import StatisticsList from './statistics_list.vue';
import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
import CiCdAnalyticsCharts from './ci_cd_analytics_charts.vue';
const defaultAnalyticsValues = {
weekPipelinesTotals: [],
......@@ -52,7 +53,7 @@ export default {
GlColumnChart,
GlSkeletonLoader,
StatisticsList,
CiCdAnalyticsAreaChart,
CiCdAnalyticsCharts,
},
inject: {
projectPath: {
......@@ -173,10 +174,11 @@ export default {
},
areaCharts() {
const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles;
const { lastWeekRange, lastMonthRange, lastYearRange } = this.$options.chartRanges;
const charts = [
{ title: lastWeek, data: this.lastWeekChartData },
{ title: lastMonth, data: this.lastMonthChartData },
{ title: lastYear, data: this.lastYearChartData },
{ title: lastWeek, range: lastWeekRange, data: this.lastWeekChartData },
{ title: lastMonth, range: lastMonthRange, data: this.lastMonthChartData },
{ title: lastYear, range: lastYearRange, data: this.lastYearChartData },
];
let areaChartsData = [];
......@@ -209,11 +211,12 @@ export default {
mergeLabelsAndValues(labels, values) {
return labels.map((label, index) => [label, values[index]]);
},
buildAreaChartData({ title, data }) {
buildAreaChartData({ title, data, range }) {
const { labels, totals, success } = data;
return {
title,
range,
data: [
{
name: 'all',
......@@ -257,20 +260,28 @@ export default {
[PARSE_FAILURE]: s__('PipelineCharts|There was an error parsing the data for the charts.'),
[DEFAULT]: s__('PipelineCharts|An unknown error occurred while processing CI/CD analytics.'),
},
get chartTitles() {
chartTitles: {
lastWeek: __('Last week'),
lastMonth: __('Last month'),
lastYear: __('Last year'),
},
get chartRanges() {
const today = dateFormat(new Date(), CHART_DATE_FORMAT);
const pastDate = (timeScale) =>
dateFormat(getDateInPast(new Date(), timeScale), CHART_DATE_FORMAT);
return {
lastWeek: sprintf(__('Pipelines for last week (%{oneWeekAgo} - %{today})'), {
lastWeekRange: sprintf(__('%{oneWeekAgo} - %{today}'), {
oneWeekAgo: pastDate(ONE_WEEK_AGO_DAYS),
today,
}),
lastMonth: sprintf(__('Pipelines for last month (%{oneMonthAgo} - %{today})'), {
lastMonthRange: sprintf(__('%{oneMonthAgo} - %{today}'), {
oneMonthAgo: pastDate(ONE_MONTH_AGO_DAYS),
today,
}),
lastYear: __('Pipelines for last year'),
lastYearRange: sprintf(__('%{oneYearAgo} - %{today}'), {
oneYearAgo: pastDate(ONE_YEAR_AGO_DAYS),
today,
}),
};
},
};
......@@ -304,13 +315,7 @@ export default {
<template v-if="!loading">
<hr />
<h4 class="gl-my-4">{{ __('Pipelines charts') }}</h4>
<ci-cd-analytics-area-chart
v-for="(chart, index) in areaCharts"
:key="index"
:chart-data="chart.data"
:area-chart-options="$options.areaChartOptions"
>{{ chart.title }}</ci-cd-analytics-area-chart
>
<ci-cd-analytics-charts :charts="areaCharts" :chart-options="$options.areaChartOptions" />
</template>
</div>
</template>
......@@ -10,6 +10,8 @@ export const ONE_WEEK_AGO_DAYS = 7;
export const ONE_MONTH_AGO_DAYS = 31;
export const ONE_YEAR_AGO_DAYS = 365;
export const CHART_DATE_FORMAT = 'dd mmm';
export const DEFAULT = 'default';
......
---
title: Only Display One Chart at a Time
merge_request: 52952
author:
type: changed
......@@ -4,7 +4,7 @@ import Api from 'ee/api';
import { s__ } from '~/locale';
import createFlash from '~/flash';
import * as Sentry from '~/sentry/wrapper';
import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
import CiCdAnalyticsCharts from '~/projects/pipelines/charts/components/ci_cd_analytics_charts.vue';
import {
allChartDefinitions,
areaChartOptions,
......@@ -19,7 +19,7 @@ export default {
components: {
GlLink,
GlSprintf,
CiCdAnalyticsAreaChart,
CiCdAnalyticsCharts,
},
inject: {
projectPath: {
......@@ -36,6 +36,14 @@ export default {
},
};
},
computed: {
charts() {
return allChartDefinitions.map((chart) => ({
...chart,
data: this.chartData[chart.id],
}));
},
},
async mounted() {
const results = await Promise.allSettled(
allChartDefinitions.map(async ({ id, requestParams, startDate, endDate }) => {
......@@ -81,13 +89,6 @@ export default {
{{ __('Learn more.') }}
</gl-link>
</p>
<ci-cd-analytics-area-chart
v-for="chart of $options.allChartDefinitions"
:key="chart.id"
:chart-data="chartData[chart.id]"
:area-chart-options="$options.areaChartOptions"
>
{{ chart.title }}
</ci-cd-analytics-area-chart>
<ci-cd-analytics-charts :charts="charts" :chart-options="$options.areaChartOptions" />
</div>
</template>
import dateFormat from 'dateformat';
import { s__, sprintf } from '~/locale';
import { __, s__, sprintf } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import { nDaysBefore, nMonthsBefore, getStartOfDay, dayAfter } from '~/lib/utils/datetime_utility';
import { LAST_WEEK, LAST_MONTH, LAST_90_DAYS } from './constants';
......@@ -30,15 +30,11 @@ const sharedRequestParams = {
export const allChartDefinitions = [
{
id: LAST_WEEK,
title: sprintf(
s__(
'DeploymentFrequencyCharts|Deployments to production for last week (%{startDate} - %{endDate})',
),
{
startDate: dateFormat(lastWeek, titleDateFormatString, true),
endDate: dateFormat(startOfToday, titleDateFormatString, true),
},
),
title: __('Last week'),
range: sprintf(s__('DeploymentFrequencyCharts|%{startDate} - %{endDate}'), {
startDate: dateFormat(lastWeek, titleDateFormatString, true),
endDate: dateFormat(startOfToday, titleDateFormatString, true),
}),
startDate: lastWeek,
endDate: startOfTomorrow,
requestParams: {
......@@ -49,15 +45,11 @@ export const allChartDefinitions = [
},
{
id: LAST_MONTH,
title: sprintf(
s__(
'DeploymentFrequencyCharts|Deployments to production for last month (%{startDate} - %{endDate})',
),
{
startDate: dateFormat(lastMonth, titleDateFormatString, true),
endDate: dateFormat(startOfToday, titleDateFormatString, true),
},
),
title: __('Last month'),
range: sprintf(s__('DeploymentFrequencyCharts|%{startDate} - %{endDate}'), {
startDate: dateFormat(lastMonth, titleDateFormatString, true),
endDate: dateFormat(startOfToday, titleDateFormatString, true),
}),
startDate: lastMonth,
endDate: startOfTomorrow,
requestParams: {
......@@ -68,15 +60,11 @@ export const allChartDefinitions = [
},
{
id: LAST_90_DAYS,
title: sprintf(
s__(
'DeploymentFrequencyCharts|Deployments to production for the last 90 days (%{startDate} - %{endDate})',
),
{
startDate: dateFormat(last90Days, titleDateFormatString, true),
endDate: dateFormat(startOfToday, titleDateFormatString, true),
},
),
title: __('Last 90 days'),
range: sprintf(s__('%{startDate} - %{endDate}'), {
startDate: dateFormat(last90Days, titleDateFormatString, true),
endDate: dateFormat(startOfToday, titleDateFormatString, true),
}),
startDate: last90Days,
endDate: startOfTomorrow,
requestParams: {
......
......@@ -2,7 +2,7 @@ import { GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { useFakeDate } from 'helpers/fake_date';
import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
import CiCdAnalyticsCharts from '~/projects/pipelines/charts/components/ci_cd_analytics_charts.vue';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import * as Sentry from '~/sentry/wrapper';
......@@ -101,9 +101,8 @@ describe('ee_component/projects/pipelines/charts/components/deployment_frequency
});
it('converts the data from the API into data usable by the chart component', () => {
wrapper.findAll(CiCdAnalyticsAreaChart).wrappers.forEach((chartWrapper) => {
expect(chartWrapper.props().chartData[0].data).toMatchSnapshot();
});
const chartWrapper = wrapper.find(CiCdAnalyticsCharts);
expect(chartWrapper.props().charts).toMatchSnapshot();
});
it('does not show a flash message', () => {
......
......@@ -682,6 +682,15 @@ msgstr[1] ""
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
msgid "%{oneMonthAgo} - %{today}"
msgstr ""
msgid "%{oneWeekAgo} - %{today}"
msgstr ""
msgid "%{oneYearAgo} - %{today}"
msgstr ""
msgid "%{openOrClose} %{noteable}"
msgstr ""
......@@ -775,6 +784,9 @@ msgstr ""
msgid "%{spanStart}in%{spanEnd} %{errorFn}"
msgstr ""
msgid "%{startDate} - %{endDate}"
msgstr ""
msgid "%{start} to %{end}"
msgstr ""
......@@ -5700,6 +5712,9 @@ msgstr ""
msgid "Choose your framework"
msgstr ""
msgid "CiCdAnalytics|Date range: %{range}"
msgstr ""
msgid "CiStatusLabel|canceled"
msgstr ""
......@@ -9822,6 +9837,9 @@ msgstr ""
msgid "Deployment Frequency"
msgstr ""
msgid "DeploymentFrequencyCharts|%{startDate} - %{endDate}"
msgstr ""
msgid "DeploymentFrequencyCharts|Date"
msgstr ""
......@@ -9831,15 +9849,6 @@ msgstr ""
msgid "DeploymentFrequencyCharts|Deployments charts"
msgstr ""
msgid "DeploymentFrequencyCharts|Deployments to production for last month (%{startDate} - %{endDate})"
msgstr ""
msgid "DeploymentFrequencyCharts|Deployments to production for last week (%{startDate} - %{endDate})"
msgstr ""
msgid "DeploymentFrequencyCharts|Deployments to production for the last 90 days (%{startDate} - %{endDate})"
msgstr ""
msgid "DeploymentFrequencyCharts|Something went wrong while getting deployment frequency data"
msgstr ""
......@@ -17073,6 +17082,9 @@ msgstr ""
msgid "Last item before this page loaded in your browser:"
msgstr ""
msgid "Last month"
msgstr ""
msgid "Last name"
msgstr ""
......@@ -17121,6 +17133,9 @@ msgstr ""
msgid "Last week"
msgstr ""
msgid "Last year"
msgstr ""
msgid "LastCommit|authored"
msgstr ""
......@@ -21446,15 +21461,6 @@ msgstr ""
msgid "Pipelines emails"
msgstr ""
msgid "Pipelines for last month (%{oneMonthAgo} - %{today})"
msgstr ""
msgid "Pipelines for last week (%{oneWeekAgo} - %{today})"
msgstr ""
msgid "Pipelines for last year"
msgstr ""
msgid "Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results."
msgstr ""
......
......@@ -72,9 +72,9 @@ RSpec.describe 'Project Graph', :js do
it 'renders CI graphs' do
expect(page).to have_content 'Overall'
expect(page).to have_content 'Pipelines for last week'
expect(page).to have_content 'Pipelines for last month'
expect(page).to have_content 'Pipelines for last year'
expect(page).to have_content 'Last week'
expect(page).to have_content 'Last month'
expect(page).to have_content 'Last year'
expect(page).to have_content 'Duration for the last 30 commits'
end
end
......
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { GlSegmentedControl } from '@gitlab/ui';
import CiCdAnalyticsCharts from '~/projects/pipelines/charts/components/ci_cd_analytics_charts.vue';
import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
import { transformedAreaChartData, chartOptions } from '../mock_data';
const DEFAULT_PROPS = {
chartOptions,
charts: [
{
range: 'test range 1',
title: 'title 1',
data: transformedAreaChartData,
},
{
range: 'test range 2',
title: 'title 2',
data: transformedAreaChartData,
},
{
range: 'test range 3',
title: 'title 3',
data: transformedAreaChartData,
},
],
};
describe('~/projects/pipelines/charts/components/ci_cd_analytics_charts.vue', () => {
let wrapper;
const createWrapper = (props = {}) =>
shallowMount(CiCdAnalyticsCharts, {
propsData: {
...DEFAULT_PROPS,
...props,
},
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
describe('segmented control', () => {
let segmentedControl;
beforeEach(() => {
wrapper = createWrapper();
segmentedControl = wrapper.find(GlSegmentedControl);
});
it('should default to the first chart', () => {
expect(segmentedControl.props('checked')).toBe(0);
});
it('should use the title and index as values', () => {
const options = segmentedControl.props('options');
expect(options).toHaveLength(3);
expect(options).toEqual([
{
text: 'title 1',
value: 0,
},
{
text: 'title 2',
value: 1,
},
{
text: 'title 3',
value: 2,
},
]);
});
it('should select a different chart on change', async () => {
segmentedControl.vm.$emit('input', 1);
const chart = wrapper.find(CiCdAnalyticsAreaChart);
await nextTick();
expect(chart.props('chartData')).toEqual(transformedAreaChartData);
expect(chart.text()).toBe('Date range: test range 2');
});
});
it('should not display charts if there are no charts', () => {
wrapper = createWrapper({ charts: [] });
expect(wrapper.find(CiCdAnalyticsAreaChart).exists()).toBe(false);
});
});
......@@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import createMockApollo from 'helpers/mock_apollo_helper';
import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
import CiCdAnalyticsCharts from '~/projects/pipelines/charts/components/ci_cd_analytics_charts.vue';
import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
import getPipelineCountByStatus from '~/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql';
import getProjectPipelineStatistics from '~/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql';
......@@ -65,20 +65,17 @@ describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => {
});
describe('pipelines charts', () => {
it('displays 3 area charts', () => {
expect(wrapper.findAll(CiCdAnalyticsAreaChart)).toHaveLength(3);
it('displays the charts components', () => {
expect(wrapper.find(CiCdAnalyticsCharts).exists()).toBe(true);
});
describe('displays individual correctly', () => {
it('renders with the correct data', () => {
const charts = wrapper.findAll(CiCdAnalyticsAreaChart);
for (let i = 0; i < charts.length; i += 1) {
const chart = charts.at(i);
expect(chart.exists()).toBeTruthy();
expect(chart.props('chartData')).toBe(wrapper.vm.areaCharts[i].data);
expect(chart.text()).toBe(wrapper.vm.areaCharts[i].title);
}
const charts = wrapper.find(CiCdAnalyticsCharts);
expect(charts.props()).toEqual({
charts: wrapper.vm.areaCharts,
chartOptions: wrapper.vm.$options.areaChartOptions,
});
});
});
});
......
......@@ -57,6 +57,16 @@ export const mockPipelineCount = {
},
};
export const chartOptions = {
xAxis: {
name: 'X axis title',
type: 'category',
},
yAxis: {
name: 'Y axis title',
},
};
export const mockPipelineStatistics = {
data: {
project: {
......
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