Commit 10e3746e authored by Mark Fletcher's avatar Mark Fletcher

Add Insights frontend to retrieve and render chart

- When mounted requests config data
- Displays nav tabs for each chart listed in the config file
- Upon changing tab, requests the chart data
- Upon receipt of chart data renders the approriate chart
- Chart JS support for now

Supported chart types:

- Bar
- Stacked Bar
- Line

Chart rendering confined to individual components

- Framework specific chart rendering is contained within the component

Don't store error, display via flash

Move Insights charts to individual components

- Chart component base class
- Remove the chart builder concept
parent 13283beb
......@@ -45,6 +45,8 @@
%span
= _('Contribution Analytics')
= render_if_exists 'layouts/nav/group_insights_link'
= render_if_exists "layouts/nav/ee/epic_link", group: @group
- if group_sidebar_link?(:issues)
......
......@@ -41,6 +41,8 @@
= link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do
%span= _('Cycle Analytics')
= render_if_exists 'layouts/nav/project_insights_link'
- if project_nav_tab? :files
= nav_link(controller: sidebar_repository_paths) do
= link_to project_tree_path(@project), class: 'shortcuts-tree qa-project-menu-repo' do
......
<script>
import BaseChart from './insights_chart.vue';
import * as chartOptions from '~/lib/utils/chart_utils';
export default {
components: {
BaseChart,
},
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: {
info: {
type: Object,
required: true,
},
data: {
type: Object,
required: true,
},
},
computed: {
config() {
return {};
},
},
mounted() {
this.drawChart();
},
methods: {
title() {
return {
title: {
display: true,
text: this.info.title,
},
};
},
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 {
components: {
BaseChart,
},
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 {
components: {
BaseChart,
},
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 { mapActions, mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import StackedBar from './chart_js/stacked_bar.vue';
import Bar from './chart_js/bar.vue';
import LineChart from './chart_js/line.vue';
export default {
components: {
GlLoadingIcon,
NavigationTabs,
StackedBar,
Bar,
LineChart,
},
props: {
endpoint: {
type: String,
required: true,
},
queryEndpoint: {
type: String,
required: true,
},
},
computed: {
...mapState('insights', [
'configData',
'configLoading',
'activeTab',
'activeChart',
'chartData',
'chartLoading',
]),
navigationTabs() {
const { configData, activeTab } = this;
if (!configData) {
return [];
}
if (!activeTab) {
this.setActiveTab(Object.keys(configData)[0]);
}
return Object.keys(configData).map(key => ({
name: configData[key].title,
scope: key,
isActive: this.activeTab === key,
}));
},
chartType() {
switch (this.activeChart.type) {
case 'line':
// Apparently Line clashes with another component
return 'line-chart';
default:
return this.activeChart.type;
}
},
drawChart() {
return this.chartData && this.activeChart && !this.chartLoading;
},
},
watch: {
activeChart() {
this.fetchChartData(this.queryEndpoint);
},
},
mounted() {
this.fetchConfigData(this.endpoint);
},
methods: {
...mapActions('insights', ['fetchConfigData', 'fetchChartData', 'setActiveTab']),
onChangeTab(scope) {
this.setActiveTab(scope);
},
},
};
</script>
<template>
<div class="insights-container">
<div v-if="configLoading" class="insights-config-loading text-center">
<gl-loading-icon :inline="true" :size="4" />
</div>
<div v-else class="insights-wrapper">
<div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
<navigation-tabs :tabs="navigationTabs" @onChangeTab="onChangeTab" />
</div>
<div class="insights-chart">
<div v-if="chartLoading" class="insights-chart-loading text-center">
<gl-loading-icon :inline="true" :size="4" />
</div>
<component :is="chartType" v-if="drawChart" :info="activeChart" :data="chartData" />
</div>
</div>
</div>
</template>
import Vue from 'vue';
import Insights from './components/insights.vue';
import store from './stores';
export default () => {
const el = document.querySelector('#js-insights-pane');
if (!el) return null;
return new Vue({
el,
store,
components: {
Insights,
},
render(createElement) {
return createElement('insights', {
props: {
endpoint: el.dataset.endpoint,
queryEndpoint: el.dataset.queryEndpoint,
},
});
},
});
};
import Vue from 'vue';
import Vuex from 'vuex';
import insights from './modules/insights';
Vue.use(Vuex);
export const createStore = () =>
new Vuex.Store({
modules: {
insights: insights(),
},
});
export default createStore();
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { __ } from '~/locale';
export const requestConfig = ({ commit }) => commit(types.REQUEST_CONFIG);
export const receiveConfigSuccess = ({ commit }, data) =>
commit(types.RECEIVE_CONFIG_SUCCESS, data);
export const receiveConfigError = ({ commit }) => commit(types.RECEIVE_CONFIG_ERROR);
export const fetchConfigData = ({ dispatch }, endpoint) => {
dispatch('requestConfig');
return axios
.get(endpoint)
.then(({ data }) => dispatch('receiveConfigSuccess', data))
.catch(error => {
const message = `${__('There was an error fetching configuration for charts')}: ${
error.response.data.message
}`;
createFlash(message);
dispatch('receiveConfigError');
});
};
export const requestChartData = ({ commit }) => commit(types.REQUEST_CHART);
export const receiveChartDataSuccess = ({ commit }, data) =>
commit(types.RECEIVE_CHART_SUCCESS, data);
export const receiveChartDataError = ({ commit }) => commit(types.RECEIVE_CHART_ERROR);
export const fetchChartData = ({ dispatch, state }, endpoint) => {
const { activeChart } = state;
dispatch('requestChartData');
return axios
.post(endpoint, {
query: activeChart.query,
chart_type: activeChart.type,
})
.then(({ data }) => dispatch('receiveChartDataSuccess', data))
.catch(error => {
const message = `${__('There was an error gathering the chart data')}: ${
error.response.data.message
}`;
createFlash(message);
dispatch('receiveChartDataError');
});
};
export const setActiveTab = ({ commit, state }, key) => {
const { configData } = state;
const chart = configData[key];
commit(types.SET_ACTIVE_TAB, key);
commit(types.SET_ACTIVE_CHART, chart);
};
export default () => {};
import state from './state';
import mutations from './mutations';
import * as actions from './actions';
export default () => ({
namespaced: true,
state: state(),
mutations,
actions,
});
export const REQUEST_CONFIG = 'REQUEST_CONFIG';
export const RECEIVE_CONFIG_SUCCESS = 'RECEIVE_CONFIG_SUCCESS';
export const RECEIVE_CONFIG_ERROR = 'RECEIVE_CONFIG_ERROR';
export const REQUEST_CHART = 'REQUEST_CHART';
export const RECEIVE_CHART_SUCCESS = 'RECEIVE_CHART_SUCCESS';
export const RECEIVE_CHART_ERROR = 'RECEIVE_CHART_ERROR';
export const SET_ACTIVE_TAB = 'SET_ACTIVE_TAB';
export const SET_ACTIVE_CHART = 'SET_ACTIVE_CHART';
import * as types from './mutation_types';
export default {
[types.REQUEST_CONFIG](state) {
state.configData = null;
state.configLoading = true;
},
[types.RECEIVE_CONFIG_SUCCESS](state, data) {
state.configData = data;
state.configLoading = false;
},
[types.RECEIVE_CONFIG_ERROR](state) {
state.configData = null;
state.configLoading = false;
},
[types.REQUEST_CHART](state) {
state.chartData = null;
state.chartLoading = true;
},
[types.RECEIVE_CHART_SUCCESS](state, data) {
state.chartData = data;
state.chartLoading = false;
state.redraw = true;
},
[types.RECEIVE_CHART_ERROR](state) {
state.chartData = null;
state.chartLoading = false;
},
[types.SET_ACTIVE_TAB](state, tab) {
state.activeTab = tab;
},
[types.SET_ACTIVE_CHART](state, chartData) {
state.activeChart = chartData;
},
};
export default () => ({
configData: null,
configLoading: true,
activeTab: null,
activeChart: null,
chartLoading: false,
chartData: null,
});
import initInsights from 'ee/insights';
document.addEventListener('DOMContentLoaded', () => {
initInsights();
});
import initInsights from 'ee/insights';
document.addEventListener('DOMContentLoaded', () => {
initInsights();
});
......@@ -10,17 +10,7 @@ module InsightsActions
def show
respond_to do |format|
# FIXME: This is temporary until we have the frontend
format.html do
insights_config = config_data
if insights_config
first_chart_hash = insights_config.first.last
params.merge!(
chart_type: first_chart_hash[:type],
query: first_chart_hash[:query])
@insights_json = insights_json # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end
format.html
format.json do
render json: config_data
end
......
......@@ -6,7 +6,10 @@ module EE
override :group_overview_nav_link_paths
def group_overview_nav_link_paths
super + %w(groups/security/dashboard#show)
super + %w[
groups/security/dashboard#show
groups/insights#show
]
end
override :group_nav_link_paths
......@@ -41,6 +44,10 @@ module EE
links << :analytics
end
if @group.insights_available?
links << :group_insights
end
links
end
end
......
......@@ -6,7 +6,10 @@ module EE
override :sidebar_projects_paths
def sidebar_projects_paths
super + %w(projects/security/dashboard#show)
super + %w[
projects/security/dashboard#show
projects/insights#show
]
end
override :sidebar_settings_paths
......@@ -44,6 +47,10 @@ module EE
nav_tabs << :operations
end
if project.insights_available?
nav_tabs << :project_insights
end
nav_tabs
end
......
- page_title _('Insights')
%h2 Insights chart data
%pre
%code= @insights_json
%div{ class: container_class }
%h2 Insights chart data
#js-insights-pane{ data: { endpoint: endpoint, query_endpoint: query_endpoint } }
---
title: Add Insights frontend to retrieve and render chart
merge_request: 9856
author:
type: added
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 || {
info: chartInfo,
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 || {
info: chartInfo,
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 || {
info: chartInfo,
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 Vue from 'vue';
import Insights from 'ee/insights/components/insights.vue';
import { createStore } from 'ee/insights/stores';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
describe('Insights component', () => {
let vm;
let store;
let mountComponent;
const Component = Vue.extend(Insights);
beforeEach(() => {
store = createStore();
spyOn(store, 'dispatch').and.stub();
mountComponent = data => {
const props = data || {
endpoint: gl.TEST_HOST,
queryEndpoint: `${gl.TEST_HOST}/query`,
};
return mountComponentWithStore(Component, { store, props });
};
vm = mountComponent();
});
afterEach(() => {
vm.$destroy();
});
it('fetches config data when mounted', done => {
expect(store.dispatch).toHaveBeenCalledWith('insights/fetchConfigData', gl.TEST_HOST);
done();
});
describe('when loading config', () => {
it('renders config loading state', done => {
vm.$store.state.insights.configLoading = true;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.insights-config-loading')).not.toBe(null);
expect(vm.$el.querySelector('.insights-wrapper')).toBe(null);
done();
});
});
});
describe('when config loaded', () => {
const title = 'Bugs Per Team';
beforeEach(() => {
vm.$store.state.insights.configLoading = false;
vm.$store.state.insights.configData = {
bugsPerTeam: {
title,
},
};
});
it('has the correct nav tabs', done => {
vm.$nextTick(() => {
expect(vm.$el.querySelector('.nav-links')).not.toBe(null);
expect(vm.$el.querySelector('.nav-links li a').innerText.trim()).toBe(title);
done();
});
});
});
describe('when activeChart changes', () => {
it('loads chart data', done => {
vm.$store.state.insights.activeChart = { key: 'chart' };
vm.$nextTick(() => {
expect(store.dispatch).toHaveBeenCalledWith(
'insights/fetchChartData',
`${gl.TEST_HOST}/query`,
);
done();
});
});
});
describe('when loading chart', () => {
beforeEach(() => {
vm.$store.state.insights.configLoading = false;
vm.$store.state.insights.chartLoading = true;
});
it('hides the config loading state', done => {
vm.$nextTick(() => {
expect(vm.$el.querySelector('.insights-config-loading')).toBe(null);
done();
});
});
it('renders chart loading state', done => {
vm.$nextTick(() => {
expect(vm.$el.querySelector('.insights-wrapper')).not.toBe(null);
expect(vm.$el.querySelector('.insights-chart .insights-chart-loading')).not.toBe(null);
done();
});
});
it('chart is not shown', done => {
vm.$nextTick(() => {
expect(vm.$el.querySelector('.insights-wrapper')).not.toBe(null);
expect(vm.$el.querySelector('.insights-chart .chart-canvas-wrapper')).toBe(null);
done();
});
});
});
describe('when chart loaded', () => {
const chart = {
title: 'Bugs Per Team',
type: 'stacked-bar',
query: {
name: 'filter_issues_by_label_category',
filter_label: 'bug',
category_labels: ['Plan', 'Create', 'Manage'],
},
};
beforeEach(() => {
vm.$store.state.insights.activeChart = chart;
vm.$store.state.insights.chartData = {};
vm.$store.state.insights.configLoading = false;
vm.$store.state.insights.chartLoading = false;
});
it('renders chart', done => {
vm.$nextTick(() => {
expect(vm.$el.querySelector('.insights-wrapper')).not.toBe(null);
expect(vm.$el.querySelector('.insights-chart .chart-canvas-wrapper')).not.toBe(null);
done();
});
});
it('hides chart loading state', done => {
vm.$nextTick(() => {
expect(vm.$el.querySelector('.insights-wrapper')).not.toBe(null);
expect(vm.$el.querySelector('.insights-chart .insights-chart-loading')).toBe(null);
done();
});
});
it('renders the correct type of chart', done => {
vm.$nextTick(() => {
expect(vm.$el.querySelector('.insights-wrapper')).not.toBe(null);
expect(
vm.$el.querySelector('.insights-chart .chart-canvas-wrapper canvas.stacked-bar'),
).not.toBe(null);
done();
});
});
});
});
export const chartInfo = {
title: 'Bugs Per Team',
type: 'bar',
query: {
name: 'filter_issues_by_label_category',
filter_label: 'bug',
category_labels: ['Plan', 'Create', 'Manage'],
},
};
export const chartData = {
labels: ['January'],
datasets: [
{
label: 'Dataset 1',
fill: true,
backgroundColor: ['rgba(255, 99, 132)'],
data: [1],
},
{
label: 'Dataset 2',
fill: true,
backgroundColor: ['rgba(54, 162, 235)'],
data: [2],
},
],
};
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'spec/helpers/vuex_action_helper';
import * as actions from 'ee/insights/stores/modules/insights/actions';
import store from 'ee/insights/stores/';
describe('Insights store actions', () => {
const key = 'bugsPerTeam';
const chart = {
title: 'Bugs Per Team',
type: 'stacked-bar',
query: {
name: 'filter_issues_by_label_category',
filter_label: 'bug',
category_labels: ['Plan', 'Create', 'Manage'],
},
};
const configData = {};
beforeEach(() => {
configData[key] = chart;
});
describe('requestConfig', () => {
it('commits REQUEST_CONFIG', done => {
testAction(actions.requestConfig, null, null, [{ type: 'REQUEST_CONFIG' }], [], done);
});
});
describe('receiveConfigSuccess', () => {
it('commits RECEIVE_CONFIG_SUCCESS', done => {
testAction(
actions.receiveConfigSuccess,
[{ chart: 'chart' }],
null,
[{ type: 'RECEIVE_CONFIG_SUCCESS', payload: [{ chart: 'chart' }] }],
[],
done,
);
});
});
describe('receiveConfigError', () => {
it('commits RECEIVE_CONFIG_ERROR', done => {
testAction(
actions.receiveConfigError,
null,
null,
[{ type: 'RECEIVE_CONFIG_ERROR' }],
[],
done,
);
});
});
describe('fetchConfigData', () => {
let mock;
let dispatch;
beforeEach(() => {
dispatch = jasmine.createSpy('dispatch');
mock = new MockAdapter(axios);
mock.onGet(gl.TEST_HOST).reply(200, configData);
});
afterEach(() => {
mock.restore();
});
it('calls requestConfig', done => {
const context = {
dispatch,
};
actions
.fetchConfigData(context, gl.TEST_HOST)
.then(() => {
expect(dispatch.calls.argsFor(0)).toEqual(['requestConfig']);
})
.then(done)
.catch(done.fail);
});
it('calls receiveConfigSuccess with config data', done => {
const context = {
dispatch,
};
actions
.fetchConfigData(context, gl.TEST_HOST)
.then(() => {
expect(dispatch.calls.argsFor(1)).toEqual(['receiveConfigSuccess', configData]);
})
.then(done)
.catch(done.fail);
});
});
describe('requestChartData', () => {
it('commits REQUEST_CHART', done => {
testAction(actions.requestChartData, null, null, [{ type: 'REQUEST_CHART' }], [], done);
});
});
describe('receiveChartDataSuccess', () => {
it('commits RECEIVE_CHART_SUCCESS', done => {
testAction(
actions.receiveChartDataSuccess,
{ type: 'bar', data: {} },
null,
[{ type: 'RECEIVE_CHART_SUCCESS', payload: { type: 'bar', data: {} } }],
[],
done,
);
});
});
describe('receiveChartDataError', () => {
it('commits RECEIVE_CHART_ERROR', done => {
testAction(
actions.receiveChartDataError,
null,
null,
[{ type: 'RECEIVE_CHART_ERROR' }],
[],
done,
);
});
});
describe('fetchChartData', () => {
let mock;
let dispatch;
let state;
const chartData = {
labels: ['January'],
datasets: [
{
label: 'Dataset 1',
fill: true,
backgroundColor: ['rgba(255, 99, 132)'],
data: [1],
},
{
label: 'Dataset 2',
fill: true,
backgroundColor: ['rgba(54, 162, 235)'],
data: [2],
},
],
};
beforeEach(() => {
store.state.insights.activeChart = chart;
state = store.state.insights;
dispatch = jasmine.createSpy('dispatch');
mock = new MockAdapter(axios);
mock
.onPost(`${gl.TEST_HOST}/query`, {
query: chart.query,
chart_type: chart.type,
})
.reply(200, chartData);
});
afterEach(() => {
mock.restore();
});
it('calls requestChartData', done => {
const context = {
dispatch,
state,
};
actions
.fetchChartData(context, `${gl.TEST_HOST}/query`)
.then(() => {
expect(dispatch.calls.argsFor(0)).toEqual(['requestChartData']);
})
.then(done)
.catch(done.fail);
});
it('calls receiveChartDataSuccess with chart data', done => {
const context = {
dispatch,
state,
};
actions
.fetchChartData(context, `${gl.TEST_HOST}/query`)
.then(() => {
expect(dispatch.calls.argsFor(1)).toEqual(['receiveChartDataSuccess', chartData]);
})
.then(done)
.catch(done.fail);
});
});
describe('setActiveTab', () => {
it('commits SET_ACTIVE_TAB and SET_ACTIVE_CHART', done => {
const state = { configData };
testAction(
actions.setActiveTab,
key,
state,
[{ type: 'SET_ACTIVE_TAB', payload: key }, { type: 'SET_ACTIVE_CHART', payload: chart }],
[],
done,
);
});
});
});
import createState from 'ee/insights/stores/modules/insights/state';
import mutations from 'ee/insights/stores/modules/insights/mutations';
import * as types from 'ee/insights/stores/modules/insights/mutation_types';
describe('Insights mutations', () => {
let state;
beforeEach(() => {
state = createState();
});
describe(types.REQUEST_CONFIG, () => {
it('sets configLoading state when starting request', () => {
mutations[types.REQUEST_CONFIG](state);
expect(state.configLoading).toBe(true);
});
it('resets configData state when starting request', () => {
mutations[types.REQUEST_CONFIG](state);
expect(state.configData).toBe(null);
});
});
describe(types.RECEIVE_CONFIG_SUCCESS, () => {
const data = [
{
key: 'chart',
},
];
it('sets configLoading state to false on success', () => {
mutations[types.RECEIVE_CONFIG_SUCCESS](state, data);
expect(state.configLoading).toBe(false);
});
it('sets configData state to incoming data on success', () => {
mutations[types.RECEIVE_CONFIG_SUCCESS](state, data);
expect(state.configData).toBe(data);
});
});
describe(types.RECEIVE_CONFIG_ERROR, () => {
it('sets configLoading state to false on error', () => {
mutations[types.RECEIVE_CONFIG_ERROR](state);
expect(state.configLoading).toBe(false);
});
it('sets configData state to null on error', () => {
mutations[types.RECEIVE_CONFIG_ERROR](state);
expect(state.configData).toBe(null);
});
});
describe(types.REQUEST_CHART, () => {
it('sets chartLoading state when starting request', () => {
mutations[types.REQUEST_CHART](state);
expect(state.chartLoading).toBe(true);
});
it('resets chartData state when starting request', () => {
mutations[types.REQUEST_CHART](state);
expect(state.chartData).toBe(null);
});
});
describe(types.RECEIVE_CHART_SUCCESS, () => {
const data = {
labels: ['January'],
datasets: [
{
label: 'Dataset 1',
fill: true,
backgroundColor: ['rgba(255, 99, 132)'],
data: [1],
},
{
label: 'Dataset 2',
fill: true,
backgroundColor: ['rgba(54, 162, 235)'],
data: [2],
},
],
};
it('sets chartLoading state to false on success', () => {
mutations[types.RECEIVE_CHART_SUCCESS](state, data);
expect(state.chartLoading).toBe(false);
});
it('sets chartData state to incoming data on success', () => {
mutations[types.RECEIVE_CHART_SUCCESS](state, data);
expect(state.chartData).toBe(data);
});
});
describe(types.RECEIVE_CHART_ERROR, () => {
it('sets chartLoading state to false on error', () => {
mutations[types.RECEIVE_CHART_ERROR](state);
expect(state.chartLoading).toBe(false);
});
it('sets chartData state to null on error', () => {
mutations[types.RECEIVE_CHART_ERROR](state);
expect(state.chartData).toBe(null);
});
});
describe(types.SET_ACTIVE_TAB, () => {
it('sets activeTab state', () => {
mutations[types.SET_ACTIVE_TAB](state, 'key');
expect(state.activeTab).toBe('key');
});
});
describe(types.SET_ACTIVE_CHART, () => {
let chartData;
beforeEach(() => {
chartData = { key: 'chart' };
});
it('sets activeChart state', () => {
mutations[types.SET_ACTIVE_CHART](state, chartData);
expect(state.activeChart).toBe(chartData);
});
});
});
......@@ -10378,6 +10378,12 @@ msgstr ""
msgid "There was an error deleting the todo."
msgstr ""
msgid "There was an error fetching configuration for charts"
msgstr ""
msgid "There was an error gathering the chart data"
msgstr ""
msgid "There was an error loading users activity calendar."
msgstr ""
......
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