Commit 1ee880c4 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '34121-group-level-no-data-store' into 'master'

Add empty region when group metrics are missing

See merge request gitlab-org/gitlab!20900
parents d1bd7207 db6629da
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import VueDraggable from 'vuedraggable'; import VueDraggable from 'vuedraggable';
import { import {
GlButton, GlButton,
...@@ -99,6 +99,10 @@ export default { ...@@ -99,6 +99,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
emptyNoDataSmallSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: { emptyUnableToConnectSvgPath: {
type: String, type: String,
required: true, required: true,
...@@ -176,11 +180,11 @@ export default { ...@@ -176,11 +180,11 @@ export default {
'showEmptyState', 'showEmptyState',
'environments', 'environments',
'deploymentData', 'deploymentData',
'metricsWithData',
'useDashboardEndpoint', 'useDashboardEndpoint',
'allDashboards', 'allDashboards',
'additionalPanelTypesEnabled', 'additionalPanelTypesEnabled',
]), ]),
...mapGetters('monitoringDashboard', ['metricsWithData']),
firstDashboard() { firstDashboard() {
return this.environmentsEndpoint.length > 0 && this.allDashboards.length > 0 return this.environmentsEndpoint.length > 0 && this.allDashboards.length > 0
? this.allDashboards[0] ? this.allDashboards[0]
...@@ -280,13 +284,8 @@ export default { ...@@ -280,13 +284,8 @@ export default {
submitCustomMetricsForm() { submitCustomMetricsForm() {
this.$refs.customMetricsForm.submit(); this.$refs.customMetricsForm.submit();
}, },
chartsWithData(panels) {
return panels.filter(panel =>
panel.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
);
},
groupHasData(group) { groupHasData(group) {
return this.chartsWithData(group.panels).length > 0; return this.metricsWithData(group.key).length > 0;
}, },
onDateTimePickerApply(timeWindowUrlParams) { onDateTimePickerApply(timeWindowUrlParams) {
return redirectTo(mergeUrlParams(timeWindowUrlParams, window.location.href)); return redirectTo(mergeUrlParams(timeWindowUrlParams, window.location.href));
...@@ -447,42 +446,61 @@ export default { ...@@ -447,42 +446,61 @@ export default {
:key="`${groupData.group}.${groupData.priority}`" :key="`${groupData.group}.${groupData.priority}`"
:name="groupData.group" :name="groupData.group"
:show-panels="showPanels" :show-panels="showPanels"
:collapse-group="groupHasData(groupData)" :collapse-group="!groupHasData(groupData)"
> >
<vue-draggable <div v-if="groupHasData(groupData)">
:value="groupData.panels" <vue-draggable
group="metrics-dashboard" :value="groupData.panels"
:component-data="{ attrs: { class: 'row mx-0 w-100' } }" group="metrics-dashboard"
:disabled="!isRearrangingPanels" :component-data="{ attrs: { class: 'row mx-0 w-100' } }"
@input="updatePanels(groupData.key, $event)" :disabled="!isRearrangingPanels"
> @input="updatePanels(groupData.key, $event)"
<div
v-for="(graphData, graphIndex) in groupData.panels"
:key="`panel-type-${graphIndex}`"
class="col-12 col-lg-6 px-2 mb-2 draggable"
:class="{ 'draggable-enabled': isRearrangingPanels }"
> >
<div class="position-relative draggable-panel js-draggable-panel"> <div
<div v-for="(graphData, graphIndex) in groupData.panels"
v-if="isRearrangingPanels" :key="`panel-type-${graphIndex}`"
class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end" class="col-12 col-lg-6 px-2 mb-2 draggable"
@click="removePanel(groupData.key, groupData.panels, graphIndex)" :class="{ 'draggable-enabled': isRearrangingPanels }"
> >
<a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')" <div class="position-relative draggable-panel js-draggable-panel">
><icon name="close" <div
/></a> v-if="isRearrangingPanels"
</div> class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
@click="removePanel(groupData.key, groupData.panels, graphIndex)"
>
<a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')"
><icon name="close"
/></a>
</div>
<panel-type <panel-type
:clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)" :clipboard-text="
:graph-data="graphData" generateLink(groupData.group, graphData.title, graphData.y_label)
:alerts-endpoint="alertsEndpoint" "
:prometheus-alerts-available="prometheusAlertsAvailable" :graph-data="graphData"
:index="`${index}-${graphIndex}`" :alerts-endpoint="alertsEndpoint"
/> :prometheus-alerts-available="prometheusAlertsAvailable"
:index="`${index}-${graphIndex}`"
/>
</div>
</div> </div>
</div> </vue-draggable>
</vue-draggable> </div>
<div v-else class="py-5 col col-sm-10 col-md-8 col-lg-7 col-xl-6">
<empty-state
ref="empty-group"
selected-state="noDataGroup"
:documentation-path="documentationPath"
:settings-path="settingsPath"
:clusters-path="clustersPath"
:empty-getting-started-svg-path="emptyGettingStartedSvgPath"
:empty-loading-svg-path="emptyLoadingSvgPath"
:empty-no-data-svg-path="emptyNoDataSvgPath"
:empty-no-data-small-svg-path="emptyNoDataSmallSvgPath"
:empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
:compact="true"
/>
</div>
</graph-group> </graph-group>
</div> </div>
<empty-state <empty-state
...@@ -494,6 +512,7 @@ export default { ...@@ -494,6 +512,7 @@ export default {
:empty-getting-started-svg-path="emptyGettingStartedSvgPath" :empty-getting-started-svg-path="emptyGettingStartedSvgPath"
:empty-loading-svg-path="emptyLoadingSvgPath" :empty-loading-svg-path="emptyLoadingSvgPath"
:empty-no-data-svg-path="emptyNoDataSvgPath" :empty-no-data-svg-path="emptyNoDataSvgPath"
:empty-no-data-small-svg-path="emptyNoDataSmallSvgPath"
:empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath" :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
:compact="smallEmptyState" :compact="smallEmptyState"
/> />
......
<script> <script>
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { getParameterValues, removeParams } from '~/lib/utils/url_utility'; import { getParameterValues, removeParams } from '~/lib/utils/url_utility';
import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue'; import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import GraphGroup from './graph_group.vue'; import GraphGroup from './graph_group.vue';
...@@ -35,7 +35,8 @@ export default { ...@@ -35,7 +35,8 @@ export default {
}; };
}, },
computed: { computed: {
...mapState('monitoringDashboard', ['dashboard', 'metricsWithData']), ...mapState('monitoringDashboard', ['dashboard']),
...mapGetters('monitoringDashboard', ['metricsWithData']),
charts() { charts() {
if (!this.dashboard || !this.dashboard.panel_groups) { if (!this.dashboard || !this.dashboard.panel_groups) {
return []; return [];
...@@ -73,7 +74,7 @@ export default { ...@@ -73,7 +74,7 @@ export default {
'setShowErrorBanner', 'setShowErrorBanner',
]), ]),
chartHasData(chart) { chartHasData(chart) {
return chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)); return chart.metrics.some(metric => this.metricsWithData().includes(metric.metric_id));
}, },
onSidebarMutation() { onSidebarMutation() {
setTimeout(() => { setTimeout(() => {
......
...@@ -37,6 +37,10 @@ export default { ...@@ -37,6 +37,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
emptyNoDataSmallSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: { emptyUnableToConnectSvgPath: {
type: String, type: String,
required: true, required: true,
...@@ -80,6 +84,11 @@ export default { ...@@ -80,6 +84,11 @@ export default {
secondaryButtonText: '', secondaryButtonText: '',
secondaryButtonPath: '', secondaryButtonPath: '',
}, },
noDataGroup: {
svgUrl: this.emptyNoDataSmallSvgPath,
title: __('No data to display'),
description: __('The data source is connected, but there is no data to display.'),
},
unableToConnect: { unableToConnect: {
svgUrl: this.emptyUnableToConnectSvgPath, svgUrl: this.emptyUnableToConnectSvgPath,
title: __('Unable to connect to Prometheus server'), title: __('Unable to connect to Prometheus server'),
......
...@@ -15,31 +15,44 @@ export default { ...@@ -15,31 +15,44 @@ export default {
required: false, required: false,
default: true, default: true,
}, },
/**
* Initial value of collapse on mount.
*/
collapseGroup: { collapseGroup: {
type: Boolean, type: Boolean,
required: true, required: false,
default: false,
}, },
}, },
data() { data() {
return { return {
showGroup: true, isCollapsed: this.collapseGroup,
}; };
}, },
computed: { computed: {
caretIcon() { caretIcon() {
return this.collapseGroup && this.showGroup ? 'angle-down' : 'angle-right'; return this.isCollapsed ? 'angle-right' : 'angle-down';
},
},
watch: {
collapseGroup(val) {
// Respond to changes in collapseGroup but do not
// collapse it once was opened by the user.
if (this.showPanels && !val) {
this.isCollapsed = false;
}
}, },
}, },
methods: { methods: {
collapse() { collapse() {
this.showGroup = !this.showGroup; this.isCollapsed = !this.isCollapsed;
}, },
}, },
}; };
</script> </script>
<template> <template>
<div v-if="showPanels" class="card prometheus-panel"> <div v-if="showPanels" ref="graph-group" class="card prometheus-panel">
<div class="card-header d-flex align-items-center"> <div class="card-header d-flex align-items-center">
<h4 class="flex-grow-1">{{ name }}</h4> <h4 class="flex-grow-1">{{ name }}</h4>
<a role="button" class="js-graph-group-toggle" @click="collapse"> <a role="button" class="js-graph-group-toggle" @click="collapse">
...@@ -47,12 +60,12 @@ export default { ...@@ -47,12 +60,12 @@ export default {
</a> </a>
</div> </div>
<div <div
v-if="collapseGroup" v-show="!isCollapsed"
v-show="collapseGroup && showGroup" ref="graph-group-content"
class="card-body prometheus-graph-group p-0" class="card-body prometheus-graph-group p-0"
> >
<slot></slot> <slot></slot>
</div> </div>
</div> </div>
<div v-else class="prometheus-graph-group"><slot></slot></div> <div v-else ref="graph-group-content" class="prometheus-graph-group"><slot></slot></div>
</template> </template>
...@@ -4,7 +4,7 @@ import createFlash from '~/flash'; ...@@ -4,7 +4,7 @@ import createFlash from '~/flash';
import trackDashboardLoad from '../monitoring_tracking_helper'; import trackDashboardLoad from '../monitoring_tracking_helper';
import statusCodes from '../../lib/utils/http_status'; import statusCodes from '../../lib/utils/http_status';
import { backOff } from '../../lib/utils/common_utils'; import { backOff } from '../../lib/utils/common_utils';
import { s__ } from '../../locale'; import { s__, sprintf } from '../../locale';
const TWO_MINUTES = 120000; const TWO_MINUTES = 120000;
...@@ -74,17 +74,21 @@ export const fetchDashboard = ({ state, dispatch }, params) => { ...@@ -74,17 +74,21 @@ export const fetchDashboard = ({ state, dispatch }, params) => {
return backOffRequest(() => axios.get(state.dashboardEndpoint, { params })) return backOffRequest(() => axios.get(state.dashboardEndpoint, { params }))
.then(resp => resp.data) .then(resp => resp.data)
.then(response => dispatch('receiveMetricsDashboardSuccess', { response, params })) .then(response => dispatch('receiveMetricsDashboardSuccess', { response, params }))
.then(() => { .catch(e => {
const dashboardType = state.currentDashboard === '' ? 'default' : 'custom'; dispatch('receiveMetricsDashboardFailure', e);
return trackDashboardLoad({ if (state.showErrorBanner) {
label: `${dashboardType}_metrics_dashboard`, if (e.response.data && e.response.data.message) {
value: state.metricsWithData.length, const { message } = e.response.data;
}); createFlash(
}) sprintf(
.catch(error => { s__('Metrics|There was an error while retrieving metrics. %{message}'),
dispatch('receiveMetricsDashboardFailure', error); { message },
if (state.setShowErrorBanner) { false,
createFlash(s__('Metrics|There was an error while retrieving metrics')); ),
);
} else {
createFlash(s__('Metrics|There was an error while retrieving metrics'));
}
} }
}); });
}; };
...@@ -126,7 +130,7 @@ export const fetchPrometheusMetric = ({ commit }, { metric, params }) => { ...@@ -126,7 +130,7 @@ export const fetchPrometheusMetric = ({ commit }, { metric, params }) => {
}); });
}; };
export const fetchPrometheusMetrics = ({ state, commit, dispatch }, params) => { export const fetchPrometheusMetrics = ({ state, commit, dispatch, getters }, params) => {
commit(types.REQUEST_METRICS_DATA); commit(types.REQUEST_METRICS_DATA);
const promises = []; const promises = [];
...@@ -140,9 +144,11 @@ export const fetchPrometheusMetrics = ({ state, commit, dispatch }, params) => { ...@@ -140,9 +144,11 @@ export const fetchPrometheusMetrics = ({ state, commit, dispatch }, params) => {
return Promise.all(promises) return Promise.all(promises)
.then(() => { .then(() => {
if (state.metricsWithData.length === 0) { const dashboardType = state.currentDashboard === '' ? 'default' : 'custom';
commit(types.SET_NO_DATA_EMPTY_STATE); trackDashboardLoad({
} label: `${dashboardType}_metrics_dashboard`,
value: getters.metricsWithData().length,
});
}) })
.catch(() => { .catch(() => {
createFlash(s__(`Metrics|There was an error while retrieving metrics`), 'warning'); createFlash(s__(`Metrics|There was an error while retrieving metrics`), 'warning');
......
const metricsIdsInPanel = panel =>
panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId);
/**
* Getter to obtain the list of metric ids that have data
*
* Useful to understand which parts of the dashboard should
* be displayed. It is a Vuex Method-Style Access getter.
*
* @param {Object} state
* @returns {Function} A function that returns an array of
* metrics in the dashboard that contain results, optionally
* filtered by group key.
*/
export const metricsWithData = state => groupKey => {
let groups = state.dashboard.panel_groups;
if (groupKey) {
groups = groups.filter(group => group.key === groupKey);
}
const res = [];
groups.forEach(group => {
group.panels.forEach(panel => {
res.push(...metricsIdsInPanel(panel));
});
});
return res;
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import * as actions from './actions'; import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
import state from './state'; import state from './state';
...@@ -12,6 +13,7 @@ export const createStore = () => ...@@ -12,6 +13,7 @@ export const createStore = () =>
monitoringDashboard: { monitoringDashboard: {
namespaced: true, namespaced: true,
actions, actions,
getters,
mutations, mutations,
state, state,
}, },
......
...@@ -67,7 +67,6 @@ export default { ...@@ -67,7 +67,6 @@ export default {
group.panels.forEach(panel => { group.panels.forEach(panel => {
panel.metrics.forEach(metric => { panel.metrics.forEach(metric => {
if (metric.metric_id === metricId) { if (metric.metric_id === metricId) {
state.metricsWithData.push(metricId);
// ensure dates/numbers are correctly formatted for charts // ensure dates/numbers are correctly formatted for charts
const normalizedResults = result.map(normalizeQueryResult); const normalizedResults = result.map(normalizeQueryResult);
Vue.set(metric, 'result', Object.freeze(normalizedResults)); Vue.set(metric, 'result', Object.freeze(normalizedResults));
......
...@@ -13,7 +13,6 @@ export default () => ({ ...@@ -13,7 +13,6 @@ export default () => ({
}, },
deploymentData: [], deploymentData: [],
environments: [], environments: [],
metricsWithData: [],
allDashboards: [], allDashboards: [],
currentDashboard: null, currentDashboard: null,
projectPath: null, projectPath: null,
......
...@@ -67,7 +67,6 @@ ...@@ -67,7 +67,6 @@
.prometheus-graph-group { .prometheus-graph-group {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
margin-top: $gl-padding-8;
} }
.prometheus-graph { .prometheus-graph {
......
...@@ -26,6 +26,7 @@ module EnvironmentsHelper ...@@ -26,6 +26,7 @@ module EnvironmentsHelper
"empty-getting-started-svg-path" => image_path('illustrations/monitoring/getting_started.svg'), "empty-getting-started-svg-path" => image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path" => image_path('illustrations/monitoring/loading.svg'), "empty-loading-svg-path" => image_path('illustrations/monitoring/loading.svg'),
"empty-no-data-svg-path" => image_path('illustrations/monitoring/no_data.svg'), "empty-no-data-svg-path" => image_path('illustrations/monitoring/no_data.svg'),
"empty-no-data-small-svg-path" => image_path('illustrations/chart-empty-state-small.svg'),
"empty-unable-to-connect-svg-path" => image_path('illustrations/monitoring/unable_to_connect.svg'), "empty-unable-to-connect-svg-path" => image_path('illustrations/monitoring/unable_to_connect.svg'),
"metrics-endpoint" => additional_metrics_project_environment_path(project, environment, format: :json), "metrics-endpoint" => additional_metrics_project_environment_path(project, environment, format: :json),
"dashboard-endpoint" => metrics_dashboard_project_environment_path(project, environment, format: :json), "dashboard-endpoint" => metrics_dashboard_project_environment_path(project, environment, format: :json),
......
---
title: Add empty region when group metrics are missing
merge_request: 20900
author:
type: fixed
...@@ -77,14 +77,31 @@ describe('Dashboard', () => { ...@@ -77,14 +77,31 @@ describe('Dashboard', () => {
} }
describe('add custom metrics', () => { describe('add custom metrics', () => {
const defaultProps = {
customMetricsPath: '/endpoint',
hasMetrics: true,
documentationPath: '/path/to/docs',
settingsPath: '/path/to/settings',
clustersPath: '/path/to/clusters',
projectPath: '/path/to/project',
metricsEndpoint: mockApiEndpoint,
tagsPath: '/path/to/tags',
emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
emptyLoadingSvgPath: '/path/to/loading.svg',
emptyNoDataSvgPath: '/path/to/no-data.svg',
emptyNoDataSmallSvgPath: '/path/to/no-data-small.svg',
emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
environmentsEndpoint: '/root/hello-prometheus/environments/35',
currentEnvironmentName: 'production',
prometheusAlertsAvailable: true,
alertsEndpoint: '/endpoint',
};
describe('when not available', () => { describe('when not available', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
customMetricsAvailable: false, customMetricsAvailable: false,
customMetricsPath: '/endpoint', ...defaultProps,
hasMetrics: true,
prometheusAlertsAvailable: true,
alertsEndpoint: '/endpoint',
}); });
}); });
...@@ -101,10 +118,7 @@ describe('Dashboard', () => { ...@@ -101,10 +118,7 @@ describe('Dashboard', () => {
createComponent({ createComponent({
customMetricsAvailable: true, customMetricsAvailable: true,
customMetricsPath: '/endpoint', ...defaultProps,
hasMetrics: true,
prometheusAlertsAvailable: true,
alertsEndpoint: '/endpoint',
}); });
setupComponentStore(wrapper); setupComponentStore(wrapper);
......
...@@ -11157,6 +11157,9 @@ msgstr "" ...@@ -11157,6 +11157,9 @@ msgstr ""
msgid "Metrics|There was an error while retrieving metrics" msgid "Metrics|There was an error while retrieving metrics"
msgstr "" msgstr ""
msgid "Metrics|There was an error while retrieving metrics. %{message}"
msgstr ""
msgid "Metrics|Unexpected deployment data response from prometheus endpoint" msgid "Metrics|Unexpected deployment data response from prometheus endpoint"
msgstr "" msgstr ""
...@@ -17478,6 +17481,9 @@ msgstr "" ...@@ -17478,6 +17481,9 @@ msgstr ""
msgid "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository." msgid "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository."
msgstr "" msgstr ""
msgid "The data source is connected, but there is no data to display."
msgstr ""
msgid "The default CI configuration path for new projects." msgid "The default CI configuration path for new projects."
msgstr "" msgstr ""
......
...@@ -45,10 +45,11 @@ describe('Time series component', () => { ...@@ -45,10 +45,11 @@ describe('Time series component', () => {
store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData); store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
// Mock data contains 2 panels, pick the first one // Mock data contains 2 panel groups, with 1 and 2 panels respectively
store.commit(`monitoringDashboard/${types.SET_QUERY_RESULT}`, mockedQueryResultPayload); store.commit(`monitoringDashboard/${types.SET_QUERY_RESULT}`, mockedQueryResultPayload);
[mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[0].panels; // Pick the second panel group and the first panel in it
[mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[1].panels;
makeTimeSeriesChart = (graphData, type) => makeTimeSeriesChart = (graphData, type) =>
shallowMount(TimeSeries, { shallowMount(TimeSeries, {
......
...@@ -11,6 +11,7 @@ function createComponent(props) { ...@@ -11,6 +11,7 @@ function createComponent(props) {
emptyGettingStartedSvgPath: '/path/to/getting-started.svg', emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
emptyLoadingSvgPath: '/path/to/loading.svg', emptyLoadingSvgPath: '/path/to/loading.svg',
emptyNoDataSvgPath: '/path/to/no-data.svg', emptyNoDataSvgPath: '/path/to/no-data.svg',
emptyNoDataSmallSvgPath: '/path/to/no-data-small.svg',
emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg', emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
}, },
}); });
......
...@@ -12,6 +12,7 @@ describe('Embed', () => { ...@@ -12,6 +12,7 @@ describe('Embed', () => {
let wrapper; let wrapper;
let store; let store;
let actions; let actions;
let metricsWithDataGetter;
function mountComponent() { function mountComponent() {
wrapper = shallowMount(Embed, { wrapper = shallowMount(Embed, {
...@@ -31,11 +32,16 @@ describe('Embed', () => { ...@@ -31,11 +32,16 @@ describe('Embed', () => {
fetchMetricsData: () => {}, fetchMetricsData: () => {},
}; };
metricsWithDataGetter = jest.fn();
store = new Vuex.Store({ store = new Vuex.Store({
modules: { modules: {
monitoringDashboard: { monitoringDashboard: {
namespaced: true, namespaced: true,
actions, actions,
getters: {
metricsWithData: () => metricsWithDataGetter,
},
state: initialState, state: initialState,
}, },
}, },
...@@ -43,6 +49,7 @@ describe('Embed', () => { ...@@ -43,6 +49,7 @@ describe('Embed', () => {
}); });
afterEach(() => { afterEach(() => {
metricsWithDataGetter.mockClear();
if (wrapper) { if (wrapper) {
wrapper.destroy(); wrapper.destroy();
} }
...@@ -63,13 +70,13 @@ describe('Embed', () => { ...@@ -63,13 +70,13 @@ describe('Embed', () => {
beforeEach(() => { beforeEach(() => {
store.state.monitoringDashboard.dashboard.panel_groups = groups; store.state.monitoringDashboard.dashboard.panel_groups = groups;
store.state.monitoringDashboard.dashboard.panel_groups[0].panels = metricsData; store.state.monitoringDashboard.dashboard.panel_groups[0].panels = metricsData;
store.state.monitoringDashboard.metricsWithData = metricsWithData;
metricsWithDataGetter.mockReturnValue(metricsWithData);
mountComponent(); mountComponent();
}); });
it('shows a chart when metrics are present', () => { it('shows a chart when metrics are present', () => {
wrapper.setProps({});
expect(wrapper.find('.metrics-embed').exists()).toBe(true); expect(wrapper.find('.metrics-embed').exists()).toBe(true);
expect(wrapper.find(PanelType).exists()).toBe(true); expect(wrapper.find(PanelType).exists()).toBe(true);
expect(wrapper.findAll(PanelType).length).toBe(2); expect(wrapper.findAll(PanelType).length).toBe(2);
......
...@@ -75,11 +75,9 @@ export const metricsData = [ ...@@ -75,11 +75,9 @@ export const metricsData = [
}, },
]; ];
export const initialState = { export const initialState = () => ({
monitoringDashboard: {},
dashboard: { dashboard: {
panel_groups: [], panel_groups: [],
}, },
metricsWithData: [],
useDashboardEndpoint: true, useDashboardEndpoint: true,
}; });
...@@ -240,6 +240,11 @@ export const metricsNewGroupsAPIResponse = [ ...@@ -240,6 +240,11 @@ export const metricsNewGroupsAPIResponse = [
}, },
]; ];
export const mockedEmptyResult = {
metricId: '1_response_metrics_nginx_ingress_throughput_status_code',
result: [],
};
export const mockedQueryResultPayload = { export const mockedQueryResultPayload = {
metricId: '17_system_metrics_kubernetes_container_memory_average', metricId: '17_system_metrics_kubernetes_container_memory_average',
result: [ result: [
...@@ -327,6 +332,30 @@ export const mockedQueryResultPayloadCoresTotal = { ...@@ -327,6 +332,30 @@ export const mockedQueryResultPayloadCoresTotal = {
}; };
export const metricsGroupsAPIResponse = [ export const metricsGroupsAPIResponse = [
{
group: 'Response metrics (NGINX Ingress VTS)',
priority: 10,
panels: [
{
metrics: [
{
id: 'response_metrics_nginx_ingress_throughput_status_code',
label: 'Status Code',
metric_id: 1,
prometheus_endpoint_path:
'/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29',
query_range:
'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)',
unit: 'req / sec',
},
],
title: 'Throughput',
type: 'area-chart',
weight: 1,
y_label: 'Requests / Sec',
},
],
},
{ {
group: 'System metrics (Kubernetes)', group: 'System metrics (Kubernetes)',
priority: 5, priority: 5,
......
...@@ -191,12 +191,11 @@ describe('Monitoring store actions', () => { ...@@ -191,12 +191,11 @@ describe('Monitoring store actions', () => {
let state; let state;
const response = metricsDashboardResponse; const response = metricsDashboardResponse;
beforeEach(() => { beforeEach(() => {
jest.spyOn(Tracking, 'event');
dispatch = jest.fn(); dispatch = jest.fn();
state = storeState(); state = storeState();
state.dashboardEndpoint = '/dashboard'; state.dashboardEndpoint = '/dashboard';
}); });
it('dispatches receive and success actions', done => { it('on success, dispatches receive and success actions', done => {
const params = {}; const params = {};
document.body.dataset.page = 'projects:environments:metrics'; document.body.dataset.page = 'projects:environments:metrics';
mock.onGet(state.dashboardEndpoint).reply(200, response); mock.onGet(state.dashboardEndpoint).reply(200, response);
...@@ -213,39 +212,65 @@ describe('Monitoring store actions', () => { ...@@ -213,39 +212,65 @@ describe('Monitoring store actions', () => {
response, response,
params, params,
}); });
})
.then(() => {
expect(Tracking.event).toHaveBeenCalledWith(
document.body.dataset.page,
'dashboard_fetch',
{
label: 'custom_metrics_dashboard',
property: 'count',
value: 0,
},
);
done(); done();
}) })
.catch(done.fail); .catch(done.fail);
}); });
it('dispatches failure action', done => {
const params = {}; describe('on failure', () => {
mock.onGet(state.dashboardEndpoint).reply(500); let result;
fetchDashboard( let errorResponse;
{ beforeEach(() => {
state, const params = {};
dispatch, result = () => {
}, mock.onGet(state.dashboardEndpoint).replyOnce(500, errorResponse);
params, return fetchDashboard({ state, dispatch }, params);
) };
.then(() => { });
expect(dispatch).toHaveBeenCalledWith(
'receiveMetricsDashboardFailure', it('dispatches a failure action', done => {
new Error('Request failed with status code 500'), errorResponse = {};
); result()
done(); .then(() => {
}) expect(dispatch).toHaveBeenCalledWith(
.catch(done.fail); 'receiveMetricsDashboardFailure',
new Error('Request failed with status code 500'),
);
expect(createFlash).toHaveBeenCalled();
done();
})
.catch(done.fail);
});
it('dispatches a failure action when a message is returned', done => {
const message = 'Something went wrong with Prometheus!';
errorResponse = { message };
result()
.then(() => {
expect(dispatch).toHaveBeenCalledWith(
'receiveMetricsDashboardFailure',
new Error('Request failed with status code 500'),
);
expect(createFlash).toHaveBeenCalledWith(expect.stringContaining(message));
done();
})
.catch(done.fail);
});
it('does not show a flash error when showErrorBanner is disabled', done => {
state.showErrorBanner = false;
result()
.then(() => {
expect(dispatch).toHaveBeenCalledWith(
'receiveMetricsDashboardFailure',
new Error('Request failed with status code 500'),
);
expect(createFlash).not.toHaveBeenCalled();
done();
})
.catch(done.fail);
});
}); });
}); });
describe('receiveMetricsDashboardSuccess', () => { describe('receiveMetricsDashboardSuccess', () => {
...@@ -317,18 +342,33 @@ describe('Monitoring store actions', () => { ...@@ -317,18 +342,33 @@ describe('Monitoring store actions', () => {
}); });
}); });
describe('fetchPrometheusMetrics', () => { describe('fetchPrometheusMetrics', () => {
const params = {};
let commit; let commit;
let dispatch; let dispatch;
let state;
beforeEach(() => { beforeEach(() => {
jest.spyOn(Tracking, 'event');
commit = jest.fn(); commit = jest.fn();
dispatch = jest.fn(); dispatch = jest.fn();
state = storeState();
}); });
it('commits empty state when state.groups is empty', done => { it('commits empty state when state.groups is empty', done => {
const state = storeState(); const getters = {
const params = {}; metricsWithData: () => [],
fetchPrometheusMetrics({ state, commit, dispatch }, params) };
fetchPrometheusMetrics({ state, commit, dispatch, getters }, params)
.then(() => { .then(() => {
expect(commit).toHaveBeenCalledWith(types.SET_NO_DATA_EMPTY_STATE); expect(Tracking.event).toHaveBeenCalledWith(
document.body.dataset.page,
'dashboard_fetch',
{
label: 'custom_metrics_dashboard',
property: 'count',
value: 0,
},
);
expect(dispatch).not.toHaveBeenCalled(); expect(dispatch).not.toHaveBeenCalled();
expect(createFlash).not.toHaveBeenCalled(); expect(createFlash).not.toHaveBeenCalled();
done(); done();
...@@ -336,19 +376,28 @@ describe('Monitoring store actions', () => { ...@@ -336,19 +376,28 @@ describe('Monitoring store actions', () => {
.catch(done.fail); .catch(done.fail);
}); });
it('dispatches fetchPrometheusMetric for each panel query', done => { it('dispatches fetchPrometheusMetric for each panel query', done => {
const params = {};
const state = storeState();
state.dashboard.panel_groups = metricsDashboardResponse.dashboard.panel_groups; state.dashboard.panel_groups = metricsDashboardResponse.dashboard.panel_groups;
const metric = state.dashboard.panel_groups[0].panels[0].metrics[0]; const [metric] = state.dashboard.panel_groups[0].panels[0].metrics;
fetchPrometheusMetrics({ state, commit, dispatch }, params) const getters = {
metricsWithData: () => [metric.id],
};
fetchPrometheusMetrics({ state, commit, dispatch, getters }, params)
.then(() => { .then(() => {
expect(dispatch).toHaveBeenCalledTimes(3);
expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', { expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
metric, metric,
params, params,
}); });
expect(createFlash).not.toHaveBeenCalled(); expect(Tracking.event).toHaveBeenCalledWith(
document.body.dataset.page,
'dashboard_fetch',
{
label: 'custom_metrics_dashboard',
property: 'count',
value: 1,
},
);
done(); done();
}) })
...@@ -357,8 +406,6 @@ describe('Monitoring store actions', () => { ...@@ -357,8 +406,6 @@ describe('Monitoring store actions', () => {
}); });
it('dispatches fetchPrometheusMetric for each panel query, handles an error', done => { it('dispatches fetchPrometheusMetric for each panel query, handles an error', done => {
const params = {};
const state = storeState();
state.dashboard.panel_groups = metricsDashboardResponse.dashboard.panel_groups; state.dashboard.panel_groups = metricsDashboardResponse.dashboard.panel_groups;
const metric = state.dashboard.panel_groups[0].panels[0].metrics[0]; const metric = state.dashboard.panel_groups[0].panels[0].metrics[0];
......
import * as getters from '~/monitoring/stores/getters';
import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import {
metricsGroupsAPIResponse,
mockedEmptyResult,
mockedQueryResultPayload,
mockedQueryResultPayloadCoresTotal,
} from '../mock_data';
describe('Monitoring store Getters', () => {
describe('metricsWithData', () => {
let metricsWithData;
let setupState;
let state;
beforeEach(() => {
setupState = (initState = {}) => {
state = initState;
metricsWithData = getters.metricsWithData(state);
};
});
afterEach(() => {
state = null;
});
it('has method-style access', () => {
setupState();
expect(metricsWithData).toEqual(expect.any(Function));
});
it('when dashboard has no panel groups, returns empty', () => {
setupState({
dashboard: {
panel_groups: [],
},
});
expect(metricsWithData()).toEqual([]);
});
describe('when the dashboard is set', () => {
beforeEach(() => {
setupState({
dashboard: { panel_groups: [] },
});
});
it('no loaded metric returns empty', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
expect(metricsWithData()).toEqual([]);
});
it('an empty metric, returns empty', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.SET_QUERY_RESULT](state, mockedEmptyResult);
expect(metricsWithData()).toEqual([]);
});
it('a metric with results, it returns a metric', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.SET_QUERY_RESULT](state, mockedQueryResultPayload);
expect(metricsWithData()).toEqual([mockedQueryResultPayload.metricId]);
});
it('multiple metrics with results, it return multiple metrics', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.SET_QUERY_RESULT](state, mockedQueryResultPayload);
mutations[types.SET_QUERY_RESULT](state, mockedQueryResultPayloadCoresTotal);
expect(metricsWithData()).toEqual([
mockedQueryResultPayload.metricId,
mockedQueryResultPayloadCoresTotal.metricId,
]);
});
it('multiple metrics with results, it returns metrics filtered by group', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.SET_QUERY_RESULT](state, mockedQueryResultPayload);
mutations[types.SET_QUERY_RESULT](state, mockedQueryResultPayloadCoresTotal);
// First group has no metrics
expect(metricsWithData(state.dashboard.panel_groups[0].key)).toEqual([]);
// Second group has metrics
expect(metricsWithData(state.dashboard.panel_groups[1].key)).toEqual([
mockedQueryResultPayload.metricId,
mockedQueryResultPayloadCoresTotal.metricId,
]);
});
});
});
});
...@@ -7,41 +7,59 @@ import { ...@@ -7,41 +7,59 @@ import {
metricsDashboardResponse, metricsDashboardResponse,
dashboardGitResponse, dashboardGitResponse,
} from '../mock_data'; } from '../mock_data';
import { uniqMetricsId } from '~/monitoring/stores/utils';
describe('Monitoring mutations', () => { describe('Monitoring mutations', () => {
let stateCopy; let stateCopy;
beforeEach(() => { beforeEach(() => {
stateCopy = state(); stateCopy = state();
}); });
describe('RECEIVE_METRICS_DATA_SUCCESS', () => { describe('RECEIVE_METRICS_DATA_SUCCESS', () => {
let groups; let payload;
const getGroups = () => stateCopy.dashboard.panel_groups;
beforeEach(() => { beforeEach(() => {
stateCopy.dashboard.panel_groups = []; stateCopy.dashboard.panel_groups = [];
groups = metricsGroupsAPIResponse; payload = metricsGroupsAPIResponse;
}); });
it('adds a key to the group', () => { it('adds a key to the group', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups); mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload);
expect(stateCopy.dashboard.panel_groups[0].key).toBe('system-metrics-kubernetes--0'); const groups = getGroups();
expect(groups[0].key).toBe('response-metrics-nginx-ingress-vts--0');
expect(groups[1].key).toBe('system-metrics-kubernetes--1');
}); });
it('normalizes values', () => { it('normalizes values', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups); mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload);
const expectedLabel = 'Pod average'; const expectedLabel = 'Pod average';
const { label, query_range } = stateCopy.dashboard.panel_groups[0].panels[0].metrics[0]; const { label, query_range } = getGroups()[1].panels[0].metrics[0];
expect(label).toEqual(expectedLabel); expect(label).toEqual(expectedLabel);
expect(query_range.length).toBeGreaterThan(0); expect(query_range.length).toBeGreaterThan(0);
}); });
it('contains one group, which it has two panels and one metrics property', () => { it('contains two groups, with panels with a metric each', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups); mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload);
expect(stateCopy.dashboard.panel_groups).toBeDefined();
expect(stateCopy.dashboard.panel_groups.length).toEqual(1); const groups = getGroups();
expect(stateCopy.dashboard.panel_groups[0].panels.length).toEqual(2);
expect(stateCopy.dashboard.panel_groups[0].panels[0].metrics.length).toEqual(1); expect(groups).toBeDefined();
expect(stateCopy.dashboard.panel_groups[0].panels[1].metrics.length).toEqual(1); expect(groups).toHaveLength(2);
expect(groups[0].panels).toHaveLength(1);
expect(groups[0].panels[0].metrics).toHaveLength(1);
expect(groups[1].panels).toHaveLength(2);
expect(groups[1].panels[0].metrics).toHaveLength(1);
expect(groups[1].panels[1].metrics).toHaveLength(1);
}); });
it('assigns metrics a metric id', () => { it('assigns metrics a metric id', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups); mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload);
expect(stateCopy.dashboard.panel_groups[0].panels[0].metrics[0].metricId).toEqual(
const groups = getGroups();
expect(groups[0].panels[0].metrics[0].metricId).toEqual(
'1_response_metrics_nginx_ingress_throughput_status_code',
);
expect(groups[1].panels[0].metrics[0].metricId).toEqual(
'17_system_metrics_kubernetes_container_memory_average', '17_system_metrics_kubernetes_container_memory_average',
); );
}); });
...@@ -52,7 +70,7 @@ describe('Monitoring mutations', () => { ...@@ -52,7 +70,7 @@ describe('Monitoring mutations', () => {
stateCopy.deploymentData = []; stateCopy.deploymentData = [];
mutations[types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS](stateCopy, deploymentData); mutations[types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS](stateCopy, deploymentData);
expect(stateCopy.deploymentData).toBeDefined(); expect(stateCopy.deploymentData).toBeDefined();
expect(stateCopy.deploymentData.length).toEqual(3); expect(stateCopy.deploymentData).toHaveLength(3);
expect(typeof stateCopy.deploymentData[0]).toEqual('object'); expect(typeof stateCopy.deploymentData[0]).toEqual('object');
}); });
}); });
...@@ -73,41 +91,38 @@ describe('Monitoring mutations', () => { ...@@ -73,41 +91,38 @@ describe('Monitoring mutations', () => {
}); });
}); });
describe('SET_QUERY_RESULT', () => { describe('SET_QUERY_RESULT', () => {
const metricId = 12; const metricId = '12_system_metrics_kubernetes_container_memory_total';
const id = 'system_metrics_kubernetes_container_memory_total';
const result = [ const result = [
{ {
values: [[0, 1], [1, 1], [1, 3]], values: [[0, 1], [1, 1], [1, 3]],
}, },
]; ];
const dashboardGroups = metricsDashboardResponse.dashboard.panel_groups;
const getMetrics = () => stateCopy.dashboard.panel_groups[0].panels[0].metrics;
beforeEach(() => { beforeEach(() => {
const dashboardGroups = metricsDashboardResponse.dashboard.panel_groups;
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups); mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups);
}); });
it('clears empty state', () => { it('clears empty state', () => {
expect(stateCopy.showEmptyState).toBe(true);
mutations[types.SET_QUERY_RESULT](stateCopy, { mutations[types.SET_QUERY_RESULT](stateCopy, {
metricId, metricId,
result, result,
}); });
expect(stateCopy.showEmptyState).toBe(false); expect(stateCopy.showEmptyState).toBe(false);
}); });
it('sets metricsWithData value', () => {
const uniqId = uniqMetricsId({ it('adds results to the store', () => {
metric_id: metricId, expect(getMetrics()[0].result).toBe(undefined);
id,
});
mutations[types.SET_QUERY_RESULT](stateCopy, {
metricId: uniqId,
result,
});
expect(stateCopy.metricsWithData).toEqual([uniqId]);
});
it('does not store empty results', () => {
mutations[types.SET_QUERY_RESULT](stateCopy, { mutations[types.SET_QUERY_RESULT](stateCopy, {
metricId, metricId,
result: [], result,
}); });
expect(stateCopy.metricsWithData).toEqual([]);
expect(getMetrics()[0].result).toHaveLength(result.length);
}); });
}); });
describe('SET_ALL_DASHBOARDS', () => { describe('SET_ALL_DASHBOARDS', () => {
......
...@@ -4,11 +4,13 @@ import { GlToast } from '@gitlab/ui'; ...@@ -4,11 +4,13 @@ import { GlToast } from '@gitlab/ui';
import VueDraggable from 'vuedraggable'; import VueDraggable from 'vuedraggable';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import Dashboard from '~/monitoring/components/dashboard.vue'; import Dashboard from '~/monitoring/components/dashboard.vue';
import EmptyState from '~/monitoring/components/empty_state.vue';
import * as types from '~/monitoring/stores/mutation_types'; import * as types from '~/monitoring/stores/mutation_types';
import { createStore } from '~/monitoring/stores'; import { createStore } from '~/monitoring/stores';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { import {
metricsGroupsAPIResponse, metricsGroupsAPIResponse,
mockedEmptyResult,
mockedQueryResultPayload, mockedQueryResultPayload,
mockedQueryResultPayloadCoresTotal, mockedQueryResultPayloadCoresTotal,
mockApiEndpoint, mockApiEndpoint,
...@@ -29,6 +31,7 @@ const propsData = { ...@@ -29,6 +31,7 @@ const propsData = {
emptyGettingStartedSvgPath: '/path/to/getting-started.svg', emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
emptyLoadingSvgPath: '/path/to/loading.svg', emptyLoadingSvgPath: '/path/to/loading.svg',
emptyNoDataSvgPath: '/path/to/no-data.svg', emptyNoDataSvgPath: '/path/to/no-data.svg',
emptyNoDataSmallSvgPath: '/path/to/no-data-small.svg',
emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg', emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
environmentsEndpoint: '/root/hello-prometheus/environments/35', environmentsEndpoint: '/root/hello-prometheus/environments/35',
currentEnvironmentName: 'production', currentEnvironmentName: 'production',
...@@ -43,15 +46,17 @@ const resetSpy = spy => { ...@@ -43,15 +46,17 @@ const resetSpy = spy => {
} }
}; };
export default propsData; let expectedPanelCount;
function setupComponentStore(component) { function setupComponentStore(component) {
// Load 2 panel groups
component.$store.commit( component.$store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
metricsGroupsAPIResponse, metricsGroupsAPIResponse,
); );
// Load 2 panels to the dashboard // Load 3 panels to the dashboard, one with an empty result
component.$store.commit(`monitoringDashboard/${types.SET_QUERY_RESULT}`, mockedEmptyResult);
component.$store.commit( component.$store.commit(
`monitoringDashboard/${types.SET_QUERY_RESULT}`, `monitoringDashboard/${types.SET_QUERY_RESULT}`,
mockedQueryResultPayload, mockedQueryResultPayload,
...@@ -61,6 +66,8 @@ function setupComponentStore(component) { ...@@ -61,6 +66,8 @@ function setupComponentStore(component) {
mockedQueryResultPayloadCoresTotal, mockedQueryResultPayloadCoresTotal,
); );
expectedPanelCount = 2;
component.$store.commit( component.$store.commit(
`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
environmentData, environmentData,
...@@ -126,13 +133,9 @@ describe('Dashboard', () => { ...@@ -126,13 +133,9 @@ describe('Dashboard', () => {
describe('no data found', () => { describe('no data found', () => {
it('shows the environment selector dropdown', () => { it('shows the environment selector dropdown', () => {
component = new DashboardComponent({ createComponentWrapper();
el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, showEmptyState: true },
store,
});
expect(component.$el.querySelector('.js-environments-dropdown')).toBeTruthy(); expect(wrapper.find('.js-environments-dropdown').exists()).toBeTruthy();
}); });
}); });
...@@ -389,9 +392,36 @@ describe('Dashboard', () => { ...@@ -389,9 +392,36 @@ describe('Dashboard', () => {
}); });
}); });
describe('drag and drop function', () => { describe('when one of the metrics is missing', () => {
let expectedPanelCount; // also called metrics, naming to be improved: https://gitlab.com/gitlab-org/gitlab/issues/31565 beforeEach(() => {
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
});
beforeEach(done => {
createComponentWrapper({ hasMetrics: true }, { attachToDocument: true });
setupComponentStore(wrapper.vm);
wrapper.vm.$nextTick(done);
});
it('shows a group empty area', () => {
const emptyGroup = wrapper.findAll({ ref: 'empty-group' });
expect(emptyGroup).toHaveLength(1);
expect(emptyGroup.is(EmptyState)).toBe(true);
});
it('group empty area displays a "noDataGroup"', () => {
expect(
wrapper
.findAll({ ref: 'empty-group' })
.at(0)
.props('selectedState'),
).toEqual('noDataGroup');
});
});
describe('drag and drop function', () => {
const findDraggables = () => wrapper.findAll(VueDraggable); const findDraggables = () => wrapper.findAll(VueDraggable);
const findEnabledDraggables = () => findDraggables().filter(f => !f.attributes('disabled')); const findEnabledDraggables = () => findDraggables().filter(f => !f.attributes('disabled'));
const findDraggablePanels = () => wrapper.findAll('.js-draggable-panel'); const findDraggablePanels = () => wrapper.findAll('.js-draggable-panel');
...@@ -399,10 +429,6 @@ describe('Dashboard', () => { ...@@ -399,10 +429,6 @@ describe('Dashboard', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse); mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
expectedPanelCount = metricsGroupsAPIResponse.reduce(
(acc, group) => group.panels.length + acc,
0,
);
}); });
beforeEach(done => { beforeEach(done => {
...@@ -417,10 +443,6 @@ describe('Dashboard', () => { ...@@ -417,10 +443,6 @@ describe('Dashboard', () => {
wrapper.destroy(); wrapper.destroy();
}); });
afterEach(() => {
wrapper.destroy();
});
it('wraps vuedraggable', () => { it('wraps vuedraggable', () => {
expect(findDraggablePanels().exists()).toBe(true); expect(findDraggablePanels().exists()).toBe(true);
expect(findDraggablePanels().length).toEqual(expectedPanelCount); expect(findDraggablePanels().length).toEqual(expectedPanelCount);
...@@ -459,22 +481,20 @@ describe('Dashboard', () => { ...@@ -459,22 +481,20 @@ describe('Dashboard', () => {
it('metrics can be swapped', done => { it('metrics can be swapped', done => {
const firstDraggable = findDraggables().at(0); const firstDraggable = findDraggables().at(0);
const mockMetrics = [...metricsGroupsAPIResponse[0].panels]; const mockMetrics = [...metricsGroupsAPIResponse[1].panels];
const value = () => firstDraggable.props('value');
expect(value().length).toBe(mockMetrics.length); const firstTitle = mockMetrics[0].title;
value().forEach((metric, i) => { const secondTitle = mockMetrics[1].title;
expect(metric.title).toBe(mockMetrics[i].title);
});
// swap two elements and `input` them // swap two elements and `input` them
[mockMetrics[0], mockMetrics[1]] = [mockMetrics[1], mockMetrics[0]]; [mockMetrics[0], mockMetrics[1]] = [mockMetrics[1], mockMetrics[0]];
firstDraggable.vm.$emit('input', mockMetrics); firstDraggable.vm.$emit('input', mockMetrics);
firstDraggable.vm.$nextTick(() => { wrapper.vm.$nextTick(() => {
value().forEach((metric, i) => { const { panels } = wrapper.vm.dashboard.panel_groups[1];
expect(metric.title).toBe(mockMetrics[i].title);
}); expect(panels[1].title).toEqual(firstTitle);
expect(panels[0].title).toEqual(secondTitle);
done(); done();
}); });
}); });
...@@ -584,7 +604,7 @@ describe('Dashboard', () => { ...@@ -584,7 +604,7 @@ describe('Dashboard', () => {
setupComponentStore(component); setupComponentStore(component);
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
promPanel = component.$el.querySelector('.prometheus-panel'); [, promPanel] = component.$el.querySelectorAll('.prometheus-panel');
promGroup = promPanel.querySelector('.prometheus-graph-group'); promGroup = promPanel.querySelector('.prometheus-graph-group');
panelToggle = promPanel.querySelector('.js-graph-group-toggle'); panelToggle = promPanel.querySelector('.js-graph-group-toggle');
chart = promGroup.querySelector('.position-relative svg'); chart = promGroup.querySelector('.position-relative svg');
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import GraphGroup from '~/monitoring/components/graph_group.vue'; import GraphGroup from '~/monitoring/components/graph_group.vue';
import Icon from '~/vue_shared/components/icon.vue';
const localVue = createLocalVue(); const localVue = createLocalVue();
describe('Graph group component', () => { describe('Graph group component', () => {
let graphGroup; let wrapper;
const findPrometheusGroup = () => graphGroup.find('.prometheus-graph-group'); const findGroup = () => wrapper.find({ ref: 'graph-group' });
const findPrometheusPanel = () => graphGroup.find('.prometheus-panel'); const findContent = () => wrapper.find({ ref: 'graph-group-content' });
const findCaretIcon = () => wrapper.find(Icon);
const createComponent = propsData => { const createComponent = propsData => {
graphGroup = shallowMount(localVue.extend(GraphGroup), { wrapper = shallowMount(localVue.extend(GraphGroup), {
propsData, propsData,
sync: false, sync: false,
localVue, localVue,
...@@ -18,57 +20,100 @@ describe('Graph group component', () => { ...@@ -18,57 +20,100 @@ describe('Graph group component', () => {
}; };
afterEach(() => { afterEach(() => {
graphGroup.destroy(); wrapper.destroy();
}); });
describe('When groups can be collapsed', () => { describe('When group is not collapsed', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
name: 'panel', name: 'panel',
collapseGroup: true, collapseGroup: false,
}); });
}); });
it('should show the angle-down caret icon when collapseGroup is true', () => { it('should show the angle-down caret icon', () => {
expect(graphGroup.vm.caretIcon).toBe('angle-down'); expect(findContent().isVisible()).toBe(true);
expect(findCaretIcon().props('name')).toBe('angle-down');
}); });
it('should show the angle-right caret icon when collapseGroup is false', () => { it('should show the angle-right caret icon when the user collapses the group', done => {
graphGroup.vm.collapse(); wrapper.vm.collapse();
expect(graphGroup.vm.caretIcon).toBe('angle-right'); wrapper.vm.$nextTick(() => {
expect(findContent().isVisible()).toBe(false);
expect(findCaretIcon().props('name')).toBe('angle-right');
done();
});
}); });
});
describe('When groups can not be collapsed', () => { it('should show the open the group when collapseGroup is set to true', done => {
beforeEach(() => { wrapper.setProps({
createComponent({
name: 'panel',
collapseGroup: true, collapseGroup: true,
showPanels: false, });
wrapper.vm.$nextTick(() => {
expect(findContent().isVisible()).toBe(true);
expect(findCaretIcon().props('name')).toBe('angle-down');
done();
}); });
}); });
it('should not contain a prometheus-panel container when showPanels is false', () => { describe('When group is collapsed', () => {
expect(findPrometheusPanel().exists()).toBe(false); beforeEach(() => {
createComponent({
name: 'panel',
collapseGroup: true,
});
});
it('should show the angle-down caret icon when collapseGroup is true', () => {
expect(wrapper.vm.caretIcon).toBe('angle-right');
});
it('should show the angle-right caret icon when collapseGroup is false', () => {
wrapper.vm.collapse();
expect(wrapper.vm.caretIcon).toBe('angle-down');
});
}); });
});
describe('When collapseGroup prop is updated', () => { describe('When groups can not be collapsed', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ name: 'panel', collapseGroup: false }); createComponent({
name: 'panel',
showPanels: false,
collapseGroup: false,
});
});
it('should not have a container when showPanels is false', () => {
expect(findGroup().exists()).toBe(false);
expect(findContent().exists()).toBe(true);
});
}); });
it('previously collapsed group should respond to the prop change', done => { describe('When group does not show a panel heading', () => {
expect(findPrometheusGroup().exists()).toBe(false); beforeEach(() => {
createComponent({
name: 'panel',
showPanels: false,
collapseGroup: false,
});
});
graphGroup.setProps({ it('should collapse the panel content', () => {
collapseGroup: true, expect(findContent().isVisible()).toBe(true);
expect(findCaretIcon().exists()).toBe(false);
}); });
graphGroup.vm.$nextTick(() => { it('should show the panel content when clicked', done => {
expect(findPrometheusGroup().exists()).toBe(true); wrapper.vm.collapse();
done();
wrapper.vm.$nextTick(() => {
expect(findContent().isVisible()).toBe(true);
expect(findCaretIcon().exists()).toBe(false);
done();
});
}); });
}); });
}); });
......
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