Commit 26ea63ae authored by Miguel Rincon's avatar Miguel Rincon

Add store action that fetch data for preview panel

This change implements the backend endpoint integration that
fetches data from prometheus to display data in the chart preview.
parent 018ffad5
...@@ -11,12 +11,13 @@ import { ...@@ -11,12 +11,13 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import DashboardPanel from './dashboard_panel.vue'; import DashboardPanel from './dashboard_panel.vue';
const initialYml = `title: const initialYml = `title: Go heap size
y_label:
type: area-chart type: area-chart
y_axis:
format: 'bytes'
metrics: metrics:
- query_range: - metric_id: 'go_memstats_alloc_bytes_1'
label: query_range: 'go_memstats_alloc_bytes'
`; `;
export default { export default {
......
...@@ -41,12 +41,3 @@ export const getPrometheusQueryData = (prometheusEndpoint, params) => ...@@ -41,12 +41,3 @@ export const getPrometheusQueryData = (prometheusEndpoint, params) =>
} }
throw error; throw error;
}); });
// eslint-disable-next-line no-unused-vars
export function getPanelJson(panelPreviewEndpoint, panelPreviewYml) {
// TODO Use a real backend when it's available
// https://gitlab.com/gitlab-org/gitlab/-/issues/228758
// eslint-disable-next-line @gitlab/require-i18n-strings
return Promise.reject(new Error('API Not implemented.'));
}
...@@ -15,7 +15,8 @@ import getAnnotations from '../queries/getAnnotations.query.graphql'; ...@@ -15,7 +15,8 @@ import getAnnotations from '../queries/getAnnotations.query.graphql';
import getDashboardValidationWarnings from '../queries/getDashboardValidationWarnings.query.graphql'; import getDashboardValidationWarnings from '../queries/getDashboardValidationWarnings.query.graphql';
import { convertObjectPropsToCamelCase } from '../../lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale'; import { s__, sprintf } from '../../locale';
import { getDashboard, getPrometheusQueryData, getPanelJson } from '../requests'; import { getDashboard, getPrometheusQueryData } from '../requests';
import { defaultTimeRange } from '~/vue_shared/constants';
import { ENVIRONMENT_AVAILABLE_STATE, OVERVIEW_DASHBOARD_PATH, VARIABLE_TYPES } from '../constants'; import { ENVIRONMENT_AVAILABLE_STATE, OVERVIEW_DASHBOARD_PATH, VARIABLE_TYPES } from '../constants';
...@@ -33,6 +34,20 @@ function prometheusMetricQueryParams(timeRange) { ...@@ -33,6 +34,20 @@ function prometheusMetricQueryParams(timeRange) {
}; };
} }
/**
* Extract error messages from API or HTTP request errors.
*
* - API errors are in `error.response.data.message`
* - HTTP (axios) errors are in `error.messsage`
*
* @param {Object} error
* @returns {String} User friendly error message
*/
function extractErrorMessage(error) {
const message = error?.response?.data?.message;
return message ?? error.message;
}
// Setup // Setup
export const setGettingStartedEmptyState = ({ commit }) => { export const setGettingStartedEmptyState = ({ commit }) => {
...@@ -482,21 +497,38 @@ export const fetchPanelPreview = ({ state, commit, dispatch }, panelPreviewYml) ...@@ -482,21 +497,38 @@ export const fetchPanelPreview = ({ state, commit, dispatch }, panelPreviewYml)
} }
commit(types.REQUEST_PANEL_PREVIEW, panelPreviewYml); commit(types.REQUEST_PANEL_PREVIEW, panelPreviewYml);
return getPanelJson(state.panelPreviewEndpoint, panelPreviewYml) return axios
.then(data => { .post(state.panelPreviewEndpoint, { panel_yaml: panelPreviewYml })
.then(({ data }) => {
commit(types.RECEIVE_PANEL_PREVIEW_SUCCESS, data); commit(types.RECEIVE_PANEL_PREVIEW_SUCCESS, data);
dispatch('fetchPanelPreviewMetrics'); dispatch('fetchPanelPreviewMetrics');
}) })
.catch(error => { .catch(error => {
commit(types.RECEIVE_PANEL_PREVIEW_FAILURE, error); commit(types.RECEIVE_PANEL_PREVIEW_FAILURE, extractErrorMessage(error));
}); });
}; };
export const fetchPanelPreviewMetrics = () => { export const fetchPanelPreviewMetrics = ({ state, commit }) => {
// TODO Use a axios mock instead of spy when backend is implemented const defaultQueryParams = prometheusMetricQueryParams(defaultTimeRange);
// https://gitlab.com/gitlab-org/gitlab/-/issues/228758
state.panelPreviewGraphData.metrics.forEach((metric, index) => {
commit(types.REQUEST_PANEL_PREVIEW_METRIC_RESULT, { index });
// eslint-disable-next-line @gitlab/require-i18n-strings const params = { ...defaultQueryParams };
throw new Error('Not implemented'); if (metric.step) {
params.step = metric.step;
}
return getPrometheusQueryData(metric.prometheusEndpointPath, params)
.then(data => {
commit(types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS, { index, data });
})
.catch(error => {
Sentry.captureException(error);
commit(types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE, { index, error });
// Continue to throw error so the panel builder can notify using createFlash
throw error;
});
});
}; };
...@@ -51,3 +51,9 @@ export const SET_EXPANDED_PANEL = 'SET_EXPANDED_PANEL'; ...@@ -51,3 +51,9 @@ export const SET_EXPANDED_PANEL = 'SET_EXPANDED_PANEL';
export const REQUEST_PANEL_PREVIEW = 'REQUEST_PANEL_PREVIEW'; export const REQUEST_PANEL_PREVIEW = 'REQUEST_PANEL_PREVIEW';
export const RECEIVE_PANEL_PREVIEW_SUCCESS = 'RECEIVE_PANEL_PREVIEW_SUCCESS'; export const RECEIVE_PANEL_PREVIEW_SUCCESS = 'RECEIVE_PANEL_PREVIEW_SUCCESS';
export const RECEIVE_PANEL_PREVIEW_FAILURE = 'RECEIVE_PANEL_PREVIEW_FAILURE'; export const RECEIVE_PANEL_PREVIEW_FAILURE = 'RECEIVE_PANEL_PREVIEW_FAILURE';
export const REQUEST_PANEL_PREVIEW_METRIC_RESULT = 'REQUEST_PANEL_PREVIEW_METRIC_RESULT';
export const RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS =
'RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS';
export const RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE =
'RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE';
...@@ -53,6 +53,14 @@ const emptyStateFromError = error => { ...@@ -53,6 +53,14 @@ const emptyStateFromError = error => {
return metricStates.UNKNOWN_ERROR; return metricStates.UNKNOWN_ERROR;
}; };
export const metricStateFromData = data => {
if (data?.result?.length) {
const result = normalizeQueryResponseData(data);
return { state: metricStates.OK, result: Object.freeze(result) };
}
return { state: metricStates.NO_DATA, result: null };
};
export default { export default {
/** /**
* Dashboard panels structure and global state * Dashboard panels structure and global state
...@@ -154,17 +162,11 @@ export default { ...@@ -154,17 +162,11 @@ export default {
}, },
[types.RECEIVE_METRIC_RESULT_SUCCESS](state, { metricId, data }) { [types.RECEIVE_METRIC_RESULT_SUCCESS](state, { metricId, data }) {
const metric = findMetricInDashboard(metricId, state.dashboard); const metric = findMetricInDashboard(metricId, state.dashboard);
metric.loading = false; const metricState = metricStateFromData(data);
if (!data.result || data.result.length === 0) {
metric.state = metricStates.NO_DATA;
metric.result = null;
} else {
const result = normalizeQueryResponseData(data);
metric.state = metricStates.OK; metric.loading = false;
metric.result = Object.freeze(result); metric.state = metricState.state;
} metric.result = metricState.result;
}, },
[types.RECEIVE_METRIC_RESULT_FAILURE](state, { metricId, error }) { [types.RECEIVE_METRIC_RESULT_FAILURE](state, { metricId, error }) {
const metric = findMetricInDashboard(metricId, state.dashboard); const metric = findMetricInDashboard(metricId, state.dashboard);
...@@ -238,4 +240,28 @@ export default { ...@@ -238,4 +240,28 @@ export default {
state.panelPreviewGraphData = null; state.panelPreviewGraphData = null;
state.panelPreviewError = error; state.panelPreviewError = error;
}, },
[types.REQUEST_PANEL_PREVIEW_METRIC_RESULT](state, { index }) {
const metric = state.panelPreviewGraphData.metrics[index];
metric.loading = true;
if (!metric.result) {
metric.state = metricStates.LOADING;
}
},
[types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS](state, { index, data }) {
const metric = state.panelPreviewGraphData.metrics[index];
const metricState = metricStateFromData(data);
metric.loading = false;
metric.state = metricState.state;
metric.result = metricState.result;
},
[types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE](state, { index, error }) {
const metric = state.panelPreviewGraphData.metrics[index];
metric.loading = false;
metric.state = emptyStateFromError(error);
metric.result = null;
},
}; };
...@@ -7,6 +7,33 @@ const intervalSeconds = 120; ...@@ -7,6 +7,33 @@ const intervalSeconds = 120;
const makeValue = val => [initTime, val]; const makeValue = val => [initTime, val];
const makeValues = vals => vals.map((val, i) => [initTime + intervalSeconds * i, val]); const makeValues = vals => vals.map((val, i) => [initTime + intervalSeconds * i, val]);
// Raw Promethues Responses
export const prometheusMatrixMultiResult = ({
values1 = ['1', '2', '3'],
values2 = ['4', '5', '6'],
} = {}) => ({
resultType: 'matrix',
result: [
{
metric: {
__name__: 'up',
job: 'prometheus',
instance: 'localhost:9090',
},
values: makeValues(values1),
},
{
metric: {
__name__: 'up',
job: 'node',
instance: 'localhost:9091',
},
values: makeValues(values2),
},
],
});
// Normalized Prometheus Responses // Normalized Prometheus Responses
const scalarResult = ({ value = '1' } = {}) => const scalarResult = ({ value = '1' } = {}) =>
......
...@@ -9,7 +9,6 @@ import { defaultTimeRange } from '~/vue_shared/constants'; ...@@ -9,7 +9,6 @@ import { defaultTimeRange } from '~/vue_shared/constants';
import * as getters from '~/monitoring/stores/getters'; import * as getters from '~/monitoring/stores/getters';
import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants'; import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
import { backoffMockImplementation } from 'jest/helpers/backoff_helper'; import { backoffMockImplementation } from 'jest/helpers/backoff_helper';
import * as requests from '~/monitoring/requests';
import { createStore } from '~/monitoring/stores'; import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types'; import * as types from '~/monitoring/stores/mutation_types';
...@@ -1158,54 +1157,64 @@ describe('Monitoring store actions', () => { ...@@ -1158,54 +1157,64 @@ describe('Monitoring store actions', () => {
}); });
describe('fetchPanelPreview', () => { describe('fetchPanelPreview', () => {
const panelPreviewEndpoint = '/builder.json';
const mockYmlContent = 'mock yml content'; const mockYmlContent = 'mock yml content';
beforeEach(() => {
state.panelPreviewEndpoint = panelPreviewEndpoint;
});
it('should not commit or dispatch if payload is empty', () => { it('should not commit or dispatch if payload is empty', () => {
testAction(fetchPanelPreview, '', state, [], []); testAction(fetchPanelPreview, '', state, [], []);
}); });
it('should store the yml content and panel in the store and fetch corresponding metrics', () => { it('should store the panel and fetch metric results', () => {
const mockPanel = { const mockPanel = {
title: 'title', title: 'Go heap size',
type: 'area-chart', type: 'area-chart',
}; };
// TODO Use a axios mock instead of spy when backend is implemented mock
// https://gitlab.com/gitlab-org/gitlab/-/issues/228758 .onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent })
jest.spyOn(requests, 'getPanelJson').mockResolvedValue(mockPanel); .reply(statusCodes.OK, mockPanel);
testAction( testAction(
fetchPanelPreview, fetchPanelPreview,
'mock yml content', mockYmlContent,
state, state,
[ [
{ type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent }, { type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
{ type: types.RECEIVE_PANEL_PREVIEW_SUCCESS, payload: mockPanel }, { type: types.RECEIVE_PANEL_PREVIEW_SUCCESS, payload: mockPanel },
], ],
[ [{ type: 'fetchPanelPreviewMetrics' }],
{
type: 'fetchPanelPreviewMetrics',
},
],
); );
}); });
it('should commit a failure when backend fails', () => { it('should display a validation error when the backend cannot process the yml', () => {
const mockError = 'error'; const mockErrorMsg = 'Each "metric" must define one of :query or :query_range';
// TODO Use a axios mock instead of spy when backend is implemented
// https://gitlab.com/gitlab-org/gitlab/-/issues/228758
jest.spyOn(requests, 'getPanelJson').mockRejectedValue(mockError);
testAction( mock
fetchPanelPreview, .onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent })
mockYmlContent, .reply(statusCodes.UNPROCESSABLE_ENTITY, {
state, message: mockErrorMsg,
[ });
{ type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
{ type: types.RECEIVE_PANEL_PREVIEW_FAILURE, payload: mockError }, testAction(fetchPanelPreview, mockYmlContent, state, [
], { type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
[], { type: types.RECEIVE_PANEL_PREVIEW_FAILURE, payload: mockErrorMsg },
); ]);
});
it('should display a generic error when the backend fails', () => {
mock.onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent }).reply(500);
testAction(fetchPanelPreview, mockYmlContent, state, [
{ type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
{
type: types.RECEIVE_PANEL_PREVIEW_FAILURE,
payload: 'Request failed with status code 500',
},
]);
}); });
}); });
}); });
...@@ -4,8 +4,8 @@ import mutations from '~/monitoring/stores/mutations'; ...@@ -4,8 +4,8 @@ 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 { dashboardEmptyStates, metricStates } from '~/monitoring/constants'; import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
import { deploymentData, dashboardGitResponse, storeTextVariables } from '../mock_data'; import { deploymentData, dashboardGitResponse, storeTextVariables } from '../mock_data';
import { prometheusMatrixMultiResult } from '../graph_data';
import { metricsDashboardPayload } from '../fixture_data'; import { metricsDashboardPayload } from '../fixture_data';
describe('Monitoring mutations', () => { describe('Monitoring mutations', () => {
...@@ -259,27 +259,6 @@ describe('Monitoring mutations', () => { ...@@ -259,27 +259,6 @@ describe('Monitoring mutations', () => {
describe('Individual panel/metric results', () => { describe('Individual panel/metric results', () => {
const metricId = 'NO_DB_response_metrics_nginx_ingress_throughput_status_code'; const metricId = 'NO_DB_response_metrics_nginx_ingress_throughput_status_code';
const data = {
resultType: 'matrix',
result: [
{
metric: {
__name__: 'up',
job: 'prometheus',
instance: 'localhost:9090',
},
values: [[1435781430.781, '1'], [1435781445.781, '1'], [1435781460.781, '1']],
},
{
metric: {
__name__: 'up',
job: 'node',
instance: 'localhost:9091',
},
values: [[1435781430.781, '0'], [1435781445.781, '0'], [1435781460.781, '1']],
},
],
};
const dashboard = metricsDashboardPayload; const dashboard = metricsDashboardPayload;
const getMetric = () => stateCopy.dashboard.panelGroups[1].panels[0].metrics[0]; const getMetric = () => stateCopy.dashboard.panelGroups[1].panels[0].metrics[0];
...@@ -307,6 +286,8 @@ describe('Monitoring mutations', () => { ...@@ -307,6 +286,8 @@ describe('Monitoring mutations', () => {
}); });
it('adds results to the store', () => { it('adds results to the store', () => {
const data = prometheusMatrixMultiResult();
expect(getMetric().result).toBe(null); expect(getMetric().result).toBe(null);
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](stateCopy, { mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](stateCopy, {
...@@ -526,4 +507,90 @@ describe('Monitoring mutations', () => { ...@@ -526,4 +507,90 @@ describe('Monitoring mutations', () => {
expect(stateCopy.panelPreviewError).toBe('Error!'); expect(stateCopy.panelPreviewError).toBe('Error!');
}); });
}); });
describe('panel preview metric', () => {
const getPreviewMetricAt = i => stateCopy.panelPreviewGraphData.metrics[i];
beforeEach(() => {
stateCopy.panelPreviewGraphData = {
title: 'Preview panel title',
metrics: [
{
query: 'query',
},
],
};
});
describe('REQUEST_PANEL_PREVIEW_METRIC_RESULT', () => {
it('sets the metric to loading for the first time', () => {
mutations[types.REQUEST_PANEL_PREVIEW_METRIC_RESULT](stateCopy, { index: 0 });
expect(getPreviewMetricAt(0).loading).toBe(true);
expect(getPreviewMetricAt(0).state).toBe(metricStates.LOADING);
});
it('sets the metric to loading and keeps the result', () => {
getPreviewMetricAt(0).result = [[0, 1]];
getPreviewMetricAt(0).state = metricStates.OK;
mutations[types.REQUEST_PANEL_PREVIEW_METRIC_RESULT](stateCopy, { index: 0 });
expect(getPreviewMetricAt(0)).toMatchObject({
loading: true,
result: [[0, 1]],
state: metricStates.OK,
});
});
});
describe('RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS', () => {
it('saves the result in the metric', () => {
const data = prometheusMatrixMultiResult();
mutations[types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS](stateCopy, {
index: 0,
data,
});
expect(getPreviewMetricAt(0)).toMatchObject({
loading: false,
state: metricStates.OK,
result: expect.any(Array),
});
expect(getPreviewMetricAt(0).result).toHaveLength(data.result.length);
});
});
describe('RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE', () => {
it('stores an error in the metric', () => {
mutations[types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE](stateCopy, {
index: 0,
});
expect(getPreviewMetricAt(0).loading).toBe(false);
expect(getPreviewMetricAt(0).state).toBe(metricStates.UNKNOWN_ERROR);
expect(getPreviewMetricAt(0).result).toBe(null);
expect(getPreviewMetricAt(0)).toMatchObject({
loading: false,
result: null,
state: metricStates.UNKNOWN_ERROR,
});
});
it('stores a timeout error in a metric', () => {
mutations[types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE](stateCopy, {
index: 0,
error: { message: 'BACKOFF_TIMEOUT' },
});
expect(getPreviewMetricAt(0)).toMatchObject({
loading: false,
result: null,
state: metricStates.TIMEOUT,
});
});
});
});
}); });
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