Commit 71a6cbe6 authored by Robert Hunt's avatar Robert Hunt Committed by Kushal Pandya

Move insights charts to echarts

First I've deleted all the old charts JS and started referencing
GlCharts instead.

I've also created a new insights_chart.vue file to separate the
page from the individual charts themselves.

Replaced, created or updated tests.

Added a changelog entry
parent 81d4fc12
---
title: Move insights charts to echarts
merge_request: 24661
author:
type: other
...@@ -96,7 +96,7 @@ The following table lists available parameters for charts: ...@@ -96,7 +96,7 @@ The following table lists available parameters for charts:
| Keyword | Description | | Keyword | Description |
|:---------------------------------------------------|:------------| |:---------------------------------------------------|:------------|
| [`title`](#title) | The title of the chart. This will displayed on the Insights page. | | [`title`](#title) | The title of the chart. This will displayed on the Insights page. |
| [`type`](#type) | The type of chart: `bar`, `line`, `stacked-bar`, `pie` etc. | | [`type`](#type) | The type of chart: `bar`, `line` or `stacked-bar`. |
| [`query`](#query) | A hash that defines the conditions for issues / merge requests to be part of the chart. | | [`query`](#query) | A hash that defines the conditions for issues / merge requests to be part of the chart. |
## Parameter details ## Parameter details
...@@ -132,7 +132,6 @@ Supported values are: ...@@ -132,7 +132,6 @@ Supported values are:
| ----- | ------- | | ----- | ------- |
| `bar` | ![Insights example bar chart](img/insights_example_bar_chart.png) | | `bar` | ![Insights example bar chart](img/insights_example_bar_chart.png) |
| `bar` (time series, i.e. when `group_by` is used) | ![Insights example bar time series chart](img/insights_example_bar_time_series_chart.png) | | `bar` (time series, i.e. when `group_by` is used) | ![Insights example bar time series chart](img/insights_example_bar_time_series_chart.png) |
| `pie` | ![Insights example pie chart](img/insights_example_pie_chart.png) |
| `line` | ![Insights example stacked bar chart](img/insights_example_line_chart.png) | | `line` | ![Insights example stacked bar chart](img/insights_example_line_chart.png) |
| `stacked-bar` | ![Insights example stacked bar chart](img/insights_example_stacked_bar_chart.png) | | `stacked-bar` | ![Insights example stacked bar chart](img/insights_example_stacked_bar_chart.png) |
......
<script>
import BaseChart from './insights_chart.vue';
import * as chartOptions from '~/lib/utils/chart_utils';
export default {
extends: BaseChart,
computed: {
config() {
return {
type: 'bar',
data: this.data,
options: {
...chartOptions.barChartOptions(),
...this.title(),
...this.commonOptions(),
},
};
},
},
};
</script>
<template>
<div class="chart-canvas-wrapper">
<canvas ref="insightsChart" class="bar" height="300"></canvas>
</div>
</template>
<script>
import Chart from 'chart.js';
export default {
props: {
chartTitle: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
},
computed: {
config() {
return {};
},
},
mounted() {
this.drawChart();
},
methods: {
title() {
return {
title: {
display: true,
text: this.chartTitle,
},
};
},
commonOptions() {
return {
responsive: true,
maintainAspectRatio: false,
legend: false,
};
},
drawChart() {
const ctx = this.$refs.insightsChart.getContext('2d');
return new Chart(ctx, this.config);
},
},
};
</script>
<template>
<div class="chart-canvas-wrapper">
<canvas ref="insightsChart" height="300"></canvas>
</div>
</template>
<script>
import BaseChart from './insights_chart.vue';
export default {
extends: BaseChart,
computed: {
config() {
return {
type: 'line',
data: this.data,
options: {
...this.title(),
...this.commonOptions(),
...this.elements(),
...this.scales(),
},
};
},
},
methods: {
elements() {
return {
elements: {
line: {
tension: 0,
fill: false,
},
},
};
},
scales() {
return {
scales: {
yAxes: [
{
ticks: {
beginAtZero: true,
},
},
],
},
};
},
},
};
</script>
<template>
<div class="chart-canvas-wrapper">
<canvas ref="insightsChart" class="line" height="300"></canvas>
</div>
</template>
<script>
import BaseChart from './insights_chart.vue';
import * as chartOptions from '~/lib/utils/chart_utils';
export default {
extends: BaseChart,
computed: {
config() {
return {
type: 'pie',
data: this.data,
options: {
...chartOptions.pieChartOptions(),
...this.title(),
...this.commonOptions(),
},
};
},
},
};
</script>
<template>
<div class="chart-canvas-wrapper">
<canvas ref="insightsChart" class="pie" height="180"></canvas>
</div>
</template>
<script>
import BaseChart from './insights_chart.vue';
import * as chartOptions from '~/lib/utils/chart_utils';
export default {
extends: BaseChart,
computed: {
config() {
return {
type: 'bar',
data: this.data,
options: {
...chartOptions.barChartOptions(),
...this.title(),
...this.commonOptions(),
...this.tooltips(),
...this.scales(),
},
};
},
},
methods: {
tooltips() {
return {
tooltips: {
mode: 'index',
},
};
},
scales() {
return {
scales: {
xAxes: [
{
stacked: true,
},
],
yAxes: [
{
stacked: true,
ticks: {
beginAtZero: true,
},
},
],
},
};
},
},
};
</script>
<template>
<div class="chart-canvas-wrapper">
<canvas ref="insightsChart" class="stacked-bar" height="300"></canvas>
</div>
</template>
<script>
import { GlColumnChart, GlLineChart, GlStackedColumnChart } from '@gitlab/ui/dist/charts';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
import InsightsChartError from './insights_chart_error.vue';
import { CHART_TYPES } from '../constants';
const CHART_HEIGHT = 300;
export default {
components: {
GlColumnChart,
GlLineChart,
GlStackedColumnChart,
ResizableChartContainer,
InsightsChartError,
},
props: {
loaded: {
type: Boolean,
required: true,
},
type: {
type: String,
required: true,
},
title: {
type: String,
required: false,
default: '',
},
data: {
type: Object,
required: true,
},
error: {
type: String,
required: false,
default: '',
},
},
data() {
return {
svgs: {},
};
},
computed: {
dataZoomConfig() {
const handleIcon = this.svgs['scroll-handle'];
return handleIcon ? { handleIcon } : {};
},
chartOptions() {
let options = {
yAxis: {
minInterval: 1,
},
};
if (this.type === this.$options.chartTypes.LINE) {
options = {
...options,
xAxis: {
...options.xAxis,
name: this.data.xAxisTitle,
type: 'category',
},
yAxis: {
...options.yAxis,
name: this.data.yAxisTitle,
type: 'value',
},
};
}
return { dataZoom: [this.dataZoomConfig], ...options };
},
isColumnChart() {
return [this.$options.chartTypes.BAR, this.$options.chartTypes.PIE].includes(this.type);
},
isStackedColumnChart() {
return this.type === this.$options.chartTypes.STACKED_BAR;
},
isLineChart() {
return this.type === this.$options.chartTypes.LINE;
},
},
methods: {
setSvg(name) {
return getSvgIconPathContent(name)
.then(path => {
if (path) {
this.$set(this.svgs, name, `path://${path}`);
}
})
.catch(e => {
// eslint-disable-next-line no-console, @gitlab/i18n/no-non-i18n-strings
console.error('SVG could not be rendered correctly: ', e);
});
},
onChartCreated() {
this.setSvg('scroll-handle');
},
},
height: CHART_HEIGHT,
chartTypes: CHART_TYPES,
};
</script>
<template>
<resizable-chart-container v-if="loaded" class="insights-chart">
<h5 class="text-center">{{ title }}</h5>
<gl-column-chart
v-if="isColumnChart"
v-bind="$attrs"
:height="$options.height"
:data="data.datasets"
x-axis-type="category"
:x-axis-title="data.xAxisTitle"
:y-axis-title="data.yAxisTitle"
:option="chartOptions"
@created="onChartCreated"
/>
<gl-stacked-column-chart
v-else-if="isStackedColumnChart"
v-bind="$attrs"
:height="$options.height"
:data="data.datasets"
:group-by="data.labels"
:series-names="data.seriesNames"
x-axis-type="category"
:x-axis-title="data.xAxisTitle"
:y-axis-title="data.yAxisTitle"
:option="chartOptions"
@created="onChartCreated"
/>
<gl-line-chart
v-else-if="isLineChart"
v-bind="$attrs"
:height="$options.height"
:data="data.datasets"
:option="chartOptions"
@created="onChartCreated"
/>
</resizable-chart-container>
<div v-else class="insights-chart">
<insights-chart-error
:chart-name="title"
:title="__('This chart could not be displayed')"
:summary="__('Please check the configuration file for this chart')"
:error="error"
/>
</div>
</template>
<script> <script>
import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { isUndefined } from 'lodash';
import { mapActions, mapState } from 'vuex';
import InsightsChartError from './insights_chart_error.vue';
import InsightsConfigWarning from './insights_config_warning.vue'; import InsightsConfigWarning from './insights_config_warning.vue';
import InsightsChart from './insights_chart.vue';
import Bar from './chart_js/bar.vue';
import LineChart from './chart_js/line.vue';
import Pie from './chart_js/pie.vue';
import StackedBar from './chart_js/stacked_bar.vue';
export default { export default {
components: { components: {
GlLoadingIcon, GlLoadingIcon,
InsightsChartError, InsightsChart,
InsightsConfigWarning, InsightsConfigWarning,
Bar,
LineChart,
Pie,
StackedBar,
}, },
props: { props: {
queryEndpoint: { queryEndpoint: {
...@@ -44,7 +34,7 @@ export default { ...@@ -44,7 +34,7 @@ export default {
return Object.keys(this.chartData); return Object.keys(this.chartData);
}, },
hasChartsConfigured() { hasChartsConfigured() {
return !_.isUndefined(this.charts) && this.charts.length > 0; return !isUndefined(this.charts) && this.charts.length > 0;
}, },
}, },
watch: { watch: {
...@@ -58,15 +48,6 @@ export default { ...@@ -58,15 +48,6 @@ export default {
}, },
methods: { methods: {
...mapActions('insights', ['fetchChartData', 'initChartData', 'setPageLoading']), ...mapActions('insights', ['fetchChartData', 'initChartData', 'setPageLoading']),
chartType(type) {
switch (type) {
case 'line':
// Apparently Line clashes with another component
return 'line-chart';
default:
return type;
}
},
fetchCharts() { fetchCharts() {
if (this.hasChartsConfigured) { if (this.hasChartsConfigured) {
this.initChartData(this.chartKeys); this.initChartData(this.chartKeys);
...@@ -94,21 +75,15 @@ export default { ...@@ -94,21 +75,15 @@ export default {
<div v-if="hasChartsConfigured" class="js-insights-page-container"> <div v-if="hasChartsConfigured" class="js-insights-page-container">
<h4 class="text-center">{{ pageConfig.title }}</h4> <h4 class="text-center">{{ pageConfig.title }}</h4>
<div v-if="!pageLoading" class="insights-charts" data-qa-selector="insights_charts"> <div v-if="!pageLoading" class="insights-charts" data-qa-selector="insights_charts">
<div v-for="(insights, key, index) in chartData" :key="index" class="insights-chart"> <insights-chart
<component v-for="({ loaded, type, data, error }, key, index) in chartData"
:is="chartType(insights.type)" :key="index"
v-if="insights.loaded" :loaded="loaded"
:chart-title="key" :type="type"
:data="insights.data" :title="key"
/> :data="data"
<insights-chart-error :error="error"
v-else />
:chart-name="key"
:title="__('This chart could not be displayed')"
:summary="__('Please check the configuration file for this chart')"
:error="insights.error"
/>
</div>
</div> </div>
<div v-else class="insights-chart-loading text-center"> <div v-else class="insights-chart-loading text-center">
<gl-loading-icon :inline="true" size="lg" /> <gl-loading-icon :inline="true" size="lg" />
......
export const CHART_TYPES = {
BAR: 'bar',
LINE: 'line',
STACKED_BAR: 'stacked-bar',
// Only used to convert to bar
PIE: 'pie',
};
export default { CHART_TYPES };
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import * as types from './mutation_types';
export const requestConfig = ({ commit }) => commit(types.REQUEST_CONFIG); export const requestConfig = ({ commit }) => commit(types.REQUEST_CONFIG);
export const receiveConfigSuccess = ({ commit }, data) => export const receiveConfigSuccess = ({ commit }, data) =>
commit(types.RECEIVE_CONFIG_SUCCESS, data); commit(types.RECEIVE_CONFIG_SUCCESS, data);
...@@ -38,7 +39,12 @@ export const receiveChartDataError = ({ commit }, { chart, error }) => ...@@ -38,7 +39,12 @@ export const receiveChartDataError = ({ commit }, { chart, error }) =>
export const fetchChartData = ({ dispatch }, { endpoint, chart }) => export const fetchChartData = ({ dispatch }, { endpoint, chart }) =>
axios axios
.post(endpoint, chart) .post(endpoint, chart)
.then(({ data }) => dispatch('receiveChartDataSuccess', { chart, data })) .then(({ data }) =>
dispatch('receiveChartDataSuccess', {
chart,
data,
}),
)
.catch(error => { .catch(error => {
let message = `${__('There was an error gathering the chart data')}`; let message = `${__('There was an error gathering the chart data')}`;
......
import { __ } from '~/locale';
import { CHART_TYPES } from 'ee/insights/constants';
const getAxisTitle = label => {
switch (label) {
case 'day':
return __('Days');
case 'week':
return __('Weeks');
case 'month':
return __('Months');
case 'issue':
return __('Issues');
case 'merge_request':
return __('Merge requests');
default:
return '';
}
};
export const transformChartDataForGlCharts = (
{ type, query: { group_by, issuable_type } },
{ labels, datasets },
) => {
const formattedData = {
xAxisTitle: getAxisTitle(group_by),
yAxisTitle: getAxisTitle(issuable_type),
labels,
datasets: [],
seriesNames: [],
};
switch (type) {
case CHART_TYPES.STACKED_BAR:
formattedData.datasets = datasets.map(dataset => dataset.data);
formattedData.seriesNames = datasets.map(dataset => dataset.label);
break;
case CHART_TYPES.LINE:
formattedData.datasets.push(
...datasets.map(dataset => ({
name: dataset.label,
data: labels.map((label, i) => [label, dataset.data[i]]),
})),
);
break;
default:
formattedData.datasets = { all: labels.map((label, i) => [label, datasets[0].data[i]]) };
}
return formattedData;
};
export default {
transformChartDataForGlCharts,
};
import _ from 'underscore'; import { pick } from 'lodash';
import { transformChartDataForGlCharts } from './helpers';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
...@@ -7,12 +9,10 @@ export default { ...@@ -7,12 +9,10 @@ export default {
state.configLoading = true; state.configLoading = true;
}, },
[types.RECEIVE_CONFIG_SUCCESS](state, data) { [types.RECEIVE_CONFIG_SUCCESS](state, data) {
const validConfig = _.pick( state.configData = pick(
data, data,
Object.keys(data).filter(key => data[key].title && data[key].charts), Object.keys(data).filter(key => data[key].title && data[key].charts),
); );
state.configData = validConfig;
state.configLoading = false; state.configLoading = false;
}, },
[types.RECEIVE_CONFIG_ERROR](state) { [types.RECEIVE_CONFIG_ERROR](state) {
...@@ -25,7 +25,7 @@ export default { ...@@ -25,7 +25,7 @@ export default {
state.chartData[chart.title] = { state.chartData[chart.title] = {
type, type,
data, data: transformChartDataForGlCharts(chart, data),
loaded: true, loaded: true,
}; };
}, },
...@@ -34,7 +34,7 @@ export default { ...@@ -34,7 +34,7 @@ export default {
state.chartData[chart.title] = { state.chartData[chart.title] = {
type, type,
data: null, data: {},
loaded: false, loaded: false,
error, error,
}; };
......
import { CHART_TYPES } from 'ee/insights/constants';
import { transformChartDataForGlCharts } from 'ee/insights/stores/modules/insights/helpers';
describe('Insights helpers', () => {
describe('transformChartDataForGlCharts', () => {
it('sets the x axis label to "Months"', () => {
const chart = {
type: CHART_TYPES.BAR,
query: { group_by: 'month', issuable_type: 'issue' },
};
const data = {
labels: ['January', 'February'],
datasets: [{ label: 'Dataset 1', data: [1] }, { label: 'Dataset 2', data: [2] }],
};
expect(transformChartDataForGlCharts(chart, data).xAxisTitle).toEqual('Months');
});
it('sets the y axis label to "Issues"', () => {
const chart = {
type: CHART_TYPES.BAR,
query: { group_by: 'month', issuable_type: 'issue' },
};
const data = {
labels: ['January', 'February'],
datasets: [{ label: 'Dataset 1', data: [1] }, { label: 'Dataset 2', data: [2] }],
};
expect(transformChartDataForGlCharts(chart, data).yAxisTitle).toEqual('Issues');
});
it('copies the data to the datasets for stacked bar charts', () => {
const chart = {
type: CHART_TYPES.STACKED_BAR,
query: { group_by: 'month', issuable_type: 'issue' },
};
const data = {
labels: ['January', 'February'],
datasets: [{ label: 'Dataset 1', data: [1] }, { label: 'Dataset 2', data: [2] }],
};
expect(transformChartDataForGlCharts(chart, data).datasets).toEqual([[1], [2]]);
});
it('copies the dataset labels to seriesNames for stacked bar charts', () => {
const chart = {
type: CHART_TYPES.STACKED_BAR,
query: { group_by: 'month', issuable_type: 'issue' },
};
const data = {
labels: ['January', 'February'],
datasets: [{ label: 'Dataset 1', data: [1] }, { label: 'Dataset 2', data: [2] }],
};
expect(transformChartDataForGlCharts(chart, data).seriesNames).toEqual([
'Dataset 1',
'Dataset 2',
]);
});
it('creates an array of objects containing name and data attributes for line charts', () => {
const chart = {
type: CHART_TYPES.LINE,
query: { group_by: 'month', issuable_type: 'issue' },
};
const data = {
labels: ['January', 'February'],
datasets: [{ label: 'Dataset 1', data: [1, 2] }, { label: 'Dataset 2', data: [2, 3] }],
};
expect(transformChartDataForGlCharts(chart, data).datasets).toStrictEqual([
{ name: 'Dataset 1', data: [['January', 1], ['February', 2]] },
{ name: 'Dataset 2', data: [['January', 2], ['February', 3]] },
]);
});
it('creates an object of all containing an array of label / data pairs for bar charts', () => {
const chart = {
type: CHART_TYPES.BAR,
query: { group_by: 'month', issuable_type: 'issue' },
};
const data = {
labels: ['January', 'February'],
datasets: [{ data: [1, 2] }],
};
expect(transformChartDataForGlCharts(chart, data).datasets).toEqual({
all: [['January', 1], ['February', 2]],
});
});
it('creates an object of all containing an array of label / data pairs for pie charts', () => {
const chart = {
type: CHART_TYPES.PIE,
query: { group_by: 'month', issuable_type: 'issue' },
};
const data = {
labels: ['January', 'February'],
datasets: [{ data: [1, 2] }],
};
expect(transformChartDataForGlCharts(chart, data).datasets).toEqual({
all: [['January', 1], ['February', 2]],
});
});
});
});
import createState from 'ee/insights/stores/modules/insights/state'; import createState from 'ee/insights/stores/modules/insights/state';
import mutations from 'ee/insights/stores/modules/insights/mutations'; import mutations from 'ee/insights/stores/modules/insights/mutations';
import * as types from 'ee/insights/stores/modules/insights/mutation_types'; import * as types from 'ee/insights/stores/modules/insights/mutation_types';
import { CHART_TYPES } from 'ee/insights/constants';
import { configData } from '../../../../javascripts/insights/mock_data'; import { configData } from '../../../../javascripts/insights/mock_data';
describe('Insights mutations', () => { describe('Insights mutations', () => {
let state; let state;
const chart = { const chart = {
title: 'Bugs Per Team', title: 'Bugs Per Team',
type: 'stacked-bar', type: CHART_TYPES.STACKED_BAR,
query: { query: {
name: 'filter_issues_by_label_category', name: 'filter_issues_by_label_category',
filter_label: 'bug', filter_label: 'bug',
category_labels: ['Plan', 'Create', 'Manage'], category_labels: ['Plan', 'Create', 'Manage'],
group_by: 'month',
issuable_type: 'issue',
}, },
}; };
...@@ -83,8 +87,8 @@ describe('Insights mutations', () => { ...@@ -83,8 +87,8 @@ describe('Insights mutations', () => {
}); });
describe(types.RECEIVE_CHART_SUCCESS, () => { describe(types.RECEIVE_CHART_SUCCESS, () => {
const data = { const incomingData = {
labels: ['January'], labels: ['January', 'February'],
datasets: [ datasets: [
{ {
label: 'Dataset 1', label: 'Dataset 1',
...@@ -101,24 +105,32 @@ describe('Insights mutations', () => { ...@@ -101,24 +105,32 @@ describe('Insights mutations', () => {
], ],
}; };
const transformedData = {
datasets: [[1], [2]],
labels: ['January', 'February'],
xAxisTitle: 'Months',
yAxisTitle: 'Issues',
seriesNames: ['Dataset 1', 'Dataset 2'],
};
it('sets charts loaded state to true on success', () => { it('sets charts loaded state to true on success', () => {
mutations[types.RECEIVE_CHART_SUCCESS](state, { chart, data }); mutations[types.RECEIVE_CHART_SUCCESS](state, { chart, data: incomingData });
const { chartData } = state; const { chartData } = state;
expect(chartData[chart.title].loaded).toBe(true); expect(chartData[chart.title].loaded).toBe(true);
}); });
it('sets charts data to incoming data on success', () => { it('sets charts data to transformed data on success', () => {
mutations[types.RECEIVE_CHART_SUCCESS](state, { chart, data }); mutations[types.RECEIVE_CHART_SUCCESS](state, { chart, data: incomingData });
const { chartData } = state; const { chartData } = state;
expect(chartData[chart.title].data).toBe(data); expect(chartData[chart.title].data).toStrictEqual(transformedData);
}); });
it('sets charts type to incoming type on success', () => { it('sets charts type to incoming type on success', () => {
mutations[types.RECEIVE_CHART_SUCCESS](state, { chart, data }); mutations[types.RECEIVE_CHART_SUCCESS](state, { chart, data: incomingData });
const { chartData } = state; const { chartData } = state;
...@@ -137,12 +149,12 @@ describe('Insights mutations', () => { ...@@ -137,12 +149,12 @@ describe('Insights mutations', () => {
expect(chartData[chart.title].loaded).toBe(false); expect(chartData[chart.title].loaded).toBe(false);
}); });
it('sets charts data state to null on error', () => { it('sets charts data state to an empty object on error', () => {
mutations[types.RECEIVE_CHART_ERROR](state, { chart, error }); mutations[types.RECEIVE_CHART_ERROR](state, { chart, error });
const { chartData } = state; const { chartData } = state;
expect(chartData[chart.title].data).toBe(null); expect(Object.keys(chartData[chart.title].data).length).toBe(0);
}); });
it('sets charts type to incoming type on error', () => { it('sets charts type to incoming type on error', () => {
......
import Vue from 'vue';
import Chart from 'ee/insights/components/chart_js/bar.vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { chartInfo, chartData } from '../../mock_data';
describe('Insights bar chart component', () => {
let vm;
let mountComponent;
const Component = Vue.extend(Chart);
beforeEach(() => {
mountComponent = data => {
const props = data || {
chartTitle: chartInfo.title,
data: chartData,
};
return mountComponentWithStore(Component, { props });
};
vm = mountComponent();
});
afterEach(() => {
vm.$destroy();
});
it('has the correct config', done => {
expect(vm.config.type).toBe('bar');
expect(vm.config.data).toBe(chartData);
expect(vm.config.options.title.text).toBe(chartInfo.title);
done();
});
});
import Vue from 'vue';
import Chart from 'ee/insights/components/chart_js/line.vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { chartInfo, chartData } from '../../mock_data';
describe('Insights line chart component', () => {
let vm;
let mountComponent;
const Component = Vue.extend(Chart);
beforeEach(() => {
mountComponent = data => {
const props = data || {
chartTitle: chartInfo.title,
data: chartData,
};
return mountComponentWithStore(Component, { props });
};
vm = mountComponent();
});
afterEach(() => {
vm.$destroy();
});
it('has the correct config', done => {
expect(vm.config.type).toBe('line');
expect(vm.config.data).toBe(chartData);
expect(vm.config.options.title.text).toBe(chartInfo.title);
expect(vm.config.options.elements.line.tension).toBe(0);
expect(vm.config.options.elements.line.fill).toBe(false);
done();
});
});
import Vue from 'vue';
import Chart from 'ee/insights/components/chart_js/stacked_bar.vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { chartInfo, chartData } from '../../mock_data';
describe('Insights Stacked Bar chart component', () => {
let vm;
let mountComponent;
const Component = Vue.extend(Chart);
beforeEach(() => {
mountComponent = data => {
const props = data || {
chartTitle: chartInfo.title,
data: chartData,
};
return mountComponentWithStore(Component, { props });
};
vm = mountComponent();
});
afterEach(() => {
vm.$destroy();
});
it('has the correct config', done => {
expect(vm.config.type).toBe('bar');
expect(vm.config.data).toBe(chartData);
expect(vm.config.options.title.text).toBe(chartInfo.title);
expect(vm.config.options.tooltips.mode).toBe('index');
expect(vm.config.options.scales.xAxes[0].stacked).toBe(true);
expect(vm.config.options.scales.yAxes[0].stacked).toBe(true);
done();
});
});
import InsightsChartError from 'ee/insights/components/insights_chart_error.vue';
import { shallowMount } from '@vue/test-utils';
describe('Insights chart error component', () => {
const chartName = 'Test chart';
const title = 'This chart could not be displayed';
const summary = 'Please check the configuration file for this chart';
const error = 'Test error';
let wrapper;
beforeEach(() => {
wrapper = shallowMount(InsightsChartError, {
propsData: { chartName, title, summary, error },
});
});
afterEach(() => {
wrapper.destroy();
});
it('renders the component', () => {
expect(wrapper.find('.content-title').text()).toEqual(`${title}: "${chartName}"`);
const summaries = wrapper.findAll('.content-summary');
expect(summaries.at(0).text()).toEqual(summary);
expect(summaries.at(1).text()).toEqual(error);
});
});
import { GlColumnChart, GlLineChart, GlStackedColumnChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import { chartInfo, barChartData, lineChartData, stackedBarChartData } from '../mock_data';
import InsightsChart from 'ee/insights/components/insights_chart.vue';
import InsightsChartError from 'ee/insights/components/insights_chart_error.vue';
import { CHART_TYPES } from 'ee/insights/constants';
describe('Insights chart component', () => {
let wrapper;
const factory = propsData =>
shallowMount(InsightsChart, {
propsData,
stubs: { 'gl-column-chart': true, 'insights-chart-error': true },
});
afterEach(() => {
wrapper.destroy();
});
describe('when chart is loaded', () => {
it('displays a bar chart', () => {
wrapper = factory({
loaded: true,
type: CHART_TYPES.BAR,
title: chartInfo.title,
data: barChartData,
error: '',
});
expect(wrapper.contains(GlColumnChart)).toBe(true);
});
it('displays a line chart', () => {
wrapper = factory({
loaded: true,
type: CHART_TYPES.LINE,
title: chartInfo.title,
data: lineChartData,
error: '',
});
expect(wrapper.contains(GlLineChart)).toBe(true);
});
it('displays a stacked bar chart', () => {
wrapper = factory({
loaded: true,
type: CHART_TYPES.STACKED_BAR,
title: chartInfo.title,
data: stackedBarChartData,
error: '',
});
expect(wrapper.contains(GlStackedColumnChart)).toBe(true);
});
it('displays a bar chart when a pie chart is requested', () => {
wrapper = factory({
loaded: true,
type: CHART_TYPES.PIE,
title: chartInfo.title,
data: barChartData,
error: '',
});
expect(wrapper.contains(GlColumnChart)).toBe(true);
});
});
describe('when chart receives an error', () => {
const error = 'my error';
beforeEach(() => {
wrapper = factory({
loaded: false,
type: chartInfo.type,
title: chartInfo.title,
data: {},
error,
});
});
it('displays info about the error', () => {
expect(wrapper.contains(InsightsChartError)).toBe(true);
});
});
});
import InsightsConfigWarning from 'ee/insights/components/insights_config_warning.vue';
import { shallowMount } from '@vue/test-utils';
describe('Insights config warning component', () => {
const image = 'illustrations/monitoring/getting_started.svg';
const title = 'There are no charts configured for this page';
const summary =
'Please check the configuration file to ensure that a collection of charts has been declared.';
let wrapper;
beforeEach(() => {
wrapper = shallowMount(InsightsConfigWarning, {
propsData: { image, title, summary },
});
});
afterEach(() => {
wrapper.destroy();
});
it('renders the component', () => {
expect(
wrapper
.findAll('.content-image')
.at(0)
.attributes('src'),
).toContain(image);
expect(wrapper.find('.content-title').text()).toEqual(title);
expect(wrapper.find('.content-summary').text()).toEqual(summary);
});
});
...@@ -2,7 +2,7 @@ import Vue from 'vue'; ...@@ -2,7 +2,7 @@ import Vue from 'vue';
import InsightsPage from 'ee/insights/components/insights_page.vue'; import InsightsPage from 'ee/insights/components/insights_page.vue';
import { createStore } from 'ee/insights/stores'; import { createStore } from 'ee/insights/stores';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { chartInfo, pageInfo, pageInfoNoCharts, chartData } from '../mock_data'; import { chartInfo, pageInfo, pageInfoNoCharts } from '../mock_data';
describe('Insights page component', () => { describe('Insights page component', () => {
let component; let component;
...@@ -77,55 +77,6 @@ describe('Insights page component', () => { ...@@ -77,55 +77,6 @@ describe('Insights page component', () => {
}); });
}); });
describe('when charts loaded', () => {
beforeEach(() => {
component.$store.state.insights.pageLoading = false;
component.$store.state.insights.chartData[chartInfo.title] = {
type: chartInfo.type,
data: chartData,
loaded: true,
};
});
it('displays correct chart post load', done => {
component.$nextTick(() => {
const chartCanvas = component.$el.querySelectorAll(
'.js-insights-page-container .insights-charts .insights-chart canvas',
);
expect(chartCanvas.length).toEqual(1);
expect(chartCanvas[0].classList).toContain('bar');
done();
});
});
});
describe('chart data retrieve error', () => {
const error = 'my error';
beforeEach(() => {
component.$store.state.insights.pageLoading = false;
component.$store.state.insights.chartData[chartInfo.title] = {
type: chartInfo.type,
data: null,
loaded: false,
error,
};
});
it('displays info about the error', done => {
component.$nextTick(() => {
const errorElements = component.$el.querySelectorAll(
'.js-insights-page-container .insights-charts .insights-chart .js-empty-state',
);
expect(errorElements.length).toEqual(1);
expect(errorElements[0].textContent).toContain(error);
done();
});
});
});
describe('pageConfig changes', () => { describe('pageConfig changes', () => {
it('reflects new state', done => { it('reflects new state', done => {
// Establish rendered state // Establish rendered state
......
import { CHART_TYPES } from 'ee/insights/constants';
export const chartInfo = { export const chartInfo = {
title: 'Bugs Per Team', title: 'Bugs Per Team',
type: 'bar', type: CHART_TYPES.BAR,
query: { query: {
name: 'filter_issues_by_label_category', name: 'filter_issues_by_label_category',
filter_label: 'bug', filter_label: 'bug',
...@@ -8,22 +10,37 @@ export const chartInfo = { ...@@ -8,22 +10,37 @@ export const chartInfo = {
}, },
}; };
export const chartData = { export const barChartData = {
labels: ['January'], labels: ['January', 'February'],
datasets: {
all: [['January', 1], ['February', 2]],
},
xAxisTitle: 'Months',
yAxisTitle: 'Issues',
};
export const lineChartData = {
labels: ['January', 'February'],
datasets: [ datasets: [
{ {
label: 'Dataset 1', data: [['January', 1], ['February', 2]],
fill: true, name: 'Alpha',
backgroundColor: ['rgba(255, 99, 132)'],
data: [1],
}, },
{ {
label: 'Dataset 2', data: [['January', 1], ['February', 2]],
fill: true, name: 'Beta',
backgroundColor: ['rgba(54, 162, 235)'],
data: [2],
}, },
], ],
xAxisTitle: 'Months',
yAxisTitle: 'Issues',
};
export const stackedBarChartData = {
labels: ['January', 'February'],
datasets: [[1, 2], [1, 2]],
seriesNames: ['Series 1', 'Series 2'],
xAxisTitle: 'Months',
yAxisTitle: 'Issues',
}; };
export const pageInfo = { export const pageInfo = {
......
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import testAction from 'spec/helpers/vuex_action_helper'; import testAction from 'spec/helpers/vuex_action_helper';
import actionsModule, * as actions from 'ee/insights/stores/modules/insights/actions'; import actionsModule, * as actions from 'ee/insights/stores/modules/insights/actions';
import axios from '~/lib/utils/axios_utils'; import { CHART_TYPES } from 'ee/insights/constants';
const ERROR_MESSAGE = 'TEST_ERROR_MESSAGE'; const ERROR_MESSAGE = 'TEST_ERROR_MESSAGE';
...@@ -9,11 +12,13 @@ describe('Insights store actions', () => { ...@@ -9,11 +12,13 @@ describe('Insights store actions', () => {
const key = 'bugsPerTeam'; const key = 'bugsPerTeam';
const chart = { const chart = {
title: 'Bugs Per Team', title: 'Bugs Per Team',
type: 'stacked-bar', type: CHART_TYPES.STACKED_BAR,
query: { query: {
name: 'filter_issues_by_label_category', name: 'filter_issues_by_label_category',
filter_label: 'bug', filter_label: 'bug',
category_labels: ['Plan', 'Create', 'Manage'], category_labels: ['Plan', 'Create', 'Manage'],
group_by: 'month',
issuable_type: 'issue',
}, },
}; };
const page = { const page = {
...@@ -149,7 +154,7 @@ describe('Insights store actions', () => { ...@@ -149,7 +154,7 @@ describe('Insights store actions', () => {
}); });
describe('receiveChartDataSuccess', () => { describe('receiveChartDataSuccess', () => {
const chartData = { type: 'bar', data: {} }; const chartData = { type: CHART_TYPES.BAR, data: {} };
it('commits RECEIVE_CHART_SUCCESS', done => { it('commits RECEIVE_CHART_SUCCESS', done => {
testAction( testAction(
...@@ -194,7 +199,7 @@ describe('Insights store actions', () => { ...@@ -194,7 +199,7 @@ describe('Insights store actions', () => {
const payload = { endpoint: `${gl.TEST_HOST}/query`, chart }; const payload = { endpoint: `${gl.TEST_HOST}/query`, chart };
const chartData = { const chartData = {
labels: ['January'], labels: ['January', 'February'],
datasets: [ datasets: [
{ {
label: 'Dataset 1', label: 'Dataset 1',
......
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