Commit f5e79f65 authored by Mike Greiling's avatar Mike Greiling

Merge branch 'dbodicherla-add-dropdown-input-based-on-dashboard-file' into 'master'

Add dropdown input based on dashboard file

See merge request gitlab-org/gitlab!31524
parents af6107b0 ea2d4fcf
<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"
:label="variable.label"
:value="variable.value"
:name="key" :name="key"
@keyup.native.enter="refreshDashboard" :options="variable.options"
@blur.native="refreshDashboard" @onUpdate="refreshDashboard"
/> />
</gl-form-group>
</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(mergeUrlParams).toHaveBeenCalledWith(
convertVariablesForURL(sampleVariables),
window.location.href,
);
expect(updateHistory).toHaveBeenCalled(); expect(updateHistory).toHaveBeenCalled();
expect(fetchDashboardData).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(mergeUrlParams).toHaveBeenCalledWith(
convertVariablesForURL(sampleVariables),
window.location.href,
);
expect(updateHistory).toHaveBeenCalled(); expect(updateHistory).toHaveBeenCalled();
expect(fetchDashboardData).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