Commit 4d791f71 authored by Dhiraj Bodicherla's avatar Dhiraj Bodicherla

Support metrics dashboard with file name

Metrics dashboard requires entire custom dashboard
path to work. This MR enables the dashboard to work
with dashboard file names
parent ce62970d
...@@ -22,6 +22,7 @@ export default (props = {}) => { ...@@ -22,6 +22,7 @@ export default (props = {}) => {
currentEnvironmentName, currentEnvironmentName,
dashboardTimezone, dashboardTimezone,
metricsDashboardBasePath, metricsDashboardBasePath,
customDashboardBasePath,
...dataProps ...dataProps
} = el.dataset; } = el.dataset;
...@@ -34,6 +35,7 @@ export default (props = {}) => { ...@@ -34,6 +35,7 @@ export default (props = {}) => {
projectPath, projectPath,
logsPath, logsPath,
currentEnvironmentName, currentEnvironmentName,
customDashboardBasePath,
}); });
// HTML attributes are always strings, parse other types. // HTML attributes are always strings, parse other types.
......
...@@ -117,12 +117,12 @@ export const fetchData = ({ dispatch }) => { ...@@ -117,12 +117,12 @@ export const fetchData = ({ dispatch }) => {
// Metrics dashboard // Metrics dashboard
export const fetchDashboard = ({ state, commit, dispatch }) => { export const fetchDashboard = ({ state, commit, dispatch, getters }) => {
dispatch('requestMetricsDashboard'); dispatch('requestMetricsDashboard');
const params = {}; const params = {};
if (state.currentDashboard) { if (getters.fullDashboardPath) {
params.dashboard = state.currentDashboard; params.dashboard = getters.fullDashboardPath;
} }
return backOffRequest(() => axios.get(state.dashboardEndpoint, { params })) return backOffRequest(() => axios.get(state.dashboardEndpoint, { params }))
...@@ -204,7 +204,7 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => { ...@@ -204,7 +204,7 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => {
return Promise.all(promises) return Promise.all(promises)
.then(() => { .then(() => {
const dashboardType = state.currentDashboard === '' ? 'default' : 'custom'; const dashboardType = getters.fullDashboardPath === '' ? 'default' : 'custom';
trackDashboardLoad({ trackDashboardLoad({
label: `${dashboardType}_metrics_dashboard`, label: `${dashboardType}_metrics_dashboard`,
value: getters.metricsWithData().length, value: getters.metricsWithData().length,
...@@ -322,9 +322,9 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => { ...@@ -322,9 +322,9 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => {
commit(types.RECEIVE_ENVIRONMENTS_DATA_FAILURE); commit(types.RECEIVE_ENVIRONMENTS_DATA_FAILURE);
}; };
export const fetchAnnotations = ({ state, dispatch }) => { export const fetchAnnotations = ({ state, dispatch, getters }) => {
const { start } = convertToFixedRange(state.timeRange); const { start } = convertToFixedRange(state.timeRange);
const dashboardPath = state.currentDashboard || DEFAULT_DASHBOARD_PATH; const dashboardPath = getters.fullDashboardPath || DEFAULT_DASHBOARD_PATH;
return gqClient return gqClient
.mutate({ .mutate({
mutation: getAnnotations, mutation: getAnnotations,
......
import { NOT_IN_DB_PREFIX } from '../constants'; import { NOT_IN_DB_PREFIX } from '../constants';
import { addPrefixToCustomVariableParams, addDashboardMetaDataToLink } from './utils'; import {
addPrefixToCustomVariableParams,
addDashboardMetaDataToLink,
normalizeCustomDashboardPath,
} from './utils';
const metricsIdsInPanel = panel => const metricsIdsInPanel = panel =>
panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId); panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId);
...@@ -10,10 +14,10 @@ const metricsIdsInPanel = panel => ...@@ -10,10 +14,10 @@ const metricsIdsInPanel = panel =>
* *
* @param {Object} state * @param {Object} state
*/ */
export const selectedDashboard = state => { export const selectedDashboard = (state, getters) => {
const { allDashboards } = state; const { allDashboards } = state;
return ( return (
allDashboards.find(d => d.path === state.currentDashboard) || allDashboards.find(d => d.path === getters.fullDashboardPath) ||
allDashboards.find(d => d.default) || allDashboards.find(d => d.default) ||
null null
); );
...@@ -154,5 +158,15 @@ export const getCustomVariablesParams = state => ...@@ -154,5 +158,15 @@ export const getCustomVariablesParams = state =>
return acc; return acc;
}, {}); }, {});
/**
* For a given custom dashboard file name, this method
* returns the full file path.
*
* @param {Object} state
* @returns {String} full dashboard path
*/
export const fullDashboardPath = state =>
normalizeCustomDashboardPath(state.currentDashboard, state.customDashboardBasePath);
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -9,6 +9,13 @@ export default () => ({ ...@@ -9,6 +9,13 @@ export default () => ({
// Dashboard request parameters // Dashboard request parameters
timeRange: null, timeRange: null,
/**
* Currently selected dashboard. For custom dashboards,
* this could be the filename or the file path.
*
* If this is the filename and full path is required,
* getters.fullDashboardPath should be used.
*/
currentDashboard: null, currentDashboard: null,
// Dashboard data // Dashboard data
...@@ -58,4 +65,7 @@ export default () => ({ ...@@ -58,4 +65,7 @@ export default () => ({
// GitLab paths to other pages // GitLab paths to other pages
projectPath: null, projectPath: null,
logsPath: invalidUrl, logsPath: invalidUrl,
// static paths
customDashboardBasePath: '',
}); });
...@@ -3,10 +3,10 @@ import createGqClient, { fetchPolicies } from '~/lib/graphql'; ...@@ -3,10 +3,10 @@ import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format'; import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { parseTemplatingVariables } from './variable_mapping'; import { parseTemplatingVariables } from './variable_mapping';
import { NOT_IN_DB_PREFIX, linkTypes } from '../constants';
import { DATETIME_RANGE_TYPES } from '~/lib/utils/constants'; import { DATETIME_RANGE_TYPES } from '~/lib/utils/constants';
import { timeRangeToParams, getRangeType } from '~/lib/utils/datetime_range'; import { timeRangeToParams, getRangeType } from '~/lib/utils/datetime_range';
import { isSafeURL, mergeUrlParams } from '~/lib/utils/url_utility'; import { isSafeURL, mergeUrlParams } from '~/lib/utils/url_utility';
import { NOT_IN_DB_PREFIX, linkTypes, DEFAULT_DASHBOARD_PATH } from '../constants';
export const gqClient = createGqClient( export const gqClient = createGqClient(
{}, {},
...@@ -440,3 +440,31 @@ export const normalizeQueryResponseData = data => { ...@@ -440,3 +440,31 @@ export const normalizeQueryResponseData = data => {
* @returns {String} * @returns {String}
*/ */
export const addPrefixToCustomVariableParams = key => `variables[${key}]`; export const addPrefixToCustomVariableParams = key => `variables[${key}]`;
/**
* Normalize custom dashboard paths. This method helps support
* metrics dashboard to work with custom dashboard file names instead
* of the entire path.
*
* If dashboard is empty, it is the default dashboard.
* If dashboard is set, it usually is a custom dashboard unless
* explicitly it is set to default dashboard path.
*
* @param {String} dashboard dashboard path
* @param {String} dashboardPrefix custom dashboard directory prefix
* @returns {String} normalized dashboard path
*/
export const normalizeCustomDashboardPath = (dashboard, dashboardPrefix = '') => {
const currDashboard = dashboard || '';
let dashboardPath = `${dashboardPrefix}/${currDashboard}`;
if (!currDashboard) {
dashboardPath = '';
} else if (
currDashboard.startsWith(dashboardPrefix) ||
currDashboard.startsWith(DEFAULT_DASHBOARD_PATH)
) {
dashboardPath = currDashboard;
}
return dashboardPath;
};
---
title: Support metrics dashboard with file name
merge_request: 34115
author:
type: added
...@@ -22,6 +22,8 @@ export const propsData = { ...@@ -22,6 +22,8 @@ export const propsData = {
validateQueryPath: '', validateQueryPath: '',
}; };
export const customDashboardBasePath = '.gitlab/dashboards';
const customDashboardsData = new Array(30).fill(null).map((_, idx) => ({ const customDashboardsData = new Array(30).fill(null).map((_, idx) => ({
default: false, default: false,
display_name: `Custom Dashboard ${idx}`, display_name: `Custom Dashboard ${idx}`,
......
...@@ -6,6 +6,7 @@ import statusCodes from '~/lib/utils/http_status'; ...@@ -6,6 +6,7 @@ import statusCodes from '~/lib/utils/http_status';
import * as commonUtils from '~/lib/utils/common_utils'; import * as commonUtils from '~/lib/utils/common_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { defaultTimeRange } from '~/vue_shared/constants'; import { defaultTimeRange } from '~/vue_shared/constants';
import * as getters from '~/monitoring/stores/getters';
import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants'; import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
import { createStore } from '~/monitoring/stores'; import { createStore } from '~/monitoring/stores';
...@@ -62,7 +63,7 @@ describe('Monitoring store actions', () => { ...@@ -62,7 +63,7 @@ describe('Monitoring store actions', () => {
let state; let state;
beforeEach(() => { beforeEach(() => {
store = createStore(); store = createStore({ getters });
state = store.state.monitoringDashboard; state = store.state.monitoringDashboard;
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
...@@ -265,6 +266,11 @@ describe('Monitoring store actions', () => { ...@@ -265,6 +266,11 @@ describe('Monitoring store actions', () => {
state.projectPath = 'gitlab-org/gitlab-test'; state.projectPath = 'gitlab-org/gitlab-test';
state.currentEnvironmentName = 'production'; state.currentEnvironmentName = 'production';
state.currentDashboard = '.gitlab/dashboards/custom_dashboard.yml'; state.currentDashboard = '.gitlab/dashboards/custom_dashboard.yml';
// testAction doesn't have access to getters. The state is passed in as getters
// instead of the actual getters inside the testAction method implementation.
// All methods downstream that needs access to getters will throw and error.
// For that reason, the result of the getter is set as a state variable.
state.fullDashboardPath = store.getters['monitoringDashboard/fullDashboardPath'];
}); });
it('fetches annotations data and dispatches receiveAnnotationsSuccess', () => { it('fetches annotations data and dispatches receiveAnnotationsSuccess', () => {
...@@ -581,9 +587,12 @@ describe('Monitoring store actions', () => { ...@@ -581,9 +587,12 @@ describe('Monitoring store actions', () => {
let result; let result;
beforeEach(() => { beforeEach(() => {
const params = {}; const params = {};
const localGetters = {
fullDashboardPath: store.getters['monitoringDashboard/fullDashboardPath'],
};
result = () => { result = () => {
mock.onGet(state.dashboardEndpoint).replyOnce(500, mockDashboardsErrorResponse); mock.onGet(state.dashboardEndpoint).replyOnce(500, mockDashboardsErrorResponse);
return fetchDashboard({ state, commit, dispatch }, params); return fetchDashboard({ state, commit, dispatch, getters: localGetters }, params);
}; };
}); });
...@@ -712,10 +721,10 @@ describe('Monitoring store actions', () => { ...@@ -712,10 +721,10 @@ describe('Monitoring store actions', () => {
}); });
it('commits empty state when state.groups is empty', done => { it('commits empty state when state.groups is empty', done => {
const getters = { const localGetters = {
metricsWithData: () => [], metricsWithData: () => [],
}; };
fetchDashboardData({ state, commit, dispatch, getters }) fetchDashboardData({ state, commit, dispatch, getters: localGetters })
.then(() => { .then(() => {
expect(Tracking.event).toHaveBeenCalledWith( expect(Tracking.event).toHaveBeenCalledWith(
document.body.dataset.page, document.body.dataset.page,
...@@ -740,11 +749,11 @@ describe('Monitoring store actions', () => { ...@@ -740,11 +749,11 @@ describe('Monitoring store actions', () => {
); );
const [metric] = state.dashboard.panelGroups[0].panels[0].metrics; const [metric] = state.dashboard.panelGroups[0].panels[0].metrics;
const getters = { const localGetters = {
metricsWithData: () => [metric.id], metricsWithData: () => [metric.id],
}; };
fetchDashboardData({ state, commit, dispatch, getters }) fetchDashboardData({ state, commit, dispatch, getters: localGetters })
.then(() => { .then(() => {
expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', { expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
metric, metric,
......
...@@ -4,6 +4,7 @@ import mutations from '~/monitoring/stores/mutations'; ...@@ -4,6 +4,7 @@ import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types'; import * as types from '~/monitoring/stores/mutation_types';
import { metricStates } from '~/monitoring/constants'; import { metricStates } from '~/monitoring/constants';
import { import {
customDashboardBasePath,
environmentData, environmentData,
metricsResult, metricsResult,
dashboardGitResponse, dashboardGitResponse,
...@@ -364,45 +365,53 @@ describe('Monitoring store Getters', () => { ...@@ -364,45 +365,53 @@ describe('Monitoring store Getters', () => {
describe('selectedDashboard', () => { describe('selectedDashboard', () => {
const { selectedDashboard } = getters; const { selectedDashboard } = getters;
const localGetters = state => ({
fullDashboardPath: getters.fullDashboardPath(state),
});
it('returns a dashboard', () => { it('returns a dashboard', () => {
const state = { const state = {
allDashboards: dashboardGitResponse, allDashboards: dashboardGitResponse,
currentDashboard: dashboardGitResponse[0].path, currentDashboard: dashboardGitResponse[0].path,
customDashboardBasePath,
}; };
expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]); expect(selectedDashboard(state, localGetters(state))).toEqual(dashboardGitResponse[0]);
}); });
it('returns a non-default dashboard', () => { it('returns a non-default dashboard', () => {
const state = { const state = {
allDashboards: dashboardGitResponse, allDashboards: dashboardGitResponse,
currentDashboard: dashboardGitResponse[1].path, currentDashboard: dashboardGitResponse[1].path,
customDashboardBasePath,
}; };
expect(selectedDashboard(state)).toEqual(dashboardGitResponse[1]); expect(selectedDashboard(state, localGetters(state))).toEqual(dashboardGitResponse[1]);
}); });
it('returns a default dashboard when no dashboard is selected', () => { it('returns a default dashboard when no dashboard is selected', () => {
const state = { const state = {
allDashboards: dashboardGitResponse, allDashboards: dashboardGitResponse,
currentDashboard: null, currentDashboard: null,
customDashboardBasePath,
}; };
expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]); expect(selectedDashboard(state, localGetters(state))).toEqual(dashboardGitResponse[0]);
}); });
it('returns a default dashboard when dashboard cannot be found', () => { it('returns a default dashboard when dashboard cannot be found', () => {
const state = { const state = {
allDashboards: dashboardGitResponse, allDashboards: dashboardGitResponse,
currentDashboard: 'wrong_path', currentDashboard: 'wrong_path',
customDashboardBasePath,
}; };
expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]); expect(selectedDashboard(state, localGetters(state))).toEqual(dashboardGitResponse[0]);
}); });
it('returns null when no dashboards are present', () => { it('returns null when no dashboards are present', () => {
const state = { const state = {
allDashboards: [], allDashboards: [],
currentDashboard: dashboardGitResponse[0].path, currentDashboard: dashboardGitResponse[0].path,
customDashboardBasePath,
}; };
expect(selectedDashboard(state)).toEqual(null); expect(selectedDashboard(state, localGetters(state))).toEqual(null);
}); });
}); });
......
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
normalizeQueryResponseData, normalizeQueryResponseData,
convertToGrafanaTimeRange, convertToGrafanaTimeRange,
addDashboardMetaDataToLink, addDashboardMetaDataToLink,
normalizeCustomDashboardPath,
} from '~/monitoring/stores/utils'; } from '~/monitoring/stores/utils';
import { annotationsData } from '../mock_data'; import { annotationsData } from '../mock_data';
import { NOT_IN_DB_PREFIX } from '~/monitoring/constants'; import { NOT_IN_DB_PREFIX } from '~/monitoring/constants';
...@@ -700,3 +701,24 @@ describe('normalizeQueryResponseData', () => { ...@@ -700,3 +701,24 @@ describe('normalizeQueryResponseData', () => {
]); ]);
}); });
}); });
describe('normalizeCustomDashboardPath', () => {
it.each`
input | expected
${[undefined]} | ${''}
${[null]} | ${''}
${[]} | ${''}
${['links.yml']} | ${'links.yml'}
${['links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/links.yml'}
${['config/prometheus/common_metrics.yml']} | ${'config/prometheus/common_metrics.yml'}
${['config/prometheus/common_metrics.yml', '.gitlab/dashboards']} | ${'config/prometheus/common_metrics.yml'}
${['dir1/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/dir1/links.yml'}
${['dir1/dir2/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/dir1/dir2/links.yml'}
${['.gitlab/dashboards/links.yml']} | ${'.gitlab/dashboards/links.yml'}
${['.gitlab/dashboards/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/links.yml'}
${['.gitlab/dashboards/dir1/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/dir1/links.yml'}
${['.gitlab/dashboards/dir1/dir2/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/dir1/dir2/links.yml'}
`(`normalizeCustomDashboardPath returns $expected for $input`, ({ input, expected }) => {
expect(normalizeCustomDashboardPath(...input)).toEqual(expected);
});
});
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