Commit 89194380 authored by Tristan Read's avatar Tristan Read Committed by Natalia Tepluhina

Allow embedded metrics charts to be hidden

Adds show and hide options to metric chart embeds
parent d8812abf
import Vue from 'vue';
import Metrics from '~/monitoring/components/embed.vue';
import { createStore } from '~/monitoring/stores';
import EmbedGroup from '~/monitoring/components/embeds/embed_group.vue';
import { createStore } from '~/monitoring/stores/embed_group/';
// TODO: Handle copy-pasting - https://gitlab.com/gitlab-org/gitlab-foss/issues/64369.
export default function renderMetrics(elements) {
......@@ -8,16 +8,36 @@ export default function renderMetrics(elements) {
return;
}
const EmbedGroupComponent = Vue.extend(EmbedGroup);
const wrapperList = [];
elements.forEach(element => {
const { dashboardUrl } = element.dataset;
const MetricsComponent = Vue.extend(Metrics);
let wrapper;
const { previousElementSibling } = element;
const isFirstElementInGroup = !previousElementSibling?.urls;
if (isFirstElementInGroup) {
wrapper = document.createElement('div');
wrapper.urls = [element.dataset.dashboardUrl];
element.parentNode.insertBefore(wrapper, element);
wrapperList.push(wrapper);
} else {
wrapper = previousElementSibling;
wrapper.urls.push(element.dataset.dashboardUrl);
}
// Clean up processed element
element.parentNode.removeChild(element);
});
wrapperList.forEach(wrapper => {
// eslint-disable-next-line no-new
new MetricsComponent({
el: element,
new EmbedGroupComponent({
el: wrapper,
store: createStore(),
propsData: {
dashboardUrl,
urls: wrapper.urls,
},
});
});
......
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import sum from 'lodash/sum';
import { GlButton, GlCard, GlIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
import { monitoringDashboard } from '~/monitoring/stores';
import MetricEmbed from './metric_embed.vue';
export default {
components: {
GlButton,
GlCard,
GlIcon,
MetricEmbed,
},
props: {
urls: {
type: Array,
required: true,
validator: urls => urls.length > 0,
},
},
data() {
return {
isCollapsed: false,
};
},
computed: {
...mapState('embedGroup', ['module']),
...mapGetters('embedGroup', ['metricsWithData']),
arrowIconName() {
return this.isCollapsed ? 'chevron-right' : 'chevron-down';
},
bodyClass() {
return ['border-top', 'pl-3', 'pt-3', { 'd-none': this.isCollapsed }];
},
buttonLabel() {
return this.isCollapsed
? n__('View chart', 'View charts', this.numCharts)
: n__('Hide chart', 'Hide charts', this.numCharts);
},
containerClass() {
return this.isSingleChart ? 'col-lg-12' : 'col-lg-6';
},
numCharts() {
if (this.metricsWithData === null) {
return 0;
}
return sum(this.metricsWithData);
},
isSingleChart() {
return this.numCharts === 1;
},
},
created() {
this.urls.forEach((url, index) => {
const name = this.getNamespace(index);
this.$store.registerModule(name, monitoringDashboard);
this.addModule(name);
});
},
methods: {
...mapActions('embedGroup', ['addModule']),
getNamespace(id) {
return `monitoringDashboard/${id}`;
},
toggleCollapsed() {
this.isCollapsed = !this.isCollapsed;
},
},
};
</script>
<template>
<gl-card
v-show="numCharts > 0"
class="collapsible-card border p-0 mb-3"
header-class="d-flex align-items-center border-bottom-0 py-2"
:body-class="bodyClass"
>
<template #header>
<gl-button
class="collapsible-card-btn d-flex text-decoration-none"
:aria-label="buttonLabel"
variant="link"
@click="toggleCollapsed"
>
<gl-icon class="mr-1" :name="arrowIconName" />
{{ buttonLabel }}
</gl-button>
</template>
<div class="d-flex flex-wrap">
<metric-embed
v-for="(url, index) in urls"
:key="`${index}/${url}`"
:dashboard-url="url"
:namespace="getNamespace(index)"
:container-class="containerClass"
/>
</div>
</gl-card>
</template>
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import { mapState, mapActions } from 'vuex';
import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { timeRangeFromUrl, removeTimeRangeParams } from '../utils';
import { sidebarAnimationDuration } from '../constants';
import { defaultTimeRange } from '~/vue_shared/constants';
import { timeRangeFromUrl, removeTimeRangeParams } from '../../utils';
import { sidebarAnimationDuration } from '../../constants';
let sidebarMutationObserver;
......@@ -13,10 +13,20 @@ export default {
PanelType,
},
props: {
containerClass: {
type: String,
required: false,
default: 'col-lg-12',
},
dashboardUrl: {
type: String,
required: true,
},
namespace: {
type: String,
required: false,
default: 'monitoringDashboard',
},
},
data() {
const timeRange = timeRangeFromUrl(this.dashboardUrl) || defaultTimeRange;
......@@ -26,21 +36,32 @@ export default {
};
},
computed: {
...mapState('monitoringDashboard', ['dashboard']),
...mapGetters('monitoringDashboard', ['metricsWithData']),
...mapState({
dashboard(state) {
return state[this.namespace].dashboard;
},
metricsWithData(state, getters) {
return getters[`${this.namespace}/metricsWithData`]();
},
}),
charts() {
if (!this.dashboard || !this.dashboard.panelGroups) {
return [];
}
const groupWithMetrics = this.dashboard.panelGroups.find(group =>
group.panels.find(chart => this.chartHasData(chart)),
) || { panels: [] };
return groupWithMetrics.panels.filter(chart => this.chartHasData(chart));
return this.dashboard.panelGroups.reduce(
(acc, currentGroup) => acc.concat(currentGroup.panels.filter(this.chartHasData)),
[],
);
},
isSingleChart() {
return this.charts.length === 1;
},
embedClass() {
return this.isSingleChart ? this.containerClass : 'col-lg-12';
},
panelClass() {
return this.isSingleChart ? 'col-lg-12' : 'col-lg-6';
},
},
mounted() {
this.setInitialState();
......@@ -60,15 +81,27 @@ export default {
}
},
methods: {
...mapActions('monitoringDashboard', [
'setTimeRange',
'fetchDashboard',
'setEndpoints',
'setFeatureFlags',
'setShowErrorBanner',
]),
// Use function args to support dynamic namespaces in mapXXX helpers. Pattern described
// in https://github.com/vuejs/vuex/issues/863#issuecomment-329510765
...mapActions({
setTimeRange(dispatch, payload) {
return dispatch(`${this.namespace}/setTimeRange`, payload);
},
fetchDashboard(dispatch, payload) {
return dispatch(`${this.namespace}/fetchDashboard`, payload);
},
setEndpoints(dispatch, payload) {
return dispatch(`${this.namespace}/setEndpoints`, payload);
},
setFeatureFlags(dispatch, payload) {
return dispatch(`${this.namespace}/setFeatureFlags`, payload);
},
setShowErrorBanner(dispatch, payload) {
return dispatch(`${this.namespace}/setShowErrorBanner`, payload);
},
}),
chartHasData(chart) {
return chart.metrics.some(metric => this.metricsWithData().includes(metric.metricId));
return chart.metrics.some(metric => this.metricsWithData.includes(metric.metricId));
},
onSidebarMutation() {
setTimeout(() => {
......@@ -85,15 +118,14 @@ export default {
};
</script>
<template>
<div class="metrics-embed" :class="{ 'd-inline-flex col-lg-6 p-0': isSingleChart }">
<div v-if="charts.length" class="row w-100 m-n2 pb-4">
<panel-type
v-for="(graphData, graphIndex) in charts"
:key="`panel-type-${graphIndex}`"
class="w-100"
:graph-data="graphData"
:group-id="dashboardUrl"
/>
</div>
<div class="metrics-embed p-0 d-flex flex-wrap" :class="embedClass">
<panel-type
v-for="(graphData, graphIndex) in charts"
:key="`panel-type-${graphIndex}`"
:class="panelClass"
:graph-data="graphData"
:group-id="dashboardUrl"
:namespace="namespace"
/>
</div>
</template>
......@@ -68,6 +68,11 @@ export default {
required: false,
default: 'panel-type-chart',
},
namespace: {
type: String,
required: false,
default: 'monitoringDashboard',
},
},
data() {
return {
......@@ -76,7 +81,22 @@ export default {
};
},
computed: {
...mapState('monitoringDashboard', ['deploymentData', 'projectPath', 'logsPath', 'timeRange']),
// Use functions to support dynamic namespaces in mapXXX helpers. Pattern described
// in https://github.com/vuejs/vuex/issues/863#issuecomment-329510765
...mapState({
deploymentData(state) {
return state[this.namespace].deploymentData;
},
projectPath(state) {
return state[this.namespace].projectPath;
},
logsPath(state) {
return state[this.namespace].logsPath;
},
timeRange(state) {
return state[this.namespace].timeRange;
},
}),
title() {
return this.graphData.title || '';
},
......
import * as types from './mutation_types';
export const addModule = ({ commit }, data) => commit(types.ADD_MODULE, data);
export default () => {};
export const metricsWithData = (state, getters, rootState, rootGetters) =>
state.modules.map(module => rootGetters[`${module}/metricsWithData`]().length);
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
// In practice this store will have a number of `monitoringDashboard` modules added dynamically
export const createStore = () =>
new Vuex.Store({
modules: {
embedGroup: {
namespaced: true,
actions,
getters,
mutations,
state,
},
},
});
export default createStore();
export const ADD_MODULE = 'ADD_MODULE';
export default () => {};
import * as types from './mutation_types';
export default {
[types.ADD_MODULE](state, module) {
state.modules.push(module);
},
};
......@@ -7,16 +7,18 @@ import state from './state';
Vue.use(Vuex);
export const monitoringDashboard = {
namespaced: true,
actions,
getters,
mutations,
state,
};
export const createStore = () =>
new Vuex.Store({
modules: {
monitoringDashboard: {
namespaced: true,
actions,
getters,
mutations,
state,
},
monitoringDashboard,
},
});
......
.collapsible-card {
.collapsible-card-btn {
color: $gl-text-color;
&:hover {
color: $blue-600;
}
}
}
---
title: Allow embedded metrics charts to be hidden
merge_request: 23929
author:
type: added
......@@ -777,7 +777,11 @@ The following requirements must be met for the metric to unfurl:
If all of the above are true, then the metric will unfurl as seen below:
![Embedded Metrics](img/embed_metrics.png)
![Embedded Metrics](img/view_embedded_metrics_v12_10.png)
Metric charts may also be hidden:
![Show Hide](img/hide_embedded_metrics_v12_10.png)
### Embedding metrics in issue templates
......
......@@ -10488,6 +10488,11 @@ msgstr ""
msgid "Hide archived projects"
msgstr ""
msgid "Hide chart"
msgid_plural "Hide charts"
msgstr[0] ""
msgstr[1] ""
msgid "Hide file browser"
msgstr ""
......@@ -22340,6 +22345,11 @@ msgstr ""
msgid "View blame prior to this change"
msgstr ""
msgid "View chart"
msgid_plural "View charts"
msgstr[0] ""
msgstr[1] ""
msgid "View dependency details for your project"
msgstr ""
......
import Vue from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import renderMetrics from '~/behaviors/markdown/render_metrics';
const originalExtend = Vue.extend;
const mockEmbedGroup = jest.fn();
describe('Render metrics for Gitlab Flavoured Markdown', () => {
const container = {
Metrics() {},
};
let spyExtend;
beforeEach(() => {
Vue.extend = () => container.Metrics;
spyExtend = jest.spyOn(Vue, 'extend');
});
jest.mock('vue', () => ({ extend: () => mockEmbedGroup }));
jest.mock('~/monitoring/components/embeds/embed_group.vue', () => jest.fn());
jest.mock('~/monitoring/stores/embed_group/', () => ({ createStore: jest.fn() }));
afterEach(() => {
Vue.extend = originalExtend;
});
const getElements = () => Array.from(document.getElementsByClassName('js-render-metrics'));
describe('Render metrics for Gitlab Flavoured Markdown', () => {
it('does nothing when no elements are found', () => {
renderMetrics([]);
expect(spyExtend).not.toHaveBeenCalled();
expect(mockEmbedGroup).not.toHaveBeenCalled();
});
it('renders a vue component when elements are found', () => {
const element = document.createElement('div');
element.setAttribute('data-dashboard-url', TEST_HOST);
document.body.innerHTML = `<div class="js-render-metrics" data-dashboard-url="${TEST_HOST}"></div>`;
renderMetrics([element]);
renderMetrics(getElements());
expect(mockEmbedGroup).toHaveBeenCalledTimes(1);
expect(mockEmbedGroup).toHaveBeenCalledWith(
expect.objectContaining({ propsData: { urls: [`${TEST_HOST}`] } }),
);
});
expect(spyExtend).toHaveBeenCalled();
it('takes sibling metrics and groups them under a shared parent', () => {
document.body.innerHTML = `
<p><span>Hello</span></p>
<div class="js-render-metrics" data-dashboard-url="${TEST_HOST}/1"></div>
<div class="js-render-metrics" data-dashboard-url="${TEST_HOST}/2"></div>
<p><span>Hello</span></p>
<div class="js-render-metrics" data-dashboard-url="${TEST_HOST}/3"></div>
`;
renderMetrics(getElements());
expect(mockEmbedGroup).toHaveBeenCalledTimes(2);
expect(mockEmbedGroup).toHaveBeenCalledWith(
expect.objectContaining({ propsData: { urls: [`${TEST_HOST}/1`, `${TEST_HOST}/2`] } }),
);
expect(mockEmbedGroup).toHaveBeenCalledWith(
expect.objectContaining({ propsData: { urls: [`${TEST_HOST}/3`] } }),
);
});
});
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlButton, GlCard } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import EmbedGroup from '~/monitoring/components/embeds/embed_group.vue';
import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
import {
addModuleAction,
initialEmbedGroupState,
singleEmbedProps,
dashboardEmbedProps,
multipleEmbedProps,
} from './mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Embed Group', () => {
let wrapper;
let store;
const metricsWithDataGetter = jest.fn();
function mountComponent({ urls = [TEST_HOST], shallow = true, stubs } = {}) {
const mountMethod = shallow ? shallowMount : mount;
wrapper = mountMethod(EmbedGroup, {
localVue,
store,
propsData: {
urls,
},
stubs,
});
}
beforeEach(() => {
store = new Vuex.Store({
modules: {
embedGroup: {
namespaced: true,
actions: { addModule: jest.fn() },
getters: { metricsWithData: metricsWithDataGetter },
state: initialEmbedGroupState,
},
},
});
store.registerModule = jest.fn();
jest.spyOn(store, 'dispatch');
});
afterEach(() => {
metricsWithDataGetter.mockReset();
if (wrapper) {
wrapper.destroy();
}
});
describe('interactivity', () => {
it('hides the component when no chart data is loaded', () => {
metricsWithDataGetter.mockReturnValue([]);
mountComponent();
expect(wrapper.find(GlCard).isVisible()).toBe(false);
});
it('shows the component when chart data is loaded', () => {
metricsWithDataGetter.mockReturnValue([1]);
mountComponent();
expect(wrapper.find(GlCard).isVisible()).toBe(true);
});
it('is expanded by default', () => {
metricsWithDataGetter.mockReturnValue([1]);
mountComponent({ shallow: false, stubs: { MetricEmbed: '<div />' } });
expect(wrapper.find('.card-body').classes()).not.toContain('d-none');
});
it('collapses when clicked', done => {
metricsWithDataGetter.mockReturnValue([1]);
mountComponent({ shallow: false, stubs: { MetricEmbed: '<div />' } });
wrapper.find(GlButton).trigger('click');
wrapper.vm.$nextTick(() => {
expect(wrapper.find('.card-body').classes()).toContain('d-none');
done();
});
});
});
describe('single metrics', () => {
beforeEach(() => {
metricsWithDataGetter.mockReturnValue([1]);
mountComponent();
});
it('renders an Embed component', () => {
expect(wrapper.find(MetricEmbed).exists()).toBe(true);
});
it('passes the correct props to the Embed component', () => {
expect(wrapper.find(MetricEmbed).props()).toEqual(singleEmbedProps());
});
it('adds the monitoring dashboard module', () => {
expect(store.dispatch).toHaveBeenCalledWith(addModuleAction, 'monitoringDashboard/0');
});
});
describe('dashboard metrics', () => {
beforeEach(() => {
metricsWithDataGetter.mockReturnValue([2]);
mountComponent();
});
it('passes the correct props to the dashboard Embed component', () => {
expect(wrapper.find(MetricEmbed).props()).toEqual(dashboardEmbedProps());
});
it('adds the monitoring dashboard module', () => {
expect(store.dispatch).toHaveBeenCalledWith(addModuleAction, 'monitoringDashboard/0');
});
});
describe('multiple metrics', () => {
beforeEach(() => {
metricsWithDataGetter.mockReturnValue([1, 1]);
mountComponent({ urls: [TEST_HOST, TEST_HOST] });
});
it('creates Embed components', () => {
expect(wrapper.findAll(MetricEmbed)).toHaveLength(2);
});
it('passes the correct props to the Embed components', () => {
expect(wrapper.findAll(MetricEmbed).wrappers.map(item => item.props())).toEqual(
multipleEmbedProps(),
);
});
it('adds multiple monitoring dashboard modules', () => {
expect(store.dispatch).toHaveBeenCalledWith(addModuleAction, 'monitoringDashboard/0');
expect(store.dispatch).toHaveBeenCalledWith(addModuleAction, 'monitoringDashboard/1');
});
});
describe('button text', () => {
it('has a singular label when there is one embed', () => {
metricsWithDataGetter.mockReturnValue([1]);
mountComponent({ shallow: false, stubs: { MetricEmbed: '<div />' } });
expect(wrapper.find(GlButton).text()).toBe('Hide chart');
});
it('has a plural label when there are multiple embeds', () => {
metricsWithDataGetter.mockReturnValue([2]);
mountComponent({ shallow: false, stubs: { MetricEmbed: '<div />' } });
expect(wrapper.find(GlButton).text()).toBe('Hide charts');
});
});
});
......@@ -2,20 +2,20 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import { TEST_HOST } from 'helpers/test_constants';
import Embed from '~/monitoring/components/embed.vue';
import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
import { groups, initialState, metricsData, metricsWithData } from './mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Embed', () => {
describe('MetricEmbed', () => {
let wrapper;
let store;
let actions;
let metricsWithDataGetter;
function mountComponent() {
wrapper = shallowMount(Embed, {
wrapper = shallowMount(MetricEmbed, {
localVue,
store,
propsData: {
......
import { TEST_HOST } from 'helpers/test_constants';
export const metricsWithData = ['15_metric_a', '16_metric_b'];
export const groups = [
......@@ -52,3 +54,34 @@ export const initialState = () => ({
},
useDashboardEndpoint: true,
});
export const initialEmbedGroupState = () => ({
modules: [],
});
export const singleEmbedProps = () => ({
dashboardUrl: TEST_HOST,
containerClass: 'col-lg-12',
namespace: 'monitoringDashboard/0',
});
export const dashboardEmbedProps = () => ({
dashboardUrl: TEST_HOST,
containerClass: 'col-lg-6',
namespace: 'monitoringDashboard/0',
});
export const multipleEmbedProps = () => [
{
dashboardUrl: TEST_HOST,
containerClass: 'col-lg-6',
namespace: 'monitoringDashboard/0',
},
{
dashboardUrl: TEST_HOST,
containerClass: 'col-lg-6',
namespace: 'monitoringDashboard/1',
},
];
export const addModuleAction = 'embedGroup/addModule';
......@@ -8,8 +8,17 @@ import PanelType from '~/monitoring/components/panel_type.vue';
import EmptyChart from '~/monitoring/components/charts/empty_chart.vue';
import TimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
import AnomalyChart from '~/monitoring/components/charts/anomaly.vue';
import { anomalyMockGraphData, graphDataPrometheusQueryRange } from 'jest/monitoring/mock_data';
import { createStore } from '~/monitoring/stores';
import {
anomalyMockGraphData,
graphDataPrometheusQueryRange,
mockLogsHref,
mockLogsPath,
mockNamespace,
mockNamespacedData,
mockTimeRange,
} from 'jest/monitoring/mock_data';
import { createStore, monitoringDashboard } from '~/monitoring/stores';
import { createStore as createEmbedGroupStore } from '~/monitoring/stores/embed_group';
global.IS_EE = true;
global.URL.createObjectURL = jest.fn();
......@@ -29,6 +38,7 @@ describe('Panel Type component', () => {
const exampleText = 'example_text';
const findCopyLink = () => wrapper.find({ ref: 'copyChartLink' });
const findTimeChart = () => wrapper.find({ ref: 'timeChart' });
const createWrapper = props => {
wrapper = shallowMount(PanelType, {
......@@ -99,8 +109,6 @@ describe('Panel Type component', () => {
});
describe('when graph data is available', () => {
const findTimeChart = () => wrapper.find({ ref: 'timeChart' });
beforeEach(() => {
createWrapper({
graphData: graphDataPrometheusQueryRange,
......@@ -242,10 +250,6 @@ describe('Panel Type component', () => {
});
describe('View Logs dropdown item', () => {
const mockLogsPath = '/path/to/logs';
const mockTimeRange = { duration: { seconds: 120 } };
const findTimeChart = () => wrapper.find({ ref: 'timeChart' });
const findViewLogsLink = () => wrapper.find({ ref: 'viewLogsLink' });
beforeEach(() => {
......@@ -292,8 +296,7 @@ describe('Panel Type component', () => {
state.timeRange = mockTimeRange;
return wrapper.vm.$nextTick(() => {
const href = `${mockLogsPath}?duration_seconds=${mockTimeRange.duration.seconds}`;
expect(findViewLogsLink().attributes('href')).toMatch(href);
expect(findViewLogsLink().attributes('href')).toMatch(mockLogsHref);
});
});
......@@ -388,4 +391,53 @@ describe('Panel Type component', () => {
});
});
});
describe('when using dynamic modules', () => {
const { mockDeploymentData, mockProjectPath } = mockNamespacedData;
beforeEach(() => {
store = createEmbedGroupStore();
store.registerModule(mockNamespace, monitoringDashboard);
store.state.embedGroup.modules.push(mockNamespace);
wrapper = shallowMount(PanelType, {
propsData: {
graphData: graphDataPrometheusQueryRange,
namespace: mockNamespace,
},
store,
mocks,
});
});
it('handles namespaced time range and logs path state', () => {
store.state[mockNamespace].timeRange = mockTimeRange;
store.state[mockNamespace].logsPath = mockLogsPath;
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find({ ref: 'viewLogsLink' }).attributes().href).toBe(mockLogsHref);
});
});
it('handles namespaced deployment data state', () => {
store.state[mockNamespace].deploymentData = mockDeploymentData;
return wrapper.vm.$nextTick().then(() => {
expect(findTimeChart().props().deploymentData).toEqual(mockDeploymentData);
});
});
it('handles namespaced project path state', () => {
store.state[mockNamespace].projectPath = mockProjectPath;
return wrapper.vm.$nextTick().then(() => {
expect(findTimeChart().props().projectPath).toBe(mockProjectPath);
});
});
it('it renders a time series chart with no errors', () => {
expect(wrapper.find(TimeSeriesChart).isVueInstance()).toBe(true);
expect(wrapper.find(TimeSeriesChart).exists()).toBe(true);
});
});
});
......@@ -750,3 +750,20 @@ export const barMockData = {
},
],
};
export const baseNamespace = 'monitoringDashboard';
export const mockNamespace = `${baseNamespace}/1`;
export const mockNamespaces = [`${baseNamespace}/1`, `${baseNamespace}/2`];
export const mockTimeRange = { duration: { seconds: 120 } };
export const mockNamespacedData = {
mockDeploymentData: ['mockDeploymentData'],
mockProjectPath: '/mockProjectPath',
};
export const mockLogsPath = '/mockLogsPath';
export const mockLogsHref = `${mockLogsPath}?duration_seconds=${mockTimeRange.duration.seconds}`;
// import store from '~/monitoring/stores/embed_group';
import * as actions from '~/monitoring/stores/embed_group/actions';
import * as types from '~/monitoring/stores/embed_group/mutation_types';
import { mockNamespace } from '../../mock_data';
describe('Embed group actions', () => {
describe('addModule', () => {
it('adds a module to the store', () => {
const commit = jest.fn();
actions.addModule({ commit }, mockNamespace);
expect(commit).toHaveBeenCalledWith(types.ADD_MODULE, mockNamespace);
});
});
});
import { metricsWithData } from '~/monitoring/stores/embed_group/getters';
import { mockNamespaces } from '../../mock_data';
describe('Embed group getters', () => {
describe('metricsWithData', () => {
it('correctly sums the number of metrics with data', () => {
const mockMetric = {};
const state = {
modules: mockNamespaces,
};
const rootGetters = {
[`${mockNamespaces[0]}/metricsWithData`]: () => [mockMetric],
[`${mockNamespaces[1]}/metricsWithData`]: () => [mockMetric, mockMetric],
};
expect(metricsWithData(state, null, null, rootGetters)).toEqual([1, 2]);
});
});
});
import state from '~/monitoring/stores/embed_group/state';
import mutations from '~/monitoring/stores/embed_group/mutations';
import * as types from '~/monitoring/stores/embed_group/mutation_types';
import { mockNamespace } from '../../mock_data';
describe('Embed group mutations', () => {
describe('ADD_MODULE', () => {
it('should add a module', () => {
const stateCopy = state();
mutations[types.ADD_MODULE](stateCopy, mockNamespace);
expect(stateCopy.modules).toEqual([mockNamespace]);
});
});
});
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