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>
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
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 {
components: {
GlFormGroup,
GlFormInput,
CustomVariable,
TextVariable,
},
computed: {
...mapState('monitoringDashboard', ['promVariables']),
},
methods: {
...mapActions('monitoringDashboard', ['fetchDashboardData', 'setVariableValues']),
refreshDashboard(event) {
const { name, value } = event.target;
if (this.promVariables[name] !== value) {
const changedVariable = { [name]: value };
this.setVariableValues(changedVariable);
updateHistory({
url: mergeUrlParams(this.promVariables, window.location.href),
title: document.title,
});
...mapActions('monitoringDashboard', ['fetchDashboardData', 'updateVariableValues']),
refreshDashboard(variable, value) {
if (this.promVariables[variable].value !== value) {
const changedVariable = { key: variable, value };
// update the Vuex store
this.updateVariableValues(changedVariable);
// the below calls can ideally be moved out of the
// component and into the actions and let the
// mutation respond directly.
// This can be further investigate in
// https://gitlab.com/gitlab-org/gitlab/-/issues/217713
setPromCustomVariablesFromUrl(this.promVariables);
// fetch data
this.fetchDashboardData();
}
},
variableComponent(type) {
const types = {
text: TextVariable,
custom: CustomVariable,
};
return types[type] || TextVariable;
},
},
};
</script>
<template>
<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">
<gl-form-group :label="key" class="mb-0 flex-grow-1">
<gl-form-input
:value="val"
:name="key"
@keyup.native.enter="refreshDashboard"
@blur.native="refreshDashboard"
/>
</gl-form-group>
<div v-for="(variable, key) in promVariables" :key="key" class="mb-1 pr-2 d-flex d-sm-block">
<component
:is="variableComponent(variable.type)"
class="mb-0 flex-grow-1"
:label="variable.label"
:value="variable.value"
:name="key"
:options="variable.options"
@onUpdate="refreshDashboard"
/>
</div>
</div>
</template>
......@@ -4,7 +4,6 @@ import Dashboard from '~/monitoring/components/dashboard.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { getParameterValues } from '~/lib/utils/url_utility';
import store from './stores';
import { promCustomVariablesFromUrl } from './utils';
Vue.use(GlToast);
......@@ -14,8 +13,6 @@ export default (props = {}) => {
if (el && el.dataset) {
const [currentDashboard] = getParameterValues('dashboard');
store.dispatch('monitoringDashboard/setVariableValues', promCustomVariablesFromUrl());
// eslint-disable-next-line no-new
new Vue({
el,
......
......@@ -3,6 +3,8 @@ import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { parseTemplatingVariables } from './variable_mapping';
import { mergeURLVariables } from '../utils';
import {
gqClient,
parseEnvironmentsResponse,
......@@ -159,6 +161,7 @@ export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response
commit(types.SET_ALL_DASHBOARDS, all_dashboards);
commit(types.RECEIVE_METRICS_DASHBOARD_SUCCESS, dashboard);
commit(types.SET_VARIABLES, mergeURLVariables(parseTemplatingVariables(dashboard.templating)));
commit(types.SET_ENDPOINTS, convertObjectPropsToCamelCase(metrics_data));
return dispatch('fetchDashboardData');
......@@ -413,7 +416,7 @@ export const duplicateSystemDashboard = ({ state }, payload) => {
// Variables manipulation
export const setVariableValues = ({ commit }, updatedVariable) => {
export const updateVariableValues = ({ commit }, updatedVariable) => {
commit(types.UPDATE_VARIABLE_VALUES, updatedVariable);
};
......
import { flatMap } from 'lodash';
import { removePrefixFromLabels } from './utils';
import { NOT_IN_DB_PREFIX } from '../constants';
const metricsIdsInPanel = panel =>
......@@ -123,10 +122,7 @@ export const filteredEnvironments = state =>
*/
export const getCustomVariablesArray = state =>
flatMap(state.promVariables, (val, key) => [
encodeURIComponent(removePrefixFromLabels(key)),
encodeURIComponent(val),
]);
flatMap(state.promVariables, (variable, key) => [key, variable.value]);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -191,11 +191,10 @@ export default {
[types.SET_VARIABLES](state, variables) {
state.promVariables = variables;
},
[types.UPDATE_VARIABLE_VALUES](state, newVariable) {
Object.keys(newVariable).forEach(key => {
if (Object.prototype.hasOwnProperty.call(state.promVariables, key)) {
state.promVariables[key] = newVariable[key];
}
[types.UPDATE_VARIABLE_VALUES](state, updatedVariable) {
Object.assign(state.promVariables[updatedVariable.key], {
...state.promVariables[updatedVariable.key],
value: updatedVariable.value,
});
},
};
......@@ -2,7 +2,7 @@ import { slugify } from '~/lib/utils/text_utility';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
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(
{},
......@@ -229,25 +229,3 @@ export const normalizeQueryResult = timeSeries => {
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 { addPrefixToLabels } from './utils';
import { VARIABLE_TYPES } from '../constants';
/**
......@@ -56,15 +55,20 @@ const normalizeCustomVariableOptions = ({ default: defaultOpt = false, text, val
* Custom advanced variables are rendered as dropdown elements in the dashboard
* 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
* @returns {Object}
*/
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 {
type: VARIABLE_TYPES.custom,
label: advVariable.label,
options: options.map(normalizeCustomVariableOptions),
value: defaultOpt?.value,
options,
};
};
......@@ -83,6 +87,9 @@ const parseSimpleCustomOptions = opt => ({ text: opt, value: opt });
*
* 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
* @returns {Object}
*/
......@@ -90,6 +97,7 @@ const customSimpleVariableParser = simpleVar => {
const options = (simpleVar || []).map(parseSimpleCustomOptions);
return {
type: VARIABLE_TYPES.custom,
value: options[0].value,
label: null,
options: options.map(normalizeCustomVariableOptions),
};
......@@ -150,7 +158,7 @@ export const parseTemplatingVariables = ({ variables = {} } = {}) =>
if (parsedVar) {
acc[key] = {
...parsedVar,
label: addPrefixToLabels(parsedVar.label || key),
label: parsedVar.label || key,
};
}
return acc;
......
import { pickBy } from 'lodash';
import { queryToObject, mergeUrlParams, removeParams } from '~/lib/utils/url_utility';
import { pickBy, mapKeys } from 'lodash';
import {
queryToObject,
mergeUrlParams,
removeParams,
updateHistory,
} from '~/lib/utils/url_utility';
import {
timeRangeParamNames,
timeRangeFromParams,
......@@ -122,6 +127,44 @@ export const timeRangeFromUrl = (search = window.location.search) => {
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
* begin with a constant prefix so that it doesn't collide with
......@@ -131,8 +174,30 @@ export const timeRangeFromUrl = (search = window.location.search) => {
* @returns {Object} The custom variables defined by the user in the URL
*/
export const promCustomVariablesFromUrl = (search = window.location.search) =>
pickBy(queryToObject(search), (val, key) => key.startsWith(VARIABLE_PREFIX));
export const getPromCustomVariablesFromUrl = (search = window.location.search) => {
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.
......@@ -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 {};
---
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 Vuex from 'vuex';
import { GlFormInput } from '@gitlab/ui';
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 { createStore } from '~/monitoring/stores';
import { convertVariablesForURL } from '~/monitoring/utils';
import * as types from '~/monitoring/stores/mutation_types';
import { mockTemplatingDataResponses } from '../mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
updateHistory: jest.fn(),
......@@ -15,8 +18,9 @@ describe('Metrics dashboard/variables section component', () => {
let store;
let wrapper;
const sampleVariables = {
'var-label1': 'pod',
'var-label2': 'main',
label1: mockTemplatingDataResponses.simpleText.simpleText,
label2: mockTemplatingDataResponses.advText.advText,
label3: mockTemplatingDataResponses.simpleCustom.simpleCustom,
};
const createShallowWrapper = () => {
......@@ -25,8 +29,8 @@ describe('Metrics dashboard/variables section component', () => {
});
};
const findAllFormInputs = () => wrapper.findAll(GlFormInput);
const getInputAt = i => findAllFormInputs().at(i);
const findTextInput = () => wrapper.findAll(TextVariable);
const findCustomInput = () => wrapper.findAll(CustomVariable);
beforeEach(() => {
store = createStore();
......@@ -36,9 +40,9 @@ describe('Metrics dashboard/variables section component', () => {
it('does not show the variables section', () => {
createShallowWrapper();
const allInputs = findAllFormInputs();
const allInputs = findTextInput().length + findCustomInput().length;
expect(allInputs).toHaveLength(0);
expect(allInputs).toBe(0);
});
it('shows the variables section', () => {
......@@ -46,15 +50,15 @@ describe('Metrics dashboard/variables section component', () => {
wrapper.vm.$store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, sampleVariables);
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', () => {
const fetchDashboardData = jest.fn();
const setVariableValues = jest.fn();
const updateVariableValues = jest.fn();
beforeEach(() => {
store = new Vuex.Store({
......@@ -67,7 +71,7 @@ describe('Metrics dashboard/variables section component', () => {
},
actions: {
fetchDashboardData,
setVariableValues,
updateVariableValues,
},
},
},
......@@ -76,39 +80,44 @@ describe('Metrics dashboard/variables section component', () => {
createShallowWrapper();
});
it('merges the url params and refreshes the dashboard when a form input is blurred', () => {
const firstInput = getInputAt(0);
it('merges the url params and refreshes the dashboard when a text-based variables inputs are updated', () => {
const firstInput = findTextInput().at(0);
firstInput.element.value = 'POD';
firstInput.vm.$emit('input');
firstInput.trigger('blur');
firstInput.vm.$emit('onUpdate', 'label1', 'test');
expect(setVariableValues).toHaveBeenCalled();
expect(mergeUrlParams).toHaveBeenCalledWith(sampleVariables, window.location.href);
expect(updateHistory).toHaveBeenCalled();
expect(fetchDashboardData).toHaveBeenCalled();
return wrapper.vm.$nextTick(() => {
expect(updateVariableValues).toHaveBeenCalled();
expect(mergeUrlParams).toHaveBeenCalledWith(
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', () => {
const firstInput = getInputAt(0);
it('merges the url params and refreshes the dashboard when a custom-based variables inputs are updated', () => {
const firstInput = findCustomInput().at(0);
firstInput.element.value = 'POD';
firstInput.vm.$emit('input');
firstInput.trigger('keyup.enter');
firstInput.vm.$emit('onUpdate', 'label1', 'test');
expect(setVariableValues).toHaveBeenCalled();
expect(mergeUrlParams).toHaveBeenCalledWith(sampleVariables, window.location.href);
expect(updateHistory).toHaveBeenCalled();
expect(fetchDashboardData).toHaveBeenCalled();
return wrapper.vm.$nextTick(() => {
expect(updateVariableValues).toHaveBeenCalled();
expect(mergeUrlParams).toHaveBeenCalledWith(
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', () => {
const firstInput = getInputAt(0);
const firstInput = findTextInput().at(0);
firstInput.vm.$emit('input');
firstInput.trigger('keyup.enter');
firstInput.vm.$emit('onUpdate', 'label1', 'Simple text');
expect(setVariableValues).not.toHaveBeenCalled();
expect(updateVariableValues).not.toHaveBeenCalled();
expect(mergeUrlParams).not.toHaveBeenCalled();
expect(updateHistory).not.toHaveBeenCalled();
expect(fetchDashboardData).not.toHaveBeenCalled();
......
......@@ -642,7 +642,7 @@ const generateMockTemplatingData = data => {
const responseForSimpleTextVariable = {
simpleText: {
label: 'var-simpleText',
label: 'simpleText',
type: 'text',
value: 'Simple text',
},
......@@ -650,7 +650,7 @@ const responseForSimpleTextVariable = {
const responseForAdvTextVariable = {
advText: {
label: 'var-Variable 4',
label: 'Variable 4',
type: 'text',
value: 'default',
},
......@@ -658,7 +658,8 @@ const responseForAdvTextVariable = {
const responseForSimpleCustomVariable = {
simpleCustom: {
label: 'var-simpleCustom',
label: 'simpleCustom',
value: 'value1',
options: [
{
default: false,
......@@ -682,7 +683,7 @@ const responseForSimpleCustomVariable = {
const responseForAdvancedCustomVariableWithoutOptions = {
advCustomWithoutOpts: {
label: 'var-advCustomWithoutOpts',
label: 'advCustomWithoutOpts',
options: [],
type: 'custom',
},
......@@ -690,7 +691,8 @@ const responseForAdvancedCustomVariableWithoutOptions = {
const responseForAdvancedCustomVariableWithoutLabel = {
advCustomWithoutLabel: {
label: 'var-advCustomWithoutLabel',
label: 'advCustomWithoutLabel',
value: 'value2',
options: [
{
default: false,
......@@ -710,7 +712,8 @@ const responseForAdvancedCustomVariableWithoutLabel = {
const responseForAdvancedCustomVariable = {
...responseForSimpleCustomVariable,
advCustomNormal: {
label: 'var-Advanced Var',
label: 'Advanced Var',
value: 'value2',
options: [
{
default: false,
......
......@@ -26,7 +26,7 @@ import {
clearExpandedPanel,
setGettingStartedEmptyState,
duplicateSystemDashboard,
setVariableValues,
updateVariableValues,
} from '~/monitoring/stores/actions';
import {
gqClient,
......@@ -40,6 +40,7 @@ import {
deploymentData,
environmentData,
annotationsData,
mockTemplatingData,
dashboardGitResponse,
mockDashboardsErrorResponse,
} from '../mock_data';
......@@ -442,14 +443,14 @@ describe('Monitoring store actions', () => {
});
});
describe('setVariableValues', () => {
describe('updateVariableValues', () => {
let mockedState;
beforeEach(() => {
mockedState = storeState();
});
it('should commit UPDATE_VARIABLE_VALUES mutation', done => {
testAction(
setVariableValues,
updateVariableValues,
{ pod: 'POD' },
mockedState,
[
......@@ -574,6 +575,33 @@ describe('Monitoring store actions', () => {
);
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', () => {
const params = {};
const response = metricsDashboardResponse;
......
......@@ -3,7 +3,12 @@ import * as getters from '~/monitoring/stores/getters';
import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import { metricStates } from '~/monitoring/constants';
import { environmentData, metricsResult, dashboardGitResponse } from '../mock_data';
import {
environmentData,
metricsResult,
dashboardGitResponse,
mockTemplatingDataResponses,
} from '../mock_data';
import {
metricsDashboardPayload,
metricResultStatus,
......@@ -326,10 +331,6 @@ describe('Monitoring store Getters', () => {
describe('getCustomVariablesArray', () => {
let state;
const sampleVariables = {
'var-label1': 'pod',
'var-label2': 'env',
};
beforeEach(() => {
state = {
......@@ -337,11 +338,20 @@ describe('Monitoring store Getters', () => {
};
});
it('transforms the promVariables object to an array in the [variable, variable_value] format', () => {
mutations[types.SET_VARIABLES](state, sampleVariables);
it('transforms the promVariables object to an array in the [variable, variable_value] format for all variable types', () => {
mutations[types.SET_VARIABLES](state, mockTemplatingDataResponses.allVariableTypes);
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', () => {
......
......@@ -427,18 +427,11 @@ describe('Monitoring mutations', () => {
mutations[types.SET_VARIABLES](stateCopy, {});
});
it('ignores updates that are not already in promVariables', () => {
mutations[types.SET_VARIABLES](stateCopy, { environment: 'prod' });
mutations[types.UPDATE_VARIABLE_VALUES](stateCopy, { pod: 'new pod' });
it('updates only the value of the variable in promVariables', () => {
mutations[types.SET_VARIABLES](stateCopy, { environment: { value: 'prod', type: 'text' } });
mutations[types.UPDATE_VARIABLE_VALUES](stateCopy, { key: 'environment', value: 'new prod' });
expect(stateCopy.promVariables).toEqual({ environment: 'prod' });
});
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' });
expect(stateCopy.promVariables).toEqual({ environment: { value: 'new prod', type: 'text' } });
});
});
});
......@@ -5,7 +5,6 @@ import {
parseAnnotationsResponse,
removeLeadingSlash,
mapToDashboardViewModel,
removePrefixFromLabels,
} from '~/monitoring/stores/utils';
import { annotationsData } from '../mock_data';
import { NOT_IN_DB_PREFIX } from '~/monitoring/constants';
......@@ -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', () => {
});
});
describe('promCustomVariablesFromUrl', () => {
const { promCustomVariablesFromUrl } = monitoringUtils;
describe('getPromCustomVariablesFromUrl', () => {
const { getPromCustomVariablesFromUrl } = monitoringUtils;
beforeEach(() => {
jest.spyOn(urlUtils, 'queryToObject');
......@@ -195,7 +195,7 @@ describe('monitoring/utils', () => {
'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', () => {
......@@ -203,7 +203,7 @@ describe('monitoring/utils', () => {
dashboard: '.gitlab/dashboards/custom_dashboard.yml',
});
expect(promCustomVariablesFromUrl()).toStrictEqual({});
expect(getPromCustomVariablesFromUrl()).toStrictEqual({});
});
});
......@@ -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