Commit 6998e6bf authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '201999-support-formatting-dashboard' into 'master'

Convert dashboard keys (e.g. panel_groups) to camel case

See merge request gitlab-org/gitlab!25522
parents 870b6dc6 f040647e
......@@ -522,7 +522,7 @@ export default {
<div v-if="!showEmptyState">
<graph-group
v-for="(groupData, index) in dashboard.panel_groups"
v-for="(groupData, index) in dashboard.panelGroups"
:key="`${groupData.group}.${groupData.priority}`"
:name="groupData.group"
:show-panels="showPanels"
......
......@@ -28,10 +28,10 @@ export default {
...mapState('monitoringDashboard', ['dashboard']),
...mapGetters('monitoringDashboard', ['metricsWithData']),
charts() {
if (!this.dashboard || !this.dashboard.panel_groups) {
if (!this.dashboard || !this.dashboard.panelGroups) {
return [];
}
const groupWithMetrics = this.dashboard.panel_groups.find(group =>
const groupWithMetrics = this.dashboard.panelGroups.find(group =>
group.panels.find(chart => this.chartHasData(chart)),
) || { panels: [] };
......
......@@ -51,9 +51,11 @@ export const requestMetricsDashboard = ({ commit }) => {
commit(types.REQUEST_METRICS_DATA);
};
export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response, params }) => {
commit(types.SET_ALL_DASHBOARDS, response.all_dashboards);
commit(types.RECEIVE_METRICS_DATA_SUCCESS, response.dashboard);
commit(types.SET_ENDPOINTS, convertObjectPropsToCamelCase(response.metrics_data));
const { all_dashboards, dashboard, metrics_data } = response;
commit(types.SET_ALL_DASHBOARDS, all_dashboards);
commit(types.RECEIVE_METRICS_DATA_SUCCESS, dashboard);
commit(types.SET_ENDPOINTS, convertObjectPropsToCamelCase(metrics_data));
return dispatch('fetchPrometheusMetrics', params);
};
......@@ -149,16 +151,16 @@ export const fetchPrometheusMetric = ({ commit }, { metric, params }) => {
step,
};
commit(types.REQUEST_METRIC_RESULT, { metricId: metric.metric_id });
commit(types.REQUEST_METRIC_RESULT, { metricId: metric.metricId });
return fetchPrometheusResult(metric.prometheus_endpoint_path, queryParams)
return fetchPrometheusResult(metric.prometheusEndpointPath, queryParams)
.then(result => {
commit(types.RECEIVE_METRIC_RESULT_SUCCESS, { metricId: metric.metric_id, result });
commit(types.RECEIVE_METRIC_RESULT_SUCCESS, { metricId: metric.metricId, result });
})
.catch(error => {
Sentry.captureException(error);
commit(types.RECEIVE_METRIC_RESULT_FAILURE, { metricId: metric.metric_id, error });
commit(types.RECEIVE_METRIC_RESULT_FAILURE, { metricId: metric.metricId, error });
// Continue to throw error so the dashboard can notify using createFlash
throw error;
});
......@@ -168,7 +170,7 @@ export const fetchPrometheusMetrics = ({ state, commit, dispatch, getters }, par
commit(types.REQUEST_METRICS_DATA);
const promises = [];
state.dashboard.panel_groups.forEach(group => {
state.dashboard.panelGroups.forEach(group => {
group.panels.forEach(panel => {
panel.metrics.forEach(metric => {
promises.push(dispatch('fetchPrometheusMetric', { metric, params }));
......
......@@ -11,7 +11,7 @@ const metricsIdsInPanel = panel =>
* states in all the metric in the dashboard or group.
*/
export const getMetricStates = state => groupKey => {
let groups = state.dashboard.panel_groups;
let groups = state.dashboard.panelGroups;
if (groupKey) {
groups = groups.filter(group => group.key === groupKey);
}
......@@ -43,7 +43,7 @@ export const getMetricStates = state => groupKey => {
* filtered by group key.
*/
export const metricsWithData = state => groupKey => {
let groups = state.dashboard.panel_groups;
let groups = state.dashboard.panelGroups;
if (groupKey) {
groups = groups.filter(group => group.key === groupKey);
}
......
import Vue from 'vue';
import pick from 'lodash/pick';
import { slugify } from '~/lib/utils/text_utility';
import * as types from './mutation_types';
import { normalizeMetric, normalizeQueryResult } from './utils';
import { mapToDashboardViewModel, normalizeQueryResult } from './utils';
import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
import { metricStates } from '../constants';
import httpStatusCodes from '~/lib/utils/http_status';
const normalizePanelMetrics = (metrics, defaultLabel) =>
metrics.map(metric => ({
...normalizeMetric(metric),
label: metric.label || defaultLabel,
}));
/**
* Locate and return a metric in the dashboard by its id
* as generated by `uniqMetricsId()`.
......@@ -21,10 +14,10 @@ const normalizePanelMetrics = (metrics, defaultLabel) =>
*/
const findMetricInDashboard = (metricId, dashboard) => {
let res = null;
dashboard.panel_groups.forEach(group => {
dashboard.panelGroups.forEach(group => {
group.panels.forEach(panel => {
panel.metrics.forEach(metric => {
if (metric.metric_id === metricId) {
if (metric.metricId === metricId) {
res = metric;
}
});
......@@ -86,27 +79,9 @@ export default {
state.showEmptyState = true;
},
[types.RECEIVE_METRICS_DATA_SUCCESS](state, dashboard) {
state.dashboard = {
...dashboard,
panel_groups: dashboard.panel_groups.map((group, i) => {
const key = `${slugify(group.group || 'default')}-${i}`;
let { panels = [] } = group;
// each panel has metric information that needs to be normalized
panels = panels.map(panel => ({
...panel,
metrics: normalizePanelMetrics(panel.metrics, panel.y_label),
}));
return {
...group,
panels,
key,
};
}),
};
state.dashboard = mapToDashboardViewModel(dashboard);
if (!state.dashboard.panel_groups.length) {
if (!state.dashboard.panelGroups.length) {
state.emptyState = 'noData';
}
},
......@@ -206,7 +181,7 @@ export default {
state.showErrorBanner = enabled;
},
[types.SET_PANEL_GROUP_METRICS](state, payload) {
const panelGroup = state.dashboard.panel_groups.find(pg => payload.key === pg.key);
const panelGroup = state.dashboard.panelGroups.find(pg => payload.key === pg.key);
panelGroup.panels = payload.panels;
},
[types.SET_ENVIRONMENTS_FILTER](state, searchTerm) {
......
......@@ -15,7 +15,7 @@ export default () => ({
showEmptyState: true,
showErrorBanner: true,
dashboard: {
panel_groups: [],
panelGroups: [],
},
allDashboards: [],
......
import { omit } from 'lodash';
import { slugify } from '~/lib/utils/text_utility';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
......@@ -9,6 +9,13 @@ export const gqClient = createGqClient(
},
);
/**
* Metrics loaded from project-defined dashboards do not have a metric_id.
* This method creates a unique ID combining metric_id and id, if either is present.
* This is hopefully a temporary solution until BE processes metrics before passing to fE
* @param {Object} metric - metric
* @returns {Object} - normalized metric with a uniqueID
*/
export const uniqMetricsId = metric => `${metric.metric_id}_${metric.id}`;
/**
......@@ -41,22 +48,75 @@ export const parseEnvironmentsResponse = (response = [], projectPath) =>
});
/**
* Metrics loaded from project-defined dashboards do not have a metric_id.
* This method creates a unique ID combining metric_id and id, if either is present.
* This is hopefully a temporary solution until BE processes metrics before passing to fE
* @param {Object} metric - metric
* @returns {Object} - normalized metric with a uniqueID
* Maps metrics to its view model
*
* This function difers from other in that is maps all
* non-define properties as-is to the object. This is not
* advisable as it could lead to unexpected side-effects.
*
* Related issue:
* https://gitlab.com/gitlab-org/gitlab/issues/207198
*
* @param {Array} metrics - Array of prometheus metrics
* @param {String} defaultLabel - Default label for metrics
* @returns {Object}
*/
const mapToMetricsViewModel = (metrics, defaultLabel) =>
metrics.map(({ label, id, metric_id, query_range, prometheus_endpoint_path, ...metric }) => ({
label: label || defaultLabel,
queryRange: query_range,
prometheusEndpointPath: prometheus_endpoint_path,
metricId: uniqMetricsId({ metric_id, id }),
export const normalizeMetric = (metric = {}) =>
omit(
{
// `metric_id` is used by embed.vue, keeping this duplicated.
// https://gitlab.com/gitlab-org/gitlab/issues/37492
metric_id: uniqMetricsId({ metric_id, id }),
...metric,
metric_id: uniqMetricsId(metric),
metricId: uniqMetricsId(metric),
},
'id',
);
}));
/**
* Maps a metrics panel to its view model
*
* @param {Object} panel - Metrics panel
* @returns {Object}
*/
const mapToPanelViewModel = ({ title = '', type, y_label, metrics = [] }) => {
return {
title,
type,
y_label,
metrics: mapToMetricsViewModel(metrics, y_label),
};
};
/**
* Maps a metrics panel group to its view model
*
* @param {Object} panelGroup - Panel Group
* @returns {Object}
*/
const mapToPanelGroupViewModel = ({ group = '', panels = [] }, i) => {
return {
key: `${slugify(group || 'default')}-${i}`,
group,
panels: panels.map(mapToPanelViewModel),
};
};
/**
* Maps a dashboard json object to its view model
*
* @param {Object} dashboard - Dashboard object
* @param {String} dashboard.dashboard - Dashboard name object
* @param {Array} dashboard.panel_groups - Panel groups array
* @returns {Object}
*/
export const mapToDashboardViewModel = ({ dashboard = '', panel_groups = [] }) => {
return {
dashboard,
panelGroups: panel_groups.map(mapToPanelGroupViewModel),
};
};
export const normalizeQueryResult = timeSeries => {
let normalizedResult = {};
......
......@@ -7,7 +7,7 @@ import {
/**
* This method is used to validate if the graph data format for a chart component
* that needs a time series as a response from a prometheus query (query_range) is
* that needs a time series as a response from a prometheus query (queryRange) is
* of a valid format or not.
* @param {Object} graphData the graph data response from a prometheus request
* @returns {boolean} whether the graphData format is correct
......
......@@ -12,6 +12,7 @@ import {
deploymentData,
metricsDashboardPayload,
mockedQueryResultPayload,
metricsDashboardViewModel,
mockProjectDir,
mockHost,
} from '../../mock_data';
......@@ -65,7 +66,7 @@ describe('Time series component', () => {
);
// 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.panelGroups[0].panels;
});
describe('general functions', () => {
......@@ -188,7 +189,7 @@ describe('Time series component', () => {
});
it('formats tooltip content', () => {
const name = 'Pod average';
const name = 'Total';
const value = '5.556';
const dataIndex = 0;
const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel);
......@@ -439,7 +440,7 @@ describe('Time series component', () => {
it('constructs a label for the chart y-axis', () => {
const { yAxis } = getChartOptions();
expect(yAxis[0].name).toBe('Memory Used per Pod');
expect(yAxis[0].name).toBe('Total Memory Used');
});
});
});
......@@ -535,46 +536,22 @@ describe('Time series component', () => {
});
describe('with multiple time series', () => {
const mockedResultMultipleSeries = [];
const [, , panelData] = metricsDashboardPayload.panel_groups[1].panels;
for (let i = 0; i < panelData.metrics.length; i += 1) {
mockedResultMultipleSeries.push(cloneDeep(mockedQueryResultPayload));
mockedResultMultipleSeries[
i
].metricId = `${panelData.metrics[i].metric_id}_${panelData.metrics[i].id}`;
}
beforeEach(() => {
setTestTimeout(1000);
describe('General functions', () => {
let timeSeriesChart;
beforeEach(done => {
store = createStore();
store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
metricsDashboardPayload,
);
store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
// Mock data contains the metric_id for a multiple time series panel
for (let i = 0; i < panelData.metrics.length; i += 1) {
store.commit(
`monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`,
mockedResultMultipleSeries[i],
const graphData = cloneDeep(metricsDashboardViewModel.panelGroups[0].panels[3]);
graphData.metrics.forEach(metric =>
Object.assign(metric, { result: mockedQueryResultPayload.result }),
);
}
// Pick the second panel group and the second panel in it
[, , mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[1].panels;
timeSeriesChart = makeTimeSeriesChart(graphData, 'area-chart');
timeSeriesChart.vm.$nextTick(done);
});
describe('General functions', () => {
let timeSeriesChart;
beforeEach(done => {
timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart');
timeSeriesChart.vm.$nextTick(done);
afterEach(() => {
timeSeriesChart.destroy();
});
describe('computed', () => {
......
......@@ -17,12 +17,13 @@ import { setupComponentStore, propsData } from '../init_utils';
import {
metricsDashboardPayload,
mockedQueryResultPayload,
metricsDashboardViewModel,
environmentData,
dashboardGitResponse,
} from '../mock_data';
const localVue = createLocalVue();
const expectedPanelCount = 3;
const expectedPanelCount = 4;
describe('Dashboard', () => {
let store;
......@@ -366,7 +367,7 @@ describe('Dashboard', () => {
it('metrics can be swapped', () => {
const firstDraggable = findDraggables().at(0);
const mockMetrics = [...metricsDashboardPayload.panel_groups[1].panels];
const mockMetrics = [...metricsDashboardViewModel.panelGroups[0].panels];
const firstTitle = mockMetrics[0].title;
const secondTitle = mockMetrics[1].title;
......@@ -376,7 +377,7 @@ describe('Dashboard', () => {
firstDraggable.vm.$emit('input', mockMetrics);
return wrapper.vm.$nextTick(() => {
const { panels } = wrapper.vm.dashboard.panel_groups[1];
const { panels } = wrapper.vm.dashboard.panelGroups[0];
expect(panels[1].title).toEqual(firstTitle);
expect(panels[0].title).toEqual(secondTitle);
......
......@@ -69,8 +69,8 @@ describe('Embed', () => {
describe('metrics are available', () => {
beforeEach(() => {
store.state.monitoringDashboard.dashboard.panel_groups = groups;
store.state.monitoringDashboard.dashboard.panel_groups[0].panels = metricsData;
store.state.monitoringDashboard.dashboard.panelGroups = groups;
store.state.monitoringDashboard.dashboard.panelGroups[0].panels = metricsData;
metricsWithDataGetter.mockReturnValue(metricsWithData);
......
import { mapToDashboardViewModel } from '~/monitoring/stores/utils';
// This import path needs to be relative for now because this mock data is used in
// Karma specs too, where the helpers/test_constants alias can not be resolved
import { TEST_HOST } from '../helpers/test_constants';
......@@ -246,7 +248,7 @@ export const mockedEmptyResult = {
};
export const mockedQueryResultPayload = {
metricId: '17_system_metrics_kubernetes_container_memory_average',
metricId: '12_system_metrics_kubernetes_container_memory_total',
result: [
{
metric: {},
......@@ -378,8 +380,7 @@ export const environmentData = [
},
].concat(extraEnvironmentData);
export const metricsDashboardResponse = {
dashboard: {
export const metricsDashboardPayload = {
dashboard: 'Environment metrics',
priority: 1,
panel_groups: [
......@@ -436,83 +437,6 @@ export const metricsDashboardResponse = {
},
],
},
],
},
],
},
status: 'success',
};
export const metricsDashboardPayload = {
dashboard: 'Environment metrics',
panel_groups: [
{
group: 'Response metrics (NGINX Ingress VTS)',
priority: 10,
panels: [
{
metrics: [
{
id: 'response_metrics_nginx_ingress_throughput_status_code',
label: 'Status Code',
metric_id: 1,
prometheus_endpoint_path:
'/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29',
query_range:
'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)',
unit: 'req / sec',
},
],
title: 'Throughput',
type: 'area-chart',
weight: 1,
y_label: 'Requests / Sec',
},
],
},
{
group: 'System metrics (Kubernetes)',
priority: 5,
panels: [
{
title: 'Memory Usage (Pod average)',
type: 'area-chart',
y_label: 'Memory Used per Pod',
weight: 2,
metrics: [
{
id: 'system_metrics_kubernetes_container_memory_average',
query_range:
'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024',
label: 'Pod average',
unit: 'MB',
metric_id: 17,
prometheus_endpoint_path:
'/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024',
appearance: {
line: {
width: 2,
},
},
},
],
},
{
title: 'Core Usage (Total)',
type: 'area-chart',
y_label: 'Total Cores',
weight: 3,
metrics: [
{
id: 'system_metrics_kubernetes_container_cores_total',
query_range:
'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)',
label: 'Total',
unit: 'cores',
metric_id: 13,
},
],
},
{
title: 'memories',
type: 'area-chart',
......@@ -557,9 +481,45 @@ export const metricsDashboardPayload = {
},
],
},
{
group: 'Response metrics (NGINX Ingress VTS)',
priority: 10,
panels: [
{
metrics: [
{
id: 'response_metrics_nginx_ingress_throughput_status_code',
label: 'Status Code',
metric_id: 1,
prometheus_endpoint_path:
'/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29',
query_range:
'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)',
unit: 'req / sec',
},
],
title: 'Throughput',
type: 'area-chart',
weight: 1,
y_label: 'Requests / Sec',
},
],
},
],
};
/**
* Mock of response of metrics_dashboard.json
*/
export const metricsDashboardResponse = {
all_dashboards: [],
dashboard: metricsDashboardPayload,
metrics_data: {},
status: 'success',
};
export const metricsDashboardViewModel = mapToDashboardViewModel(metricsDashboardPayload);
const customDashboardsData = new Array(30).fill(null).map((_, idx) => ({
default: false,
display_name: `Custom Dashboard ${idx}`,
......
......@@ -3,7 +3,7 @@ import testAction from 'helpers/vuex_action_helper';
import Tracking from '~/tracking';
import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status';
import { backOff } from '~/lib/utils/common_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import createFlash from '~/flash';
import store from '~/monitoring/stores';
......@@ -28,11 +28,10 @@ import {
deploymentData,
environmentData,
metricsDashboardResponse,
metricsDashboardPayload,
metricsDashboardViewModel,
dashboardGitResponse,
} from '../mock_data';
jest.mock('~/lib/utils/common_utils');
jest.mock('~/flash');
const resetStore = str => {
......@@ -44,14 +43,17 @@ const resetStore = str => {
};
describe('Monitoring store actions', () => {
const { convertObjectPropsToCamelCase } = commonUtils;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
// Mock `backOff` function to remove exponential algorithm delay.
jest.useFakeTimers();
backOff.mockImplementation(callback => {
jest.spyOn(commonUtils, 'backOff').mockImplementation(callback => {
const q = new Promise((resolve, reject) => {
const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
const next = () => callback(next, stop);
......@@ -69,7 +71,7 @@ describe('Monitoring store actions', () => {
resetStore(store);
mock.reset();
backOff.mockReset();
commonUtils.backOff.mockReset();
createFlash.mockReset();
});
......@@ -115,7 +117,6 @@ describe('Monitoring store actions', () => {
afterEach(() => {
resetStore(store);
jest.restoreAllMocks();
});
it('setting SET_ENVIRONMENTS_FILTER should dispatch fetchEnvironmentsData', () => {
......@@ -365,6 +366,7 @@ describe('Monitoring store actions', () => {
);
expect(commit).toHaveBeenCalledWith(
types.RECEIVE_METRICS_DATA_SUCCESS,
metricsDashboardResponse.dashboard,
);
expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetrics', params);
......@@ -443,8 +445,11 @@ describe('Monitoring store actions', () => {
.catch(done.fail);
});
it('dispatches fetchPrometheusMetric for each panel query', done => {
state.dashboard.panel_groups = metricsDashboardResponse.dashboard.panel_groups;
const [metric] = state.dashboard.panel_groups[0].panels[0].metrics;
state.dashboard.panelGroups = convertObjectPropsToCamelCase(
metricsDashboardResponse.dashboard.panel_groups,
);
const [metric] = state.dashboard.panelGroups[0].panels[0].metrics;
const getters = {
metricsWithData: () => [metric.id],
};
......@@ -473,16 +478,16 @@ describe('Monitoring store actions', () => {
});
it('dispatches fetchPrometheusMetric for each panel query, handles an error', done => {
state.dashboard.panel_groups = metricsDashboardResponse.dashboard.panel_groups;
const metric = state.dashboard.panel_groups[0].panels[0].metrics[0];
state.dashboard.panelGroups = metricsDashboardViewModel.panelGroups;
const metric = state.dashboard.panelGroups[0].panels[0].metrics[0];
// Mock having one out of three metrics failing
// Mock having one out of four metrics failing
dispatch.mockRejectedValueOnce(new Error('Error fetching this metric'));
dispatch.mockResolvedValue();
fetchPrometheusMetrics({ state, commit, dispatch }, params)
.then(() => {
expect(dispatch).toHaveBeenCalledTimes(3);
expect(dispatch).toHaveBeenCalledTimes(9); // one per metric
expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
metric,
params,
......@@ -508,7 +513,12 @@ describe('Monitoring store actions', () => {
beforeEach(() => {
state = storeState();
[metric] = metricsDashboardResponse.dashboard.panel_groups[0].panels[0].metrics;
[data] = metricsDashboardPayload.panel_groups[0].panels[0].metrics;
metric = convertObjectPropsToCamelCase(metric, { deep: true });
data = {
metricId: metric.metricId,
result: [1582065167.353, 5, 1582065599.353],
};
});
it('commits result', done => {
......@@ -522,13 +532,13 @@ describe('Monitoring store actions', () => {
{
type: types.REQUEST_METRIC_RESULT,
payload: {
metricId: metric.metric_id,
metricId: metric.metricId,
},
},
{
type: types.RECEIVE_METRIC_RESULT_SUCCESS,
payload: {
metricId: metric.metric_id,
metricId: metric.metricId,
result: data.result,
},
},
......@@ -556,13 +566,13 @@ describe('Monitoring store actions', () => {
{
type: types.REQUEST_METRIC_RESULT,
payload: {
metricId: metric.metric_id,
metricId: metric.metricId,
},
},
{
type: types.RECEIVE_METRIC_RESULT_SUCCESS,
payload: {
metricId: metric.metric_id,
metricId: metric.metricId,
result: data.result,
},
},
......@@ -592,13 +602,13 @@ describe('Monitoring store actions', () => {
{
type: types.REQUEST_METRIC_RESULT,
payload: {
metricId: metric.metric_id,
metricId: metric.metricId,
},
},
{
type: types.RECEIVE_METRIC_RESULT_FAILURE,
payload: {
metricId: metric.metric_id,
metricId: metric.metricId,
error,
},
},
......
......@@ -32,7 +32,7 @@ describe('Monitoring store Getters', () => {
it('when dashboard has no panel groups, returns empty', () => {
setupState({
dashboard: {
panel_groups: [],
panelGroups: [],
},
});
......@@ -43,10 +43,10 @@ describe('Monitoring store Getters', () => {
let groups;
beforeEach(() => {
setupState({
dashboard: { panel_groups: [] },
dashboard: { panelGroups: [] },
});
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload);
groups = state.dashboard.panel_groups;
groups = state.dashboard.panelGroups;
});
it('no loaded metric returns empty', () => {
......@@ -84,8 +84,8 @@ describe('Monitoring store Getters', () => {
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]);
expect(getMetricStates(state.dashboard.panelGroups[0].key)).toEqual([metricStates.OK]);
expect(getMetricStates(state.dashboard.panelGroups[1].key)).toEqual([]);
});
it('on multiple metrics errors', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload);
......@@ -94,10 +94,10 @@ describe('Monitoring store Getters', () => {
metricId: groups[0].panels[0].metrics[0].metricId,
});
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
metricId: groups[1].panels[0].metrics[0].metricId,
metricId: groups[0].panels[0].metrics[0].metricId,
});
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
metricId: groups[1].panels[1].metrics[0].metricId,
metricId: groups[1].panels[0].metrics[0].metricId,
});
// Entire dashboard fails
......@@ -113,18 +113,18 @@ describe('Monitoring store Getters', () => {
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,
metricId: groups[0].panels[1].metrics[0].metricId,
});
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
metricId: groups[1].panels[1].metrics[0].metricId,
metricId: groups[1].panels[0].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([
expect(getMetricStates(groups[0].key)).toEqual([
metricStates.OK,
metricStates.UNKNOWN_ERROR,
]);
expect(getMetricStates(groups[1].key)).toEqual([metricStates.UNKNOWN_ERROR]);
});
});
});
......@@ -154,7 +154,7 @@ describe('Monitoring store Getters', () => {
it('when dashboard has no panel groups, returns empty', () => {
setupState({
dashboard: {
panel_groups: [],
panelGroups: [],
},
});
......@@ -164,7 +164,7 @@ describe('Monitoring store Getters', () => {
describe('when the dashboard is set', () => {
beforeEach(() => {
setupState({
dashboard: { panel_groups: [] },
dashboard: { panelGroups: [] },
});
});
......@@ -204,14 +204,14 @@ describe('Monitoring store Getters', () => {
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayload);
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayloadCoresTotal);
// First group has no metrics
expect(metricsWithData(state.dashboard.panel_groups[0].key)).toEqual([]);
// Second group has metrics
expect(metricsWithData(state.dashboard.panel_groups[1].key)).toEqual([
// First group has metrics
expect(metricsWithData(state.dashboard.panelGroups[0].key)).toEqual([
mockedQueryResultPayload.metricId,
mockedQueryResultPayloadCoresTotal.metricId,
]);
// Second group has no metrics
expect(metricsWithData(state.dashboard.panelGroups[1].key)).toEqual([]);
});
});
});
......
......@@ -4,12 +4,8 @@ import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import state from '~/monitoring/stores/state';
import { metricStates } from '~/monitoring/constants';
import {
metricsDashboardPayload,
deploymentData,
metricsDashboardResponse,
dashboardGitResponse,
} from '../mock_data';
import { metricsDashboardPayload, deploymentData, dashboardGitResponse } from '../mock_data';
describe('Monitoring mutations', () => {
let stateCopy;
......@@ -17,27 +13,29 @@ describe('Monitoring mutations', () => {
beforeEach(() => {
stateCopy = state();
});
describe('RECEIVE_METRICS_DATA_SUCCESS', () => {
let payload;
const getGroups = () => stateCopy.dashboard.panel_groups;
const getGroups = () => stateCopy.dashboard.panelGroups;
beforeEach(() => {
stateCopy.dashboard.panel_groups = [];
stateCopy.dashboard.panelGroups = [];
payload = metricsDashboardPayload;
});
it('adds a key to the group', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload);
const groups = getGroups();
expect(groups[0].key).toBe('response-metrics-nginx-ingress-vts-0');
expect(groups[1].key).toBe('system-metrics-kubernetes-1');
expect(groups[0].key).toBe('system-metrics-kubernetes-0');
expect(groups[1].key).toBe('response-metrics-nginx-ingress-vts-1');
});
it('normalizes values', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload);
const expectedLabel = 'Pod average';
const { label, query_range } = getGroups()[1].panels[0].metrics[0];
const { label, queryRange } = getGroups()[0].panels[2].metrics[0];
expect(label).toEqual(expectedLabel);
expect(query_range.length).toBeGreaterThan(0);
expect(queryRange.length).toBeGreaterThan(0);
});
it('contains two groups, with panels with a metric each', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload);
......@@ -47,13 +45,14 @@ describe('Monitoring mutations', () => {
expect(groups).toBeDefined();
expect(groups).toHaveLength(2);
expect(groups[0].panels).toHaveLength(1);
expect(groups[0].panels).toHaveLength(4);
expect(groups[0].panels[0].metrics).toHaveLength(1);
expect(groups[0].panels[1].metrics).toHaveLength(1);
expect(groups[0].panels[2].metrics).toHaveLength(1);
expect(groups[0].panels[3].metrics).toHaveLength(5);
expect(groups[1].panels).toHaveLength(3);
expect(groups[1].panels).toHaveLength(1);
expect(groups[1].panels[0].metrics).toHaveLength(1);
expect(groups[1].panels[1].metrics).toHaveLength(1);
expect(groups[1].panels[2].metrics).toHaveLength(5);
});
it('assigns metrics a metric id', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload);
......@@ -61,10 +60,10 @@ describe('Monitoring mutations', () => {
const groups = getGroups();
expect(groups[0].panels[0].metrics[0].metricId).toEqual(
'1_response_metrics_nginx_ingress_throughput_status_code',
'12_system_metrics_kubernetes_container_memory_total',
);
expect(groups[1].panels[0].metrics[0].metricId).toEqual(
'17_system_metrics_kubernetes_container_memory_average',
'1_response_metrics_nginx_ingress_throughput_status_code',
);
});
});
......@@ -130,8 +129,8 @@ describe('Monitoring mutations', () => {
values: [[0, 1], [1, 1], [1, 3]],
},
];
const { dashboard } = metricsDashboardResponse;
const getMetric = () => stateCopy.dashboard.panel_groups[0].panels[0].metrics[0];
const dashboard = metricsDashboardPayload;
const getMetric = () => stateCopy.dashboard.panelGroups[0].panels[0].metrics[0];
describe('REQUEST_METRIC_RESULT', () => {
beforeEach(() => {
......
import {
normalizeMetric,
uniqMetricsId,
parseEnvironmentsResponse,
removeLeadingSlash,
mapToDashboardViewModel,
} from '~/monitoring/stores/utils';
const projectPath = 'gitlab-org/gitlab-test';
describe('normalizeMetric', () => {
[
{ args: [], expected: 'undefined_undefined' },
{ args: [undefined], expected: 'undefined_undefined' },
{ args: [{ id: 'something' }], expected: 'undefined_something' },
{ args: [{ id: 45 }], expected: 'undefined_45' },
{ args: [{ metric_id: 5 }], expected: '5_undefined' },
{ args: [{ metric_id: 'something' }], expected: 'something_undefined' },
{
args: [{ metric_id: 5, id: 'system_metrics_kubernetes_container_memory_total' }],
expected: '5_system_metrics_kubernetes_container_memory_total',
},
].forEach(({ args, expected }) => {
it(`normalizes metric to "${expected}" with args=${JSON.stringify(args)}`, () => {
expect(normalizeMetric(...args)).toEqual({ metric_id: expected, metricId: expected });
describe('mapToDashboardViewModel', () => {
it('maps an empty dashboard', () => {
expect(mapToDashboardViewModel({})).toEqual({
dashboard: '',
panelGroups: [],
});
});
it('maps a simple dashboard', () => {
const response = {
dashboard: 'Dashboard Name',
panel_groups: [
{
group: 'Group 1',
panels: [
{
title: 'Title A',
type: 'chart-type',
y_label: 'Y Label A',
metrics: [],
},
],
},
],
};
expect(mapToDashboardViewModel(response)).toEqual({
dashboard: 'Dashboard Name',
panelGroups: [
{
group: 'Group 1',
key: 'group-1-0',
panels: [
{
title: 'Title A',
type: 'chart-type',
y_label: 'Y Label A',
metrics: [],
},
],
},
],
});
});
describe('panel groups mapping', () => {
it('key', () => {
const response = {
dashboard: 'Dashboard Name',
panel_groups: [
{
group: 'Group A',
},
{
group: 'Group B',
},
{
group: '',
unsupported_property: 'This should be removed',
},
],
};
expect(mapToDashboardViewModel(response).panelGroups).toEqual([
{
group: 'Group A',
key: 'group-a-0',
panels: [],
},
{
group: 'Group B',
key: 'group-b-1',
panels: [],
},
{
group: '',
key: 'default-2',
panels: [],
},
]);
});
});
describe('metrics mapping', () => {
const defaultLabel = 'Panel Label';
const dashboardWithMetric = (metric, label = defaultLabel) => ({
panel_groups: [
{
panels: [
{
y_label: label,
metrics: [metric],
},
],
},
],
});
const getMappedMetric = dashboard => {
return mapToDashboardViewModel(dashboard).panelGroups[0].panels[0].metrics[0];
};
it('creates a metric', () => {
const dashboard = dashboardWithMetric({});
expect(getMappedMetric(dashboard)).toEqual({
label: expect.any(String),
metricId: expect.any(String),
metric_id: expect.any(String),
});
});
it('creates a metric with a correct ids', () => {
const dashboard = dashboardWithMetric({
id: 'http_responses',
metric_id: 1,
});
expect(getMappedMetric(dashboard)).toMatchObject({
metricId: '1_http_responses',
metric_id: '1_http_responses',
});
});
it('creates a metric with a default label', () => {
const dashboard = dashboardWithMetric({});
expect(getMappedMetric(dashboard)).toMatchObject({
label: defaultLabel,
});
});
it('creates a metric with an endpoint and query', () => {
const dashboard = dashboardWithMetric({
prometheus_endpoint_path: 'http://test',
query_range: 'http_responses',
});
expect(getMappedMetric(dashboard)).toMatchObject({
prometheusEndpointPath: 'http://test',
queryRange: 'http_responses',
});
});
it('creates a metric with an ad-hoc property', () => {
// This behavior is deprecated and should be removed
// https://gitlab.com/gitlab-org/gitlab/issues/207198
const dashboard = dashboardWithMetric({
x_label: 'Another label',
unkown_option: 'unkown_data',
});
expect(getMappedMetric(dashboard)).toMatchObject({
x_label: 'Another label',
unkown_option: 'unkown_data',
});
});
});
});
......
......@@ -112,7 +112,7 @@ describe('Dashboard', () => {
setupComponentStore(component);
return Vue.nextTick().then(() => {
[, promPanel] = component.$el.querySelectorAll('.prometheus-panel');
[promPanel] = component.$el.querySelectorAll('.prometheus-panel');
promGroup = promPanel.querySelector('.prometheus-graph-group');
panelToggle = promPanel.querySelector('.js-graph-group-toggle');
chart = promGroup.querySelector('.position-relative svg');
......
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