Commit f036c0ef authored by Martin Wortschack's avatar Martin Wortschack

Merge branch 'jivanvl-add-text-variable-input-from-variables' into 'master'

add text variable input from variables

See merge request gitlab-org/gitlab!30790
parents 19031b99 0870e096
...@@ -30,6 +30,7 @@ import GraphGroup from './graph_group.vue'; ...@@ -30,6 +30,7 @@ import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import GroupEmptyState from './group_empty_state.vue'; import GroupEmptyState from './group_empty_state.vue';
import DashboardsDropdown from './dashboards_dropdown.vue'; import DashboardsDropdown from './dashboards_dropdown.vue';
import VariablesSection from './variables_section.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event'; import TrackEventDirective from '~/vue_shared/directives/track_event';
import { import {
...@@ -64,6 +65,8 @@ export default { ...@@ -64,6 +65,8 @@ export default {
EmptyState, EmptyState,
GroupEmptyState, GroupEmptyState,
DashboardsDropdown, DashboardsDropdown,
VariablesSection,
}, },
directives: { directives: {
GlModal: GlModalDirective, GlModal: GlModalDirective,
...@@ -222,6 +225,7 @@ export default { ...@@ -222,6 +225,7 @@ export default {
'allDashboards', 'allDashboards',
'environmentsLoading', 'environmentsLoading',
'expandedPanel', 'expandedPanel',
'promVariables',
]), ]),
...mapGetters('monitoringDashboard', ['getMetricStates', 'filteredEnvironments']), ...mapGetters('monitoringDashboard', ['getMetricStates', 'filteredEnvironments']),
firstDashboard() { firstDashboard() {
...@@ -243,6 +247,9 @@ export default { ...@@ -243,6 +247,9 @@ export default {
shouldShowEnvironmentsDropdownNoMatchedMsg() { shouldShowEnvironmentsDropdownNoMatchedMsg() {
return !this.environmentsLoading && this.filteredEnvironments.length === 0; return !this.environmentsLoading && this.filteredEnvironments.length === 0;
}, },
shouldShowVariablesSection() {
return Object.keys(this.promVariables).length > 0;
},
}, },
watch: { watch: {
dashboard(newDashboard) { dashboard(newDashboard) {
...@@ -584,7 +591,7 @@ export default { ...@@ -584,7 +591,7 @@ export default {
</div> </div>
</div> </div>
</div> </div>
<variables-section v-if="shouldShowVariablesSection && !showEmptyState" />
<div v-if="!showEmptyState"> <div v-if="!showEmptyState">
<dashboard-panel <dashboard-panel
v-show="expandedPanel.panel" v-show="expandedPanel.panel"
......
<script>
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
export default {
components: {
GlFormGroup,
GlFormInput,
},
computed: {
...mapState('monitoringDashboard', ['promVariables']),
},
methods: {
...mapActions('monitoringDashboard', ['fetchDashboardData', 'setVariableData']),
refreshDashboard(event) {
const { name, value } = event.target;
if (this.promVariables[name] !== value) {
const changedVariable = { [name]: value };
this.setVariableData(changedVariable);
updateHistory({
url: mergeUrlParams(this.promVariables, window.location.href),
title: document.title,
});
this.fetchDashboardData();
}
},
},
};
</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>
</div>
</template>
...@@ -222,14 +222,17 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => { ...@@ -222,14 +222,17 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => {
* *
* @param {metric} metric * @param {metric} metric
*/ */
export const fetchPrometheusMetric = ({ commit, state }, { metric, defaultQueryParams }) => { export const fetchPrometheusMetric = (
{ commit, state, getters },
{ metric, defaultQueryParams },
) => {
const queryParams = { ...defaultQueryParams }; const queryParams = { ...defaultQueryParams };
if (metric.step) { if (metric.step) {
queryParams.step = metric.step; queryParams.step = metric.step;
} }
if (state.promVariables.length > 0) { if (Object.keys(state.promVariables).length > 0) {
queryParams.variables = state.promVariables; queryParams.variables = getters.getCustomVariablesArray;
} }
commit(types.REQUEST_METRIC_RESULT, { metricId: metric.metricId }); commit(types.REQUEST_METRIC_RESULT, { metricId: metric.metricId });
...@@ -390,5 +393,11 @@ export const duplicateSystemDashboard = ({ state }, payload) => { ...@@ -390,5 +393,11 @@ export const duplicateSystemDashboard = ({ state }, payload) => {
}); });
}; };
// Variables manipulation
export const setVariableData = ({ commit }, updatedVariable) => {
commit(types.UPDATE_VARIABLE_DATA, updatedVariable);
};
// 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 () => {};
...@@ -96,5 +96,17 @@ export const filteredEnvironments = state => ...@@ -96,5 +96,17 @@ export const filteredEnvironments = state =>
env.name.toLowerCase().includes((state.environmentsSearchTerm || '').trim().toLowerCase()), env.name.toLowerCase().includes((state.environmentsSearchTerm || '').trim().toLowerCase()),
); );
/**
* Maps an variables object to an array
* @param {Object} variables - Custom variables provided by the user
* @returns {Array} The custom variables array to be send to the API
* in the format of [variable1, variable1_value]
*/
export const getCustomVariablesArray = state =>
Object.entries(state.promVariables)
.flat()
.map(encodeURIComponent);
// 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 () => {};
...@@ -3,6 +3,7 @@ export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD'; ...@@ -3,6 +3,7 @@ export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD';
export const RECEIVE_METRICS_DASHBOARD_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS'; export const RECEIVE_METRICS_DASHBOARD_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS';
export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE'; export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE';
export const SET_PROM_QUERY_VARIABLES = 'SET_PROM_QUERY_VARIABLES'; export const SET_PROM_QUERY_VARIABLES = 'SET_PROM_QUERY_VARIABLES';
export const UPDATE_VARIABLE_DATA = 'UPDATE_VARIABLE_DATA';
// Annotations // Annotations
export const RECEIVE_ANNOTATIONS_SUCCESS = 'RECEIVE_ANNOTATIONS_SUCCESS'; export const RECEIVE_ANNOTATIONS_SUCCESS = 'RECEIVE_ANNOTATIONS_SUCCESS';
......
import pick from 'lodash/pick'; import { pick } from 'lodash';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { mapToDashboardViewModel, normalizeQueryResult } from './utils'; import { mapToDashboardViewModel, normalizeQueryResult } from './utils';
import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils'; import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
...@@ -51,18 +51,6 @@ const emptyStateFromError = error => { ...@@ -51,18 +51,6 @@ const emptyStateFromError = error => {
return metricStates.UNKNOWN_ERROR; return metricStates.UNKNOWN_ERROR;
}; };
/**
* Maps an variables object to an array
* @returns {Array} The custom variables array to be send to the API
* in the format of [variable1, variable1_value]
* @param {Object} variables - Custom variables provided by the user
*/
const transformVariablesObjectArray = variables =>
Object.entries(variables)
.flat()
.map(encodeURIComponent);
export default { export default {
/** /**
* Dashboard panels structure and global state * Dashboard panels structure and global state
...@@ -182,6 +170,12 @@ export default { ...@@ -182,6 +170,12 @@ export default {
state.expandedPanel.panel = panel; state.expandedPanel.panel = panel;
}, },
[types.SET_PROM_QUERY_VARIABLES](state, variables) { [types.SET_PROM_QUERY_VARIABLES](state, variables) {
state.promVariables = transformVariablesObjectArray(variables); state.promVariables = variables;
},
[types.UPDATE_VARIABLE_DATA](state, newVariable) {
Object.assign(state.promVariables, {
...state.promVariables,
...newVariable,
});
}, },
}; };
...@@ -33,7 +33,7 @@ export default () => ({ ...@@ -33,7 +33,7 @@ export default () => ({
panel: null, panel: null,
}, },
allDashboards: [], allDashboards: [],
promVariables: [], promVariables: {},
// Other project data // Other project data
annotations: [], annotations: [],
......
...@@ -13,6 +13,14 @@ ...@@ -13,6 +13,14 @@
.form-group { .form-group {
margin-bottom: map-get($spacing-scale, 3); margin-bottom: map-get($spacing-scale, 3);
} }
.variables-section {
input {
@include media-breakpoint-up(sm) {
width: 160px;
}
}
}
} }
.draggable { .draggable {
......
...@@ -111,6 +111,8 @@ exports[`Dashboard template matches the default snapshot 1`] = ` ...@@ -111,6 +111,8 @@ exports[`Dashboard template matches the default snapshot 1`] = `
</div> </div>
</div> </div>
<!---->
<empty-state-stub <empty-state-stub
clusterspath="/path/to/clusters" clusterspath="/path/to/clusters"
documentationpath="/path/to/docs" documentationpath="/path/to/docs"
......
...@@ -18,7 +18,12 @@ import GroupEmptyState from '~/monitoring/components/group_empty_state.vue'; ...@@ -18,7 +18,12 @@ import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue'; import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import { createStore } from '~/monitoring/stores'; import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types'; import * as types from '~/monitoring/stores/mutation_types';
import { setupStoreWithDashboard, setMetricResult, setupStoreWithData } from '../store_utils'; import {
setupStoreWithDashboard,
setMetricResult,
setupStoreWithData,
setupStoreWithVariable,
} from '../store_utils';
import { environmentData, dashboardGitResponse, propsData } from '../mock_data'; import { environmentData, dashboardGitResponse, propsData } from '../mock_data';
import { metricsDashboardViewModel, metricsDashboardPanelCount } from '../fixture_data'; import { metricsDashboardViewModel, metricsDashboardPanelCount } from '../fixture_data';
import createFlash from '~/flash'; import createFlash from '~/flash';
...@@ -381,6 +386,20 @@ describe('Dashboard', () => { ...@@ -381,6 +386,20 @@ describe('Dashboard', () => {
}); });
}); });
describe('variables section', () => {
beforeEach(() => {
createShallowWrapper({ hasMetrics: true });
setupStoreWithData(wrapper.vm.$store);
setupStoreWithVariable(wrapper.vm.$store);
return wrapper.vm.$nextTick();
});
it('shows the variables section', () => {
expect(wrapper.vm.shouldShowVariablesSection).toBe(true);
});
});
describe('single panel expands to "full screen" mode', () => { describe('single panel expands to "full screen" mode', () => {
const findExpandedPanel = () => wrapper.find({ ref: 'expandedPanel' }); const findExpandedPanel = () => wrapper.find({ ref: 'expandedPanel' });
......
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlFormInput } from '@gitlab/ui';
import VariablesSection from '~/monitoring/components/variables_section.vue';
import { updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
jest.mock('~/lib/utils/url_utility', () => ({
updateHistory: jest.fn(),
mergeUrlParams: jest.fn(),
}));
describe('Metrics dashboard/variables section component', () => {
let store;
let wrapper;
const sampleVariables = {
label1: 'pod',
label2: 'main',
};
const createShallowWrapper = () => {
wrapper = shallowMount(VariablesSection, {
store,
});
};
const findAllFormInputs = () => wrapper.findAll(GlFormInput);
const getInputAt = i => findAllFormInputs().at(i);
beforeEach(() => {
store = createStore();
store.state.monitoringDashboard.showEmptyState = false;
});
it('does not show the variables section', () => {
createShallowWrapper();
const allInputs = findAllFormInputs();
expect(allInputs).toHaveLength(0);
});
it('shows the variables section', () => {
createShallowWrapper();
wrapper.vm.$store.commit(
`monitoringDashboard/${types.SET_PROM_QUERY_VARIABLES}`,
sampleVariables,
);
return wrapper.vm.$nextTick(() => {
const allInputs = findAllFormInputs();
expect(allInputs).toHaveLength(Object.keys(sampleVariables).length);
});
});
describe('when changing the variable inputs', () => {
const fetchDashboardData = jest.fn();
const setVariableData = jest.fn();
beforeEach(() => {
store = new Vuex.Store({
modules: {
monitoringDashboard: {
namespaced: true,
state: {
showEmptyState: false,
promVariables: sampleVariables,
},
actions: {
fetchDashboardData,
setVariableData,
},
},
},
});
createShallowWrapper();
});
it('merges the url params and refreshes the dashboard when a form input is blurred', () => {
const firstInput = getInputAt(0);
firstInput.element.value = 'POD';
firstInput.vm.$emit('input');
firstInput.trigger('blur');
expect(setVariableData).toHaveBeenCalled();
expect(mergeUrlParams).toHaveBeenCalledWith(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);
firstInput.element.value = 'POD';
firstInput.vm.$emit('input');
firstInput.trigger('keyup.enter');
expect(setVariableData).toHaveBeenCalled();
expect(mergeUrlParams).toHaveBeenCalledWith(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);
firstInput.vm.$emit('input');
firstInput.trigger('keyup.enter');
expect(setVariableData).not.toHaveBeenCalled();
expect(mergeUrlParams).not.toHaveBeenCalled();
expect(updateHistory).not.toHaveBeenCalled();
expect(fetchDashboardData).not.toHaveBeenCalled();
});
});
});
...@@ -323,4 +323,31 @@ describe('Monitoring store Getters', () => { ...@@ -323,4 +323,31 @@ describe('Monitoring store Getters', () => {
expect(metricsSavedToDb).toEqual([`${id1}_${metric1.id}`, `${id2}_${metric2.id}`]); expect(metricsSavedToDb).toEqual([`${id1}_${metric1.id}`, `${id2}_${metric2.id}`]);
}); });
}); });
describe('getCustomVariablesArray', () => {
let state;
const sampleVariables = {
label1: 'pod',
};
beforeEach(() => {
state = {
promVariables: {},
};
});
it('transforms the promVariables object to an array in the [variable, variable_value] format', () => {
mutations[types.SET_PROM_QUERY_VARIABLES](state, sampleVariables);
const variablesArray = getters.getCustomVariablesArray(state);
expect(variablesArray).toEqual(['label1', 'pod']);
});
it('transforms the promVariables object to an empty array when no keys are present', () => {
mutations[types.SET_PROM_QUERY_VARIABLES](state, {});
const variablesArray = getters.getCustomVariablesArray(state);
expect(variablesArray).toEqual([]);
});
});
}); });
...@@ -369,13 +369,25 @@ describe('Monitoring mutations', () => { ...@@ -369,13 +369,25 @@ describe('Monitoring mutations', () => {
it('stores an empty variables array when no custom variables are given', () => { it('stores an empty variables array when no custom variables are given', () => {
mutations[types.SET_PROM_QUERY_VARIABLES](stateCopy, {}); mutations[types.SET_PROM_QUERY_VARIABLES](stateCopy, {});
expect(stateCopy.promVariables).toEqual([]); expect(stateCopy.promVariables).toEqual({});
}); });
it('stores variables in the key key_value format in the array', () => { it('stores variables in the key key_value format in the array', () => {
mutations[types.SET_PROM_QUERY_VARIABLES](stateCopy, { pod: 'POD', stage: 'main ops' }); mutations[types.SET_PROM_QUERY_VARIABLES](stateCopy, { pod: 'POD', stage: 'main ops' });
expect(stateCopy.promVariables).toEqual(['pod', 'POD', 'stage', 'main%20ops']); expect(stateCopy.promVariables).toEqual({ pod: 'POD', stage: 'main ops' });
});
});
describe('UPDATE_VARIABLE_DATA', () => {
beforeEach(() => {
mutations[types.SET_PROM_QUERY_VARIABLES](stateCopy, { pod: 'POD' });
});
it('sets a new value for an existing key', () => {
mutations[types.UPDATE_VARIABLE_DATA](stateCopy, { pod: 'new pod' });
expect(stateCopy.promVariables).toEqual({ pod: 'new pod' });
}); });
}); });
}); });
...@@ -23,6 +23,12 @@ export const setupStoreWithDashboard = $store => { ...@@ -23,6 +23,12 @@ export const setupStoreWithDashboard = $store => {
); );
}; };
export const setupStoreWithVariable = $store => {
$store.commit(`monitoringDashboard/${types.SET_PROM_QUERY_VARIABLES}`, {
label1: 'pod',
});
};
export const setupStoreWithData = $store => { export const setupStoreWithData = $store => {
setupStoreWithDashboard($store); setupStoreWithDashboard($store);
......
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