Commit cfc4068e authored by Brandon Labuschagne's avatar Brandon Labuschagne

Migrate CI CD charts to VueJS

This is the first MR working towards migrating the current
project pipelines charts page to VueJS.

The aim of migrating to VueJS is in order to make use of chart
tools from our design system.
parent bd3f5d88
import $ from 'jquery';
import Chart from 'chart.js';
import { barChartOptions, lineChartOptions } from '~/lib/utils/chart_utils';
import { lineChartOptions } from '~/lib/utils/chart_utils';
import initProjectPipelinesChartsApp from '~/projects/pipelines/charts/index';
const SUCCESS_LINE_COLOR = '#1aaa55';
......@@ -44,40 +46,13 @@ const buildChart = (chartScope, shouldAdjustFontSize) => {
});
};
const buildBarChart = (chartTimesData, shouldAdjustFontSize) => {
const data = {
labels: chartTimesData.labels,
datasets: [
{
backgroundColor: 'rgba(220,220,220,0.5)',
borderColor: 'rgba(220,220,220,1)',
borderWidth: 1,
barValueSpacing: 1,
barDatasetSpacing: 1,
data: chartTimesData.values,
},
],
};
return new Chart(
$('#build_timesChart')
.get(0)
.getContext('2d'),
{
type: 'bar',
data,
options: barChartOptions(shouldAdjustFontSize),
},
);
};
document.addEventListener('DOMContentLoaded', () => {
const chartTimesData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML);
const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML);
// Scale fonts if window width lower than 768px (iPad portrait)
const shouldAdjustFontSize = window.innerWidth < 768;
buildBarChart(chartTimesData, shouldAdjustFontSize);
chartsData.forEach(scope => buildChart(scope, shouldAdjustFontSize));
});
document.addEventListener('DOMContentLoaded', initProjectPipelinesChartsApp);
<script>
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import StatisticsList from './statistics_list.vue';
import {
CHART_CONTAINER_HEIGHT,
INNER_CHART_HEIGHT,
X_AXIS_LABEL_ROTATION,
X_AXIS_TITLE_OFFSET,
} from '../constants';
export default {
components: {
StatisticsList,
GlColumnChart,
},
props: {
counts: {
type: Object,
required: true,
},
timesChartData: {
type: Object,
required: true,
},
},
data() {
return {
timesChartTransformedData: {
full: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values),
},
};
},
methods: {
mergeLabelsAndValues(labels, values) {
return labels.map((label, index) => [label, values[index]]);
},
},
chartContainerHeight: CHART_CONTAINER_HEIGHT,
timesChartOptions: {
height: INNER_CHART_HEIGHT,
xAxis: {
axisLabel: {
rotate: X_AXIS_LABEL_ROTATION,
},
nameGap: X_AXIS_TITLE_OFFSET,
},
},
};
</script>
<template>
<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"
:data="timesChartTransformedData"
:y-axis-title="__('Minutes')"
:x-axis-title="__('Commit')"
x-axis-type="category"
/>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
counts: {
type: Object,
required: true,
},
},
};
</script>
<template>
<ul>
<li>
<span>{{ s__('PipelineCharts|Total:') }}</span>
<strong>{{ n__('1 pipeline', '%d pipelines', counts.total) }}</strong>
</li>
<li>
<span>{{ s__('PipelineCharts|Successful:') }}</span>
<strong>{{ n__('1 pipeline', '%d pipelines', counts.success) }}</strong>
</li>
<li>
<span>{{ s__('PipelineCharts|Failed:') }}</span>
<strong>{{ n__('1 pipeline', '%d pipelines', counts.failed) }}</strong>
</li>
<li>
<span>{{ s__('PipelineCharts|Success ratio:') }}</span>
<strong>{{ counts.successRatio }}%</strong>
</li>
</ul>
</template>
export const CHART_CONTAINER_HEIGHT = 300;
export const INNER_CHART_HEIGHT = 200;
export const X_AXIS_LABEL_ROTATION = 45;
export const X_AXIS_TITLE_OFFSET = 60;
import Vue from 'vue';
import ProjectPipelinesCharts from './components/app.vue';
export default () => {
const el = document.querySelector('#js-project-pipelines-charts-app');
const {
countsFailed,
countsSuccess,
countsTotal,
successRatio,
timesChartLabels,
timesChartValues,
} = el.dataset;
return new Vue({
el,
name: 'ProjectPipelinesChartsApp',
components: {
ProjectPipelinesCharts,
},
render: createElement =>
createElement(ProjectPipelinesCharts, {
props: {
counts: {
failed: countsFailed,
success: countsSuccess,
total: countsTotal,
successRatio,
},
timesChartData: {
labels: JSON.parse(timesChartLabels),
values: JSON.parse(timesChartValues),
},
},
}),
});
};
- 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 } } }
#charts.ci-charts
= render 'projects/pipelines/charts/overall'
%hr
= render 'projects/pipelines/charts/pipelines'
%h4.mt-4.mb-4= s_("PipelineCharts|Overall statistics")
.row
.col-md-6
= render 'projects/pipelines/charts/pipeline_statistics'
.col-md-6
= render 'projects/pipelines/charts/pipeline_times'
%ul
%li
= s_("PipelineCharts|Total:")
%strong= n_("1 pipeline", "%d pipelines", @counts[:total]) % @counts[:total]
%li
= s_("PipelineCharts|Successful:")
%strong= n_("1 pipeline", "%d pipelines", @counts[:success]) % @counts[:success]
%li
= s_("PipelineCharts|Failed:")
%strong= n_("1 pipeline", "%d pipelines", @counts[:failed]) % @counts[:failed]
%li
= s_("PipelineCharts|Success ratio:")
%strong
#{success_ratio(@counts)}%
%p.light
= _("Commit duration in minutes for last 30 commits")
%div
%canvas#build_timesChart{ height: 200 }
-# haml-lint:disable InlineJavaScript
%script#pipelinesTimesChartsData{ type: "application/json" }= { :labels => @charts[:pipeline_times].labels, :values => @charts[:pipeline_times].pipeline_times }.to_json.html_safe
---
title: Migrate CI CD statistics + duration chart to VueJS
merge_request: 23840
author:
type: changed
......@@ -4801,9 +4801,6 @@ msgstr ""
msgid "Commit deleted"
msgstr ""
msgid "Commit duration in minutes for last 30 commits"
msgstr ""
msgid "Commit message"
msgstr ""
......@@ -6813,6 +6810,9 @@ msgstr ""
msgid "Duration"
msgstr ""
msgid "Duration for the last 30 commits"
msgstr ""
msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
msgstr ""
......
......@@ -85,7 +85,7 @@ describe 'Project Graph', :js do
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 'Commit duration in minutes for last 30 commits'
expect(page).to have_content 'Duration for the last 30 commits'
end
end
end
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StatisticsList matches the snapshot 1`] = `
<ul>
<li>
<span>
Total:
</span>
<strong>
4 pipelines
</strong>
</li>
<li>
<span>
Successful:
</span>
<strong>
2 pipelines
</strong>
</li>
<li>
<span>
Failed:
</span>
<strong>
2 pipelines
</strong>
</li>
<li>
<span>
Success ratio:
</span>
<strong>
50%
</strong>
</li>
</ul>
`;
import { shallowMount } from '@vue/test-utils';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import Component from '~/projects/pipelines/charts/components/app';
import StatisticsList from '~/projects/pipelines/charts/components/statistics_list';
import { counts, timesChartData } from '../mock_data';
describe('ProjectsPipelinesChartsApp', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(Component, {
propsData: {
counts,
timesChartData,
},
});
});
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('data')).toBe(wrapper.vm.timesChartTransformedData);
expect(chart.props('option')).toBe(wrapper.vm.$options.timesChartOptions);
});
});
});
import { shallowMount } from '@vue/test-utils';
import Component from '~/projects/pipelines/charts/components/statistics_list';
import { counts } from '../mock_data';
describe('StatisticsList', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(Component, {
propsData: {
counts,
},
});
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
export const counts = {
failed: 2,
success: 2,
total: 4,
successRatio: 50,
};
export const timesChartData = {
labels: ['as1234', 'kh423hy', 'ji56bvg', 'th23po'],
values: [5, 3, 7, 4],
};
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