Commit 84d8c00f authored by Miguel Rincon's avatar Miguel Rincon

Add error states to the dashboard metrics object

In order to have each individual error displayed in the dashboard, the
error status of the metrics should be stored.

The error store is added to the metrics, so each panel can display it's
own individual error state if needed.

Specifics:

- Add error states as constants
- Add mutations to capture loading and error states in each metric
- Capture each of the conditions that can create a given error
- Add specs for specific error cases
- Add new error cases to be tested under different server responses.
- Refactor to use the testAction util.
parent ca37a05d
...@@ -490,6 +490,8 @@ export const historyPushState = newUrl => { ...@@ -490,6 +490,8 @@ export const historyPushState = newUrl => {
*/ */
export const parseBoolean = value => (value && value.toString()) === 'true'; export const parseBoolean = value => (value && value.toString()) === 'true';
export const BACKOFF_TIMEOUT = 'BACKOFF_TIMEOUT';
/** /**
* @callback backOffCallback * @callback backOffCallback
* @param {Function} next * @param {Function} next
...@@ -541,7 +543,7 @@ export const backOff = (fn, timeout = 60000) => { ...@@ -541,7 +543,7 @@ export const backOff = (fn, timeout = 60000) => {
timeElapsed += nextInterval; timeElapsed += nextInterval;
nextInterval = Math.min(nextInterval + nextInterval, maxInterval); nextInterval = Math.min(nextInterval + nextInterval, maxInterval);
} else { } else {
reject(new Error('BACKOFF_TIMEOUT')); reject(new Error(BACKOFF_TIMEOUT));
} }
}; };
......
...@@ -21,6 +21,7 @@ const httpStatusCodes = { ...@@ -21,6 +21,7 @@ const httpStatusCodes = {
NOT_FOUND: 404, NOT_FOUND: 404,
GONE: 410, GONE: 410,
UNPROCESSABLE_ENTITY: 422, UNPROCESSABLE_ENTITY: 422,
SERVICE_UNAVAILABLE: 503,
}; };
export const successCodes = [ export const successCodes = [
......
import { __ } from '~/locale'; import { __ } from '~/locale';
export const PROMETHEUS_TIMEOUT = 120000; // TWO_MINUTES
/**
* Errors in Prometheus Queries (PromQL) for metrics
*/
export const metricsErrors = {
/**
* Connection timed out to prometheus server
* the timeout is set to PROMETHEUS_TIMEOUT
*
*/
TIMEOUT: 'TIMEOUT',
/**
* The prometheus server replies with an empty data set
*/
NO_DATA: 'NO_DATA',
/**
* The prometheus server cannot be reached
*/
CONNECTION_FAILED: 'CONNECTION_FAILED',
/**
* The prometheus server was reach 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',
/**
* No specific reason found for error
*/
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
};
export const sidebarAnimationDuration = 300; // milliseconds. export const sidebarAnimationDuration = 300; // milliseconds.
export const chartHeight = 300; export const chartHeight = 300;
......
...@@ -6,7 +6,7 @@ import statusCodes from '../../lib/utils/http_status'; ...@@ -6,7 +6,7 @@ import statusCodes from '../../lib/utils/http_status';
import { backOff } from '../../lib/utils/common_utils'; import { backOff } from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale'; import { s__, sprintf } from '../../locale';
const TWO_MINUTES = 120000; import { PROMETHEUS_TIMEOUT } from '../constants';
function backOffRequest(makeRequestCallback) { function backOffRequest(makeRequestCallback) {
return backOff((next, stop) => { return backOff((next, stop) => {
...@@ -19,7 +19,7 @@ function backOffRequest(makeRequestCallback) { ...@@ -19,7 +19,7 @@ function backOffRequest(makeRequestCallback) {
} }
}) })
.catch(stop); .catch(stop);
}, TWO_MINUTES); }, PROMETHEUS_TIMEOUT);
} }
export const setGettingStartedEmptyState = ({ commit }) => { export const setGettingStartedEmptyState = ({ commit }) => {
...@@ -125,8 +125,16 @@ export const fetchPrometheusMetric = ({ commit }, { metric, params }) => { ...@@ -125,8 +125,16 @@ export const fetchPrometheusMetric = ({ commit }, { metric, params }) => {
step, step,
}; };
return fetchPrometheusResult(metric.prometheus_endpoint_path, queryParams).then(result => { commit(types.REQUEST_METRIC_RESULT, { metricId: metric.metric_id });
commit(types.SET_QUERY_RESULT, { metricId: metric.metric_id, result });
return fetchPrometheusResult(metric.prometheus_endpoint_path, queryParams)
.then(result => {
commit(types.RECEIVE_METRIC_RESULT_SUCCESS, { metricId: metric.metric_id, result });
})
.catch(error => {
commit(types.RECEIVE_METRIC_RESULT_ERROR, { metricId: metric.metric_id, error });
// Continue to throw error so the dashboard can notify using createFlash
throw error;
}); });
}; };
...@@ -159,7 +167,8 @@ export const fetchDeploymentsData = ({ state, dispatch }) => { ...@@ -159,7 +167,8 @@ export const fetchDeploymentsData = ({ state, dispatch }) => {
if (!state.deploymentsEndpoint) { if (!state.deploymentsEndpoint) {
return Promise.resolve([]); return Promise.resolve([]);
} }
return backOffRequest(() => axios.get(state.deploymentsEndpoint)) return axios
.get(state.deploymentsEndpoint)
.then(resp => resp.data) .then(resp => resp.data)
.then(response => { .then(response => {
if (!response || !response.deployments) { if (!response || !response.deployments) {
......
export const REQUEST_METRICS_DATA = 'REQUEST_METRICS_DATA'; export const REQUEST_METRICS_DATA = 'REQUEST_METRICS_DATA';
export const RECEIVE_METRICS_DATA_SUCCESS = 'RECEIVE_METRICS_DATA_SUCCESS'; export const RECEIVE_METRICS_DATA_SUCCESS = 'RECEIVE_METRICS_DATA_SUCCESS';
export const RECEIVE_METRICS_DATA_FAILURE = 'RECEIVE_METRICS_DATA_FAILURE'; export const RECEIVE_METRICS_DATA_FAILURE = 'RECEIVE_METRICS_DATA_FAILURE';
export const REQUEST_DEPLOYMENTS_DATA = 'REQUEST_DEPLOYMENTS_DATA'; export const REQUEST_DEPLOYMENTS_DATA = 'REQUEST_DEPLOYMENTS_DATA';
export const RECEIVE_DEPLOYMENTS_DATA_SUCCESS = 'RECEIVE_DEPLOYMENTS_DATA_SUCCESS'; export const RECEIVE_DEPLOYMENTS_DATA_SUCCESS = 'RECEIVE_DEPLOYMENTS_DATA_SUCCESS';
export const RECEIVE_DEPLOYMENTS_DATA_FAILURE = 'RECEIVE_DEPLOYMENTS_DATA_FAILURE'; export const RECEIVE_DEPLOYMENTS_DATA_FAILURE = 'RECEIVE_DEPLOYMENTS_DATA_FAILURE';
export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA'; export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA';
export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS'; export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS';
export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAILURE'; export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAILURE';
export const SET_QUERY_RESULT = 'SET_QUERY_RESULT';
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 SET_TIME_WINDOW = 'SET_TIME_WINDOW'; export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS'; export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS';
export const SET_ENDPOINTS = 'SET_ENDPOINTS'; export const SET_ENDPOINTS = 'SET_ENDPOINTS';
......
...@@ -2,6 +2,9 @@ import Vue from 'vue'; ...@@ -2,6 +2,9 @@ import Vue from 'vue';
import { slugify } from '~/lib/utils/text_utility'; import { slugify } from '~/lib/utils/text_utility';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { normalizeMetric, normalizeQueryResult } from './utils'; import { normalizeMetric, normalizeQueryResult } from './utils';
import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
import { metricsErrors } from '../constants';
import httpStatusCodes from '~/lib/utils/http_status';
const normalizePanelMetrics = (metrics, defaultLabel) => const normalizePanelMetrics = (metrics, defaultLabel) =>
metrics.map(metric => ({ metrics.map(metric => ({
...@@ -9,7 +12,70 @@ const normalizePanelMetrics = (metrics, defaultLabel) => ...@@ -9,7 +12,70 @@ const normalizePanelMetrics = (metrics, defaultLabel) =>
label: metric.label || defaultLabel, label: metric.label || defaultLabel,
})); }));
/**
* Locate and return a metric in the dashboard by its id
* as generated by `uniqMetricsId()`.
* @param {String} metricId Unique id in the dashboard
* @param {Object} dashboard Full dashboard object
*/
const findMetricInDashboard = (metricId, dashboard) => {
let res = null;
dashboard.panel_groups.forEach(group => {
group.panels.forEach(panel => {
panel.metrics.forEach(metric => {
if (metric.metric_id === metricId) {
res = metric;
}
});
});
});
return res;
};
/**
* Set a new state for a metric
* @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 {Boolean} state.loading - True if the metric is loading
*/
const setMetricState = (metric, { result = null, error = null, loading = false }) => {
Vue.set(metric, 'result', result);
Vue.set(metric, 'error', error);
Vue.set(metric, 'loading', loading);
};
/**
* Maps a backened error state to a `metricsErrors` constant
* @param {Object} error - Error from backend response
*/
const getMetricError = error => {
if (!error) {
return metricsErrors.UNKNOWN_ERROR;
}
// Special error responses
if (error.message === BACKOFF_TIMEOUT) {
return metricsErrors.TIMEOUT;
}
// Axios error responses
const { response } = error;
if (response && response.status === httpStatusCodes.SERVICE_UNAVAILABLE) {
return metricsErrors.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 metricsErrors.UNKNOWN_ERROR;
};
export default { export default {
/**
* Dashboard panels structure and global state
*/
[types.REQUEST_METRICS_DATA](state) { [types.REQUEST_METRICS_DATA](state) {
state.emptyState = 'loading'; state.emptyState = 'loading';
state.showEmptyState = true; state.showEmptyState = true;
...@@ -40,6 +106,10 @@ export default { ...@@ -40,6 +106,10 @@ export default {
state.emptyState = error ? 'unableToConnect' : 'noData'; state.emptyState = error ? 'unableToConnect' : 'noData';
state.showEmptyState = true; state.showEmptyState = true;
}, },
/**
* Deployments and environments
*/
[types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS](state, deployments) { [types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS](state, deployments) {
state.deploymentData = deployments; state.deploymentData = deployments;
}, },
...@@ -53,28 +123,46 @@ export default { ...@@ -53,28 +123,46 @@ export default {
state.environments = []; state.environments = [];
}, },
[types.SET_QUERY_RESULT](state, { metricId, result }) { /**
if (!metricId || !result || result.length === 0) { * Individual panel/metric results
*/
[types.REQUEST_METRIC_RESULT](state, { metricId }) {
const metric = findMetricInDashboard(metricId, state.dashboard);
setMetricState(metric, {
loading: true,
});
},
[types.RECEIVE_METRIC_RESULT_SUCCESS](state, { metricId, result }) {
if (!metricId) {
return; return;
} }
state.showEmptyState = false; state.showEmptyState = false;
/** const metric = findMetricInDashboard(metricId, state.dashboard);
* Search the dashboard state for a matching id if (!result || result.length === 0) {
*/ // If no data is return we still consider it an error and set it to undefined
state.dashboard.panel_groups.forEach(group => { setMetricState(metric, {
group.panels.forEach(panel => { error: metricsErrors.NO_DATA,
panel.metrics.forEach(metric => {
if (metric.metric_id === metricId) {
// ensure dates/numbers are correctly formatted for charts
const normalizedResults = result.map(normalizeQueryResult);
Vue.set(metric, 'result', Object.freeze(normalizedResults));
}
}); });
} else {
const normalizedResults = result.map(normalizeQueryResult);
setMetricState(metric, {
result: Object.freeze(normalizedResults),
}); });
}
},
[types.RECEIVE_METRIC_RESULT_ERROR](state, { metricId, error }) {
if (!metricId) {
return;
}
const metric = findMetricInDashboard(metricId, state.dashboard);
setMetricState(metric, {
error: getMetricError(error),
}); });
}, },
[types.SET_ENDPOINTS](state, endpoints) { [types.SET_ENDPOINTS](state, endpoints) {
state.metricsEndpoint = endpoints.metricsEndpoint; state.metricsEndpoint = endpoints.metricsEndpoint;
state.environmentsEndpoint = endpoints.environmentsEndpoint; state.environmentsEndpoint = endpoints.environmentsEndpoint;
......
...@@ -8,9 +8,11 @@ export default () => ({ ...@@ -8,9 +8,11 @@ export default () => ({
emptyState: 'gettingStarted', emptyState: 'gettingStarted',
showEmptyState: true, showEmptyState: true,
showErrorBanner: true, showErrorBanner: true,
dashboard: { dashboard: {
panel_groups: [], panel_groups: [],
}, },
deploymentData: [], deploymentData: [],
environments: [], environments: [],
allDashboards: [], allDashboards: [],
......
...@@ -67,7 +67,7 @@ describe('Dashboard', () => { ...@@ -67,7 +67,7 @@ describe('Dashboard', () => {
metricsGroupsAPIResponse, metricsGroupsAPIResponse,
); );
component.vm.$store.commit( component.vm.$store.commit(
`monitoringDashboard/${types.SET_QUERY_RESULT}`, `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`,
mockedQueryResultPayload, mockedQueryResultPayload,
); );
component.vm.$store.commit( component.vm.$store.commit(
......
...@@ -46,7 +46,10 @@ describe('Time series component', () => { ...@@ -46,7 +46,10 @@ 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 panel groups, with 1 and 2 panels respectively // Mock data contains 2 panel groups, with 1 and 2 panels respectively
store.commit(`monitoringDashboard/${types.SET_QUERY_RESULT}`, mockedQueryResultPayload); store.commit(
`monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`,
mockedQueryResultPayload,
);
// Pick the second panel group and the first panel in it // Pick the second panel group and the first panel in it
[mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[1].panels; [mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[1].panels;
......
...@@ -434,13 +434,11 @@ describe('Monitoring store actions', () => { ...@@ -434,13 +434,11 @@ describe('Monitoring store actions', () => {
start: '2019-08-06T12:40:02.184Z', start: '2019-08-06T12:40:02.184Z',
end: '2019-08-06T20:40:02.184Z', end: '2019-08-06T20:40:02.184Z',
}; };
let commit;
let metric; let metric;
let state; let state;
let data; let data;
beforeEach(() => { beforeEach(() => {
commit = jest.fn();
state = storeState(); state = storeState();
[metric] = metricsDashboardResponse.dashboard.panel_groups[0].panels[0].metrics; [metric] = metricsDashboardResponse.dashboard.panel_groups[0].panels[0].metrics;
[data] = metricsGroupsAPIResponse[0].panels[0].metrics; [data] = metricsGroupsAPIResponse[0].panels[0].metrics;
...@@ -449,17 +447,31 @@ describe('Monitoring store actions', () => { ...@@ -449,17 +447,31 @@ describe('Monitoring store actions', () => {
it('commits result', done => { it('commits result', done => {
mock.onGet('http://test').reply(200, { data }); // One attempt mock.onGet('http://test').reply(200, { data }); // One attempt
fetchPrometheusMetric({ state, commit }, { metric, params }) testAction(
.then(() => { fetchPrometheusMetric,
expect(commit).toHaveBeenCalledWith(types.SET_QUERY_RESULT, { { metric, params },
state,
[
{
type: types.REQUEST_METRIC_RESULT,
payload: {
metricId: metric.metric_id,
},
},
{
type: types.RECEIVE_METRIC_RESULT_SUCCESS,
payload: {
metricId: metric.metric_id, metricId: metric.metric_id,
result: data.result, result: data.result,
}); },
},
],
[],
() => {
expect(mock.history.get).toHaveLength(1); expect(mock.history.get).toHaveLength(1);
done(); done();
}) },
.catch(done.fail); ).catch(done.fail);
}); });
it('commits result, when waiting for results', done => { it('commits result, when waiting for results', done => {
...@@ -469,18 +481,31 @@ describe('Monitoring store actions', () => { ...@@ -469,18 +481,31 @@ describe('Monitoring store actions', () => {
mock.onGet('http://test').replyOnce(statusCodes.NO_CONTENT); mock.onGet('http://test').replyOnce(statusCodes.NO_CONTENT);
mock.onGet('http://test').reply(200, { data }); // 4th attempt mock.onGet('http://test').reply(200, { data }); // 4th attempt
const fetch = fetchPrometheusMetric({ state, commit }, { metric, params }); testAction(
fetchPrometheusMetric,
fetch { metric, params },
.then(() => { state,
expect(commit).toHaveBeenCalledWith(types.SET_QUERY_RESULT, { [
{
type: types.REQUEST_METRIC_RESULT,
payload: {
metricId: metric.metric_id,
},
},
{
type: types.RECEIVE_METRIC_RESULT_SUCCESS,
payload: {
metricId: metric.metric_id, metricId: metric.metric_id,
result: data.result, result: data.result,
}); },
},
],
[],
() => {
expect(mock.history.get).toHaveLength(4); expect(mock.history.get).toHaveLength(4);
done(); done();
}) },
.catch(done.fail); ).catch(done.fail);
}); });
it('commits failure, when waiting for results and getting a server error', done => { it('commits failure, when waiting for results and getting a server error', done => {
...@@ -490,13 +515,31 @@ describe('Monitoring store actions', () => { ...@@ -490,13 +515,31 @@ describe('Monitoring store actions', () => {
mock.onGet('http://test').replyOnce(statusCodes.NO_CONTENT); mock.onGet('http://test').replyOnce(statusCodes.NO_CONTENT);
mock.onGet('http://test').reply(500); // 4th attempt mock.onGet('http://test').reply(500); // 4th attempt
fetchPrometheusMetric({ state, commit }, { metric, params }) const error = new Error('Request failed with status code 500');
.then(() => {
done.fail(); testAction(
}) fetchPrometheusMetric,
.catch(() => { { metric, params },
expect(commit).not.toHaveBeenCalled(); state,
[
{
type: types.REQUEST_METRIC_RESULT,
payload: {
metricId: metric.metric_id,
},
},
{
type: types.RECEIVE_METRIC_RESULT_ERROR,
payload: {
metricId: metric.metric_id,
error,
},
},
],
[],
).catch(e => {
expect(mock.history.get).toHaveLength(4); expect(mock.history.get).toHaveLength(4);
expect(e).toEqual(error);
done(); done();
}); });
}); });
......
...@@ -57,22 +57,22 @@ describe('Monitoring store Getters', () => { ...@@ -57,22 +57,22 @@ describe('Monitoring store Getters', () => {
it('an empty metric, returns empty', () => { it('an empty metric, returns empty', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse); mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.SET_QUERY_RESULT](state, mockedEmptyResult); mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedEmptyResult);
expect(metricsWithData()).toEqual([]); expect(metricsWithData()).toEqual([]);
}); });
it('a metric with results, it returns a metric', () => { it('a metric with results, it returns a metric', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse); mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.SET_QUERY_RESULT](state, mockedQueryResultPayload); mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayload);
expect(metricsWithData()).toEqual([mockedQueryResultPayload.metricId]); expect(metricsWithData()).toEqual([mockedQueryResultPayload.metricId]);
}); });
it('multiple metrics with results, it return multiple metrics', () => { it('multiple metrics with results, it return multiple metrics', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse); mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.SET_QUERY_RESULT](state, mockedQueryResultPayload); mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayload);
mutations[types.SET_QUERY_RESULT](state, mockedQueryResultPayloadCoresTotal); mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayloadCoresTotal);
expect(metricsWithData()).toEqual([ expect(metricsWithData()).toEqual([
mockedQueryResultPayload.metricId, mockedQueryResultPayload.metricId,
...@@ -82,8 +82,8 @@ describe('Monitoring store Getters', () => { ...@@ -82,8 +82,8 @@ describe('Monitoring store Getters', () => {
it('multiple metrics with results, it returns metrics filtered by group', () => { it('multiple metrics with results, it returns metrics filtered by group', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse); mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.SET_QUERY_RESULT](state, mockedQueryResultPayload); mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayload);
mutations[types.SET_QUERY_RESULT](state, mockedQueryResultPayloadCoresTotal); mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayloadCoresTotal);
// First group has no metrics // First group has no metrics
expect(metricsWithData(state.dashboard.panel_groups[0].key)).toEqual([]); expect(metricsWithData(state.dashboard.panel_groups[0].key)).toEqual([]);
......
import httpStatusCodes from '~/lib/utils/http_status';
import mutations from '~/monitoring/stores/mutations'; import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types'; import * as types from '~/monitoring/stores/mutation_types';
import state from '~/monitoring/stores/state'; import state from '~/monitoring/stores/state';
import { metricsErrors } from '~/monitoring/constants';
import { import {
metricsGroupsAPIResponse, metricsGroupsAPIResponse,
deploymentData, deploymentData,
...@@ -90,7 +93,7 @@ describe('Monitoring mutations', () => { ...@@ -90,7 +93,7 @@ describe('Monitoring mutations', () => {
expect(stateCopy.projectPath).toEqual('/gitlab-org/gitlab-foss'); expect(stateCopy.projectPath).toEqual('/gitlab-org/gitlab-foss');
}); });
}); });
describe('SET_QUERY_RESULT', () => { describe('Individual panel/metric results', () => {
const metricId = '12_system_metrics_kubernetes_container_memory_total'; const metricId = '12_system_metrics_kubernetes_container_memory_total';
const result = [ const result = [
{ {
...@@ -98,15 +101,39 @@ describe('Monitoring mutations', () => { ...@@ -98,15 +101,39 @@ describe('Monitoring mutations', () => {
}, },
]; ];
const dashboardGroups = metricsDashboardResponse.dashboard.panel_groups; const dashboardGroups = metricsDashboardResponse.dashboard.panel_groups;
const getMetrics = () => stateCopy.dashboard.panel_groups[0].panels[0].metrics; const getMetric = () => stateCopy.dashboard.panel_groups[0].panels[0].metrics[0];
describe('REQUEST_METRIC_RESULT', () => {
beforeEach(() => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups);
});
it('stores a loading state on a metric', () => {
expect(stateCopy.showEmptyState).toBe(true);
mutations[types.REQUEST_METRIC_RESULT](stateCopy, {
metricId,
result,
});
expect(stateCopy.showEmptyState).toBe(true);
expect(getMetric()).toEqual(
expect.objectContaining({
loading: true,
result: null,
error: null,
}),
);
});
});
describe('RECEIVE_METRIC_RESULT_SUCCESS', () => {
beforeEach(() => { beforeEach(() => {
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); expect(stateCopy.showEmptyState).toBe(true);
mutations[types.SET_QUERY_RESULT](stateCopy, { mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](stateCopy, {
metricId, metricId,
result, result,
}); });
...@@ -115,14 +142,104 @@ describe('Monitoring mutations', () => { ...@@ -115,14 +142,104 @@ describe('Monitoring mutations', () => {
}); });
it('adds results to the store', () => { it('adds results to the store', () => {
expect(getMetrics()[0].result).toBe(undefined); expect(getMetric().result).toBe(undefined);
mutations[types.SET_QUERY_RESULT](stateCopy, { mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](stateCopy, {
metricId, metricId,
result, result,
}); });
expect(getMetrics()[0].result).toHaveLength(result.length); expect(getMetric().result).toHaveLength(result.length);
expect(getMetric()).toEqual(
expect.objectContaining({
loading: false,
error: null,
}),
);
});
});
describe('RECEIVE_METRIC_RESULT_ERROR', () => {
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, {
metricId,
error: 'an error',
});
expect(stateCopy.showEmptyState).toBe(true);
});
it('stores a timeout error in a metric', () => {
mutations[types.RECEIVE_METRIC_RESULT_ERROR](stateCopy, {
metricId,
error: { message: 'BACKOFF_TIMEOUT' },
});
expect(getMetric()).toEqual(
expect.objectContaining({
loading: false,
result: null,
error: metricsErrors.TIMEOUT,
}),
);
});
it('stores a connection failed error in a metric', () => {
mutations[types.RECEIVE_METRIC_RESULT_ERROR](stateCopy, {
metricId,
error: {
response: {
status: httpStatusCodes.SERVICE_UNAVAILABLE,
},
},
});
expect(getMetric()).toEqual(
expect.objectContaining({
loading: false,
result: null,
error: metricsErrors.CONNECTION_FAILED,
}),
);
});
it('stores a bad data error in a metric', () => {
mutations[types.RECEIVE_METRIC_RESULT_ERROR](stateCopy, {
metricId,
error: {
response: {
status: httpStatusCodes.BAD_REQUEST,
},
},
});
expect(getMetric()).toEqual(
expect.objectContaining({
loading: false,
result: null,
error: metricsErrors.BAD_DATA,
}),
);
});
it('stores an unknown error in a metric', () => {
mutations[types.RECEIVE_METRIC_RESULT_ERROR](stateCopy, {
metricId,
error: null, // no reason in response
});
expect(getMetric()).toEqual(
expect.objectContaining({
loading: false,
result: null,
error: metricsErrors.UNKNOWN_ERROR,
}),
);
});
}); });
}); });
describe('SET_ALL_DASHBOARDS', () => { describe('SET_ALL_DASHBOARDS', () => {
......
...@@ -56,13 +56,16 @@ function setupComponentStore(component) { ...@@ -56,13 +56,16 @@ function setupComponentStore(component) {
); );
// Load 3 panels to the dashboard, one with an empty result // 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.RECEIVE_METRIC_RESULT_SUCCESS}`,
mockedEmptyResult,
);
component.$store.commit(
`monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`,
mockedQueryResultPayload, mockedQueryResultPayload,
); );
component.$store.commit( component.$store.commit(
`monitoringDashboard/${types.SET_QUERY_RESULT}`, `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`,
mockedQueryResultPayloadCoresTotal, mockedQueryResultPayloadCoresTotal,
); );
...@@ -269,7 +272,7 @@ describe('Dashboard', () => { ...@@ -269,7 +272,7 @@ describe('Dashboard', () => {
metricsGroupsAPIResponse, metricsGroupsAPIResponse,
); );
component.$store.commit( component.$store.commit(
`monitoringDashboard/${types.SET_QUERY_RESULT}`, `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`,
mockedQueryResultPayload, mockedQueryResultPayload,
); );
......
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