Commit 7b7e0167 authored by Miguel Rincon's avatar Miguel Rincon Committed by Phil Hughes

Add specific error states to dashboard

- Create a new getter to retreive the error state in a group
- Add a component to show empty states in a group
- Add OK state to error states, making it just states
- Add translations and changelog
- Added specs to empty state conditions
- Added links and buttons to empty state
parent 61487a35
......@@ -20,8 +20,10 @@ import invalidUrl from '~/lib/utils/invalid_url';
import DateTimePicker from './date_time_picker/date_time_picker.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import GroupEmptyState from './group_empty_state.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { getTimeDiff, isValidDate, getAddMetricTrackingOptions } from '../utils';
import { metricStates } from '../constants';
export default {
components: {
......@@ -29,6 +31,7 @@ export default {
PanelType,
GraphGroup,
EmptyState,
GroupEmptyState,
Icon,
GlButton,
GlDropdown,
......@@ -184,7 +187,7 @@ export default {
'allDashboards',
'additionalPanelTypesEnabled',
]),
...mapGetters('monitoringDashboard', ['metricsWithData']),
...mapGetters('monitoringDashboard', ['getMetricStates']),
firstDashboard() {
return this.environmentsEndpoint.length > 0 && this.allDashboards.length > 0
? this.allDashboards[0]
......@@ -284,12 +287,35 @@ export default {
submitCustomMetricsForm() {
this.$refs.customMetricsForm.submit();
},
groupHasData(group) {
return this.metricsWithData(group.key).length > 0;
},
onDateTimePickerApply(timeWindowUrlParams) {
return redirectTo(mergeUrlParams(timeWindowUrlParams, window.location.href));
},
/**
* Return a single empty state for a group.
*
* If all states are the same a single state is returned to be displayed
* Except if the state is OK, in which case the group is displayed.
*
* @param {String} groupKey - Identifier for group
* @returns {String} state code from `metricStates`
*/
groupSingleEmptyState(groupKey) {
const states = this.getMetricStates(groupKey);
if (states.length === 1 && states[0] !== metricStates.OK) {
return states[0];
}
return null;
},
/**
* A group should be not collapsed if any metric is loaded (OK)
*
* @param {String} groupKey - Identifier for group
* @returns {Boolean} If the group should be collapsed
*/
collapseGroup(groupKey) {
// Collapse group if no data is available
return !this.getMetricStates(groupKey).includes(metricStates.OK);
},
getAddMetricTrackingOptions,
},
addMetric: {
......@@ -446,9 +472,9 @@ export default {
:key="`${groupData.group}.${groupData.priority}`"
:name="groupData.group"
:show-panels="showPanels"
:collapse-group="!groupHasData(groupData)"
:collapse-group="collapseGroup(groupData.key)"
>
<div v-if="groupHasData(groupData)">
<div v-if="!groupSingleEmptyState(groupData.key)">
<vue-draggable
:value="groupData.panels"
group="metrics-dashboard"
......@@ -487,18 +513,12 @@ export default {
</vue-draggable>
</div>
<div v-else class="py-5 col col-sm-10 col-md-8 col-lg-7 col-xl-6">
<empty-state
<group-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"
:selected-state="groupSingleEmptyState(groupData.key)"
:svg-path="emptyNoDataSmallSvgPath"
/>
</div>
</graph-group>
......
......@@ -84,11 +84,6 @@ export default {
secondaryButtonText: '',
secondaryButtonPath: '',
},
noDataGroup: {
svgUrl: this.emptyNoDataSmallSvgPath,
title: __('No data to display'),
description: __('The data source is connected, but there is no data to display.'),
},
unableToConnect: {
svgUrl: this.emptyUnableToConnectSvgPath,
title: __('Unable to connect to Prometheus server'),
......
<script>
import { __, sprintf } from '~/locale';
import { GlEmptyState } from '@gitlab/ui';
import { metricStates } from '../constants';
export default {
components: {
GlEmptyState,
},
props: {
documentationPath: {
type: String,
required: true,
},
settingsPath: {
type: String,
required: true,
},
selectedState: {
type: String,
required: true,
},
svgPath: {
type: String,
required: true,
},
},
data() {
const documentationLink = `<a href="${this.documentationPath}">${__('More information')}</a>`;
return {
states: {
[metricStates.NO_DATA]: {
title: __('No data to display'),
slottedDescription: sprintf(
__(
'The data source is connected, but there is no data to display. %{documentationLink}',
),
{ documentationLink },
false,
),
},
[metricStates.TIMEOUT]: {
title: __('Connection timed out'),
slottedDescription: sprintf(
__(
"Charts can't be displayed as the request for data has timed out. %{documentationLink}",
),
{ documentationLink },
false,
),
},
[metricStates.CONNECTION_FAILED]: {
title: __('Connection failed'),
description: __(`We couldn't reach the Prometheus server.
Either the server no longer exists or the configuration details need updating.`),
buttonText: __('Verify configuration'),
buttonPath: this.settingsPath,
},
[metricStates.BAD_QUERY]: {
title: __('Query cannot be processed'),
slottedDescription: sprintf(
__(
`The Prometheus server responded with "bad request".
Please check your queries are correct and are supported in your Prometheus version. %{documentationLink}`,
),
{ documentationLink },
false,
),
buttonText: __('Verify configuration'),
buttonPath: this.settingsPath,
},
[metricStates.LOADING]: {
title: __('Waiting for performance data'),
description: __(`Creating graphs uses the data from the Prometheus server.
If this takes a long time, ensure that data is available.`),
},
[metricStates.UNKNOWN_ERROR]: {
title: __('An error has occurred'),
description: __('An error occurred while loading the data. Please try again.'),
},
},
};
},
computed: {
currentState() {
return this.states[this.selectedState] || this.states[metricStates.UNKNOWN_ERROR];
},
},
};
</script>
<template>
<gl-empty-state
:title="currentState.title"
:primary-button-text="currentState.buttonText"
:primary-button-link="currentState.buttonPath"
:description="currentState.description"
:svg-path="svgPath"
:compact="true"
>
<template v-if="currentState.slottedDescription" #description>
<div v-html="currentState.slottedDescription"></div>
</template>
</gl-empty-state>
</template>
......@@ -3,9 +3,19 @@ import { __ } from '~/locale';
export const PROMETHEUS_TIMEOUT = 120000; // TWO_MINUTES
/**
* Errors in Prometheus Queries (PromQL) for metrics
* States and error states in Prometheus Queries (PromQL) for metrics
*/
export const metricsErrors = {
export const metricStates = {
/**
* Metric data is available
*/
OK: 'OK',
/**
* Metric data is being fetched
*/
LOADING: 'LOADING',
/**
* Connection timed out to prometheus server
* the timeout is set to PROMETHEUS_TIMEOUT
......@@ -24,12 +34,12 @@ export const metricsErrors = {
CONNECTION_FAILED: 'CONNECTION_FAILED',
/**
* The prometheus server was reach but it cannot process
* The prometheus server was reached but it cannot process
* the query. This can happen for several reasons:
* - PromQL syntax is incorrect
* - An operator is not supported
*/
BAD_DATA: 'BAD_DATA',
BAD_QUERY: 'BAD_QUERY',
/**
* No specific reason found for error
......
......@@ -132,7 +132,7 @@ export const fetchPrometheusMetric = ({ commit }, { metric, params }) => {
commit(types.RECEIVE_METRIC_RESULT_SUCCESS, { metricId: metric.metric_id, result });
})
.catch(error => {
commit(types.RECEIVE_METRIC_RESULT_ERROR, { metricId: metric.metric_id, error });
commit(types.RECEIVE_METRIC_RESULT_FAILURE, { metricId: metric.metric_id, error });
// Continue to throw error so the dashboard can notify using createFlash
throw error;
});
......
const metricsIdsInPanel = panel =>
panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId);
/**
* Get all state for metric in the dashboard or a group. The
* states are not repeated so the dashboard or group can show
* a global state.
*
* @param {Object} state
* @returns {Function} A function that returns an array of
* states in all the metric in the dashboard or group.
*/
export const getMetricStates = state => groupKey => {
let groups = state.dashboard.panel_groups;
if (groupKey) {
groups = groups.filter(group => group.key === groupKey);
}
const metricStates = groups.reduce((acc, group) => {
group.panels.forEach(panel => {
panel.metrics.forEach(metric => {
if (metric.state) {
acc.push(metric.state);
}
});
});
return acc;
}, []);
// Deduplicate and sort array
return Array.from(new Set(metricStates)).sort();
};
/**
* Getter to obtain the list of metric ids that have data
*
......
......@@ -12,7 +12,7 @@ export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAIL
export const REQUEST_METRIC_RESULT = 'REQUEST_METRIC_RESULT';
export const RECEIVE_METRIC_RESULT_SUCCESS = 'RECEIVE_METRIC_RESULT_SUCCESS';
export const RECEIVE_METRIC_RESULT_ERROR = 'RECEIVE_METRIC_RESULT_ERROR';
export const RECEIVE_METRIC_RESULT_FAILURE = 'RECEIVE_METRIC_RESULT_FAILURE';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS';
......
......@@ -3,7 +3,7 @@ import { slugify } from '~/lib/utils/text_utility';
import * as types from './mutation_types';
import { normalizeMetric, normalizeQueryResult } from './utils';
import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
import { metricsErrors } from '../constants';
import { metricStates } from '../constants';
import httpStatusCodes from '~/lib/utils/http_status';
const normalizePanelMetrics = (metrics, defaultLabel) =>
......@@ -41,39 +41,39 @@ const findMetricInDashboard = (metricId, dashboard) => {
* @param {Object} metric - Metric object as defined in the dashboard
* @param {Object} state - New state
* @param {Array|null} state.result - Array of results
* @param {String} state.error - Error code from metricsErrors
* @param {String} state.error - Error code from metricStates
* @param {Boolean} state.loading - True if the metric is loading
*/
const setMetricState = (metric, { result = null, error = null, loading = false }) => {
const setMetricState = (metric, { result = null, loading = false, state = null }) => {
Vue.set(metric, 'result', result);
Vue.set(metric, 'error', error);
Vue.set(metric, 'loading', loading);
Vue.set(metric, 'state', state);
};
/**
* Maps a backened error state to a `metricsErrors` constant
* Maps a backened error state to a `metricStates` constant
* @param {Object} error - Error from backend response
*/
const getMetricError = error => {
const emptyStateFromError = error => {
if (!error) {
return metricsErrors.UNKNOWN_ERROR;
return metricStates.UNKNOWN_ERROR;
}
// Special error responses
if (error.message === BACKOFF_TIMEOUT) {
return metricsErrors.TIMEOUT;
return metricStates.TIMEOUT;
}
// Axios error responses
const { response } = error;
if (response && response.status === httpStatusCodes.SERVICE_UNAVAILABLE) {
return metricsErrors.CONNECTION_FAILED;
return metricStates.CONNECTION_FAILED;
} else if (response && response.status === httpStatusCodes.BAD_REQUEST) {
// Note: "error.response.data.error" may contain Prometheus error information
return metricsErrors.BAD_DATA;
return metricStates.BAD_QUERY;
}
return metricsErrors.UNKNOWN_ERROR;
return metricStates.UNKNOWN_ERROR;
};
export default {
......@@ -132,9 +132,9 @@ export default {
*/
[types.REQUEST_METRIC_RESULT](state, { metricId }) {
const metric = findMetricInDashboard(metricId, state.dashboard);
setMetricState(metric, {
loading: true,
state: metricStates.LOADING,
});
},
[types.RECEIVE_METRIC_RESULT_SUCCESS](state, { metricId, result }) {
......@@ -146,24 +146,24 @@ export default {
const metric = findMetricInDashboard(metricId, state.dashboard);
if (!result || result.length === 0) {
// If no data is return we still consider it an error and set it to undefined
setMetricState(metric, {
error: metricsErrors.NO_DATA,
state: metricStates.NO_DATA,
});
} else {
const normalizedResults = result.map(normalizeQueryResult);
setMetricState(metric, {
result: Object.freeze(normalizedResults),
state: metricStates.OK,
});
}
},
[types.RECEIVE_METRIC_RESULT_ERROR](state, { metricId, error }) {
[types.RECEIVE_METRIC_RESULT_FAILURE](state, { metricId, error }) {
if (!metricId) {
return;
}
const metric = findMetricInDashboard(metricId, state.dashboard);
setMetricState(metric, {
error: getMetricError(error),
state: emptyStateFromError(error),
});
},
......
---
title: Add specific error states to dashboard
merge_request: 21618
author:
type: added
......@@ -1720,6 +1720,9 @@ msgstr ""
msgid "An error occurred while loading issues"
msgstr ""
msgid "An error occurred while loading the data. Please try again."
msgstr ""
msgid "An error occurred while loading the file"
msgstr ""
......@@ -3151,6 +3154,9 @@ msgstr ""
msgid "Charts"
msgstr ""
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
msgid "Chat"
msgstr ""
......@@ -4716,9 +4722,15 @@ msgstr ""
msgid "Connecting..."
msgstr ""
msgid "Connection failed"
msgstr ""
msgid "Connection failure"
msgstr ""
msgid "Connection timed out"
msgstr ""
msgid "Contact an owner of group %{namespace_name} to upgrade the plan."
msgstr ""
......@@ -14612,6 +14624,9 @@ msgstr ""
msgid "Query"
msgstr ""
msgid "Query cannot be processed"
msgstr ""
msgid "Query is valid"
msgstr ""
......@@ -17717,6 +17732,9 @@ msgstr ""
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
msgstr ""
msgid "The Prometheus server responded with \"bad request\". Please check your queries are correct and are supported in your Prometheus version. %{documentationLink}"
msgstr ""
msgid "The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., \"http://localhost:9200, http://localhost:9201\")."
msgstr ""
......@@ -17747,7 +17765,7 @@ msgstr ""
msgid "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository."
msgstr ""
msgid "The data source is connected, but there is no data to display."
msgid "The data source is connected, but there is no data to display. %{documentationLink}"
msgstr ""
msgid "The default CI configuration path for new projects."
......@@ -19931,6 +19949,9 @@ msgstr ""
msgid "Verify SAML Configuration"
msgstr ""
msgid "Verify configuration"
msgstr ""
msgid "Version"
msgstr ""
......@@ -20140,6 +20161,9 @@ msgstr ""
msgid "We could not determine the path to remove the issue"
msgstr ""
msgid "We couldn't reach the Prometheus server. Either the server no longer exists or the configuration details need updating."
msgstr ""
msgid "We created a short guided tour that will help you learn the basics of GitLab and how it will help you be better at your job. It should only take a couple of minutes. You will be guided by two types of helpers, best recognized by their color."
msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GroupEmptyState Renders an empty state for BAD_QUERY 1`] = `
<glemptystate-stub
compact="true"
primarybuttonlink="/path/to/settings"
primarybuttontext="Verify configuration"
svgpath="/path/to/empty-group-illustration.svg"
title="Query cannot be processed"
/>
`;
exports[`GroupEmptyState Renders an empty state for BAD_QUERY 2`] = `"The Prometheus server responded with \\"bad request\\". Please check your queries are correct and are supported in your Prometheus version. <a href=\\"/path/to/docs\\">More information</a>"`;
exports[`GroupEmptyState Renders an empty state for CONNECTION_FAILED 1`] = `
<glemptystate-stub
compact="true"
description="We couldn't reach the Prometheus server. Either the server no longer exists or the configuration details need updating."
primarybuttonlink="/path/to/settings"
primarybuttontext="Verify configuration"
svgpath="/path/to/empty-group-illustration.svg"
title="Connection failed"
/>
`;
exports[`GroupEmptyState Renders an empty state for CONNECTION_FAILED 2`] = `undefined`;
exports[`GroupEmptyState Renders an empty state for FOO STATE 1`] = `
<glemptystate-stub
compact="true"
description="An error occurred while loading the data. Please try again."
svgpath="/path/to/empty-group-illustration.svg"
title="An error has occurred"
/>
`;
exports[`GroupEmptyState Renders an empty state for FOO STATE 2`] = `undefined`;
exports[`GroupEmptyState Renders an empty state for LOADING 1`] = `
<glemptystate-stub
compact="true"
description="Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available."
svgpath="/path/to/empty-group-illustration.svg"
title="Waiting for performance data"
/>
`;
exports[`GroupEmptyState Renders an empty state for LOADING 2`] = `undefined`;
exports[`GroupEmptyState Renders an empty state for NO_DATA 1`] = `
<glemptystate-stub
compact="true"
svgpath="/path/to/empty-group-illustration.svg"
title="No data to display"
/>
`;
exports[`GroupEmptyState Renders an empty state for NO_DATA 2`] = `"The data source is connected, but there is no data to display. <a href=\\"/path/to/docs\\">More information</a>"`;
exports[`GroupEmptyState Renders an empty state for TIMEOUT 1`] = `
<glemptystate-stub
compact="true"
svgpath="/path/to/empty-group-illustration.svg"
title="Connection timed out"
/>
`;
exports[`GroupEmptyState Renders an empty state for TIMEOUT 2`] = `"Charts can't be displayed as the request for data has timed out. <a href=\\"/path/to/docs\\">More information</a>"`;
exports[`GroupEmptyState Renders an empty state for UNKNOWN_ERROR 1`] = `
<glemptystate-stub
compact="true"
description="An error occurred while loading the data. Please try again."
svgpath="/path/to/empty-group-illustration.svg"
title="An error has occurred"
/>
`;
exports[`GroupEmptyState Renders an empty state for UNKNOWN_ERROR 2`] = `undefined`;
import { shallowMount } from '@vue/test-utils';
import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
import { metricStates } from '~/monitoring/constants';
function createComponent(props) {
return shallowMount(GroupEmptyState, {
propsData: {
...props,
documentationPath: '/path/to/docs',
settingsPath: '/path/to/settings',
svgPath: '/path/to/empty-group-illustration.svg',
},
});
}
describe('GroupEmptyState', () => {
const supportedStates = [
metricStates.NO_DATA,
metricStates.TIMEOUT,
metricStates.CONNECTION_FAILED,
metricStates.BAD_QUERY,
metricStates.LOADING,
metricStates.UNKNOWN_ERROR,
'FOO STATE', // does not fail with unknown states
];
test.each(supportedStates)('Renders an empty state for %s', selectedState => {
const wrapper = createComponent({ selectedState });
expect(wrapper.element).toMatchSnapshot();
// slot is not rendered by the stub, test it separately
expect(wrapper.vm.currentState.slottedDescription).toMatchSnapshot();
});
});
......@@ -529,7 +529,7 @@ describe('Monitoring store actions', () => {
},
},
{
type: types.RECEIVE_METRIC_RESULT_ERROR,
type: types.RECEIVE_METRIC_RESULT_FAILURE,
payload: {
metricId: metric.metric_id,
error,
......
import * as getters from '~/monitoring/stores/getters';
import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import { metricStates } from '~/monitoring/constants';
import {
metricsGroupsAPIResponse,
mockedEmptyResult,
......@@ -10,6 +10,124 @@ import {
} from '../mock_data';
describe('Monitoring store Getters', () => {
describe('getMetricStates', () => {
let setupState;
let state;
let getMetricStates;
beforeEach(() => {
setupState = (initState = {}) => {
state = initState;
getMetricStates = getters.getMetricStates(state);
};
});
it('has method-style access', () => {
setupState();
expect(getMetricStates).toEqual(expect.any(Function));
});
it('when dashboard has no panel groups, returns empty', () => {
setupState({
dashboard: {
panel_groups: [],
},
});
expect(getMetricStates()).toEqual([]);
});
describe('when the dashboard is set', () => {
let groups;
beforeEach(() => {
setupState({
dashboard: { panel_groups: [] },
});
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
groups = state.dashboard.panel_groups;
});
it('no loaded metric returns empty', () => {
expect(getMetricStates()).toEqual([]);
});
it('on an empty metric with no result, returns NO_DATA', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedEmptyResult);
expect(getMetricStates()).toEqual([metricStates.NO_DATA]);
});
it('on a metric with a result, returns OK', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayload);
expect(getMetricStates()).toEqual([metricStates.OK]);
});
it('on a metric with an error, returns an error', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
metricId: groups[0].panels[0].metrics[0].metricId,
});
expect(getMetricStates()).toEqual([metricStates.UNKNOWN_ERROR]);
});
it('on multiple metrics with results, returns OK', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayload);
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayloadCoresTotal);
expect(getMetricStates()).toEqual([metricStates.OK]);
// Filtered by groups
expect(getMetricStates(state.dashboard.panel_groups[0].key)).toEqual([]);
expect(getMetricStates(state.dashboard.panel_groups[1].key)).toEqual([metricStates.OK]);
});
it('on multiple metrics errors', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
metricId: groups[0].panels[0].metrics[0].metricId,
});
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
metricId: groups[1].panels[0].metrics[0].metricId,
});
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
metricId: groups[1].panels[1].metrics[0].metricId,
});
// Entire dashboard fails
expect(getMetricStates()).toEqual([metricStates.UNKNOWN_ERROR]);
expect(getMetricStates(groups[0].key)).toEqual([metricStates.UNKNOWN_ERROR]);
expect(getMetricStates(groups[1].key)).toEqual([metricStates.UNKNOWN_ERROR]);
});
it('on multiple metrics with errors', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
// An success in 1 group
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayload);
// An error in 2 groups
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
metricId: groups[0].panels[0].metrics[0].metricId,
});
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
metricId: groups[1].panels[1].metrics[0].metricId,
});
expect(getMetricStates()).toEqual([metricStates.OK, metricStates.UNKNOWN_ERROR]);
expect(getMetricStates(groups[0].key)).toEqual([metricStates.UNKNOWN_ERROR]);
expect(getMetricStates(groups[1].key)).toEqual([
metricStates.OK,
metricStates.UNKNOWN_ERROR,
]);
});
});
});
describe('metricsWithData', () => {
let metricsWithData;
let setupState;
......
......@@ -3,7 +3,7 @@ import httpStatusCodes from '~/lib/utils/http_status';
import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import state from '~/monitoring/stores/state';
import { metricsErrors } from '~/monitoring/constants';
import { metricStates } from '~/monitoring/constants';
import {
metricsGroupsAPIResponse,
deploymentData,
......@@ -120,7 +120,7 @@ describe('Monitoring mutations', () => {
expect.objectContaining({
loading: true,
result: null,
error: null,
state: metricStates.LOADING,
}),
);
});
......@@ -153,20 +153,20 @@ describe('Monitoring mutations', () => {
expect(getMetric()).toEqual(
expect.objectContaining({
loading: false,
error: null,
state: metricStates.OK,
}),
);
});
});
describe('RECEIVE_METRIC_RESULT_ERROR', () => {
describe('RECEIVE_METRIC_RESULT_FAILURE', () => {
beforeEach(() => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups);
});
it('maintains the loading state when a metric fails', () => {
expect(stateCopy.showEmptyState).toBe(true);
mutations[types.RECEIVE_METRIC_RESULT_ERROR](stateCopy, {
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
metricId,
error: 'an error',
});
......@@ -175,7 +175,7 @@ describe('Monitoring mutations', () => {
});
it('stores a timeout error in a metric', () => {
mutations[types.RECEIVE_METRIC_RESULT_ERROR](stateCopy, {
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
metricId,
error: { message: 'BACKOFF_TIMEOUT' },
});
......@@ -184,13 +184,13 @@ describe('Monitoring mutations', () => {
expect.objectContaining({
loading: false,
result: null,
error: metricsErrors.TIMEOUT,
state: metricStates.TIMEOUT,
}),
);
});
it('stores a connection failed error in a metric', () => {
mutations[types.RECEIVE_METRIC_RESULT_ERROR](stateCopy, {
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
metricId,
error: {
response: {
......@@ -202,13 +202,13 @@ describe('Monitoring mutations', () => {
expect.objectContaining({
loading: false,
result: null,
error: metricsErrors.CONNECTION_FAILED,
state: metricStates.CONNECTION_FAILED,
}),
);
});
it('stores a bad data error in a metric', () => {
mutations[types.RECEIVE_METRIC_RESULT_ERROR](stateCopy, {
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
metricId,
error: {
response: {
......@@ -221,13 +221,13 @@ describe('Monitoring mutations', () => {
expect.objectContaining({
loading: false,
result: null,
error: metricsErrors.BAD_DATA,
state: metricStates.BAD_QUERY,
}),
);
});
it('stores an unknown error in a metric', () => {
mutations[types.RECEIVE_METRIC_RESULT_ERROR](stateCopy, {
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
metricId,
error: null, // no reason in response
});
......@@ -236,7 +236,7 @@ describe('Monitoring mutations', () => {
expect.objectContaining({
loading: false,
result: null,
error: metricsErrors.UNKNOWN_ERROR,
state: metricStates.UNKNOWN_ERROR,
}),
);
});
......
......@@ -4,7 +4,8 @@ import { GlToast } from '@gitlab/ui';
import VueDraggable from 'vuedraggable';
import MockAdapter from 'axios-mock-adapter';
import Dashboard from '~/monitoring/components/dashboard.vue';
import EmptyState from '~/monitoring/components/empty_state.vue';
import { metricStates } from '~/monitoring/constants';
import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
import * as types from '~/monitoring/stores/mutation_types';
import { createStore } from '~/monitoring/stores';
import axios from '~/lib/utils/axios_utils';
......@@ -401,7 +402,7 @@ describe('Dashboard', () => {
});
beforeEach(done => {
createComponentWrapper({ hasMetrics: true }, { attachToDocument: true });
createComponentWrapper({ hasMetrics: true });
setupComponentStore(wrapper.vm);
wrapper.vm.$nextTick(done);
......@@ -411,16 +412,16 @@ describe('Dashboard', () => {
const emptyGroup = wrapper.findAll({ ref: 'empty-group' });
expect(emptyGroup).toHaveLength(1);
expect(emptyGroup.is(EmptyState)).toBe(true);
expect(emptyGroup.is(GroupEmptyState)).toBe(true);
});
it('group empty area displays a "noDataGroup"', () => {
it('group empty area displays a NO_DATA state', () => {
expect(
wrapper
.findAll({ ref: 'empty-group' })
.at(0)
.props('selectedState'),
).toEqual('noDataGroup');
).toEqual(metricStates.NO_DATA);
});
});
......
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