Commit 82351981 authored by Jose Vargas's avatar Jose Vargas

Refactor tests and improve error handling

This improves on the existing error handling
to use the GlAlert component. This also
adds comments to remove/refactor the parts
that will be affected once the feature flag
rolls out
parent 666bbc50
<script> <script>
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { GlColumnChart } from '@gitlab/ui/dist/charts'; import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { GlAlert } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import createFlash, { FLASH_TYPES } from '~/flash';
import { getDateInPast } from '~/lib/utils/datetime_utility'; import { getDateInPast } from '~/lib/utils/datetime_utility';
import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql'; import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql';
import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql'; import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql';
...@@ -11,9 +11,14 @@ import PipelinesAreaChart from './pipelines_area_chart.vue'; ...@@ -11,9 +11,14 @@ import PipelinesAreaChart from './pipelines_area_chart.vue';
import { import {
CHART_CONTAINER_HEIGHT, CHART_CONTAINER_HEIGHT,
CHART_DATE_FORMAT, CHART_DATE_FORMAT,
DEFAULT,
INNER_CHART_HEIGHT, INNER_CHART_HEIGHT,
LOAD_ANALYTICS_FAILURE,
LOAD_PIPELINES_FAILURE,
ONE_WEEK_AGO_DAYS, ONE_WEEK_AGO_DAYS,
ONE_MONTH_AGO_DAYS, ONE_MONTH_AGO_DAYS,
PARSE_FAILURE,
UNSUPPORTED_DATA,
X_AXIS_LABEL_ROTATION, X_AXIS_LABEL_ROTATION,
X_AXIS_TITLE_OFFSET, X_AXIS_TITLE_OFFSET,
} from '../constants'; } from '../constants';
...@@ -43,6 +48,7 @@ const defaultAnalyticsValues = { ...@@ -43,6 +48,7 @@ const defaultAnalyticsValues = {
export default { export default {
components: { components: {
GlAlert,
GlColumnChart, GlColumnChart,
StatisticsList, StatisticsList,
PipelinesAreaChart, PipelinesAreaChart,
...@@ -61,6 +67,8 @@ export default { ...@@ -61,6 +67,8 @@ export default {
analytics: { analytics: {
...defaultAnalyticsValues, ...defaultAnalyticsValues,
}, },
showFailureAlert: false,
failureType: null,
}; };
}, },
apollo: { apollo: {
...@@ -71,14 +79,11 @@ export default { ...@@ -71,14 +79,11 @@ export default {
projectPath: this.projectPath, projectPath: this.projectPath,
}; };
}, },
update(res) { update(data) {
return res.project; return data?.project;
}, },
error() { error() {
createFlash({ this.reportFailure(LOAD_PIPELINES_FAILURE);
message: s__('PipelineCharts|An error has ocurred when retrieving the pipeline data'),
type: FLASH_TYPES.ALERT,
});
}, },
}, },
analytics: { analytics: {
...@@ -88,18 +93,39 @@ export default { ...@@ -88,18 +93,39 @@ export default {
projectPath: this.projectPath, projectPath: this.projectPath,
}; };
}, },
update(res) { update(data) {
return res.project.pipelineAnalytics; return data?.project?.pipelineAnalytics;
}, },
error() { error() {
createFlash({ this.reportFailure(LOAD_ANALYTICS_FAILURE);
message: s__('PipelineCharts|An error has ocurred when retrieving the analytics data'),
type: FLASH_TYPES.ALERT,
});
}, },
}, },
}, },
computed: { computed: {
failure() {
switch (this.failureType) {
case LOAD_ANALYTICS_FAILURE:
return {
text: this.$options.errorTexts[LOAD_ANALYTICS_FAILURE],
variant: 'danger',
};
case PARSE_FAILURE:
return {
text: this.$options.errorTexts[PARSE_FAILURE],
variant: 'danger',
};
case UNSUPPORTED_DATA:
return {
text: this.$options.errorTexts[UNSUPPORTED_DATA],
variant: 'info',
};
default:
return {
text: this.$options.errorTexts[DEFAULT],
variant: 'danger',
};
}
},
successRatio() { successRatio() {
const { successfulPipelines, failedPipelines } = this.counts; const { successfulPipelines, failedPipelines } = this.counts;
const successfulCount = successfulPipelines?.count; const successfulCount = successfulPipelines?.count;
...@@ -126,12 +152,20 @@ export default { ...@@ -126,12 +152,20 @@ export default {
}, },
areaCharts() { areaCharts() {
const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles; const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles;
let areaChartsData = [];
return [ try {
areaChartsData = [
this.buildAreaChartData(lastWeek, this.lastWeekChartData), this.buildAreaChartData(lastWeek, this.lastWeekChartData),
this.buildAreaChartData(lastMonth, this.lastMonthChartData), this.buildAreaChartData(lastMonth, this.lastMonthChartData),
this.buildAreaChartData(lastYear, this.lastYearChartData), this.buildAreaChartData(lastYear, this.lastYearChartData),
]; ];
} catch {
areaChartsData = [];
this.reportFailure(PARSE_FAILURE);
}
return areaChartsData;
}, },
lastWeekChartData() { lastWeekChartData() {
return { return {
...@@ -187,6 +221,13 @@ export default { ...@@ -187,6 +221,13 @@ export default {
], ],
}; };
}, },
hideAlert() {
this.showFailureAlert = false;
},
reportFailure(type) {
this.showFailureAlert = true;
this.failureType = type;
},
}, },
chartContainerHeight: CHART_CONTAINER_HEIGHT, chartContainerHeight: CHART_CONTAINER_HEIGHT,
timesChartOptions: { timesChartOptions: {
...@@ -198,6 +239,16 @@ export default { ...@@ -198,6 +239,16 @@ export default {
nameGap: X_AXIS_TITLE_OFFSET, nameGap: X_AXIS_TITLE_OFFSET,
}, },
}, },
errorTexts: {
[LOAD_ANALYTICS_FAILURE]: s__(
'PipelineCharts|An error has ocurred when retrieving the analytics data',
),
[LOAD_PIPELINES_FAILURE]: s__(
'PipelineCharts|An error has ocurred when retrieving the pipelines data',
),
[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() { get chartTitles() {
const today = dateFormat(new Date(), CHART_DATE_FORMAT); const today = dateFormat(new Date(), CHART_DATE_FORMAT);
const pastDate = timeScale => const pastDate = timeScale =>
...@@ -218,6 +269,9 @@ export default { ...@@ -218,6 +269,9 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">
{{ failure.text }}
</gl-alert>
<div class="gl-mb-3"> <div class="gl-mb-3">
<h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3> <h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3>
</div> </div>
......
...@@ -11,3 +11,9 @@ export const ONE_WEEK_AGO_DAYS = 7; ...@@ -11,3 +11,9 @@ export const ONE_WEEK_AGO_DAYS = 7;
export const ONE_MONTH_AGO_DAYS = 31; export const ONE_MONTH_AGO_DAYS = 31;
export const CHART_DATE_FORMAT = 'dd mmm'; export const CHART_DATE_FORMAT = 'dd mmm';
export const DEFAULT = 'default';
export const PARSE_FAILURE = 'parse_failure';
export const LOAD_ANALYTICS_FAILURE = 'load_analytics_failure';
export const LOAD_PIPELINES_FAILURE = 'load_analytics_failure';
export const UNSUPPORTED_DATA = 'unsupported_data';
...@@ -10,8 +10,11 @@ const apolloProvider = new VueApollo({ ...@@ -10,8 +10,11 @@ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(), defaultClient: createDefaultClient(),
}); });
export default () => { const mountPipelineChartsApp = el => {
const el = document.querySelector('#js-project-pipelines-charts-app'); // Not all of the values will be defined since some them will be
// empty depending on the value of the graphql_pipeline_analytics
// feature flag, once the rollout of the feature flag is completed
// the undefined values will be deleted
const { const {
countsFailed, countsFailed,
countsSuccess, countsSuccess,
...@@ -32,13 +35,23 @@ export default () => { ...@@ -32,13 +35,23 @@ export default () => {
projectPath, projectPath,
} = el.dataset; } = el.dataset;
const parseAreaChartData = (labels, totals, success) => ({ const parseAreaChartData = (labels, totals, success) => {
let parsedData = {};
try {
parsedData = {
labels: JSON.parse(labels), labels: JSON.parse(labels),
totals: JSON.parse(totals), totals: JSON.parse(totals),
success: JSON.parse(success), success: JSON.parse(success),
}); };
} catch {
parsedData = {};
}
return parsedData;
};
if (gon.features.graphqlPipelineAnalytics) { if (gon?.features?.graphqlPipelineAnalytics) {
return new Vue({ return new Vue({
el, el,
name: 'ProjectPipelinesChartsApp', name: 'ProjectPipelinesChartsApp',
...@@ -55,7 +68,7 @@ export default () => { ...@@ -55,7 +68,7 @@ export default () => {
return new Vue({ return new Vue({
el, el,
name: 'ProjectPipelinesChartsApp', name: 'ProjectPipelinesChartsAppLegacy',
components: { components: {
ProjectPipelinesChartsLegacy, ProjectPipelinesChartsLegacy,
}, },
...@@ -92,3 +105,8 @@ export default () => { ...@@ -92,3 +105,8 @@ export default () => {
}), }),
}); });
}; };
export default () => {
const el = document.querySelector('#js-project-pipelines-charts-app');
return !el ? {} : mountPipelineChartsApp(el);
};
- page_title _('CI / CD Analytics') - page_title _('CI / CD Analytics')
#js-project-pipelines-charts-app{ data: { counts: @counts, success_ratio: success_ratio(@counts), - if Feature.enabled?(:graphql_pipeline_analytics)
#js-project-pipelines-charts-app{ data: { project_path: @project.full_path } }
- else
#js-project-pipelines-charts-app{ data: { counts: @counts, success_ratio: success_ratio(@counts),
times_chart: { labels: @charts[:pipeline_times].labels, values: @charts[:pipeline_times].pipeline_times }, times_chart: { labels: @charts[:pipeline_times].labels, values: @charts[:pipeline_times].pipeline_times },
last_week_chart: { labels: @charts[:week].labels, totals: @charts[:week].total, success: @charts[:week].success }, last_week_chart: { labels: @charts[:week].labels, totals: @charts[:week].total, success: @charts[:week].success },
last_month_chart: { labels: @charts[:month].labels, totals: @charts[:month].total, success: @charts[:month].success }, last_month_chart: { labels: @charts[:month].labels, totals: @charts[:month].total, success: @charts[:month].success },
......
...@@ -20116,7 +20116,10 @@ msgstr "" ...@@ -20116,7 +20116,10 @@ msgstr ""
msgid "PipelineCharts|An error has ocurred when retrieving the analytics data" msgid "PipelineCharts|An error has ocurred when retrieving the analytics data"
msgstr "" msgstr ""
msgid "PipelineCharts|An error has ocurred when retrieving the pipeline data" msgid "PipelineCharts|An error has ocurred when retrieving the pipelines data"
msgstr ""
msgid "PipelineCharts|An unknown error occurred while processing CI/CD analytics."
msgstr "" msgstr ""
msgid "PipelineCharts|CI / CD Analytics" msgid "PipelineCharts|CI / CD Analytics"
...@@ -20134,6 +20137,9 @@ msgstr "" ...@@ -20134,6 +20137,9 @@ msgstr ""
msgid "PipelineCharts|Successful:" msgid "PipelineCharts|Successful:"
msgstr "" msgstr ""
msgid "PipelineCharts|There was an error parsing the data for the charts."
msgstr ""
msgid "PipelineCharts|Total duration:" msgid "PipelineCharts|Total duration:"
msgstr "" msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StatisticsList matches the snapshot 1`] = ` exports[`StatisticsList displays the counts data with labels 1`] = `
<ul> <ul>
<li> <li>
<span> <span>
...@@ -35,7 +35,7 @@ exports[`StatisticsList matches the snapshot 1`] = ` ...@@ -35,7 +35,7 @@ exports[`StatisticsList matches the snapshot 1`] = `
</span> </span>
<strong> <strong>
50% 50.00%
</strong> </strong>
</li> </li>
<li> <li>
......
...@@ -15,23 +15,31 @@ localVue.use(VueApollo); ...@@ -15,23 +15,31 @@ localVue.use(VueApollo);
describe('ProjectsPipelinesChartsApp', () => { describe('ProjectsPipelinesChartsApp', () => {
let wrapper; let wrapper;
let fakeApollo;
beforeEach(() => { function createMockApolloProvider() {
const requestHandlers = [ const requestHandlers = [
[getPipelineCountByStatus, jest.fn().mockResolvedValue(mockPipelineCount)], [getPipelineCountByStatus, jest.fn().mockResolvedValue(mockPipelineCount)],
[getProjectPipelineStatistics, jest.fn().mockResolvedValue(mockPipelineStatistics)], [getProjectPipelineStatistics, jest.fn().mockResolvedValue(mockPipelineStatistics)],
]; ];
fakeApollo = createMockApollo(requestHandlers); return createMockApollo(requestHandlers);
}
function createComponent(options = {}) {
const { fakeApollo } = options;
wrapper = shallowMount(Component, { return shallowMount(Component, {
provide: { provide: {
projectPath, projectPath,
}, },
localVue, localVue,
apolloProvider: fakeApollo, apolloProvider: fakeApollo,
}); });
}
beforeEach(() => {
const fakeApollo = createMockApolloProvider();
wrapper = createComponent({ fakeApollo });
}); });
afterEach(() => { afterEach(() => {
...@@ -77,6 +85,8 @@ describe('ProjectsPipelinesChartsApp', () => { ...@@ -77,6 +85,8 @@ describe('ProjectsPipelinesChartsApp', () => {
const chart = charts.at(i); const chart = charts.at(i);
expect(chart.exists()).toBe(true); expect(chart.exists()).toBe(true);
// TODO: Refactor this to use the mocked data instead of the vm data
// https://gitlab.com/gitlab-org/gitlab/-/issues/292085
expect(chart.props('chartData')).toBe(wrapper.vm.areaCharts[i].data); expect(chart.props('chartData')).toBe(wrapper.vm.areaCharts[i].data);
expect(chart.text()).toBe(wrapper.vm.areaCharts[i].title); expect(chart.text()).toBe(wrapper.vm.areaCharts[i].title);
} }
......
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