Commit 43b27ea4 authored by Miguel Rincon's avatar Miguel Rincon

Simplify variable manipulation by using arrays

Variables are represented by users in the backend by using hashes
of objects to represent unique fields in their dashboards.

However, this can lead to difficulties when creating mock data and
constructing flexible data structures.

This change addresses this issue by using a simpler data structure for
variables based on arrays.
parent a055c03a
...@@ -164,7 +164,7 @@ export default { ...@@ -164,7 +164,7 @@ export default {
]), ]),
...mapGetters('monitoringDashboard', ['selectedDashboard', 'getMetricStates']), ...mapGetters('monitoringDashboard', ['selectedDashboard', 'getMetricStates']),
shouldShowVariablesSection() { shouldShowVariablesSection() {
return Object.keys(this.variables).length > 0; return Boolean(this.variables.length);
}, },
shouldShowLinksSection() { shouldShowLinksSection() {
return Object.keys(this.links).length > 0; return Object.keys(this.links).length > 0;
......
...@@ -34,7 +34,7 @@ export default { ...@@ -34,7 +34,7 @@ export default {
}, },
methods: { methods: {
onUpdate(value) { onUpdate(value) {
this.$emit('onUpdate', this.name, value); this.$emit('input', value);
}, },
}, },
}; };
......
...@@ -22,7 +22,7 @@ export default { ...@@ -22,7 +22,7 @@ export default {
}, },
methods: { methods: {
onUpdate(event) { onUpdate(event) {
this.$emit('onUpdate', this.name, event.target.value); this.$emit('input', event.target.value);
}, },
}, },
}; };
......
...@@ -16,10 +16,9 @@ export default { ...@@ -16,10 +16,9 @@ export default {
methods: { methods: {
...mapActions('monitoringDashboard', ['updateVariablesAndFetchData']), ...mapActions('monitoringDashboard', ['updateVariablesAndFetchData']),
refreshDashboard(variable, value) { refreshDashboard(variable, value) {
if (this.variables[variable].value !== value) { if (variable.value !== value) {
const changedVariable = { key: variable, value }; this.updateVariablesAndFetchData({ name: variable.name, value });
// update the Vuex store // update the Vuex store
this.updateVariablesAndFetchData(changedVariable);
// the below calls can ideally be moved out of the // the below calls can ideally be moved out of the
// component and into the actions and let the // component and into the actions and let the
// mutation respond directly. // mutation respond directly.
...@@ -39,15 +38,15 @@ export default { ...@@ -39,15 +38,15 @@ export default {
</script> </script>
<template> <template>
<div ref="variablesSection" class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section"> <div ref="variablesSection" class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section">
<div v-for="(variable, key) in variables" :key="key" class="mb-1 pr-2 d-flex d-sm-block"> <div v-for="variable in variables" :key="variable.name" class="mb-1 pr-2 d-flex d-sm-block">
<component <component
:is="variableField(variable.type)" :is="variableField(variable.type)"
class="mb-0 flex-grow-1" class="mb-0 flex-grow-1"
:label="variable.label" :label="variable.label"
:value="variable.value" :value="variable.value"
:name="key" :name="variable.name"
:options="variable.options" :options="variable.options"
@onUpdate="refreshDashboard" @input="refreshDashboard(variable, $event)"
/> />
</div> </div>
</div> </div>
......
...@@ -77,10 +77,6 @@ export const setTimeRange = ({ commit }, timeRange) => { ...@@ -77,10 +77,6 @@ export const setTimeRange = ({ commit }, timeRange) => {
commit(types.SET_TIME_RANGE, timeRange); commit(types.SET_TIME_RANGE, timeRange);
}; };
export const setVariables = ({ commit }, variables) => {
commit(types.SET_VARIABLES, variables);
};
export const filterEnvironments = ({ commit, dispatch }, searchTerm) => { export const filterEnvironments = ({ commit, dispatch }, searchTerm) => {
commit(types.SET_ENVIRONMENTS_FILTER, searchTerm); commit(types.SET_ENVIRONMENTS_FILTER, searchTerm);
dispatch('fetchEnvironmentsData'); dispatch('fetchEnvironmentsData');
...@@ -235,7 +231,7 @@ export const fetchPrometheusMetric = ( ...@@ -235,7 +231,7 @@ export const fetchPrometheusMetric = (
queryParams.step = metric.step; queryParams.step = metric.step;
} }
if (Object.keys(state.variables).length > 0) { if (state.variables.length > 0) {
queryParams = { queryParams = {
...queryParams, ...queryParams,
...getters.getCustomVariablesParams, ...getters.getCustomVariablesParams,
...@@ -480,7 +476,7 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery ...@@ -480,7 +476,7 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery
const { start_time, end_time } = defaultQueryParams; const { start_time, end_time } = defaultQueryParams;
const optionsRequests = []; const optionsRequests = [];
Object.entries(state.variables).forEach(([key, variable]) => { state.variables.forEach(variable => {
if (variable.type === VARIABLE_TYPES.metric_label_values) { if (variable.type === VARIABLE_TYPES.metric_label_values) {
const { prometheusEndpointPath, label } = variable.options; const { prometheusEndpointPath, label } = variable.options;
...@@ -496,7 +492,7 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery ...@@ -496,7 +492,7 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery
.catch(() => { .catch(() => {
createFlash( createFlash(
sprintf(s__('Metrics|There was an error getting options for variable "%{name}".'), { sprintf(s__('Metrics|There was an error getting options for variable "%{name}".'), {
name: key, name: variable.name,
}), }),
); );
}); });
......
...@@ -133,8 +133,8 @@ export const linksWithMetadata = state => { ...@@ -133,8 +133,8 @@ export const linksWithMetadata = state => {
}; };
/** /**
* Maps an variables object to an array along with stripping * Maps a variables array to an object for replacement in
* the variable prefix. * prometheus queries.
* *
* This method outputs an object in the below format * This method outputs an object in the below format
* *
...@@ -147,14 +147,17 @@ export const linksWithMetadata = state => { ...@@ -147,14 +147,17 @@ export const linksWithMetadata = state => {
* user-defined variables coming through the URL and differentiate * user-defined variables coming through the URL and differentiate
* from other variables used for Prometheus API endpoint. * from other variables used for Prometheus API endpoint.
* *
* @param {Object} variables - Custom variables provided by the user * @param {Object} state - State containing variables provided by the user
* @returns {Array} The custom variables array to be send to the API * @returns {Array} The custom variables object to be send to the API
* in the format of {variables[key1]=value1, variables[key2]=value2} * in the format of {variables[key1]=value1, variables[key2]=value2}
*/ */
export const getCustomVariablesParams = state => export const getCustomVariablesParams = state =>
Object.keys(state.variables).reduce((acc, variable) => { state.variables.reduce((acc, variable) => {
acc[addPrefixToCustomVariableParams(variable)] = state.variables[variable]?.value; const { name, value } = variable;
if (value !== null) {
acc[addPrefixToCustomVariableParams(name)] = value;
}
return acc; return acc;
}, {}); }, {});
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD'; export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD';
export const RECEIVE_METRICS_DASHBOARD_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS'; export const RECEIVE_METRICS_DASHBOARD_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS';
export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE'; export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE';
export const SET_VARIABLES = 'SET_VARIABLES';
export const UPDATE_VARIABLE_VALUE = 'UPDATE_VARIABLE_VALUE'; export const UPDATE_VARIABLE_VALUE = 'UPDATE_VARIABLE_VALUE';
export const UPDATE_VARIABLE_METRIC_LABEL_VALUES = 'UPDATE_VARIABLE_METRIC_LABEL_VALUES'; export const UPDATE_VARIABLE_METRIC_LABEL_VALUES = 'UPDATE_VARIABLE_METRIC_LABEL_VALUES';
......
...@@ -203,14 +203,13 @@ export default { ...@@ -203,14 +203,13 @@ export default {
state.expandedPanel.group = group; state.expandedPanel.group = group;
state.expandedPanel.panel = panel; state.expandedPanel.panel = panel;
}, },
[types.SET_VARIABLES](state, variables) { [types.UPDATE_VARIABLE_VALUE](state, { name, value }) {
state.variables = variables; const variable = state.variables.find(v => v.name === name);
}, if (variable) {
[types.UPDATE_VARIABLE_VALUE](state, { key, value }) { Object.assign(variable, {
Object.assign(state.variables[key], { value,
...state.variables[key], });
value, }
});
}, },
[types.UPDATE_VARIABLE_METRIC_LABEL_VALUES](state, { variable, label, data = [] }) { [types.UPDATE_VARIABLE_METRIC_LABEL_VALUES](state, { variable, label, data = [] }) {
const values = optionsFromSeriesData({ label, data }); const values = optionsFromSeriesData({ label, data });
......
...@@ -47,7 +47,7 @@ export default () => ({ ...@@ -47,7 +47,7 @@ export default () => ({
* User-defined custom variables are passed * User-defined custom variables are passed
* via the dashboard yml file. * via the dashboard yml file.
*/ */
variables: {}, variables: [],
/** /**
* User-defined custom links are passed * User-defined custom links are passed
* via the dashboard yml file. * via the dashboard yml file.
......
...@@ -289,7 +289,7 @@ export const mapToDashboardViewModel = ({ ...@@ -289,7 +289,7 @@ export const mapToDashboardViewModel = ({
}) => { }) => {
return { return {
dashboard, dashboard,
variables: mergeURLVariables(parseTemplatingVariables(templating)), variables: mergeURLVariables(parseTemplatingVariables(templating.variables)),
links: links.map(mapLinksToViewModel), links: links.map(mapLinksToViewModel),
panelGroups: panel_groups.map(mapToPanelGroupViewModel), panelGroups: panel_groups.map(mapToPanelGroupViewModel),
}; };
...@@ -453,10 +453,10 @@ export const normalizeQueryResponseData = data => { ...@@ -453,10 +453,10 @@ export const normalizeQueryResponseData = data => {
* *
* This is currently only used by getters/getCustomVariablesParams * This is currently only used by getters/getCustomVariablesParams
* *
* @param {String} key Variable key that needs to be prefixed * @param {String} name Variable key that needs to be prefixed
* @returns {String} * @returns {String}
*/ */
export const addPrefixToCustomVariableParams = key => `variables[${key}]`; export const addPrefixToCustomVariableParams = name => `variables[${name}]`;
/** /**
* Normalize custom dashboard paths. This method helps support * Normalize custom dashboard paths. This method helps support
......
...@@ -46,7 +46,7 @@ const textAdvancedVariableParser = advTextVar => ({ ...@@ -46,7 +46,7 @@ const textAdvancedVariableParser = advTextVar => ({
* @param {Object} custom variable option * @param {Object} custom variable option
* @returns {Object} normalized custom variable options * @returns {Object} normalized custom variable options
*/ */
const normalizeVariableValues = ({ default: defaultOpt = false, text, value }) => ({ const normalizeVariableValues = ({ default: defaultOpt = false, text, value = null }) => ({
default: defaultOpt, default: defaultOpt,
text: text || value, text: text || value,
value, value,
...@@ -68,10 +68,10 @@ const customAdvancedVariableParser = advVariable => { ...@@ -68,10 +68,10 @@ const customAdvancedVariableParser = advVariable => {
return { return {
type: VARIABLE_TYPES.custom, type: VARIABLE_TYPES.custom,
label: advVariable.label, label: advVariable.label,
value: defaultValue?.value,
options: { options: {
values, values,
}, },
value: defaultValue?.value || null,
}; };
}; };
...@@ -100,27 +100,24 @@ const customSimpleVariableParser = simpleVar => { ...@@ -100,27 +100,24 @@ const customSimpleVariableParser = simpleVar => {
const values = (simpleVar || []).map(parseSimpleCustomValues); const values = (simpleVar || []).map(parseSimpleCustomValues);
return { return {
type: VARIABLE_TYPES.custom, type: VARIABLE_TYPES.custom,
value: values[0].value,
label: null, label: null,
value: values[0].value || null,
options: { options: {
values: values.map(normalizeVariableValues), values: values.map(normalizeVariableValues),
}, },
}; };
}; };
const metricLabelValuesVariableParser = variable => { const metricLabelValuesVariableParser = ({ label, options = {} }) => ({
const { label, options = {} } = variable; type: VARIABLE_TYPES.metric_label_values,
return { label,
type: VARIABLE_TYPES.metric_label_values, value: null,
value: null, options: {
label, prometheusEndpointPath: options.prometheus_endpoint_path || '',
options: { label: options.label || null,
prometheusEndpointPath: options.prometheus_endpoint_path || '', values: [], // values are initially empty
label: options.label || null, },
values: [], // values are initially empty });
},
};
};
/** /**
* Utility method to determine if a custom variable is * Utility method to determine if a custom variable is
...@@ -161,29 +158,26 @@ const getVariableParser = variable => { ...@@ -161,29 +158,26 @@ const getVariableParser = variable => {
* for the user to edit. The values from input elements are relayed to * for the user to edit. The values from input elements are relayed to
* backend and eventually Prometheus API. * backend and eventually Prometheus API.
* *
* This method currently is not used anywhere. Once the issue * @param {Object} templating variables from the dashboard yml file
* https://gitlab.com/gitlab-org/gitlab/-/issues/214536 is completed, * @returns {array} An array of variables to display as inputs
* this method will have been used by the monitoring dashboard.
*
* @param {Object} templating templating variables from the dashboard yml file
* @returns {Object} a map of processed templating variables
*/ */
export const parseTemplatingVariables = ({ variables = {} } = {}) => export const parseTemplatingVariables = (ymlVariables = {}) =>
Object.entries(variables).reduce((acc, [key, variable]) => { Object.entries(ymlVariables).reduce((acc, [name, ymlVariable]) => {
// get the parser // get the parser
const parser = getVariableParser(variable); const parser = getVariableParser(ymlVariable);
// parse the variable // parse the variable
const parsedVar = parser(variable); const variable = parser(ymlVariable);
// for simple custom variable label is null and it should be // for simple custom variable label is null and it should be
// replace with key instead // replace with key instead
if (parsedVar) { if (variable) {
acc[key] = { acc.push({
...parsedVar, ...variable,
label: parsedVar.label || key, name,
}; label: variable.label || name,
});
} }
return acc; return acc;
}, {}); }, []);
/** /**
* Custom variables are defined in the dashboard yml file * Custom variables are defined in the dashboard yml file
...@@ -201,23 +195,18 @@ export const parseTemplatingVariables = ({ variables = {} } = {}) => ...@@ -201,23 +195,18 @@ export const parseTemplatingVariables = ({ variables = {} } = {}) =>
* This method can be improved further. See the below issue * This method can be improved further. See the below issue
* https://gitlab.com/gitlab-org/gitlab/-/issues/217713 * https://gitlab.com/gitlab-org/gitlab/-/issues/217713
* *
* @param {Object} varsFromYML template variables from yml file * @param {array} parsedYmlVariables - template variables from yml file
* @returns {Object} * @returns {Object}
*/ */
export const mergeURLVariables = (varsFromYML = {}) => { export const mergeURLVariables = (parsedYmlVariables = []) => {
const varsFromURL = templatingVariablesFromUrl(); const varsFromURL = templatingVariablesFromUrl();
const variables = {}; parsedYmlVariables.forEach(variable => {
Object.keys(varsFromYML).forEach(key => { const { name } = variable;
if (Object.prototype.hasOwnProperty.call(varsFromURL, key)) { if (Object.prototype.hasOwnProperty.call(varsFromURL, name)) {
variables[key] = { Object.assign(variable, { value: varsFromURL[name] });
...varsFromYML[key],
value: varsFromURL[key],
};
} else {
variables[key] = varsFromYML[key];
} }
}); });
return variables; return parsedYmlVariables;
}; };
/** /**
......
...@@ -201,8 +201,10 @@ export const removePrefixFromLabel = label => ...@@ -201,8 +201,10 @@ export const removePrefixFromLabel = label =>
* @returns {Object} * @returns {Object}
*/ */
export const convertVariablesForURL = variables => export const convertVariablesForURL = variables =>
Object.keys(variables || {}).reduce((acc, key) => { variables.reduce((acc, { name, value }) => {
acc[addPrefixToLabel(key)] = variables[key]?.value; if (value !== null) {
acc[addPrefixToLabel(name)] = value;
}
return acc; return acc;
}, {}); }, {});
......
...@@ -26,10 +26,9 @@ import { ...@@ -26,10 +26,9 @@ import {
setMetricResult, setMetricResult,
setupStoreWithData, setupStoreWithData,
setupStoreWithDataForPanelCount, setupStoreWithDataForPanelCount,
setupStoreWithVariable,
setupStoreWithLinks, setupStoreWithLinks,
} from '../store_utils'; } from '../store_utils';
import { environmentData, dashboardGitResponse } from '../mock_data'; import { environmentData, dashboardGitResponse, storeVariables } from '../mock_data';
import { import {
metricsDashboardViewModel, metricsDashboardViewModel,
metricsDashboardPanelCount, metricsDashboardPanelCount,
...@@ -604,8 +603,7 @@ describe('Dashboard', () => { ...@@ -604,8 +603,7 @@ describe('Dashboard', () => {
beforeEach(() => { beforeEach(() => {
createShallowWrapper({ hasMetrics: true }); createShallowWrapper({ hasMetrics: true });
setupStoreWithData(store); setupStoreWithData(store);
setupStoreWithVariable(store); store.state.monitoringDashboard.variables = storeVariables;
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
......
...@@ -59,7 +59,7 @@ describe('Custom variable component', () => { ...@@ -59,7 +59,7 @@ describe('Custom variable component', () => {
.vm.$emit('click'); .vm.$emit('click');
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'env', 'canary'); expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'canary');
}); });
}); });
}); });
...@@ -40,7 +40,7 @@ describe('Text variable component', () => { ...@@ -40,7 +40,7 @@ describe('Text variable component', () => {
findInput().trigger('keyup.enter'); findInput().trigger('keyup.enter');
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'pod', 'prod-pod'); expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'prod-pod');
}); });
}); });
...@@ -53,7 +53,7 @@ describe('Text variable component', () => { ...@@ -53,7 +53,7 @@ describe('Text variable component', () => {
findInput().trigger('blur'); findInput().trigger('blur');
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'pod', 'canary-pod'); expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'canary-pod');
}); });
}); });
}); });
...@@ -6,8 +6,7 @@ import TextField from '~/monitoring/components/variables/text_field.vue'; ...@@ -6,8 +6,7 @@ import TextField from '~/monitoring/components/variables/text_field.vue';
import { updateHistory, mergeUrlParams } from '~/lib/utils/url_utility'; import { updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
import { createStore } from '~/monitoring/stores'; import { createStore } from '~/monitoring/stores';
import { convertVariablesForURL } from '~/monitoring/utils'; import { convertVariablesForURL } from '~/monitoring/utils';
import * as types from '~/monitoring/stores/mutation_types'; import { storeVariables } from '../mock_data';
import { mockTemplatingDataResponses } from '../mock_data';
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
updateHistory: jest.fn(), updateHistory: jest.fn(),
...@@ -17,12 +16,6 @@ jest.mock('~/lib/utils/url_utility', () => ({ ...@@ -17,12 +16,6 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('Metrics dashboard/variables section component', () => { describe('Metrics dashboard/variables section component', () => {
let store; let store;
let wrapper; let wrapper;
const sampleVariables = {
label1: mockTemplatingDataResponses.simpleText.simpleText,
label2: mockTemplatingDataResponses.advText.advText,
label3: mockTemplatingDataResponses.simpleCustom.simpleCustom,
label4: mockTemplatingDataResponses.metricLabelValues.simple,
};
const createShallowWrapper = () => { const createShallowWrapper = () => {
wrapper = shallowMount(VariablesSection, { wrapper = shallowMount(VariablesSection, {
...@@ -48,22 +41,23 @@ describe('Metrics dashboard/variables section component', () => { ...@@ -48,22 +41,23 @@ describe('Metrics dashboard/variables section component', () => {
describe('when variables are set', () => { describe('when variables are set', () => {
beforeEach(() => { beforeEach(() => {
store.state.monitoringDashboard.variables = storeVariables;
createShallowWrapper(); createShallowWrapper();
store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, sampleVariables);
return wrapper.vm.$nextTick; return wrapper.vm.$nextTick;
}); });
it('shows the variables section', () => { it('shows the variables section', () => {
const allInputs = findTextInputs().length + findCustomInputs().length; const allInputs = findTextInputs().length + findCustomInputs().length;
expect(allInputs).toBe(Object.keys(sampleVariables).length); expect(allInputs).toBe(storeVariables.length);
}); });
it('shows the right custom variable inputs', () => { it('shows the right custom variable inputs', () => {
const customInputs = findCustomInputs(); const customInputs = findCustomInputs();
expect(customInputs.at(0).props('name')).toBe('label3'); expect(customInputs.at(0).props('name')).toBe('customSimple');
expect(customInputs.at(1).props('name')).toBe('label4'); expect(customInputs.at(1).props('name')).toBe('customAdvanced');
}); });
}); });
...@@ -77,7 +71,7 @@ describe('Metrics dashboard/variables section component', () => { ...@@ -77,7 +71,7 @@ describe('Metrics dashboard/variables section component', () => {
namespaced: true, namespaced: true,
state: { state: {
showEmptyState: false, showEmptyState: false,
variables: sampleVariables, variables: storeVariables,
}, },
actions: { actions: {
updateVariablesAndFetchData, updateVariablesAndFetchData,
...@@ -92,12 +86,12 @@ describe('Metrics dashboard/variables section component', () => { ...@@ -92,12 +86,12 @@ describe('Metrics dashboard/variables section component', () => {
it('merges the url params and refreshes the dashboard when a text-based variables inputs are updated', () => { it('merges the url params and refreshes the dashboard when a text-based variables inputs are updated', () => {
const firstInput = findTextInputs().at(0); const firstInput = findTextInputs().at(0);
firstInput.vm.$emit('onUpdate', 'label1', 'test'); firstInput.vm.$emit('input', 'test');
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(updateVariablesAndFetchData).toHaveBeenCalled(); expect(updateVariablesAndFetchData).toHaveBeenCalled();
expect(mergeUrlParams).toHaveBeenCalledWith( expect(mergeUrlParams).toHaveBeenCalledWith(
convertVariablesForURL(sampleVariables), convertVariablesForURL(storeVariables),
window.location.href, window.location.href,
); );
expect(updateHistory).toHaveBeenCalled(); expect(updateHistory).toHaveBeenCalled();
...@@ -107,12 +101,12 @@ describe('Metrics dashboard/variables section component', () => { ...@@ -107,12 +101,12 @@ describe('Metrics dashboard/variables section component', () => {
it('merges the url params and refreshes the dashboard when a custom-based variables inputs are updated', () => { it('merges the url params and refreshes the dashboard when a custom-based variables inputs are updated', () => {
const firstInput = findCustomInputs().at(0); const firstInput = findCustomInputs().at(0);
firstInput.vm.$emit('onUpdate', 'label1', 'test'); firstInput.vm.$emit('input', 'test');
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(updateVariablesAndFetchData).toHaveBeenCalled(); expect(updateVariablesAndFetchData).toHaveBeenCalled();
expect(mergeUrlParams).toHaveBeenCalledWith( expect(mergeUrlParams).toHaveBeenCalledWith(
convertVariablesForURL(sampleVariables), convertVariablesForURL(storeVariables),
window.location.href, window.location.href,
); );
expect(updateHistory).toHaveBeenCalled(); expect(updateHistory).toHaveBeenCalled();
...@@ -122,7 +116,7 @@ describe('Metrics dashboard/variables section component', () => { ...@@ -122,7 +116,7 @@ describe('Metrics dashboard/variables section component', () => {
it('does not merge the url params and refreshes the dashboard if the value entered is not different that is what currently stored', () => { it('does not merge the url params and refreshes the dashboard if the value entered is not different that is what currently stored', () => {
const firstInput = findTextInputs().at(0); const firstInput = findTextInputs().at(0);
firstInput.vm.$emit('onUpdate', 'label1', 'Simple text'); firstInput.vm.$emit('input', 'My default value');
expect(updateVariablesAndFetchData).not.toHaveBeenCalled(); expect(updateVariablesAndFetchData).not.toHaveBeenCalled();
expect(mergeUrlParams).not.toHaveBeenCalled(); expect(mergeUrlParams).not.toHaveBeenCalled();
......
This diff is collapsed.
...@@ -44,7 +44,6 @@ import { ...@@ -44,7 +44,6 @@ import {
deploymentData, deploymentData,
environmentData, environmentData,
annotationsData, annotationsData,
mockTemplatingData,
dashboardGitResponse, dashboardGitResponse,
mockDashboardsErrorResponse, mockDashboardsErrorResponse,
} from '../mock_data'; } from '../mock_data';
...@@ -305,32 +304,6 @@ describe('Monitoring store actions', () => { ...@@ -305,32 +304,6 @@ describe('Monitoring store actions', () => {
expect(dispatch).toHaveBeenCalledWith('fetchDashboardData'); expect(dispatch).toHaveBeenCalledWith('fetchDashboardData');
}); });
it('stores templating variables', () => {
const response = {
...metricsDashboardResponse.dashboard,
...mockTemplatingData.allVariableTypes.dashboard,
};
receiveMetricsDashboardSuccess(
{ state, commit, dispatch },
{
response: {
...metricsDashboardResponse,
dashboard: {
...metricsDashboardResponse.dashboard,
...mockTemplatingData.allVariableTypes.dashboard,
},
},
},
);
expect(commit).toHaveBeenCalledWith(
types.RECEIVE_METRICS_DASHBOARD_SUCCESS,
response,
);
});
it('sets the dashboards loaded from the repository', () => { it('sets the dashboards loaded from the repository', () => {
const params = {}; const params = {};
const response = metricsDashboardResponse; const response = metricsDashboardResponse;
...@@ -1144,11 +1117,13 @@ describe('Monitoring store actions', () => { ...@@ -1144,11 +1117,13 @@ describe('Monitoring store actions', () => {
describe('fetchVariableMetricLabelValues', () => { describe('fetchVariableMetricLabelValues', () => {
const variable = { const variable = {
type: 'metric_label_values', type: 'metric_label_values',
name: 'label1',
options: { options: {
prometheusEndpointPath: '/series', prometheusEndpointPath: '/series?match[]=metric_name',
label: 'job', label: 'job',
}, },
}; };
const defaultQueryParams = { const defaultQueryParams = {
start_time: '2019-08-06T12:40:02.184Z', start_time: '2019-08-06T12:40:02.184Z',
end_time: '2019-08-06T20:40:02.184Z', end_time: '2019-08-06T20:40:02.184Z',
...@@ -1158,9 +1133,7 @@ describe('Monitoring store actions', () => { ...@@ -1158,9 +1133,7 @@ describe('Monitoring store actions', () => {
state = { state = {
...state, ...state,
timeRange: defaultTimeRange, timeRange: defaultTimeRange,
variables: { variables: [variable],
label1: variable,
},
}; };
}); });
...@@ -1176,7 +1149,7 @@ describe('Monitoring store actions', () => { ...@@ -1176,7 +1149,7 @@ describe('Monitoring store actions', () => {
}, },
]; ];
mock.onGet('/series').reply(200, { mock.onGet('/series?match[]=metric_name').reply(200, {
status: 'success', status: 'success',
data, data,
}); });
...@@ -1196,7 +1169,7 @@ describe('Monitoring store actions', () => { ...@@ -1196,7 +1169,7 @@ describe('Monitoring store actions', () => {
}); });
it('should notify the user that dynamic options were not loaded', () => { it('should notify the user that dynamic options were not loaded', () => {
mock.onGet('/series').reply(500); mock.onGet('/series?match[]=metric_name').reply(500);
return testAction(fetchVariableMetricLabelValues, { defaultQueryParams }, state, [], []).then( return testAction(fetchVariableMetricLabelValues, { defaultQueryParams }, state, [], []).then(
() => { () => {
......
...@@ -8,7 +8,7 @@ import { ...@@ -8,7 +8,7 @@ import {
environmentData, environmentData,
metricsResult, metricsResult,
dashboardGitResponse, dashboardGitResponse,
mockTemplatingDataResponses, storeVariables,
mockLinks, mockLinks,
} from '../mock_data'; } from '../mock_data';
import { import {
...@@ -344,19 +344,21 @@ describe('Monitoring store Getters', () => { ...@@ -344,19 +344,21 @@ describe('Monitoring store Getters', () => {
}); });
it('transforms the variables object to an array in the [variable, variable_value] format for all variable types', () => { it('transforms the variables object to an array in the [variable, variable_value] format for all variable types', () => {
mutations[types.SET_VARIABLES](state, mockTemplatingDataResponses.allVariableTypes); state.variables = storeVariables;
const variablesArray = getters.getCustomVariablesParams(state); const variablesArray = getters.getCustomVariablesParams(state);
expect(variablesArray).toEqual({ expect(variablesArray).toEqual({
'variables[advCustomNormal]': 'value2', 'variables[textSimple]': 'My default value',
'variables[advText]': 'default', 'variables[textAdvanced]': 'A default value',
'variables[simpleCustom]': 'value1', 'variables[customSimple]': 'value1',
'variables[simpleText]': 'Simple text', 'variables[customAdvanced]': 'value2',
'variables[customAdvancedWithoutLabel]': 'value2',
'variables[customAdvancedWithoutOptText]': 'value2',
}); });
}); });
it('transforms the variables object to an empty array when no keys are present', () => { it('transforms the variables object to an empty array when no keys are present', () => {
mutations[types.SET_VARIABLES](state, {}); state.variables = [];
const variablesArray = getters.getCustomVariablesParams(state); const variablesArray = getters.getCustomVariablesParams(state);
expect(variablesArray).toEqual({}); expect(variablesArray).toEqual({});
......
...@@ -5,7 +5,7 @@ import * as types from '~/monitoring/stores/mutation_types'; ...@@ -5,7 +5,7 @@ import * as types from '~/monitoring/stores/mutation_types';
import state from '~/monitoring/stores/state'; import state from '~/monitoring/stores/state';
import { metricStates } from '~/monitoring/constants'; import { metricStates } from '~/monitoring/constants';
import { deploymentData, dashboardGitResponse } from '../mock_data'; import { deploymentData, dashboardGitResponse, storeTextVariables } from '../mock_data';
import { metricsDashboardPayload } from '../fixture_data'; import { metricsDashboardPayload } from '../fixture_data';
describe('Monitoring mutations', () => { describe('Monitoring mutations', () => {
...@@ -427,30 +427,12 @@ describe('Monitoring mutations', () => { ...@@ -427,30 +427,12 @@ describe('Monitoring mutations', () => {
}); });
}); });
describe('SET_VARIABLES', () => {
it('stores an empty variables array when no custom variables are given', () => {
mutations[types.SET_VARIABLES](stateCopy, {});
expect(stateCopy.variables).toEqual({});
});
it('stores variables in the key key_value format in the array', () => {
mutations[types.SET_VARIABLES](stateCopy, { pod: 'POD', stage: 'main ops' });
expect(stateCopy.variables).toEqual({ pod: 'POD', stage: 'main ops' });
});
});
describe('UPDATE_VARIABLE_VALUE', () => { describe('UPDATE_VARIABLE_VALUE', () => {
afterEach(() => {
mutations[types.SET_VARIABLES](stateCopy, {});
});
it('updates only the value of the variable in variables', () => { it('updates only the value of the variable in variables', () => {
mutations[types.SET_VARIABLES](stateCopy, { environment: { value: 'prod', type: 'text' } }); stateCopy.variables = storeTextVariables;
mutations[types.UPDATE_VARIABLE_VALUE](stateCopy, { key: 'environment', value: 'new prod' }); mutations[types.UPDATE_VARIABLE_VALUE](stateCopy, { name: 'textSimple', value: 'New Value' });
expect(stateCopy.variables).toEqual({ environment: { value: 'new prod', type: 'text' } }); expect(stateCopy.variables[0].value).toEqual('New Value');
}); });
}); });
......
...@@ -22,7 +22,7 @@ describe('mapToDashboardViewModel', () => { ...@@ -22,7 +22,7 @@ describe('mapToDashboardViewModel', () => {
dashboard: '', dashboard: '',
panelGroups: [], panelGroups: [],
links: [], links: [],
variables: {}, variables: [],
}); });
}); });
...@@ -52,7 +52,7 @@ describe('mapToDashboardViewModel', () => { ...@@ -52,7 +52,7 @@ describe('mapToDashboardViewModel', () => {
expect(mapToDashboardViewModel(response)).toEqual({ expect(mapToDashboardViewModel(response)).toEqual({
dashboard: 'Dashboard Name', dashboard: 'Dashboard Name',
links: [], links: [],
variables: {}, variables: [],
panelGroups: [ panelGroups: [
{ {
group: 'Group 1', group: 'Group 1',
...@@ -424,22 +424,20 @@ describe('mapToDashboardViewModel', () => { ...@@ -424,22 +424,20 @@ describe('mapToDashboardViewModel', () => {
urlUtils.queryToObject.mockReturnValueOnce(); urlUtils.queryToObject.mockReturnValueOnce();
expect(mapToDashboardViewModel(response)).toMatchObject({ expect(mapToDashboardViewModel(response).variables).toEqual([
dashboard: 'Dashboard Name', {
links: [], name: 'pod',
variables: { label: 'pod',
pod: { type: 'text',
label: 'pod', value: 'kubernetes',
type: 'text',
value: 'kubernetes',
},
pod_2: {
label: 'pod_2',
type: 'text',
value: 'kubernetes-2',
},
}, },
}); {
name: 'pod_2',
label: 'pod_2',
type: 'text',
value: 'kubernetes-2',
},
]);
}); });
it('sets variables as-is from yml file if URL has no matching variables', () => { it('sets variables as-is from yml file if URL has no matching variables', () => {
...@@ -458,22 +456,20 @@ describe('mapToDashboardViewModel', () => { ...@@ -458,22 +456,20 @@ describe('mapToDashboardViewModel', () => {
'var-environment': 'POD', 'var-environment': 'POD',
}); });
expect(mapToDashboardViewModel(response)).toMatchObject({ expect(mapToDashboardViewModel(response).variables).toEqual([
dashboard: 'Dashboard Name', {
links: [], label: 'pod',
variables: { name: 'pod',
pod: { type: 'text',
label: 'pod', value: 'kubernetes',
type: 'text',
value: 'kubernetes',
},
pod_2: {
label: 'pod_2',
type: 'text',
value: 'kubernetes-2',
},
}, },
}); {
label: 'pod_2',
name: 'pod_2',
type: 'text',
value: 'kubernetes-2',
},
]);
}); });
it('merges variables from URL with the ones from yml file', () => { it('merges variables from URL with the ones from yml file', () => {
...@@ -494,22 +490,20 @@ describe('mapToDashboardViewModel', () => { ...@@ -494,22 +490,20 @@ describe('mapToDashboardViewModel', () => {
'var-pod_2': 'POD2', 'var-pod_2': 'POD2',
}); });
expect(mapToDashboardViewModel(response)).toMatchObject({ expect(mapToDashboardViewModel(response).variables).toEqual([
dashboard: 'Dashboard Name', {
links: [], label: 'pod',
variables: { name: 'pod',
pod: { type: 'text',
label: 'pod', value: 'POD1',
type: 'text',
value: 'POD1',
},
pod_2: {
label: 'pod_2',
type: 'text',
value: 'POD2',
},
}, },
}); {
label: 'pod_2',
name: 'pod_2',
type: 'text',
value: 'POD2',
},
]);
}); });
}); });
}); });
......
...@@ -3,29 +3,31 @@ import { ...@@ -3,29 +3,31 @@ import {
mergeURLVariables, mergeURLVariables,
optionsFromSeriesData, optionsFromSeriesData,
} from '~/monitoring/stores/variable_mapping'; } from '~/monitoring/stores/variable_mapping';
import {
templatingVariablesExamples,
storeTextVariables,
storeCustomVariables,
storeMetricLabelValuesVariables,
} from '../mock_data';
import * as urlUtils from '~/lib/utils/url_utility'; import * as urlUtils from '~/lib/utils/url_utility';
import { mockTemplatingData, mockTemplatingDataResponses } from '../mock_data';
describe('Monitoring variable mapping', () => { describe('Monitoring variable mapping', () => {
describe('parseTemplatingVariables', () => { describe('parseTemplatingVariables', () => {
it.each` it.each`
case | input | expected case | input
${'Returns empty object for no dashboard input'} | ${{}} | ${{}} ${'For undefined templating object'} | ${undefined}
${'Returns empty object for empty dashboard input'} | ${{ dashboard: {} }} | ${{}} ${'For empty templating object'} | ${{}}
${'Returns empty object for empty templating prop'} | ${mockTemplatingData.emptyTemplatingProp} | ${{}} `('$case, returns an empty array', ({ input }) => {
${'Returns empty object for empty variables prop'} | ${mockTemplatingData.emptyVariablesProp} | ${{}} expect(parseTemplatingVariables(input)).toEqual([]);
${'Returns parsed object for simple text variable'} | ${mockTemplatingData.simpleText} | ${mockTemplatingDataResponses.simpleText} });
${'Returns parsed object for advanced text variable'} | ${mockTemplatingData.advText} | ${mockTemplatingDataResponses.advText}
${'Returns parsed object for simple custom variable'} | ${mockTemplatingData.simpleCustom} | ${mockTemplatingDataResponses.simpleCustom} it.each`
${'Returns parsed object for advanced custom variable without options'} | ${mockTemplatingData.advCustomWithoutOpts} | ${mockTemplatingDataResponses.advCustomWithoutOpts} case | input | output
${'Returns parsed object for advanced custom variable for option without text'} | ${mockTemplatingData.advCustomWithoutOptText} | ${mockTemplatingDataResponses.advCustomWithoutOptText} ${'Returns parsed object for text variables'} | ${templatingVariablesExamples.text} | ${storeTextVariables}
${'Returns parsed object for advanced custom variable without type'} | ${mockTemplatingData.advCustomWithoutType} | ${{}} ${'Returns parsed object for custom variables'} | ${templatingVariablesExamples.custom} | ${storeCustomVariables}
${'Returns parsed object for advanced custom variable without label'} | ${mockTemplatingData.advCustomWithoutLabel} | ${mockTemplatingDataResponses.advCustomWithoutLabel} ${'Returns parsed object for metric label value variables'} | ${templatingVariablesExamples.metricLabelValues} | ${storeMetricLabelValuesVariables}
${'Returns parsed object for simple and advanced custom variables'} | ${mockTemplatingData.simpleAndAdv} | ${mockTemplatingDataResponses.simpleAndAdv} `('$case, returns an empty array', ({ input, output }) => {
${'Returns parsed object for metricLabelValues'} | ${mockTemplatingData.metricLabelValues} | ${mockTemplatingDataResponses.metricLabelValues} expect(parseTemplatingVariables(input)).toEqual(output);
${'Returns parsed object for all variable types'} | ${mockTemplatingData.allVariableTypes} | ${mockTemplatingDataResponses.allVariableTypes}
`('$case', ({ input, expected }) => {
expect(parseTemplatingVariables(input?.dashboard?.templating)).toEqual(expected);
}); });
}); });
...@@ -41,7 +43,7 @@ describe('Monitoring variable mapping', () => { ...@@ -41,7 +43,7 @@ describe('Monitoring variable mapping', () => {
it('returns empty object if variables are not defined in yml or URL', () => { it('returns empty object if variables are not defined in yml or URL', () => {
urlUtils.queryToObject.mockReturnValueOnce({}); urlUtils.queryToObject.mockReturnValueOnce({});
expect(mergeURLVariables({})).toEqual({}); expect(mergeURLVariables([])).toEqual([]);
}); });
it('returns empty object if variables are defined in URL but not in yml', () => { it('returns empty object if variables are defined in URL but not in yml', () => {
...@@ -50,18 +52,24 @@ describe('Monitoring variable mapping', () => { ...@@ -50,18 +52,24 @@ describe('Monitoring variable mapping', () => {
'var-instance': 'localhost', 'var-instance': 'localhost',
}); });
expect(mergeURLVariables({})).toEqual({}); expect(mergeURLVariables([])).toEqual([]);
}); });
it('returns yml variables if variables defined in yml but not in the URL', () => { it('returns yml variables if variables defined in yml but not in the URL', () => {
urlUtils.queryToObject.mockReturnValueOnce({}); urlUtils.queryToObject.mockReturnValueOnce({});
const params = { const variables = [
env: 'one', {
instance: 'localhost', name: 'env',
}; value: 'one',
},
{
name: 'instance',
value: 'localhost',
},
];
expect(mergeURLVariables(params)).toEqual(params); expect(mergeURLVariables(variables)).toEqual(variables);
}); });
it('returns yml variables if variables defined in URL do not match with yml variables', () => { it('returns yml variables if variables defined in URL do not match with yml variables', () => {
...@@ -69,13 +77,19 @@ describe('Monitoring variable mapping', () => { ...@@ -69,13 +77,19 @@ describe('Monitoring variable mapping', () => {
'var-env': 'one', 'var-env': 'one',
'var-instance': 'localhost', 'var-instance': 'localhost',
}; };
const ymlParams = { const variables = [
pod: { value: 'one' }, {
service: { value: 'database' }, name: 'env',
}; value: 'one',
},
{
name: 'service',
value: 'database',
},
];
urlUtils.queryToObject.mockReturnValueOnce(urlParams); urlUtils.queryToObject.mockReturnValueOnce(urlParams);
expect(mergeURLVariables(ymlParams)).toEqual(ymlParams); expect(mergeURLVariables(variables)).toEqual(variables);
}); });
it('returns merged yml and URL variables if there is some match', () => { it('returns merged yml and URL variables if there is some match', () => {
...@@ -83,19 +97,29 @@ describe('Monitoring variable mapping', () => { ...@@ -83,19 +97,29 @@ describe('Monitoring variable mapping', () => {
'var-env': 'one', 'var-env': 'one',
'var-instance': 'localhost:8080', 'var-instance': 'localhost:8080',
}; };
const ymlParams = { const variables = [
instance: { value: 'localhost' }, {
service: { value: 'database' }, name: 'instance',
}; value: 'localhost',
},
const merged = { {
instance: { value: 'localhost:8080' }, name: 'service',
service: { value: 'database' }, value: 'database',
}; },
];
urlUtils.queryToObject.mockReturnValueOnce(urlParams); urlUtils.queryToObject.mockReturnValueOnce(urlParams);
expect(mergeURLVariables(ymlParams)).toEqual(merged); expect(mergeURLVariables(variables)).toEqual([
{
name: 'instance',
value: 'localhost:8080',
},
{
name: 'service',
value: 'database',
},
]);
}); });
}); });
......
...@@ -35,12 +35,6 @@ export const setupStoreWithDashboard = store => { ...@@ -35,12 +35,6 @@ export const setupStoreWithDashboard = store => {
); );
}; };
export const setupStoreWithVariable = store => {
store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, {
label1: 'pod',
});
};
export const setupStoreWithLinks = store => { export const setupStoreWithLinks = store => {
store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, { store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, {
...metricsDashboardPayload, ...metricsDashboardPayload,
......
...@@ -429,14 +429,41 @@ describe('monitoring/utils', () => { ...@@ -429,14 +429,41 @@ describe('monitoring/utils', () => {
describe('convertVariablesForURL', () => { describe('convertVariablesForURL', () => {
it.each` it.each`
input | expected input | expected
${undefined} | ${{}} ${[]} | ${{}}
${null} | ${{}} ${[{ name: 'env', value: 'prod' }]} | ${{ 'var-env': 'prod' }}
${{}} | ${{}} ${[{ name: 'env1', value: 'prod' }, { name: 'env2', value: null }]} | ${{ 'var-env1': 'prod' }}
${{ env: { value: 'prod' } }} | ${{ 'var-env': 'prod' }} ${[{ name: 'var-env', value: 'prod' }]} | ${{ 'var-var-env': 'prod' }}
${{ 'var-env': { value: 'prod' } }} | ${{ 'var-var-env': 'prod' }}
`('convertVariablesForURL returns $expected with input $input', ({ input, expected }) => { `('convertVariablesForURL returns $expected with input $input', ({ input, expected }) => {
expect(monitoringUtils.convertVariablesForURL(input)).toEqual(expected); expect(monitoringUtils.convertVariablesForURL(input)).toEqual(expected);
}); });
}); });
describe('setCustomVariablesFromUrl', () => {
beforeEach(() => {
jest.spyOn(urlUtils, 'updateHistory');
});
afterEach(() => {
urlUtils.updateHistory.mockRestore();
});
it.each`
input | urlParams
${[]} | ${''}
${[{ name: 'env', value: 'prod' }]} | ${'?var-env=prod'}
${[{ name: 'env1', value: 'prod' }, { name: 'env2', value: null }]} | ${'?var-env=prod&var-env1=prod'}
`(
'setCustomVariablesFromUrl updates history with query "$urlParams" with input $input',
({ input, urlParams }) => {
monitoringUtils.setCustomVariablesFromUrl(input);
expect(urlUtils.updateHistory).toHaveBeenCalledTimes(1);
expect(urlUtils.updateHistory).toHaveBeenCalledWith({
url: `http://localhost/${urlParams}`,
title: '',
});
},
);
});
}); });
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