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

Migrate CI CD last week chart

The chart is being migrated to make use of ECharts. This is
in order to bring the chart in line with our design system.
parent 7e3f8440
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,23 @@ export default { ...@@ -45,6 +93,23 @@ export default {
nameGap: X_AXIS_TITLE_OFFSET, nameGap: X_AXIS_TITLE_OFFSET,
}, },
}, },
today: dateFormat(new Date(), CHART_DATE_FORMAT),
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 +133,14 @@ export default { ...@@ -68,5 +133,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 { __ } from '~/locale';
import { CHART_CONTAINER_HEIGHT } from '../constants';
export default {
components: {
GlAreaChart,
},
props: {
chartData: {
type: Array,
required: true,
},
},
areaChartOptions: {
xAxis: {
name: __('Date'),
type: 'category',
},
yAxis: {
name: __('Pipelines'),
},
},
chartContainerHeight: CHART_CONTAINER_HEIGHT,
};
</script>
<template>
<div class="prepend-top-default">
<p>
<slot></slot>
</p>
<div>
<gl-area-chart
:height="$options.chartContainerHeight"
:data="chartData"
:include-legend-avg-max="false"
:option="$options.areaChartOptions"
/>
</div>
</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
...@@ -13654,10 +13654,10 @@ msgstr "" ...@@ -13654,10 +13654,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"
...@@ -13798,18 +13798,12 @@ msgstr "" ...@@ -13798,18 +13798,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>
<gl-area-chart-stub
data="[object Object],[object Object]"
height="300"
legendaveragetext="Avg"
legendmaxtext="Max"
option="[object Object]"
thresholds=""
/>
</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++) {
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 Component from '~/projects/pipelines/charts/components/pipelines_area_chart.vue';
import { transformedAreaChartData } from '../mock_data';
describe('PipelinesAreaChart', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(Component, {
propsData: {
chartData: transformedAreaChartData,
},
slots: {
default: 'Some title',
},
});
});
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