Commit 2cb388cd authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '196183-convert-pipelines-last-week-chart-to-echarts' into 'master'

Migrate CI / CD pipelines for the past week chart into VueJS and ECharts.

See merge request gitlab-org/gitlab!24057
parents 6abca817 406885a2
import $ from 'jquery';
import Chart from 'chart.js';
import { lineChartOptions } from '~/lib/utils/chart_utils';
import initProjectPipelinesChartsApp from '~/projects/pipelines/charts/index'; import initProjectPipelinesChartsApp from '~/projects/pipelines/charts/index';
const SUCCESS_LINE_COLOR = '#1aaa55';
const TOTAL_LINE_COLOR = '#707070';
const buildChart = (chartScope, shouldAdjustFontSize) => {
const data = {
labels: chartScope.labels,
datasets: [
{
backgroundColor: SUCCESS_LINE_COLOR,
borderColor: SUCCESS_LINE_COLOR,
pointBackgroundColor: SUCCESS_LINE_COLOR,
pointBorderColor: '#fff',
data: chartScope.successValues,
fill: 'origin',
},
{
backgroundColor: TOTAL_LINE_COLOR,
borderColor: TOTAL_LINE_COLOR,
pointBackgroundColor: TOTAL_LINE_COLOR,
pointBorderColor: '#EEE',
data: chartScope.totalValues,
fill: '-1',
},
],
};
const ctx = $(`#${chartScope.scope}Chart`)
.get(0)
.getContext('2d');
return new Chart(ctx, {
type: 'line',
data,
options: lineChartOptions({
width: ctx.canvas.width,
numberOfPoints: chartScope.totalValues.length,
shouldAdjustFontSize,
}),
});
};
document.addEventListener('DOMContentLoaded', () => {
const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML);
// Scale fonts if window width lower than 768px (iPad portrait)
const shouldAdjustFontSize = window.innerWidth < 768;
chartsData.forEach(scope => buildChart(scope, shouldAdjustFontSize));
});
document.addEventListener('DOMContentLoaded', initProjectPipelinesChartsApp); document.addEventListener('DOMContentLoaded', initProjectPipelinesChartsApp);
<script> <script>
import dateFormat from 'dateformat';
import { __, sprintf } from '~/locale';
import { GlColumnChart } from '@gitlab/ui/dist/charts'; import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import StatisticsList from './statistics_list.vue'; import StatisticsList from './statistics_list.vue';
import PipelinesAreaChart from './pipelines_area_chart.vue';
import { import {
CHART_CONTAINER_HEIGHT, CHART_CONTAINER_HEIGHT,
INNER_CHART_HEIGHT, INNER_CHART_HEIGHT,
X_AXIS_LABEL_ROTATION, X_AXIS_LABEL_ROTATION,
X_AXIS_TITLE_OFFSET, X_AXIS_TITLE_OFFSET,
CHART_DATE_FORMAT,
ONE_WEEK_AGO_DAYS,
ONE_MONTH_AGO_DAYS,
} from '../constants'; } from '../constants';
export default { export default {
components: { components: {
StatisticsList, StatisticsList,
GlColumnChart, GlColumnChart,
PipelinesAreaChart,
}, },
props: { props: {
counts: { counts: {
...@@ -22,6 +30,18 @@ export default { ...@@ -22,6 +30,18 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
lastWeekChartData: {
type: Object,
required: true,
},
lastMonthChartData: {
type: Object,
required: true,
},
lastYearChartData: {
type: Object,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -30,10 +50,38 @@ export default { ...@@ -30,10 +50,38 @@ export default {
}, },
}; };
}, },
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: { methods: {
mergeLabelsAndValues(labels, values) { mergeLabelsAndValues(labels, values) {
return labels.map((label, index) => [label, values[index]]); 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, chartContainerHeight: CHART_CONTAINER_HEIGHT,
timesChartOptions: { timesChartOptions: {
...@@ -45,6 +93,22 @@ export default { ...@@ -45,6 +93,22 @@ export default {
nameGap: X_AXIS_TITLE_OFFSET, 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> </script>
<template> <template>
...@@ -68,5 +132,14 @@ export default { ...@@ -68,5 +132,14 @@ export default {
/> />
</div> </div>
</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> </div>
</template> </template>
<script>
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { s__ } from '~/locale';
import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
import { CHART_CONTAINER_HEIGHT } from '../constants';
export default {
components: {
GlAreaChart,
ResizableChartContainer,
},
props: {
chartData: {
type: Array,
required: true,
},
},
areaChartOptions: {
xAxis: {
name: s__('Pipeline|Date'),
type: 'category',
},
yAxis: {
name: s__('Pipeline|Pipelines'),
},
},
chartContainerHeight: CHART_CONTAINER_HEIGHT,
};
</script>
<template>
<div class="prepend-top-default">
<p>
<slot></slot>
</p>
<resizable-chart-container>
<gl-area-chart
slot-scope="{ width }"
:width="width"
:height="$options.chartContainerHeight"
:data="chartData"
:include-legend-avg-max="false"
:option="$options.areaChartOptions"
/>
</resizable-chart-container>
</div>
</template>
...@@ -5,3 +5,9 @@ export const INNER_CHART_HEIGHT = 200; ...@@ -5,3 +5,9 @@ export const INNER_CHART_HEIGHT = 200;
export const X_AXIS_LABEL_ROTATION = 45; export const X_AXIS_LABEL_ROTATION = 45;
export const X_AXIS_TITLE_OFFSET = 60; export const X_AXIS_TITLE_OFFSET = 60;
export const ONE_WEEK_AGO_DAYS = 7;
export const ONE_MONTH_AGO_DAYS = 31;
export const CHART_DATE_FORMAT = 'dd mmm';
...@@ -10,8 +10,23 @@ export default () => { ...@@ -10,8 +10,23 @@ export default () => {
successRatio, successRatio,
timesChartLabels, timesChartLabels,
timesChartValues, timesChartValues,
lastWeekChartLabels,
lastWeekChartTotals,
lastWeekChartSuccess,
lastMonthChartLabels,
lastMonthChartTotals,
lastMonthChartSuccess,
lastYearChartLabels,
lastYearChartTotals,
lastYearChartSuccess,
} = el.dataset; } = el.dataset;
const parseAreaChartData = (labels, totals, success) => ({
labels: JSON.parse(labels),
totals: JSON.parse(totals),
success: JSON.parse(success),
});
return new Vue({ return new Vue({
el, el,
name: 'ProjectPipelinesChartsApp', name: 'ProjectPipelinesChartsApp',
...@@ -31,6 +46,21 @@ export default () => { ...@@ -31,6 +46,21 @@ export default () => {
labels: JSON.parse(timesChartLabels), labels: JSON.parse(timesChartLabels),
values: JSON.parse(timesChartValues), values: JSON.parse(timesChartValues),
}, },
lastWeekChartData: parseAreaChartData(
lastWeekChartLabels,
lastWeekChartTotals,
lastWeekChartSuccess,
),
lastMonthChartData: parseAreaChartData(
lastMonthChartLabels,
lastMonthChartTotals,
lastMonthChartSuccess,
),
lastYearChartData: parseAreaChartData(
lastYearChartLabels,
lastYearChartTotals,
lastYearChartSuccess,
),
}, },
}), }),
}); });
......
- page_title _('CI / CD Charts') - page_title _('CI / CD Charts')
#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 } } } #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 },
#charts.ci-charts last_week_chart: { labels: @charts[:week].labels, totals: @charts[:week].total, success: @charts[:week].success },
%hr last_month_chart: { labels: @charts[:month].labels, totals: @charts[:month].total, success: @charts[:month].success },
= render 'projects/pipelines/charts/pipelines' last_year_chart: { labels: @charts[:year].labels, totals: @charts[:year].total, success: @charts[:year].success } } }
%h4.mt-4.mb-4= _("Pipelines charts")
%p
&nbsp;
%span.legend-success
= icon("circle")
= s_("Pipeline|success")
&nbsp;
%span.legend-all
= icon("circle")
= s_("Pipeline|all")
.prepend-top-default
%p.light
= _("Pipelines for last week")
(#{date_from_to(Date.today - 7.days, Date.today)})
%div
%canvas#weekChart{ height: 200 }
.prepend-top-default
%p.light
= _("Pipelines for last month")
(#{date_from_to(Date.today - 30.days, Date.today)})
%div
%canvas#monthChart{ height: 200 }
.prepend-top-default
%p.light
= _("Pipelines for last year")
%div
%canvas#yearChart.padded{ height: 250 }
-# haml-lint:disable InlineJavaScript
%script#pipelinesChartsData{ type: "application/json" }
- chartData = []
- [:week, :month, :year].each do |scope|
- chartData.push({ 'scope' => scope, 'labels' => @charts[scope].labels, 'totalValues' => @charts[scope].total, 'successValues' => @charts[scope].success })
= chartData.to_json.html_safe
---
title: Migrate CI CD pipelines charts to ECharts
merge_request: 24057
author:
type: changed
...@@ -13672,10 +13672,10 @@ msgstr "" ...@@ -13672,10 +13672,10 @@ msgstr ""
msgid "Pipelines emails" msgid "Pipelines emails"
msgstr "" msgstr ""
msgid "Pipelines for last month" msgid "Pipelines for last month (%{oneMonthAgo} - %{today})"
msgstr "" msgstr ""
msgid "Pipelines for last week" msgid "Pipelines for last week (%{oneWeekAgo} - %{today})"
msgstr "" msgstr ""
msgid "Pipelines for last year" msgid "Pipelines for last year"
...@@ -13759,6 +13759,9 @@ msgstr "" ...@@ -13759,6 +13759,9 @@ msgstr ""
msgid "Pipeline|Coverage" msgid "Pipeline|Coverage"
msgstr "" msgstr ""
msgid "Pipeline|Date"
msgstr ""
msgid "Pipeline|Detached merge request pipeline" msgid "Pipeline|Detached merge request pipeline"
msgstr "" msgstr ""
...@@ -13780,6 +13783,9 @@ msgstr "" ...@@ -13780,6 +13783,9 @@ msgstr ""
msgid "Pipeline|Pipeline" msgid "Pipeline|Pipeline"
msgstr "" msgstr ""
msgid "Pipeline|Pipelines"
msgstr ""
msgid "Pipeline|Run Pipeline" msgid "Pipeline|Run Pipeline"
msgstr "" msgstr ""
...@@ -13816,18 +13822,12 @@ msgstr "" ...@@ -13816,18 +13822,12 @@ msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}." msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
msgstr "" msgstr ""
msgid "Pipeline|all"
msgstr ""
msgid "Pipeline|for" msgid "Pipeline|for"
msgstr "" msgstr ""
msgid "Pipeline|on" msgid "Pipeline|on"
msgstr "" msgstr ""
msgid "Pipeline|success"
msgstr ""
msgid "Pipeline|with stage" msgid "Pipeline|with stage"
msgstr "" msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelinesAreaChart matches the snapshot 1`] = `
<div
class="prepend-top-default"
>
<p>
Some title
</p>
<div>
<glareachart-stub
data="[object Object],[object Object]"
height="300"
legendaveragetext="Avg"
legendmaxtext="Max"
option="[object Object]"
thresholds=""
width="0"
/>
</div>
</div>
`;
...@@ -2,7 +2,14 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,7 +2,14 @@ import { shallowMount } from '@vue/test-utils';
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 { counts, timesChartData } from '../mock_data'; 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', () => { describe('ProjectsPipelinesChartsApp', () => {
let wrapper; let wrapper;
...@@ -12,6 +19,9 @@ describe('ProjectsPipelinesChartsApp', () => { ...@@ -12,6 +19,9 @@ describe('ProjectsPipelinesChartsApp', () => {
propsData: { propsData: {
counts, counts,
timesChartData, timesChartData,
lastWeekChartData,
lastMonthChartData,
lastYearChartData,
}, },
}); });
}); });
...@@ -39,4 +49,24 @@ describe('ProjectsPipelinesChartsApp', () => { ...@@ -39,4 +49,24 @@ describe('ProjectsPipelinesChartsApp', () => {
expect(chart.props('option')).toBe(wrapper.vm.$options.timesChartOptions); 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 { mount } from '@vue/test-utils';
import Component from '~/projects/pipelines/charts/components/pipelines_area_chart.vue';
import { transformedAreaChartData } from '../mock_data';
describe('PipelinesAreaChart', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(Component, {
propsData: {
chartData: transformedAreaChartData,
},
slots: {
default: 'Some title',
},
stubs: {
GlAreaChart: true,
},
});
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
...@@ -9,3 +9,25 @@ export const timesChartData = { ...@@ -9,3 +9,25 @@ export const timesChartData = {
labels: ['as1234', 'kh423hy', 'ji56bvg', 'th23po'], labels: ['as1234', 'kh423hy', 'ji56bvg', 'th23po'],
values: [5, 3, 7, 4], values: [5, 3, 7, 4],
}; };
export const areaChartData = {
labels: ['01 Jan', '02 Jan', '03 Jan', '04 Jan', '05 Jan'],
totals: [4, 6, 3, 6, 7],
success: [3, 5, 3, 3, 5],
};
export const lastYearChartData = {
...areaChartData,
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
};
export const transformedAreaChartData = [
{
name: 'all',
data: [['01 Jan', 4], ['02 Jan', 6], ['03 Jan', 3], ['04 Jan', 6], ['05 Jan', 7]],
},
{
name: 'success',
data: [['01 Jan', 3], ['02 Jan', 3], ['03 Jan', 3], ['04 Jan', 3], ['05 Jan', 5]],
},
];
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