Commit f87e72cd authored by Sarah Groff Hennigh-Palermo's avatar Sarah Groff Hennigh-Palermo

Merge branch 'jivanvl-migrate-analytics-graphql' into 'master'

Refactor CI/CD analytics page to GraphQL

See merge request gitlab-org/gitlab!48267
parents a4bae2b3 82351981
<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 { __, sprintf } from '~/locale'; import { GlAlert } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
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 getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql';
import StatisticsList from './statistics_list.vue'; import StatisticsList from './statistics_list.vue';
import PipelinesAreaChart from './pipelines_area_chart.vue'; import PipelinesAreaChart from './pipelines_area_chart.vue';
import { import {
CHART_CONTAINER_HEIGHT, CHART_CONTAINER_HEIGHT,
INNER_CHART_HEIGHT,
X_AXIS_LABEL_ROTATION,
X_AXIS_TITLE_OFFSET,
CHART_DATE_FORMAT, CHART_DATE_FORMAT,
DEFAULT,
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_TITLE_OFFSET,
} from '../constants'; } from '../constants';
const defaultCountValues = {
totalPipelines: {
count: 0,
},
successfulPipelines: {
count: 0,
},
};
const defaultAnalyticsValues = {
weekPipelinesTotals: [],
weekPipelinesLabels: [],
weekPipelinesSuccessful: [],
monthPipelinesLabels: [],
monthPipelinesTotals: [],
monthPipelinesSuccessful: [],
yearPipelinesLabels: [],
yearPipelinesTotals: [],
yearPipelinesSuccessful: [],
pipelineTimesLabels: [],
pipelineTimesValues: [],
};
export default { export default {
components: { components: {
StatisticsList, GlAlert,
GlColumnChart, GlColumnChart,
StatisticsList,
PipelinesAreaChart, PipelinesAreaChart,
}, },
props: { inject: {
projectPath: {
type: String,
default: '',
},
},
data() {
return {
counts: { counts: {
type: Object, ...defaultCountValues,
required: true,
}, },
timesChartData: { analytics: {
type: Object, ...defaultAnalyticsValues,
required: true,
}, },
lastWeekChartData: { showFailureAlert: false,
type: Object, failureType: null,
required: true, };
}, },
lastMonthChartData: { apollo: {
type: Object, counts: {
required: true, query: getPipelineCountByStatus,
variables() {
return {
projectPath: this.projectPath,
};
}, },
lastYearChartData: { update(data) {
type: Object, return data?.project;
required: true,
}, },
error() {
this.reportFailure(LOAD_PIPELINES_FAILURE);
}, },
data() {
return {
timesChartTransformedData: [
{
name: 'full',
data: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values),
}, },
], analytics: {
query: getProjectPipelineStatistics,
variables() {
return {
projectPath: this.projectPath,
}; };
}, },
update(data) {
return data?.project?.pipelineAnalytics;
},
error() {
this.reportFailure(LOAD_ANALYTICS_FAILURE);
},
},
},
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() {
const { successfulPipelines, failedPipelines } = this.counts;
const successfulCount = successfulPipelines?.count;
const failedCount = failedPipelines?.count;
const ratio = (successfulCount / (successfulCount + failedCount)) * 100;
return failedCount === 0 ? 100 : ratio;
},
formattedCounts() {
const {
totalPipelines,
successfulPipelines,
failedPipelines,
totalPipelineDuration,
} = this.counts;
return {
total: totalPipelines?.count,
success: successfulPipelines?.count,
failed: failedPipelines?.count,
successRatio: this.successRatio,
totalDuration: totalPipelineDuration,
};
},
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() {
return {
labels: this.analytics.weekPipelinesLabels,
totals: this.analytics.weekPipelinesTotals,
success: this.analytics.weekPipelinesSuccessful,
};
},
lastMonthChartData() {
return {
labels: this.analytics.monthPipelinesLabels,
totals: this.analytics.monthPipelinesTotals,
success: this.analytics.monthPipelinesSuccessful,
};
},
lastYearChartData() {
return {
labels: this.analytics.yearPipelinesLabels,
totals: this.analytics.yearPipelinesTotals,
success: this.analytics.yearPipelinesSuccessful,
};
},
timesChartTransformedData() {
return [
{
name: 'full',
data: this.mergeLabelsAndValues(
this.analytics.pipelineTimesLabels,
this.analytics.pipelineTimesValues,
),
},
];
}, },
}, },
methods: { methods: {
...@@ -85,6 +221,13 @@ export default { ...@@ -85,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: {
...@@ -96,6 +239,16 @@ export default { ...@@ -96,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 =>
...@@ -116,13 +269,16 @@ export default { ...@@ -116,13 +269,16 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<div class="mb-3"> <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">
{{ failure.text }}
</gl-alert>
<div class="gl-mb-3">
<h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3> <h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3>
</div> </div>
<h4 class="my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4> <h4 class="gl-my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<statistics-list :counts="counts" /> <statistics-list :counts="formattedCounts" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<strong> <strong>
...@@ -139,7 +295,7 @@ export default { ...@@ -139,7 +295,7 @@ export default {
</div> </div>
</div> </div>
<hr /> <hr />
<h4 class="my-4">{{ __('Pipelines charts') }}</h4> <h4 class="gl-my-4">{{ __('Pipelines charts') }}</h4>
<pipelines-area-chart <pipelines-area-chart
v-for="(chart, index) in areaCharts" v-for="(chart, index) in areaCharts"
:key="index" :key="index"
......
<script>
import dateFormat from 'dateformat';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { __, sprintf } from '~/locale';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import StatisticsList from './statistics_list.vue';
import PipelinesAreaChart from './pipelines_area_chart.vue';
import {
CHART_CONTAINER_HEIGHT,
INNER_CHART_HEIGHT,
X_AXIS_LABEL_ROTATION,
X_AXIS_TITLE_OFFSET,
CHART_DATE_FORMAT,
ONE_WEEK_AGO_DAYS,
ONE_MONTH_AGO_DAYS,
} from '../constants';
export default {
components: {
StatisticsList,
GlColumnChart,
PipelinesAreaChart,
},
props: {
counts: {
type: Object,
required: true,
},
timesChartData: {
type: Object,
required: true,
},
lastWeekChartData: {
type: Object,
required: true,
},
lastMonthChartData: {
type: Object,
required: true,
},
lastYearChartData: {
type: Object,
required: true,
},
},
data() {
return {
timesChartTransformedData: [
{
name: 'full',
data: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values),
},
],
};
},
computed: {
areaCharts() {
const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles;
return [
this.buildAreaChartData(lastWeek, this.lastWeekChartData),
this.buildAreaChartData(lastMonth, this.lastMonthChartData),
this.buildAreaChartData(lastYear, this.lastYearChartData),
];
},
},
methods: {
mergeLabelsAndValues(labels, values) {
return labels.map((label, index) => [label, values[index]]);
},
buildAreaChartData(title, data) {
const { labels, totals, success } = data;
return {
title,
data: [
{
name: 'all',
data: this.mergeLabelsAndValues(labels, totals),
},
{
name: 'success',
data: this.mergeLabelsAndValues(labels, success),
},
],
};
},
},
chartContainerHeight: CHART_CONTAINER_HEIGHT,
timesChartOptions: {
height: INNER_CHART_HEIGHT,
xAxis: {
axisLabel: {
rotate: X_AXIS_LABEL_ROTATION,
},
nameGap: X_AXIS_TITLE_OFFSET,
},
},
get chartTitles() {
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})'), {
oneWeekAgo: pastDate(ONE_WEEK_AGO_DAYS),
today,
}),
lastMonth: sprintf(__('Pipelines for last month (%{oneMonthAgo} - %{today})'), {
oneMonthAgo: pastDate(ONE_MONTH_AGO_DAYS),
today,
}),
lastYear: __('Pipelines for last year'),
};
},
};
</script>
<template>
<div>
<div class="mb-3">
<h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3>
</div>
<h4 class="my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4>
<div class="row">
<div class="col-md-6">
<statistics-list :counts="counts" />
</div>
<div class="col-md-6">
<strong>
{{ __('Duration for the last 30 commits') }}
</strong>
<gl-column-chart
:height="$options.chartContainerHeight"
:option="$options.timesChartOptions"
:bars="timesChartTransformedData"
:y-axis-title="__('Minutes')"
:x-axis-title="__('Commit')"
x-axis-type="category"
/>
</div>
</div>
<hr />
<h4 class="my-4">{{ __('Pipelines charts') }}</h4>
<pipelines-area-chart
v-for="(chart, index) in areaCharts"
:key="index"
:chart-data="chart.data"
>
{{ chart.title }}
</pipelines-area-chart>
</div>
</template>
<script> <script>
import { formatTime } from '~/lib/utils/datetime_utility'; import { formatTime } from '~/lib/utils/datetime_utility';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
import { s__, n__ } from '~/locale'; import { s__, n__ } from '~/locale';
const defaultPrecision = 2;
export default { export default {
props: { props: {
counts: { counts: {
...@@ -14,6 +17,8 @@ export default { ...@@ -14,6 +17,8 @@ export default {
return formatTime(this.counts.totalDuration); return formatTime(this.counts.totalDuration);
}, },
statistics() { statistics() {
const formatter = getFormatter(SUPPORTED_FORMATS.percentHundred);
return [ return [
{ {
title: s__('PipelineCharts|Total:'), title: s__('PipelineCharts|Total:'),
...@@ -29,7 +34,7 @@ export default { ...@@ -29,7 +34,7 @@ export default {
}, },
{ {
title: s__('PipelineCharts|Success ratio:'), title: s__('PipelineCharts|Success ratio:'),
value: `${this.counts.successRatio}%`, value: formatter(this.counts.successRatio, defaultPrecision),
}, },
{ {
title: s__('PipelineCharts|Total duration:'), title: s__('PipelineCharts|Total duration:'),
......
...@@ -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';
query getPipelineCountByStatus($projectPath: ID!) {
project(fullPath: $projectPath) {
totalPipelines: pipelines {
count
}
successfulPipelines: pipelines(status: SUCCESS) {
count
}
failedPipelines: pipelines(status: FAILED) {
count
}
totalPipelineDuration
}
}
query getProjectPipelineStatistics($projectPath: ID!) {
project(fullPath: $projectPath) {
pipelineAnalytics {
weekPipelinesTotals
weekPipelinesLabels
weekPipelinesSuccessful
monthPipelinesLabels
monthPipelinesTotals
monthPipelinesSuccessful
yearPipelinesLabels
yearPipelinesTotals
yearPipelinesSuccessful
pipelineTimesLabels
pipelineTimesValues
}
}
}
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import ProjectPipelinesChartsLegacy from './components/app_legacy.vue';
import ProjectPipelinesCharts from './components/app.vue'; import ProjectPipelinesCharts from './components/app.vue';
export default () => { Vue.use(VueApollo);
const el = document.querySelector('#js-project-pipelines-charts-app');
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
const mountPipelineChartsApp = el => {
// 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,
...@@ -20,22 +32,48 @@ export default () => { ...@@ -20,22 +32,48 @@ export default () => {
lastYearChartLabels, lastYearChartLabels,
lastYearChartTotals, lastYearChartTotals,
lastYearChartSuccess, lastYearChartSuccess,
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) {
return new Vue({ return new Vue({
el, el,
name: 'ProjectPipelinesChartsApp', name: 'ProjectPipelinesChartsApp',
components: { components: {
ProjectPipelinesCharts, ProjectPipelinesCharts,
}, },
apolloProvider,
provide: {
projectPath,
},
render: createElement => createElement(ProjectPipelinesCharts, {}),
});
}
return new Vue({
el,
name: 'ProjectPipelinesChartsAppLegacy',
components: {
ProjectPipelinesChartsLegacy,
},
render: createElement => render: createElement =>
createElement(ProjectPipelinesCharts, { createElement(ProjectPipelinesChartsLegacy, {
props: { props: {
counts: { counts: {
failed: countsFailed, failed: countsFailed,
...@@ -67,3 +105,8 @@ export default () => { ...@@ -67,3 +105,8 @@ export default () => {
}), }),
}); });
}; };
export default () => {
const el = document.querySelector('#js-project-pipelines-charts-app');
return !el ? {} : mountPipelineChartsApp(el);
};
...@@ -17,6 +17,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -17,6 +17,7 @@ class Projects::PipelinesController < Projects::ApplicationController
push_frontend_feature_flag(:new_pipeline_form, project, default_enabled: true) push_frontend_feature_flag(:new_pipeline_form, project, default_enabled: true)
push_frontend_feature_flag(:graphql_pipeline_header, project, type: :development, default_enabled: false) push_frontend_feature_flag(:graphql_pipeline_header, project, type: :development, default_enabled: false)
push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: false) push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: false)
push_frontend_feature_flag(:graphql_pipeline_analytics, project, type: :development)
push_frontend_feature_flag(:new_pipeline_form_prefilled_vars, project, type: :development) push_frontend_feature_flag(:new_pipeline_form_prefilled_vars, project, type: :development)
end end
before_action :ensure_pipeline, only: [:show] before_action :ensure_pipeline, only: [:show]
......
- 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 },
......
---
name: graphql_pipeline_analytics
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48267
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/290153
milestone: '13.7'
type: development
group: group::continuos integration
default_enabled: false
...@@ -20104,6 +20104,15 @@ msgstr "" ...@@ -20104,6 +20104,15 @@ msgstr ""
msgid "Pipeline: %{status}" msgid "Pipeline: %{status}"
msgstr "" msgstr ""
msgid "PipelineCharts|An error has ocurred when retrieving the analytics data"
msgstr ""
msgid "PipelineCharts|An error has ocurred when retrieving the pipelines data"
msgstr ""
msgid "PipelineCharts|An unknown error occurred while processing CI/CD analytics."
msgstr ""
msgid "PipelineCharts|CI / CD Analytics" msgid "PipelineCharts|CI / CD Analytics"
msgstr "" msgstr ""
...@@ -20119,6 +20128,9 @@ msgstr "" ...@@ -20119,6 +20128,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>
......
import { shallowMount } from '@vue/test-utils';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import Component from '~/projects/pipelines/charts/components/app_legacy.vue';
import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
import PipelinesAreaChart from '~/projects/pipelines/charts/components/pipelines_area_chart.vue';
import {
counts,
timesChartData,
areaChartData as lastWeekChartData,
areaChartData as lastMonthChartData,
lastYearChartData,
} from '../mock_data';
describe('ProjectsPipelinesChartsApp', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(Component, {
propsData: {
counts,
timesChartData,
lastWeekChartData,
lastMonthChartData,
lastYearChartData,
},
});
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('overall statistics', () => {
it('displays the statistics list', () => {
const list = wrapper.find(StatisticsList);
expect(list.exists()).toBeTruthy();
expect(list.props('counts')).toBe(counts);
});
it('displays the commit duration chart', () => {
const chart = wrapper.find(GlColumnChart);
expect(chart.exists()).toBeTruthy();
expect(chart.props('yAxisTitle')).toBe('Minutes');
expect(chart.props('xAxisTitle')).toBe('Commit');
expect(chart.props('bars')).toBe(wrapper.vm.timesChartTransformedData);
expect(chart.props('option')).toBe(wrapper.vm.$options.timesChartOptions);
});
});
describe('pipelines charts', () => {
it('displays 3 area charts', () => {
expect(wrapper.findAll(PipelinesAreaChart).length).toBe(3);
});
describe('displays individual correctly', () => {
it('renders with the correct data', () => {
const charts = wrapper.findAll(PipelinesAreaChart);
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);
}
});
});
});
});
import { shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import { GlColumnChart } from '@gitlab/ui/dist/charts'; import { GlColumnChart } from '@gitlab/ui/dist/charts';
import Component from '~/projects/pipelines/charts/components/app.vue'; import Component from '~/projects/pipelines/charts/components/app.vue';
import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue'; import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
import PipelinesAreaChart from '~/projects/pipelines/charts/components/pipelines_area_chart.vue'; import PipelinesAreaChart from '~/projects/pipelines/charts/components/pipelines_area_chart.vue';
import { import getPipelineCountByStatus from '~/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql';
counts, import getProjectPipelineStatistics from '~/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql';
timesChartData, import { mockPipelineCount, mockPipelineStatistics } from '../mock_data';
areaChartData as lastWeekChartData,
areaChartData as lastMonthChartData, const projectPath = 'gitlab-org/gitlab';
lastYearChartData, const localVue = createLocalVue();
} from '../mock_data'; localVue.use(VueApollo);
describe('ProjectsPipelinesChartsApp', () => { describe('ProjectsPipelinesChartsApp', () => {
let wrapper; let wrapper;
beforeEach(() => { function createMockApolloProvider() {
wrapper = shallowMount(Component, { const requestHandlers = [
propsData: { [getPipelineCountByStatus, jest.fn().mockResolvedValue(mockPipelineCount)],
counts, [getProjectPipelineStatistics, jest.fn().mockResolvedValue(mockPipelineStatistics)],
timesChartData, ];
lastWeekChartData,
lastMonthChartData, return createMockApollo(requestHandlers);
lastYearChartData, }
function createComponent(options = {}) {
const { fakeApollo } = options;
return shallowMount(Component, {
provide: {
projectPath,
}, },
localVue,
apolloProvider: fakeApollo,
}); });
}
beforeEach(() => {
const fakeApollo = createMockApolloProvider();
wrapper = createComponent({ fakeApollo });
}); });
afterEach(() => { afterEach(() => {
...@@ -35,14 +51,20 @@ describe('ProjectsPipelinesChartsApp', () => { ...@@ -35,14 +51,20 @@ describe('ProjectsPipelinesChartsApp', () => {
it('displays the statistics list', () => { it('displays the statistics list', () => {
const list = wrapper.find(StatisticsList); const list = wrapper.find(StatisticsList);
expect(list.exists()).toBeTruthy(); expect(list.exists()).toBe(true);
expect(list.props('counts')).toBe(counts); expect(list.props('counts')).toMatchObject({
failed: 1,
success: 23,
total: 34,
successRatio: 95.83333333333334,
totalDuration: 2471,
});
}); });
it('displays the commit duration chart', () => { it('displays the commit duration chart', () => {
const chart = wrapper.find(GlColumnChart); const chart = wrapper.find(GlColumnChart);
expect(chart.exists()).toBeTruthy(); expect(chart.exists()).toBe(true);
expect(chart.props('yAxisTitle')).toBe('Minutes'); expect(chart.props('yAxisTitle')).toBe('Minutes');
expect(chart.props('xAxisTitle')).toBe('Commit'); expect(chart.props('xAxisTitle')).toBe('Commit');
expect(chart.props('bars')).toBe(wrapper.vm.timesChartTransformedData); expect(chart.props('bars')).toBe(wrapper.vm.timesChartTransformedData);
...@@ -52,7 +74,7 @@ describe('ProjectsPipelinesChartsApp', () => { ...@@ -52,7 +74,7 @@ describe('ProjectsPipelinesChartsApp', () => {
describe('pipelines charts', () => { describe('pipelines charts', () => {
it('displays 3 area charts', () => { it('displays 3 area charts', () => {
expect(wrapper.findAll(PipelinesAreaChart).length).toBe(3); expect(wrapper.findAll(PipelinesAreaChart)).toHaveLength(3);
}); });
describe('displays individual correctly', () => { describe('displays individual correctly', () => {
...@@ -62,7 +84,9 @@ describe('ProjectsPipelinesChartsApp', () => { ...@@ -62,7 +84,9 @@ describe('ProjectsPipelinesChartsApp', () => {
for (let i = 0; i < charts.length; i += 1) { for (let i = 0; i < charts.length; i += 1) {
const chart = charts.at(i); const chart = charts.at(i);
expect(chart.exists()).toBeTruthy(); 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);
} }
......
...@@ -18,7 +18,7 @@ describe('StatisticsList', () => { ...@@ -18,7 +18,7 @@ describe('StatisticsList', () => {
wrapper = null; wrapper = null;
}); });
it('matches the snapshot', () => { it('displays the counts data with labels', () => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
}); });
...@@ -32,3 +32,218 @@ export const transformedAreaChartData = [ ...@@ -32,3 +32,218 @@ export const transformedAreaChartData = [
data: [['01 Jan', 3], ['02 Jan', 3], ['03 Jan', 3], ['04 Jan', 3], ['05 Jan', 5]], data: [['01 Jan', 3], ['02 Jan', 3], ['03 Jan', 3], ['04 Jan', 3], ['05 Jan', 5]],
}, },
]; ];
export const mockPipelineCount = {
data: {
project: {
totalPipelines: { count: 34, __typename: 'PipelineConnection' },
successfulPipelines: { count: 23, __typename: 'PipelineConnection' },
failedPipelines: { count: 1, __typename: 'PipelineConnection' },
totalPipelineDuration: 2471,
__typename: 'Project',
},
},
};
export const mockPipelineStatistics = {
data: {
project: {
pipelineAnalytics: {
weekPipelinesTotals: [0, 0, 0, 0, 0, 0, 0, 0],
weekPipelinesLabels: [
'24 November',
'25 November',
'26 November',
'27 November',
'28 November',
'29 November',
'30 November',
'01 December',
],
weekPipelinesSuccessful: [0, 0, 0, 0, 0, 0, 0, 0],
monthPipelinesLabels: [
'01 November',
'02 November',
'03 November',
'04 November',
'05 November',
'06 November',
'07 November',
'08 November',
'09 November',
'10 November',
'11 November',
'12 November',
'13 November',
'14 November',
'15 November',
'16 November',
'17 November',
'18 November',
'19 November',
'20 November',
'21 November',
'22 November',
'23 November',
'24 November',
'25 November',
'26 November',
'27 November',
'28 November',
'29 November',
'30 November',
'01 December',
],
monthPipelinesTotals: [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
],
monthPipelinesSuccessful: [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
],
yearPipelinesLabels: [
'December 2019',
'January 2020',
'February 2020',
'March 2020',
'April 2020',
'May 2020',
'June 2020',
'July 2020',
'August 2020',
'September 2020',
'October 2020',
'November 2020',
'December 2020',
],
yearPipelinesTotals: [0, 0, 0, 0, 0, 0, 0, 0, 23, 7, 2, 2, 0],
yearPipelinesSuccessful: [0, 0, 0, 0, 0, 0, 0, 0, 17, 5, 1, 0, 0],
pipelineTimesLabels: [
'b3781247',
'b3781247',
'a50ba059',
'8e414f3b',
'b2964d50',
'7caa525b',
'761b164e',
'd3eccd18',
'e2750f63',
'e2750f63',
'1dfb4b96',
'b49d6f94',
'66fa2f80',
'e2750f63',
'fc82cf15',
'19fb20b2',
'25f03a24',
'e054110f',
'0278b7b2',
'38478c16',
'38478c16',
'38478c16',
'1fb2103e',
'97b99fb5',
'8abc6e87',
'c94e80e3',
'5d349a50',
'5d349a50',
'9c581037',
'02d95fb2',
],
pipelineTimesValues: [
1,
0,
0,
0,
0,
1,
1,
2,
1,
0,
1,
2,
2,
0,
4,
2,
1,
2,
1,
1,
0,
1,
1,
0,
1,
5,
2,
0,
0,
0,
],
__typename: 'Analytics',
},
__typename: '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