Commit 8110c024 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'support-file-names-for-metrics-dashboards' into 'master'

Support loading metrics dashboard with file name

See merge request gitlab-org/gitlab!34115
parents 483be3ca 4d791f71
...@@ -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