Commit bc085f11 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '34121-add-error-states-to-metrics-store' into 'master'

Add error states to the dashboard metrics internal objects

See merge request gitlab-org/gitlab!21478
parents 9682501f a3e50db6
...@@ -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,74 @@ const normalizePanelMetrics = (metrics, defaultLabel) => ...@@ -9,7 +12,74 @@ 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.
*
* Initally metric data is not populated, so `Vue.set` is
* used to add new properties to the 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 +110,10 @@ export default { ...@@ -40,6 +110,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 +127,46 @@ export default { ...@@ -53,28 +127,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