Commit ea2d4fcf authored by Dhiraj Bodicherla's avatar Dhiraj Bodicherla

Render input elements from template variables

Based on the custom template variables specified
in the monitoring dashboard yml file, text and
dropdowns should be rendered. This MR adds the
logic to render elements based on parsed templating
variables
parent 6717786f
<script>
import { GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui';
export default {
components: {
GlFormGroup,
GlDropdown,
GlDropdownItem,
},
props: {
name: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
value: {
type: String,
required: false,
default: '',
},
options: {
type: Array,
required: true,
},
},
computed: {
defaultText() {
const selectedOpt = this.options.find(opt => opt.value === this.value);
return selectedOpt?.text || this.value;
},
},
methods: {
onUpdate(value) {
this.$emit('onUpdate', this.name, value);
},
},
};
</script>
<template>
<gl-form-group :label="label">
<gl-dropdown toggle-class="dropdown-menu-toggle" :text="defaultText">
<gl-dropdown-item v-for="(opt, key) in options" :key="key" @click="onUpdate(opt.value)">{{
opt.text
}}</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
</template>
<script>
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
export default {
components: {
GlFormGroup,
GlFormInput,
},
props: {
name: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
value: {
type: String,
required: true,
},
},
methods: {
onUpdate(event) {
this.$emit('onUpdate', this.name, event.target.value);
},
},
};
</script>
<template>
<gl-form-group :label="label">
<gl-form-input
:value="value"
:name="name"
@keyup.native.enter="onUpdate"
@blur.native="onUpdate"
/>
</gl-form-group>
</template>
<script> <script>
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility'; import CustomVariable from './variables/custom_variable.vue';
import TextVariable from './variables/text_variable.vue';
import { setPromCustomVariablesFromUrl } from '../utils';
export default { export default {
components: { components: {
GlFormGroup, CustomVariable,
GlFormInput, TextVariable,
}, },
computed: { computed: {
...mapState('monitoringDashboard', ['promVariables']), ...mapState('monitoringDashboard', ['promVariables']),
}, },
methods: { methods: {
...mapActions('monitoringDashboard', ['fetchDashboardData', 'setVariableValues']), ...mapActions('monitoringDashboard', ['fetchDashboardData', 'updateVariableValues']),
refreshDashboard(event) { refreshDashboard(variable, value) {
const { name, value } = event.target; if (this.promVariables[variable].value !== value) {
const changedVariable = { key: variable, value };
if (this.promVariables[name] !== value) { // update the Vuex store
const changedVariable = { [name]: value }; this.updateVariableValues(changedVariable);
// the below calls can ideally be moved out of the
this.setVariableValues(changedVariable); // component and into the actions and let the
// mutation respond directly.
updateHistory({ // This can be further investigate in
url: mergeUrlParams(this.promVariables, window.location.href), // https://gitlab.com/gitlab-org/gitlab/-/issues/217713
title: document.title, setPromCustomVariablesFromUrl(this.promVariables);
}); // fetch data
this.fetchDashboardData(); this.fetchDashboardData();
} }
}, },
variableComponent(type) {
const types = {
text: TextVariable,
custom: CustomVariable,
};
return types[type] || TextVariable;
},
}, },
}; };
</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="(val, key) in promVariables" :key="key" class="mb-1 pr-2 d-flex d-sm-block"> <div v-for="(variable, key) in promVariables" :key="key" class="mb-1 pr-2 d-flex d-sm-block">
<gl-form-group :label="key" class="mb-0 flex-grow-1"> <component
<gl-form-input :is="variableComponent(variable.type)"
:value="val" class="mb-0 flex-grow-1"
:name="key" :label="variable.label"
@keyup.native.enter="refreshDashboard" :value="variable.value"
@blur.native="refreshDashboard" :name="key"
/> :options="variable.options"
</gl-form-group> @onUpdate="refreshDashboard"
/>
</div> </div>
</div> </div>
</template> </template>
...@@ -4,7 +4,6 @@ import Dashboard from '~/monitoring/components/dashboard.vue'; ...@@ -4,7 +4,6 @@ import Dashboard from '~/monitoring/components/dashboard.vue';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import { getParameterValues } from '~/lib/utils/url_utility'; import { getParameterValues } from '~/lib/utils/url_utility';
import store from './stores'; import store from './stores';
import { promCustomVariablesFromUrl } from './utils';
Vue.use(GlToast); Vue.use(GlToast);
...@@ -14,8 +13,6 @@ export default (props = {}) => { ...@@ -14,8 +13,6 @@ export default (props = {}) => {
if (el && el.dataset) { if (el && el.dataset) {
const [currentDashboard] = getParameterValues('dashboard'); const [currentDashboard] = getParameterValues('dashboard');
store.dispatch('monitoringDashboard/setVariableValues', promCustomVariablesFromUrl());
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el, el,
......
...@@ -3,6 +3,8 @@ import * as types from './mutation_types'; ...@@ -3,6 +3,8 @@ import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { convertToFixedRange } from '~/lib/utils/datetime_range'; import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { parseTemplatingVariables } from './variable_mapping';
import { mergeURLVariables } from '../utils';
import { import {
gqClient, gqClient,
parseEnvironmentsResponse, parseEnvironmentsResponse,
...@@ -159,6 +161,7 @@ export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response ...@@ -159,6 +161,7 @@ export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response
commit(types.SET_ALL_DASHBOARDS, all_dashboards); commit(types.SET_ALL_DASHBOARDS, all_dashboards);
commit(types.RECEIVE_METRICS_DASHBOARD_SUCCESS, dashboard); commit(types.RECEIVE_METRICS_DASHBOARD_SUCCESS, dashboard);
commit(types.SET_VARIABLES, mergeURLVariables(parseTemplatingVariables(dashboard.templating)));
commit(types.SET_ENDPOINTS, convertObjectPropsToCamelCase(metrics_data)); commit(types.SET_ENDPOINTS, convertObjectPropsToCamelCase(metrics_data));
return dispatch('fetchDashboardData'); return dispatch('fetchDashboardData');
...@@ -413,7 +416,7 @@ export const duplicateSystemDashboard = ({ state }, payload) => { ...@@ -413,7 +416,7 @@ export const duplicateSystemDashboard = ({ state }, payload) => {
// Variables manipulation // Variables manipulation
export const setVariableValues = ({ commit }, updatedVariable) => { export const updateVariableValues = ({ commit }, updatedVariable) => {
commit(types.UPDATE_VARIABLE_VALUES, updatedVariable); commit(types.UPDATE_VARIABLE_VALUES, updatedVariable);
}; };
......
import { flatMap } from 'lodash'; import { flatMap } from 'lodash';
import { removePrefixFromLabels } from './utils';
import { NOT_IN_DB_PREFIX } from '../constants'; import { NOT_IN_DB_PREFIX } from '../constants';
const metricsIdsInPanel = panel => const metricsIdsInPanel = panel =>
...@@ -123,10 +122,7 @@ export const filteredEnvironments = state => ...@@ -123,10 +122,7 @@ export const filteredEnvironments = state =>
*/ */
export const getCustomVariablesArray = state => export const getCustomVariablesArray = state =>
flatMap(state.promVariables, (val, key) => [ flatMap(state.promVariables, (variable, key) => [key, variable.value]);
encodeURIComponent(removePrefixFromLabels(key)),
encodeURIComponent(val),
]);
// 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 () => {};
...@@ -191,11 +191,10 @@ export default { ...@@ -191,11 +191,10 @@ export default {
[types.SET_VARIABLES](state, variables) { [types.SET_VARIABLES](state, variables) {
state.promVariables = variables; state.promVariables = variables;
}, },
[types.UPDATE_VARIABLE_VALUES](state, newVariable) { [types.UPDATE_VARIABLE_VALUES](state, updatedVariable) {
Object.keys(newVariable).forEach(key => { Object.assign(state.promVariables[updatedVariable.key], {
if (Object.prototype.hasOwnProperty.call(state.promVariables, key)) { ...state.promVariables[updatedVariable.key],
state.promVariables[key] = newVariable[key]; value: updatedVariable.value,
}
}); });
}, },
}; };
...@@ -2,7 +2,7 @@ import { slugify } from '~/lib/utils/text_utility'; ...@@ -2,7 +2,7 @@ import { slugify } from '~/lib/utils/text_utility';
import createGqClient, { fetchPolicies } from '~/lib/graphql'; 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 { NOT_IN_DB_PREFIX, VARIABLE_PREFIX } from '../constants'; import { NOT_IN_DB_PREFIX } from '../constants';
export const gqClient = createGqClient( export const gqClient = createGqClient(
{}, {},
...@@ -229,25 +229,3 @@ export const normalizeQueryResult = timeSeries => { ...@@ -229,25 +229,3 @@ export const normalizeQueryResult = timeSeries => {
return normalizedResult; return normalizedResult;
}; };
/**
* Variable labels are used as names for the dropdowns and also
* as URL params. Prefixing the name reduces the risk of
* collision with other URL params
*
* @param {String} label label for the template variable
* @returns {String}
*/
export const addPrefixToLabels = label => `${VARIABLE_PREFIX}${label}`;
/**
* Before the templating variables are passed to the backend the
* prefix needs to be removed.
*
* This method removes the prefix at the beginning of the string.
*
* @param {String} label label to remove prefix from
* @returns {String}
*/
export const removePrefixFromLabels = label =>
(label || '').replace(new RegExp(`^${VARIABLE_PREFIX}`), '');
import { isString } from 'lodash'; import { isString } from 'lodash';
import { addPrefixToLabels } from './utils';
import { VARIABLE_TYPES } from '../constants'; import { VARIABLE_TYPES } from '../constants';
/** /**
...@@ -56,15 +55,20 @@ const normalizeCustomVariableOptions = ({ default: defaultOpt = false, text, val ...@@ -56,15 +55,20 @@ const normalizeCustomVariableOptions = ({ default: defaultOpt = false, text, val
* Custom advanced variables are rendered as dropdown elements in the dashboard * Custom advanced variables are rendered as dropdown elements in the dashboard
* header. This method parses advanced custom variables. * header. This method parses advanced custom variables.
* *
* The default value is the option with default set to true or the first option
* if none of the options have default prop true.
*
* @param {Object} advVariable advance custom variable * @param {Object} advVariable advance custom variable
* @returns {Object} * @returns {Object}
*/ */
const customAdvancedVariableParser = advVariable => { const customAdvancedVariableParser = advVariable => {
const options = advVariable?.options?.values ?? []; const options = (advVariable?.options?.values ?? []).map(normalizeCustomVariableOptions);
const defaultOpt = options.find(opt => opt.default === true) || options[0];
return { return {
type: VARIABLE_TYPES.custom, type: VARIABLE_TYPES.custom,
label: advVariable.label, label: advVariable.label,
options: options.map(normalizeCustomVariableOptions), value: defaultOpt?.value,
options,
}; };
}; };
...@@ -83,6 +87,9 @@ const parseSimpleCustomOptions = opt => ({ text: opt, value: opt }); ...@@ -83,6 +87,9 @@ const parseSimpleCustomOptions = opt => ({ text: opt, value: opt });
* *
* Simple custom variables do not have labels so its set to null here. * Simple custom variables do not have labels so its set to null here.
* *
* The default value is set to the first option as the user cannot
* set a default value for this format
*
* @param {Array} customVariable array of options * @param {Array} customVariable array of options
* @returns {Object} * @returns {Object}
*/ */
...@@ -90,6 +97,7 @@ const customSimpleVariableParser = simpleVar => { ...@@ -90,6 +97,7 @@ const customSimpleVariableParser = simpleVar => {
const options = (simpleVar || []).map(parseSimpleCustomOptions); const options = (simpleVar || []).map(parseSimpleCustomOptions);
return { return {
type: VARIABLE_TYPES.custom, type: VARIABLE_TYPES.custom,
value: options[0].value,
label: null, label: null,
options: options.map(normalizeCustomVariableOptions), options: options.map(normalizeCustomVariableOptions),
}; };
...@@ -150,7 +158,7 @@ export const parseTemplatingVariables = ({ variables = {} } = {}) => ...@@ -150,7 +158,7 @@ export const parseTemplatingVariables = ({ variables = {} } = {}) =>
if (parsedVar) { if (parsedVar) {
acc[key] = { acc[key] = {
...parsedVar, ...parsedVar,
label: addPrefixToLabels(parsedVar.label || key), label: parsedVar.label || key,
}; };
} }
return acc; return acc;
......
import { pickBy } from 'lodash'; import { pickBy, mapKeys } from 'lodash';
import { queryToObject, mergeUrlParams, removeParams } from '~/lib/utils/url_utility'; import {
queryToObject,
mergeUrlParams,
removeParams,
updateHistory,
} from '~/lib/utils/url_utility';
import { import {
timeRangeParamNames, timeRangeParamNames,
timeRangeFromParams, timeRangeFromParams,
...@@ -122,6 +127,44 @@ export const timeRangeFromUrl = (search = window.location.search) => { ...@@ -122,6 +127,44 @@ export const timeRangeFromUrl = (search = window.location.search) => {
return timeRangeFromParams(params); return timeRangeFromParams(params);
}; };
/**
* Variable labels are used as names for the dropdowns and also
* as URL params. Prefixing the name reduces the risk of
* collision with other URL params
*
* @param {String} label label for the template variable
* @returns {String}
*/
export const addPrefixToLabel = label => `${VARIABLE_PREFIX}${label}`;
/**
* Before the templating variables are passed to the backend the
* prefix needs to be removed.
*
* This method removes the prefix at the beginning of the string.
*
* @param {String} label label to remove prefix from
* @returns {String}
*/
export const removePrefixFromLabel = label =>
(label || '').replace(new RegExp(`^${VARIABLE_PREFIX}`), '');
/**
* Convert parsed template variables to an object
* with just keys and values. Prepare the promVariables
* to be added to the URL. Keys of the object will
* have a prefix so that these params can be
* differentiated from other URL params.
*
* @param {Object} variables
* @returns {Object}
*/
export const convertVariablesForURL = variables =>
Object.keys(variables || {}).reduce((acc, key) => {
acc[addPrefixToLabel(key)] = variables[key]?.value;
return acc;
}, {});
/** /**
* User-defined variables from the URL are extracted. The variables * User-defined variables from the URL are extracted. The variables
* begin with a constant prefix so that it doesn't collide with * begin with a constant prefix so that it doesn't collide with
...@@ -131,8 +174,30 @@ export const timeRangeFromUrl = (search = window.location.search) => { ...@@ -131,8 +174,30 @@ export const timeRangeFromUrl = (search = window.location.search) => {
* @returns {Object} The custom variables defined by the user in the URL * @returns {Object} The custom variables defined by the user in the URL
*/ */
export const promCustomVariablesFromUrl = (search = window.location.search) => export const getPromCustomVariablesFromUrl = (search = window.location.search) => {
pickBy(queryToObject(search), (val, key) => key.startsWith(VARIABLE_PREFIX)); const params = queryToObject(search);
// pick the params with variable prefix
const paramsWithVars = pickBy(params, (val, key) => key.startsWith(VARIABLE_PREFIX));
// remove the prefix before storing in the Vuex store
return mapKeys(paramsWithVars, (val, key) => removePrefixFromLabel(key));
};
/**
* Update the URL with promVariables. This usually get triggered when
* the user interacts with the dynamic input elements in the monitoring
* dashboard header.
*
* @param {Object} promVariables user defined variables
*/
export const setPromCustomVariablesFromUrl = promVariables => {
// prep the variables to append to URL
const parsedVariables = convertVariablesForURL(promVariables);
// update the URL
updateHistory({
url: mergeUrlParams(parsedVariables, window.location.href),
title: document.title,
});
};
/** /**
* Returns a URL with no time range based on the current URL. * Returns a URL with no time range based on the current URL.
...@@ -280,4 +345,39 @@ export const barChartsDataParser = (data = []) => ...@@ -280,4 +345,39 @@ export const barChartsDataParser = (data = []) =>
{}, {},
); );
/**
* Custom variables are defined in the dashboard yml file
* and their values can be passed through the URL.
*
* On component load, this method merges variables data
* from the yml file with URL data to store in the Vuex store.
* Not all params coming from the URL need to be stored. Only
* the ones that have a corresponding variable defined in the
* yml file.
*
* This ensures that there is always a single source of truth
* for variables
*
* This method can be improved further. See the below issue
* https://gitlab.com/gitlab-org/gitlab/-/issues/217713
*
* @param {Object} varsFromYML template variables from yml file
* @returns {Object}
*/
export const mergeURLVariables = (varsFromYML = {}) => {
const varsFromURL = getPromCustomVariablesFromUrl();
const variables = {};
Object.keys(varsFromYML).forEach(key => {
if (Object.prototype.hasOwnProperty.call(varsFromURL, key)) {
variables[key] = {
...varsFromYML[key],
value: varsFromURL[key],
};
} else {
variables[key] = varsFromYML[key];
}
});
return variables;
};
export default {}; export default {};
---
title: Render dropdown and text elements based on variables defined in monitoring
dashboard yml file
merge_request: 31524
author:
type: added
import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import CustomVariable from '~/monitoring/components/variables/custom_variable.vue';
describe('Custom variable component', () => {
let wrapper;
const propsData = {
name: 'env',
label: 'Select environment',
value: 'Production',
options: [{ text: 'Production', value: 'prod' }, { text: 'Canary', value: 'canary' }],
};
const createShallowWrapper = () => {
wrapper = shallowMount(CustomVariable, {
propsData,
});
};
const findDropdown = () => wrapper.find(GlDropdown);
const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
it('renders dropdown element when all necessary props are passed', () => {
createShallowWrapper();
expect(findDropdown()).toExist();
});
it('renders dropdown element with a text', () => {
createShallowWrapper();
expect(findDropdown().attributes('text')).toBe(propsData.value);
});
it('renders all the dropdown items', () => {
createShallowWrapper();
expect(findDropdownItems()).toHaveLength(propsData.options.length);
});
it('changing dropdown items triggers update', () => {
createShallowWrapper();
jest.spyOn(wrapper.vm, '$emit');
findDropdownItems()
.at(1)
.vm.$emit('click');
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'env', 'canary');
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlFormInput } from '@gitlab/ui';
import TextVariable from '~/monitoring/components/variables/text_variable.vue';
describe('Text variable component', () => {
let wrapper;
const propsData = {
name: 'pod',
label: 'Select pod',
value: 'test-pod',
};
const createShallowWrapper = () => {
wrapper = shallowMount(TextVariable, {
propsData,
});
};
const findInput = () => wrapper.find(GlFormInput);
it('renders a text input when all props are passed', () => {
createShallowWrapper();
expect(findInput()).toExist();
});
it('always has a default value', () => {
createShallowWrapper();
return wrapper.vm.$nextTick(() => {
expect(findInput().attributes('value')).toBe(propsData.value);
});
});
it('triggers keyup enter', () => {
createShallowWrapper();
jest.spyOn(wrapper.vm, '$emit');
findInput().element.value = 'prod-pod';
findInput().trigger('input');
findInput().trigger('keyup.enter');
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'pod', 'prod-pod');
});
});
it('triggers blur enter', () => {
createShallowWrapper();
jest.spyOn(wrapper.vm, '$emit');
findInput().element.value = 'canary-pod';
findInput().trigger('input');
findInput().trigger('blur');
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'pod', 'canary-pod');
});
});
});
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { GlFormInput } from '@gitlab/ui';
import VariablesSection from '~/monitoring/components/variables_section.vue'; import VariablesSection from '~/monitoring/components/variables_section.vue';
import CustomVariable from '~/monitoring/components/variables/custom_variable.vue';
import TextVariable from '~/monitoring/components/variables/text_variable.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 * as types from '~/monitoring/stores/mutation_types'; import * as types from '~/monitoring/stores/mutation_types';
import { mockTemplatingDataResponses } from '../mock_data';
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
updateHistory: jest.fn(), updateHistory: jest.fn(),
...@@ -15,8 +18,9 @@ describe('Metrics dashboard/variables section component', () => { ...@@ -15,8 +18,9 @@ describe('Metrics dashboard/variables section component', () => {
let store; let store;
let wrapper; let wrapper;
const sampleVariables = { const sampleVariables = {
'var-label1': 'pod', label1: mockTemplatingDataResponses.simpleText.simpleText,
'var-label2': 'main', label2: mockTemplatingDataResponses.advText.advText,
label3: mockTemplatingDataResponses.simpleCustom.simpleCustom,
}; };
const createShallowWrapper = () => { const createShallowWrapper = () => {
...@@ -25,8 +29,8 @@ describe('Metrics dashboard/variables section component', () => { ...@@ -25,8 +29,8 @@ describe('Metrics dashboard/variables section component', () => {
}); });
}; };
const findAllFormInputs = () => wrapper.findAll(GlFormInput); const findTextInput = () => wrapper.findAll(TextVariable);
const getInputAt = i => findAllFormInputs().at(i); const findCustomInput = () => wrapper.findAll(CustomVariable);
beforeEach(() => { beforeEach(() => {
store = createStore(); store = createStore();
...@@ -36,9 +40,9 @@ describe('Metrics dashboard/variables section component', () => { ...@@ -36,9 +40,9 @@ describe('Metrics dashboard/variables section component', () => {
it('does not show the variables section', () => { it('does not show the variables section', () => {
createShallowWrapper(); createShallowWrapper();
const allInputs = findAllFormInputs(); const allInputs = findTextInput().length + findCustomInput().length;
expect(allInputs).toHaveLength(0); expect(allInputs).toBe(0);
}); });
it('shows the variables section', () => { it('shows the variables section', () => {
...@@ -46,15 +50,15 @@ describe('Metrics dashboard/variables section component', () => { ...@@ -46,15 +50,15 @@ describe('Metrics dashboard/variables section component', () => {
wrapper.vm.$store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, sampleVariables); wrapper.vm.$store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, sampleVariables);
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
const allInputs = findAllFormInputs(); const allInputs = findTextInput().length + findCustomInput().length;
expect(allInputs).toHaveLength(Object.keys(sampleVariables).length); expect(allInputs).toBe(Object.keys(sampleVariables).length);
}); });
}); });
describe('when changing the variable inputs', () => { describe('when changing the variable inputs', () => {
const fetchDashboardData = jest.fn(); const fetchDashboardData = jest.fn();
const setVariableValues = jest.fn(); const updateVariableValues = jest.fn();
beforeEach(() => { beforeEach(() => {
store = new Vuex.Store({ store = new Vuex.Store({
...@@ -67,7 +71,7 @@ describe('Metrics dashboard/variables section component', () => { ...@@ -67,7 +71,7 @@ describe('Metrics dashboard/variables section component', () => {
}, },
actions: { actions: {
fetchDashboardData, fetchDashboardData,
setVariableValues, updateVariableValues,
}, },
}, },
}, },
...@@ -76,39 +80,44 @@ describe('Metrics dashboard/variables section component', () => { ...@@ -76,39 +80,44 @@ describe('Metrics dashboard/variables section component', () => {
createShallowWrapper(); createShallowWrapper();
}); });
it('merges the url params and refreshes the dashboard when a form input is blurred', () => { it('merges the url params and refreshes the dashboard when a text-based variables inputs are updated', () => {
const firstInput = getInputAt(0); const firstInput = findTextInput().at(0);
firstInput.element.value = 'POD'; firstInput.vm.$emit('onUpdate', 'label1', 'test');
firstInput.vm.$emit('input');
firstInput.trigger('blur');
expect(setVariableValues).toHaveBeenCalled(); return wrapper.vm.$nextTick(() => {
expect(mergeUrlParams).toHaveBeenCalledWith(sampleVariables, window.location.href); expect(updateVariableValues).toHaveBeenCalled();
expect(updateHistory).toHaveBeenCalled(); expect(mergeUrlParams).toHaveBeenCalledWith(
expect(fetchDashboardData).toHaveBeenCalled(); convertVariablesForURL(sampleVariables),
window.location.href,
);
expect(updateHistory).toHaveBeenCalled();
expect(fetchDashboardData).toHaveBeenCalled();
});
}); });
it('merges the url params and refreshes the dashboard when a form input has received an enter key press', () => { it('merges the url params and refreshes the dashboard when a custom-based variables inputs are updated', () => {
const firstInput = getInputAt(0); const firstInput = findCustomInput().at(0);
firstInput.element.value = 'POD'; firstInput.vm.$emit('onUpdate', 'label1', 'test');
firstInput.vm.$emit('input');
firstInput.trigger('keyup.enter');
expect(setVariableValues).toHaveBeenCalled(); return wrapper.vm.$nextTick(() => {
expect(mergeUrlParams).toHaveBeenCalledWith(sampleVariables, window.location.href); expect(updateVariableValues).toHaveBeenCalled();
expect(updateHistory).toHaveBeenCalled(); expect(mergeUrlParams).toHaveBeenCalledWith(
expect(fetchDashboardData).toHaveBeenCalled(); convertVariablesForURL(sampleVariables),
window.location.href,
);
expect(updateHistory).toHaveBeenCalled();
expect(fetchDashboardData).toHaveBeenCalled();
});
}); });
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 = getInputAt(0); const firstInput = findTextInput().at(0);
firstInput.vm.$emit('input'); firstInput.vm.$emit('onUpdate', 'label1', 'Simple text');
firstInput.trigger('keyup.enter');
expect(setVariableValues).not.toHaveBeenCalled(); expect(updateVariableValues).not.toHaveBeenCalled();
expect(mergeUrlParams).not.toHaveBeenCalled(); expect(mergeUrlParams).not.toHaveBeenCalled();
expect(updateHistory).not.toHaveBeenCalled(); expect(updateHistory).not.toHaveBeenCalled();
expect(fetchDashboardData).not.toHaveBeenCalled(); expect(fetchDashboardData).not.toHaveBeenCalled();
......
...@@ -642,7 +642,7 @@ const generateMockTemplatingData = data => { ...@@ -642,7 +642,7 @@ const generateMockTemplatingData = data => {
const responseForSimpleTextVariable = { const responseForSimpleTextVariable = {
simpleText: { simpleText: {
label: 'var-simpleText', label: 'simpleText',
type: 'text', type: 'text',
value: 'Simple text', value: 'Simple text',
}, },
...@@ -650,7 +650,7 @@ const responseForSimpleTextVariable = { ...@@ -650,7 +650,7 @@ const responseForSimpleTextVariable = {
const responseForAdvTextVariable = { const responseForAdvTextVariable = {
advText: { advText: {
label: 'var-Variable 4', label: 'Variable 4',
type: 'text', type: 'text',
value: 'default', value: 'default',
}, },
...@@ -658,7 +658,8 @@ const responseForAdvTextVariable = { ...@@ -658,7 +658,8 @@ const responseForAdvTextVariable = {
const responseForSimpleCustomVariable = { const responseForSimpleCustomVariable = {
simpleCustom: { simpleCustom: {
label: 'var-simpleCustom', label: 'simpleCustom',
value: 'value1',
options: [ options: [
{ {
default: false, default: false,
...@@ -682,7 +683,7 @@ const responseForSimpleCustomVariable = { ...@@ -682,7 +683,7 @@ const responseForSimpleCustomVariable = {
const responseForAdvancedCustomVariableWithoutOptions = { const responseForAdvancedCustomVariableWithoutOptions = {
advCustomWithoutOpts: { advCustomWithoutOpts: {
label: 'var-advCustomWithoutOpts', label: 'advCustomWithoutOpts',
options: [], options: [],
type: 'custom', type: 'custom',
}, },
...@@ -690,7 +691,8 @@ const responseForAdvancedCustomVariableWithoutOptions = { ...@@ -690,7 +691,8 @@ const responseForAdvancedCustomVariableWithoutOptions = {
const responseForAdvancedCustomVariableWithoutLabel = { const responseForAdvancedCustomVariableWithoutLabel = {
advCustomWithoutLabel: { advCustomWithoutLabel: {
label: 'var-advCustomWithoutLabel', label: 'advCustomWithoutLabel',
value: 'value2',
options: [ options: [
{ {
default: false, default: false,
...@@ -710,7 +712,8 @@ const responseForAdvancedCustomVariableWithoutLabel = { ...@@ -710,7 +712,8 @@ const responseForAdvancedCustomVariableWithoutLabel = {
const responseForAdvancedCustomVariable = { const responseForAdvancedCustomVariable = {
...responseForSimpleCustomVariable, ...responseForSimpleCustomVariable,
advCustomNormal: { advCustomNormal: {
label: 'var-Advanced Var', label: 'Advanced Var',
value: 'value2',
options: [ options: [
{ {
default: false, default: false,
......
...@@ -26,7 +26,7 @@ import { ...@@ -26,7 +26,7 @@ import {
clearExpandedPanel, clearExpandedPanel,
setGettingStartedEmptyState, setGettingStartedEmptyState,
duplicateSystemDashboard, duplicateSystemDashboard,
setVariableValues, updateVariableValues,
} from '~/monitoring/stores/actions'; } from '~/monitoring/stores/actions';
import { import {
gqClient, gqClient,
...@@ -40,6 +40,7 @@ import { ...@@ -40,6 +40,7 @@ import {
deploymentData, deploymentData,
environmentData, environmentData,
annotationsData, annotationsData,
mockTemplatingData,
dashboardGitResponse, dashboardGitResponse,
mockDashboardsErrorResponse, mockDashboardsErrorResponse,
} from '../mock_data'; } from '../mock_data';
...@@ -442,14 +443,14 @@ describe('Monitoring store actions', () => { ...@@ -442,14 +443,14 @@ describe('Monitoring store actions', () => {
}); });
}); });
describe('setVariableValues', () => { describe('updateVariableValues', () => {
let mockedState; let mockedState;
beforeEach(() => { beforeEach(() => {
mockedState = storeState(); mockedState = storeState();
}); });
it('should commit UPDATE_VARIABLE_VALUES mutation', done => { it('should commit UPDATE_VARIABLE_VALUES mutation', done => {
testAction( testAction(
setVariableValues, updateVariableValues,
{ pod: 'POD' }, { pod: 'POD' },
mockedState, mockedState,
[ [
...@@ -574,6 +575,33 @@ describe('Monitoring store actions', () => { ...@@ -574,6 +575,33 @@ 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;
......
...@@ -3,7 +3,12 @@ import * as getters from '~/monitoring/stores/getters'; ...@@ -3,7 +3,12 @@ import * as getters from '~/monitoring/stores/getters';
import mutations from '~/monitoring/stores/mutations'; 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 { environmentData, metricsResult, dashboardGitResponse } from '../mock_data'; import {
environmentData,
metricsResult,
dashboardGitResponse,
mockTemplatingDataResponses,
} from '../mock_data';
import { import {
metricsDashboardPayload, metricsDashboardPayload,
metricResultStatus, metricResultStatus,
...@@ -326,10 +331,6 @@ describe('Monitoring store Getters', () => { ...@@ -326,10 +331,6 @@ describe('Monitoring store Getters', () => {
describe('getCustomVariablesArray', () => { describe('getCustomVariablesArray', () => {
let state; let state;
const sampleVariables = {
'var-label1': 'pod',
'var-label2': 'env',
};
beforeEach(() => { beforeEach(() => {
state = { state = {
...@@ -337,11 +338,20 @@ describe('Monitoring store Getters', () => { ...@@ -337,11 +338,20 @@ describe('Monitoring store Getters', () => {
}; };
}); });
it('transforms the promVariables object to an array in the [variable, variable_value] format', () => { it('transforms the promVariables object to an array in the [variable, variable_value] format for all variable types', () => {
mutations[types.SET_VARIABLES](state, sampleVariables); mutations[types.SET_VARIABLES](state, mockTemplatingDataResponses.allVariableTypes);
const variablesArray = getters.getCustomVariablesArray(state); const variablesArray = getters.getCustomVariablesArray(state);
expect(variablesArray).toEqual(['label1', 'pod', 'label2', 'env']); expect(variablesArray).toEqual([
'simpleText',
'Simple text',
'advText',
'default',
'simpleCustom',
'value1',
'advCustomNormal',
'value2',
]);
}); });
it('transforms the promVariables object to an empty array when no keys are present', () => { it('transforms the promVariables object to an empty array when no keys are present', () => {
......
...@@ -427,18 +427,11 @@ describe('Monitoring mutations', () => { ...@@ -427,18 +427,11 @@ describe('Monitoring mutations', () => {
mutations[types.SET_VARIABLES](stateCopy, {}); mutations[types.SET_VARIABLES](stateCopy, {});
}); });
it('ignores updates that are not already in promVariables', () => { it('updates only the value of the variable in promVariables', () => {
mutations[types.SET_VARIABLES](stateCopy, { environment: 'prod' }); mutations[types.SET_VARIABLES](stateCopy, { environment: { value: 'prod', type: 'text' } });
mutations[types.UPDATE_VARIABLE_VALUES](stateCopy, { pod: 'new pod' }); mutations[types.UPDATE_VARIABLE_VALUES](stateCopy, { key: 'environment', value: 'new prod' });
expect(stateCopy.promVariables).toEqual({ environment: 'prod' }); expect(stateCopy.promVariables).toEqual({ environment: { value: 'new prod', type: 'text' } });
});
it('only updates existing variables', () => {
mutations[types.SET_VARIABLES](stateCopy, { pod: 'POD' });
mutations[types.UPDATE_VARIABLE_VALUES](stateCopy, { pod: 'new pod' });
expect(stateCopy.promVariables).toEqual({ pod: 'new pod' });
}); });
}); });
}); });
...@@ -5,7 +5,6 @@ import { ...@@ -5,7 +5,6 @@ import {
parseAnnotationsResponse, parseAnnotationsResponse,
removeLeadingSlash, removeLeadingSlash,
mapToDashboardViewModel, mapToDashboardViewModel,
removePrefixFromLabels,
} 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';
...@@ -420,24 +419,3 @@ describe('removeLeadingSlash', () => { ...@@ -420,24 +419,3 @@ describe('removeLeadingSlash', () => {
}); });
}); });
}); });
describe('removePrefixFromLabels', () => {
it.each`
input | expected
${undefined} | ${''}
${null} | ${''}
${''} | ${''}
${' '} | ${' '}
${'pod-1'} | ${'pod-1'}
${'pod-var-1'} | ${'pod-var-1'}
${'pod-1-var'} | ${'pod-1-var'}
${'podvar--1'} | ${'podvar--1'}
${'povar-d-1'} | ${'povar-d-1'}
${'var-pod-1'} | ${'pod-1'}
${'var-var-pod-1'} | ${'var-pod-1'}
${'varvar-pod-1'} | ${'varvar-pod-1'}
${'var-pod-1-var-'} | ${'pod-1-var-'}
`('removePrefixFromLabels returns $expected with input $input', ({ input, expected }) => {
expect(removePrefixFromLabels(input)).toEqual(expected);
});
});
...@@ -169,8 +169,8 @@ describe('monitoring/utils', () => { ...@@ -169,8 +169,8 @@ describe('monitoring/utils', () => {
}); });
}); });
describe('promCustomVariablesFromUrl', () => { describe('getPromCustomVariablesFromUrl', () => {
const { promCustomVariablesFromUrl } = monitoringUtils; const { getPromCustomVariablesFromUrl } = monitoringUtils;
beforeEach(() => { beforeEach(() => {
jest.spyOn(urlUtils, 'queryToObject'); jest.spyOn(urlUtils, 'queryToObject');
...@@ -195,7 +195,7 @@ describe('monitoring/utils', () => { ...@@ -195,7 +195,7 @@ describe('monitoring/utils', () => {
'var-pod': 'POD', 'var-pod': 'POD',
}); });
expect(promCustomVariablesFromUrl()).toEqual(expect.objectContaining({ 'var-pod': 'POD' })); expect(getPromCustomVariablesFromUrl()).toEqual(expect.objectContaining({ pod: 'POD' }));
}); });
it('returns an empty object when no custom variables are present', () => { it('returns an empty object when no custom variables are present', () => {
...@@ -203,7 +203,7 @@ describe('monitoring/utils', () => { ...@@ -203,7 +203,7 @@ describe('monitoring/utils', () => {
dashboard: '.gitlab/dashboards/custom_dashboard.yml', dashboard: '.gitlab/dashboards/custom_dashboard.yml',
}); });
expect(promCustomVariablesFromUrl()).toStrictEqual({}); expect(getPromCustomVariablesFromUrl()).toStrictEqual({});
}); });
}); });
...@@ -398,4 +398,108 @@ describe('monitoring/utils', () => { ...@@ -398,4 +398,108 @@ describe('monitoring/utils', () => {
}); });
}); });
}); });
describe('removePrefixFromLabel', () => {
it.each`
input | expected
${undefined} | ${''}
${null} | ${''}
${''} | ${''}
${' '} | ${' '}
${'pod-1'} | ${'pod-1'}
${'pod-var-1'} | ${'pod-var-1'}
${'pod-1-var'} | ${'pod-1-var'}
${'podvar--1'} | ${'podvar--1'}
${'povar-d-1'} | ${'povar-d-1'}
${'var-pod-1'} | ${'pod-1'}
${'var-var-pod-1'} | ${'var-pod-1'}
${'varvar-pod-1'} | ${'varvar-pod-1'}
${'var-pod-1-var-'} | ${'pod-1-var-'}
`('removePrefixFromLabel returns $expected with input $input', ({ input, expected }) => {
expect(monitoringUtils.removePrefixFromLabel(input)).toEqual(expected);
});
});
describe('mergeURLVariables', () => {
beforeEach(() => {
jest.spyOn(urlUtils, 'queryToObject');
});
afterEach(() => {
urlUtils.queryToObject.mockRestore();
});
it('returns empty object if variables are not defined in yml or URL', () => {
urlUtils.queryToObject.mockReturnValueOnce({});
expect(monitoringUtils.mergeURLVariables({})).toEqual({});
});
it('returns empty object if variables are defined in URL but not in yml', () => {
urlUtils.queryToObject.mockReturnValueOnce({
'var-env': 'one',
'var-instance': 'localhost',
});
expect(monitoringUtils.mergeURLVariables({})).toEqual({});
});
it('returns yml variables if variables defined in yml but not in the URL', () => {
urlUtils.queryToObject.mockReturnValueOnce({});
const params = {
env: 'one',
instance: 'localhost',
};
expect(monitoringUtils.mergeURLVariables(params)).toEqual(params);
});
it('returns yml variables if variables defined in URL do not match with yml variables', () => {
const urlParams = {
'var-env': 'one',
'var-instance': 'localhost',
};
const ymlParams = {
pod: { value: 'one' },
service: { value: 'database' },
};
urlUtils.queryToObject.mockReturnValueOnce(urlParams);
expect(monitoringUtils.mergeURLVariables(ymlParams)).toEqual(ymlParams);
});
it('returns merged yml and URL variables if there is some match', () => {
const urlParams = {
'var-env': 'one',
'var-instance': 'localhost:8080',
};
const ymlParams = {
instance: { value: 'localhost' },
service: { value: 'database' },
};
const merged = {
instance: { value: 'localhost:8080' },
service: { value: 'database' },
};
urlUtils.queryToObject.mockReturnValueOnce(urlParams);
expect(monitoringUtils.mergeURLVariables(ymlParams)).toEqual(merged);
});
});
describe('convertVariablesForURL', () => {
it.each`
input | expected
${undefined} | ${{}}
${null} | ${{}}
${{}} | ${{}}
${{ env: { value: 'prod' } }} | ${{ 'var-env': 'prod' }}
${{ 'var-env': { value: 'prod' } }} | ${{ 'var-var-env': 'prod' }}
`('convertVariablesForURL returns $expected with input $input', ({ input, expected }) => {
expect(monitoringUtils.convertVariablesForURL(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