Commit 2c4dbd41 authored by Miguel Rincon's avatar Miguel Rincon

Fetch variable options from the Prometheus API

Add a new action to load dynamic options from the Prometheus endpoint
provided by the backend.

New options are loaded and replaced in the variables, allowing the user
to select them.
parent d9a1e681
...@@ -22,13 +22,13 @@ export default { ...@@ -22,13 +22,13 @@ export default {
default: '', default: '',
}, },
options: { options: {
type: Array, type: Object,
required: true, required: true,
}, },
}, },
computed: { computed: {
defaultText() { text() {
const selectedOpt = this.options.find(opt => opt.value === this.value); const selectedOpt = this.options.values?.find(opt => opt.value === this.value);
return selectedOpt?.text || this.value; return selectedOpt?.text || this.value;
}, },
}, },
...@@ -41,10 +41,13 @@ export default { ...@@ -41,10 +41,13 @@ export default {
</script> </script>
<template> <template>
<gl-form-group :label="label"> <gl-form-group :label="label">
<gl-dropdown toggle-class="dropdown-menu-toggle" :text="defaultText"> <gl-dropdown toggle-class="dropdown-menu-toggle" :text="text || s__('Metrics|Select a value')">
<gl-dropdown-item v-for="(opt, key) in options" :key="key" @click="onUpdate(opt.value)">{{ <gl-dropdown-item
opt.text v-for="val in options.values"
}}</gl-dropdown-item> :key="val.value"
@click="onUpdate(val.value)"
>{{ val.text }}</gl-dropdown-item
>
</gl-dropdown> </gl-dropdown>
</gl-form-group> </gl-form-group>
</template> </template>
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import CustomVariable from './variables/custom_variable.vue'; import DropdownField from './variables/dropdown_field.vue';
import TextVariable from './variables/text_variable.vue'; import TextField from './variables/text_field.vue';
import { setCustomVariablesFromUrl } from '../utils'; import { setCustomVariablesFromUrl } from '../utils';
import { VARIABLE_TYPES } from '../constants';
export default { export default {
components: { components: {
CustomVariable, DropdownField,
TextVariable, TextField,
}, },
computed: { computed: {
...mapState('monitoringDashboard', ['variables']), ...mapState('monitoringDashboard', ['variables']),
...@@ -27,12 +28,11 @@ export default { ...@@ -27,12 +28,11 @@ export default {
setCustomVariablesFromUrl(this.variables); setCustomVariablesFromUrl(this.variables);
} }
}, },
variableComponent(type) { variableField(type) {
const types = { if (type === VARIABLE_TYPES.custom || type === VARIABLE_TYPES.metric_label_values) {
text: TextVariable, return DropdownField;
custom: CustomVariable, }
}; return TextField;
return types[type] || TextVariable;
}, },
}, },
}; };
...@@ -41,7 +41,7 @@ export default { ...@@ -41,7 +41,7 @@ export default {
<div ref="variablesSection" class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section"> <div ref="variablesSection" class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section">
<div v-for="(variable, key) in variables" :key="key" class="mb-1 pr-2 d-flex d-sm-block"> <div v-for="(variable, key) in variables" :key="key" class="mb-1 pr-2 d-flex d-sm-block">
<component <component
:is="variableComponent(variable.type)" :is="variableField(variable.type)"
class="mb-0 flex-grow-1" class="mb-0 flex-grow-1"
:label="variable.label" :label="variable.label"
:value="variable.value" :value="variable.value"
......
...@@ -230,6 +230,7 @@ export const OPERATORS = { ...@@ -230,6 +230,7 @@ export const OPERATORS = {
export const VARIABLE_TYPES = { export const VARIABLE_TYPES = {
custom: 'custom', custom: 'custom',
text: 'text', text: 'text',
metric_label_values: 'metric_label_values',
}; };
/** /**
......
...@@ -21,6 +21,7 @@ import { ...@@ -21,6 +21,7 @@ import {
PROMETHEUS_TIMEOUT, PROMETHEUS_TIMEOUT,
ENVIRONMENT_AVAILABLE_STATE, ENVIRONMENT_AVAILABLE_STATE,
DEFAULT_DASHBOARD_PATH, DEFAULT_DASHBOARD_PATH,
VARIABLE_TYPES,
} from '../constants'; } from '../constants';
function prometheusMetricQueryParams(timeRange) { function prometheusMetricQueryParams(timeRange) {
...@@ -191,8 +192,12 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => { ...@@ -191,8 +192,12 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => {
return Promise.reject(); return Promise.reject();
} }
// Time range params must be pre-calculated once for all metrics and options
// A subsequent call, may calculate a different time range
const defaultQueryParams = prometheusMetricQueryParams(state.timeRange); const defaultQueryParams = prometheusMetricQueryParams(state.timeRange);
dispatch('fetchVariableMetricLabelValues', { defaultQueryParams });
const promises = []; const promises = [];
state.dashboard.panelGroups.forEach(group => { state.dashboard.panelGroups.forEach(group => {
group.panels.forEach(panel => { group.panels.forEach(panel => {
...@@ -466,10 +471,41 @@ export const duplicateSystemDashboard = ({ state }, payload) => { ...@@ -466,10 +471,41 @@ export const duplicateSystemDashboard = ({ state }, payload) => {
// Variables manipulation // Variables manipulation
export const updateVariablesAndFetchData = ({ commit, dispatch }, updatedVariable) => { export const updateVariablesAndFetchData = ({ commit, dispatch }, updatedVariable) => {
commit(types.UPDATE_VARIABLES, updatedVariable); commit(types.UPDATE_VARIABLE_VALUE, updatedVariable);
return dispatch('fetchDashboardData'); return dispatch('fetchDashboardData');
}; };
export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQueryParams }) => {
const { start_time, end_time } = defaultQueryParams;
const optionsRequests = [];
Object.entries(state.variables).forEach(([key, variable]) => {
if (variable.type === VARIABLE_TYPES.metric_label_values) {
const { prometheusEndpointPath, label } = variable.options;
const optionsRequest = backOffRequest(() =>
axios.get(prometheusEndpointPath, {
params: { start_time, end_time },
}),
)
.then(({ data }) => data.data)
.then(data => {
commit(types.UPDATE_VARIABLE_METRIC_LABEL_VALUES, { variable, label, data });
})
.catch(() => {
createFlash(
sprintf(s__('Metrics|There was an error getting options for variable "%{name}".'), {
name: key,
}),
);
});
optionsRequests.push(optionsRequest);
}
});
return Promise.all(optionsRequests);
};
// 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,7 +3,8 @@ export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD'; ...@@ -3,7 +3,8 @@ export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD';
export const RECEIVE_METRICS_DASHBOARD_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS'; export const RECEIVE_METRICS_DASHBOARD_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS';
export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE'; export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE';
export const SET_VARIABLES = 'SET_VARIABLES'; export const SET_VARIABLES = 'SET_VARIABLES';
export const UPDATE_VARIABLES = 'UPDATE_VARIABLES'; export const UPDATE_VARIABLE_VALUE = 'UPDATE_VARIABLE_VALUE';
export const UPDATE_VARIABLE_METRIC_LABEL_VALUES = 'UPDATE_VARIABLE_METRIC_LABEL_VALUES';
export const REQUEST_DASHBOARD_STARRING = 'REQUEST_DASHBOARD_STARRING'; export const REQUEST_DASHBOARD_STARRING = 'REQUEST_DASHBOARD_STARRING';
export const RECEIVE_DASHBOARD_STARRING_SUCCESS = 'RECEIVE_DASHBOARD_STARRING_SUCCESS'; export const RECEIVE_DASHBOARD_STARRING_SUCCESS = 'RECEIVE_DASHBOARD_STARRING_SUCCESS';
......
...@@ -2,9 +2,10 @@ import Vue from 'vue'; ...@@ -2,9 +2,10 @@ import Vue from 'vue';
import { pick } from 'lodash'; import { pick } from 'lodash';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { mapToDashboardViewModel, normalizeQueryResponseData } from './utils'; import { mapToDashboardViewModel, normalizeQueryResponseData } from './utils';
import httpStatusCodes from '~/lib/utils/http_status';
import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils'; import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
import { endpointKeys, initialStateKeys, metricStates } from '../constants'; import { endpointKeys, initialStateKeys, metricStates } from '../constants';
import httpStatusCodes from '~/lib/utils/http_status'; import { optionsFromSeriesData } from './variable_mapping';
/** /**
* Locate and return a metric in the dashboard by its id * Locate and return a metric in the dashboard by its id
...@@ -205,10 +206,16 @@ export default { ...@@ -205,10 +206,16 @@ export default {
[types.SET_VARIABLES](state, variables) { [types.SET_VARIABLES](state, variables) {
state.variables = variables; state.variables = variables;
}, },
[types.UPDATE_VARIABLES](state, updatedVariable) { [types.UPDATE_VARIABLE_VALUE](state, { key, value }) {
Object.assign(state.variables[updatedVariable.key], { Object.assign(state.variables[key], {
...state.variables[updatedVariable.key], ...state.variables[key],
value: updatedVariable.value, value,
}); });
}, },
[types.UPDATE_VARIABLE_METRIC_LABEL_VALUES](state, { variable, label, data = [] }) {
const values = optionsFromSeriesData({ label, data });
// Add new options with assign to ensure Vue reactivity
Object.assign(variable.options, { values });
},
}; };
...@@ -46,7 +46,7 @@ const textAdvancedVariableParser = advTextVar => ({ ...@@ -46,7 +46,7 @@ const textAdvancedVariableParser = advTextVar => ({
* @param {Object} custom variable option * @param {Object} custom variable option
* @returns {Object} normalized custom variable options * @returns {Object} normalized custom variable options
*/ */
const normalizeCustomVariableOptions = ({ default: defaultOpt = false, text, value }) => ({ const normalizeVariableValues = ({ default: defaultOpt = false, text, value }) => ({
default: defaultOpt, default: defaultOpt,
text: text || value, text: text || value,
value, value,
...@@ -59,17 +59,19 @@ const normalizeCustomVariableOptions = ({ default: defaultOpt = false, text, val ...@@ -59,17 +59,19 @@ const normalizeCustomVariableOptions = ({ default: defaultOpt = false, text, val
* The default value is the option with default set to true or the first option * The default value is the option with default set to true or the first option
* if none of the options have default prop true. * if none of the options have default prop true.
* *
* @param {Object} advVariable advance custom variable * @param {Object} advVariable advanced custom variable
* @returns {Object} * @returns {Object}
*/ */
const customAdvancedVariableParser = advVariable => { const customAdvancedVariableParser = advVariable => {
const options = (advVariable?.options?.values ?? []).map(normalizeCustomVariableOptions); const values = (advVariable?.options?.values ?? []).map(normalizeVariableValues);
const defaultOpt = options.find(opt => opt.default === true) || options[0]; const defaultValue = values.find(opt => opt.default === true) || values[0];
return { return {
type: VARIABLE_TYPES.custom, type: VARIABLE_TYPES.custom,
label: advVariable.label, label: advVariable.label,
value: defaultOpt?.value, value: defaultValue?.value,
options, options: {
values,
},
}; };
}; };
...@@ -80,7 +82,7 @@ const customAdvancedVariableParser = advVariable => { ...@@ -80,7 +82,7 @@ const customAdvancedVariableParser = advVariable => {
* @param {String} opt option from simple custom variable * @param {String} opt option from simple custom variable
* @returns {Object} * @returns {Object}
*/ */
const parseSimpleCustomOptions = opt => ({ text: opt, value: opt }); export const parseSimpleCustomValues = opt => ({ text: opt, value: opt });
/** /**
* Custom simple variables are rendered as dropdown elements in the dashboard * Custom simple variables are rendered as dropdown elements in the dashboard
...@@ -95,12 +97,28 @@ const parseSimpleCustomOptions = opt => ({ text: opt, value: opt }); ...@@ -95,12 +97,28 @@ const parseSimpleCustomOptions = opt => ({ text: opt, value: opt });
* @returns {Object} * @returns {Object}
*/ */
const customSimpleVariableParser = simpleVar => { const customSimpleVariableParser = simpleVar => {
const options = (simpleVar || []).map(parseSimpleCustomOptions); const values = (simpleVar || []).map(parseSimpleCustomValues);
return { return {
type: VARIABLE_TYPES.custom, type: VARIABLE_TYPES.custom,
value: options[0].value, value: values[0].value,
label: null, label: null,
options: options.map(normalizeCustomVariableOptions), options: {
values: values.map(normalizeVariableValues),
},
};
};
const metricLabelValuesVariableParser = variable => {
const { label, options = {} } = variable;
return {
type: VARIABLE_TYPES.metric_label_values,
value: null,
label,
options: {
prometheusEndpointPath: options.prometheus_endpoint_path || '',
label: options.label || null,
values: [], // values are initially empty
},
}; };
}; };
...@@ -123,14 +141,16 @@ const isSimpleCustomVariable = customVar => Array.isArray(customVar); ...@@ -123,14 +141,16 @@ const isSimpleCustomVariable = customVar => Array.isArray(customVar);
* @return {Function} parser method * @return {Function} parser method
*/ */
const getVariableParser = variable => { const getVariableParser = variable => {
if (isSimpleCustomVariable(variable)) { if (isString(variable)) {
return textSimpleVariableParser;
} else if (isSimpleCustomVariable(variable)) {
return customSimpleVariableParser; return customSimpleVariableParser;
} else if (variable.type === VARIABLE_TYPES.custom) {
return customAdvancedVariableParser;
} else if (variable.type === VARIABLE_TYPES.text) { } else if (variable.type === VARIABLE_TYPES.text) {
return textAdvancedVariableParser; return textAdvancedVariableParser;
} else if (isString(variable)) { } else if (variable.type === VARIABLE_TYPES.custom) {
return textSimpleVariableParser; return customAdvancedVariableParser;
} else if (variable.type === VARIABLE_TYPES.metric_label_values) {
return metricLabelValuesVariableParser;
} }
return () => null; return () => null;
}; };
...@@ -200,4 +220,67 @@ export const mergeURLVariables = (varsFromYML = {}) => { ...@@ -200,4 +220,67 @@ export const mergeURLVariables = (varsFromYML = {}) => {
return variables; return variables;
}; };
/**
* Converts series data to options that can be added to a
* variable. Series data is returned from the Prometheus API
* `/api/v1/series`.
*
* Finds a `label` in the series data, so it can be used as
* a filter.
*
* For example, for the arguments:
*
* {
* "label": "job"
* "data" : [
* {
* "__name__" : "up",
* "job" : "prometheus",
* "instance" : "localhost:9090"
* },
* {
* "__name__" : "up",
* "job" : "node",
* "instance" : "localhost:9091"
* },
* {
* "__name__" : "process_start_time_seconds",
* "job" : "prometheus",
* "instance" : "localhost:9090"
* }
* ]
* }
*
* It returns all the different "job" values:
*
* [
* {
* "label": "node",
* "value": "node"
* },
* {
* "label": "prometheus",
* "value": "prometheus"
* }
* ]
*
* @param {options} options object
* @param {options.seriesLabel} name of the searched series label
* @param {options.data} series data from the series API
* @return {array} Options objects with the shape `{ label, value }`
*
* @see https://prometheus.io/docs/prometheus/latest/querying/api/#finding-series-by-label-matchers
*/
export const optionsFromSeriesData = ({ label, data = [] }) => {
const optionsSet = data.reduce((set, seriesObject) => {
// Use `new Set` to deduplicate options
if (seriesObject[label]) {
set.add(seriesObject[label]);
}
return set;
}, new Set());
return [...optionsSet].map(parseSimpleCustomValues);
};
export default {}; export default {};
---
title: Fetch metrics dashboard templating variable options using a Prometheus query
merge_request: 34607
author:
type: added
...@@ -14308,6 +14308,9 @@ msgstr "" ...@@ -14308,6 +14308,9 @@ msgstr ""
msgid "Metrics|Refresh dashboard" msgid "Metrics|Refresh dashboard"
msgstr "" msgstr ""
msgid "Metrics|Select a value"
msgstr ""
msgid "Metrics|Star dashboard" msgid "Metrics|Star dashboard"
msgstr "" msgstr ""
...@@ -14335,6 +14338,9 @@ msgstr "" ...@@ -14335,6 +14338,9 @@ msgstr ""
msgid "Metrics|There was an error getting environments information." msgid "Metrics|There was an error getting environments information."
msgstr "" msgstr ""
msgid "Metrics|There was an error getting options for variable \"%{name}\"."
msgstr ""
msgid "Metrics|There was an error trying to validate your query" msgid "Metrics|There was an error trying to validate your query"
msgstr "" msgstr ""
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import CustomVariable from '~/monitoring/components/variables/custom_variable.vue'; import DropdownField from '~/monitoring/components/variables/dropdown_field.vue';
describe('Custom variable component', () => { describe('Custom variable component', () => {
let wrapper; let wrapper;
const propsData = {
const defaultProps = {
name: 'env', name: 'env',
label: 'Select environment', label: 'Select environment',
value: 'Production', value: 'Production',
options: [{ text: 'Production', value: 'prod' }, { text: 'Canary', value: 'canary' }], options: {
values: [{ text: 'Production', value: 'prod' }, { text: 'Canary', value: 'canary' }],
},
}; };
const createShallowWrapper = () => {
wrapper = shallowMount(CustomVariable, { const createShallowWrapper = props => {
propsData, wrapper = shallowMount(DropdownField, {
propsData: {
...defaultProps,
...props,
},
}); });
}; };
...@@ -22,19 +29,25 @@ describe('Custom variable component', () => { ...@@ -22,19 +29,25 @@ describe('Custom variable component', () => {
it('renders dropdown element when all necessary props are passed', () => { it('renders dropdown element when all necessary props are passed', () => {
createShallowWrapper(); createShallowWrapper();
expect(findDropdown()).toExist(); expect(findDropdown().exists()).toBe(true);
}); });
it('renders dropdown element with a text', () => { it('renders dropdown element with a text', () => {
createShallowWrapper(); createShallowWrapper();
expect(findDropdown().attributes('text')).toBe(propsData.value); expect(findDropdown().attributes('text')).toBe(defaultProps.value);
}); });
it('renders all the dropdown items', () => { it('renders all the dropdown items', () => {
createShallowWrapper(); createShallowWrapper();
expect(findDropdownItems()).toHaveLength(propsData.options.length); expect(findDropdownItems()).toHaveLength(defaultProps.options.values.length);
});
it('renders dropdown when values are missing', () => {
createShallowWrapper({ options: {} });
expect(findDropdown().exists()).toBe(true);
}); });
it('changing dropdown items triggers update', () => { it('changing dropdown items triggers update', () => {
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlFormInput } from '@gitlab/ui'; import { GlFormInput } from '@gitlab/ui';
import TextVariable from '~/monitoring/components/variables/text_variable.vue'; import TextField from '~/monitoring/components/variables/text_field.vue';
describe('Text variable component', () => { describe('Text variable component', () => {
let wrapper; let wrapper;
...@@ -10,7 +10,7 @@ describe('Text variable component', () => { ...@@ -10,7 +10,7 @@ describe('Text variable component', () => {
value: 'test-pod', value: 'test-pod',
}; };
const createShallowWrapper = () => { const createShallowWrapper = () => {
wrapper = shallowMount(TextVariable, { wrapper = shallowMount(TextField, {
propsData, propsData,
}); });
}; };
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
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 DropdownField from '~/monitoring/components/variables/dropdown_field.vue';
import TextVariable from '~/monitoring/components/variables/text_variable.vue'; import TextField from '~/monitoring/components/variables/text_field.vue';
import { updateHistory, mergeUrlParams } from '~/lib/utils/url_utility'; import { updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
import { createStore } from '~/monitoring/stores'; import { createStore } from '~/monitoring/stores';
import { convertVariablesForURL } from '~/monitoring/utils'; import { convertVariablesForURL } from '~/monitoring/utils';
...@@ -21,6 +21,7 @@ describe('Metrics dashboard/variables section component', () => { ...@@ -21,6 +21,7 @@ describe('Metrics dashboard/variables section component', () => {
label1: mockTemplatingDataResponses.simpleText.simpleText, label1: mockTemplatingDataResponses.simpleText.simpleText,
label2: mockTemplatingDataResponses.advText.advText, label2: mockTemplatingDataResponses.advText.advText,
label3: mockTemplatingDataResponses.simpleCustom.simpleCustom, label3: mockTemplatingDataResponses.simpleCustom.simpleCustom,
label4: mockTemplatingDataResponses.metricLabelValues.simple,
}; };
const createShallowWrapper = () => { const createShallowWrapper = () => {
...@@ -29,8 +30,8 @@ describe('Metrics dashboard/variables section component', () => { ...@@ -29,8 +30,8 @@ describe('Metrics dashboard/variables section component', () => {
}); });
}; };
const findTextInput = () => wrapper.findAll(TextVariable); const findTextInputs = () => wrapper.findAll(TextField);
const findCustomInput = () => wrapper.findAll(CustomVariable); const findCustomInputs = () => wrapper.findAll(DropdownField);
beforeEach(() => { beforeEach(() => {
store = createStore(); store = createStore();
...@@ -40,20 +41,30 @@ describe('Metrics dashboard/variables section component', () => { ...@@ -40,20 +41,30 @@ describe('Metrics dashboard/variables section component', () => {
it('does not show the variables section', () => { it('does not show the variables section', () => {
createShallowWrapper(); createShallowWrapper();
const allInputs = findTextInput().length + findCustomInput().length; const allInputs = findTextInputs().length + findCustomInputs().length;
expect(allInputs).toBe(0); expect(allInputs).toBe(0);
}); });
it('shows the variables section', () => { describe('when variables are set', () => {
createShallowWrapper(); beforeEach(() => {
store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, sampleVariables); createShallowWrapper();
store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, sampleVariables);
return wrapper.vm.$nextTick;
});
return wrapper.vm.$nextTick(() => { it('shows the variables section', () => {
const allInputs = findTextInput().length + findCustomInput().length; const allInputs = findTextInputs().length + findCustomInputs().length;
expect(allInputs).toBe(Object.keys(sampleVariables).length); expect(allInputs).toBe(Object.keys(sampleVariables).length);
}); });
it('shows the right custom variable inputs', () => {
const customInputs = findCustomInputs();
expect(customInputs.at(0).props('name')).toBe('label3');
expect(customInputs.at(1).props('name')).toBe('label4');
});
}); });
describe('when changing the variable inputs', () => { describe('when changing the variable inputs', () => {
...@@ -79,7 +90,7 @@ describe('Metrics dashboard/variables section component', () => { ...@@ -79,7 +90,7 @@ describe('Metrics dashboard/variables section component', () => {
}); });
it('merges the url params and refreshes the dashboard when a text-based variables inputs are updated', () => { it('merges the url params and refreshes the dashboard when a text-based variables inputs are updated', () => {
const firstInput = findTextInput().at(0); const firstInput = findTextInputs().at(0);
firstInput.vm.$emit('onUpdate', 'label1', 'test'); firstInput.vm.$emit('onUpdate', 'label1', 'test');
...@@ -94,7 +105,7 @@ describe('Metrics dashboard/variables section component', () => { ...@@ -94,7 +105,7 @@ describe('Metrics dashboard/variables section component', () => {
}); });
it('merges the url params and refreshes the dashboard when a custom-based variables inputs are updated', () => { it('merges the url params and refreshes the dashboard when a custom-based variables inputs are updated', () => {
const firstInput = findCustomInput().at(0); const firstInput = findCustomInputs().at(0);
firstInput.vm.$emit('onUpdate', 'label1', 'test'); firstInput.vm.$emit('onUpdate', 'label1', 'test');
...@@ -109,7 +120,7 @@ describe('Metrics dashboard/variables section component', () => { ...@@ -109,7 +120,7 @@ describe('Metrics dashboard/variables section component', () => {
}); });
it('does not merge the url params and refreshes the dashboard if the value entered is not different that is what currently stored', () => { it('does not merge the url params and refreshes the dashboard if the value entered is not different that is what currently stored', () => {
const firstInput = findTextInput().at(0); const firstInput = findTextInputs().at(0);
firstInput.vm.$emit('onUpdate', 'label1', 'Simple text'); firstInput.vm.$emit('onUpdate', 'label1', 'Simple text');
......
...@@ -717,6 +717,17 @@ const templatingVariableTypes = { ...@@ -717,6 +717,17 @@ const templatingVariableTypes = {
}, },
}, },
}, },
metricLabelValues: {
simple: {
label: 'Metric Label Values',
type: 'metric_label_values',
options: {
prometheus_endpoint_path: '/series',
series_selector: 'backend:haproxy_backend_availability:ratio{env="{{env}}"}',
label: 'backend',
},
},
},
}; };
const generateMockTemplatingData = data => { const generateMockTemplatingData = data => {
...@@ -754,23 +765,25 @@ const responseForSimpleCustomVariable = { ...@@ -754,23 +765,25 @@ const responseForSimpleCustomVariable = {
simpleCustom: { simpleCustom: {
label: 'simpleCustom', label: 'simpleCustom',
value: 'value1', value: 'value1',
options: [ options: {
{ values: [
default: false, {
text: 'value1', default: false,
value: 'value1', text: 'value1',
}, value: 'value1',
{ },
default: false, {
text: 'value2', default: false,
value: 'value2', text: 'value2',
}, value: 'value2',
{ },
default: false, {
text: 'value3', default: false,
value: 'value3', text: 'value3',
}, value: 'value3',
], },
],
},
type: 'custom', type: 'custom',
}, },
}; };
...@@ -778,7 +791,9 @@ const responseForSimpleCustomVariable = { ...@@ -778,7 +791,9 @@ const responseForSimpleCustomVariable = {
const responseForAdvancedCustomVariableWithoutOptions = { const responseForAdvancedCustomVariableWithoutOptions = {
advCustomWithoutOpts: { advCustomWithoutOpts: {
label: 'advCustomWithoutOpts', label: 'advCustomWithoutOpts',
options: [], options: {
values: [],
},
type: 'custom', type: 'custom',
}, },
}; };
...@@ -787,18 +802,20 @@ const responseForAdvancedCustomVariableWithoutLabel = { ...@@ -787,18 +802,20 @@ const responseForAdvancedCustomVariableWithoutLabel = {
advCustomWithoutLabel: { advCustomWithoutLabel: {
label: 'advCustomWithoutLabel', label: 'advCustomWithoutLabel',
value: 'value2', value: 'value2',
options: [ options: {
{ values: [
default: false, {
text: 'Var 1 Option 1', default: false,
value: 'value1', text: 'Var 1 Option 1',
}, value: 'value1',
{ },
default: true, {
text: 'Var 1 Option 2', default: true,
value: 'value2', text: 'Var 1 Option 2',
}, value: 'value2',
], },
],
},
type: 'custom', type: 'custom',
}, },
}; };
...@@ -807,39 +824,56 @@ const responseForAdvancedCustomVariableWithoutOptText = { ...@@ -807,39 +824,56 @@ const responseForAdvancedCustomVariableWithoutOptText = {
advCustomWithoutOptText: { advCustomWithoutOptText: {
label: 'Options without text', label: 'Options without text',
value: 'value2', value: 'value2',
options: [ options: {
{ values: [
default: false, {
text: 'value1', default: false,
value: 'value1', text: 'value1',
}, value: 'value1',
{ },
default: true, {
text: 'value2', default: true,
value: 'value2', text: 'value2',
}, value: 'value2',
], },
],
},
type: 'custom', type: 'custom',
}, },
}; };
const responseForMetricLabelValues = {
simple: {
label: 'Metric Label Values',
type: 'metric_label_values',
value: null,
options: {
prometheusEndpointPath: '/series',
label: 'backend',
values: [],
},
},
};
const responseForAdvancedCustomVariable = { const responseForAdvancedCustomVariable = {
...responseForSimpleCustomVariable, ...responseForSimpleCustomVariable,
advCustomNormal: { advCustomNormal: {
label: 'Advanced Var', label: 'Advanced Var',
value: 'value2', value: 'value2',
options: [ options: {
{ values: [
default: false, {
text: 'Var 1 Option 1', default: false,
value: 'value1', text: 'Var 1 Option 1',
}, value: 'value1',
{ },
default: true, {
text: 'Var 1 Option 2', default: true,
value: 'value2', text: 'Var 1 Option 2',
}, value: 'value2',
], },
],
},
type: 'custom', type: 'custom',
}, },
}; };
...@@ -873,6 +907,9 @@ export const mockTemplatingData = { ...@@ -873,6 +907,9 @@ export const mockTemplatingData = {
simpleCustom: templatingVariableTypes.custom.simple, simpleCustom: templatingVariableTypes.custom.simple,
advCustomNormal: templatingVariableTypes.custom.advanced.normal, advCustomNormal: templatingVariableTypes.custom.advanced.normal,
}), }),
metricLabelValues: generateMockTemplatingData({
simple: templatingVariableTypes.metricLabelValues.simple,
}),
allVariableTypes: generateMockTemplatingData({ allVariableTypes: generateMockTemplatingData({
simpleText: templatingVariableTypes.text.simple, simpleText: templatingVariableTypes.text.simple,
advText: templatingVariableTypes.text.advanced, advText: templatingVariableTypes.text.advanced,
...@@ -893,4 +930,5 @@ export const mockTemplatingDataResponses = { ...@@ -893,4 +930,5 @@ export const mockTemplatingDataResponses = {
advCustomWithoutOptText: responseForAdvancedCustomVariableWithoutOptText, advCustomWithoutOptText: responseForAdvancedCustomVariableWithoutOptText,
simpleAndAdv: responseForAdvancedCustomVariable, simpleAndAdv: responseForAdvancedCustomVariable,
allVariableTypes: responsesForAllVariableTypes, allVariableTypes: responsesForAllVariableTypes,
metricLabelValues: responseForMetricLabelValues,
}; };
...@@ -29,6 +29,7 @@ import { ...@@ -29,6 +29,7 @@ import {
toggleStarredValue, toggleStarredValue,
duplicateSystemDashboard, duplicateSystemDashboard,
updateVariablesAndFetchData, updateVariablesAndFetchData,
fetchVariableMetricLabelValues,
} from '~/monitoring/stores/actions'; } from '~/monitoring/stores/actions';
import { import {
gqClient, gqClient,
...@@ -384,14 +385,22 @@ describe('Monitoring store actions', () => { ...@@ -384,14 +385,22 @@ describe('Monitoring store actions', () => {
value: 0, value: 0,
}, },
); );
expect(dispatch).toHaveBeenCalledTimes(1); expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData'); expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData');
expect(dispatch).toHaveBeenCalledWith('fetchVariableMetricLabelValues', {
defaultQueryParams: {
start_time: expect.any(String),
end_time: expect.any(String),
step: expect.any(Number),
},
});
expect(createFlash).not.toHaveBeenCalled(); expect(createFlash).not.toHaveBeenCalled();
done(); done();
}) })
.catch(done.fail); .catch(done.fail);
}); });
it('dispatches fetchPrometheusMetric for each panel query', done => { it('dispatches fetchPrometheusMetric for each panel query', done => {
state.dashboard.panelGroups = convertObjectPropsToCamelCase( state.dashboard.panelGroups = convertObjectPropsToCamelCase(
metricsDashboardResponse.dashboard.panel_groups, metricsDashboardResponse.dashboard.panel_groups,
...@@ -434,21 +443,27 @@ describe('Monitoring store actions', () => { ...@@ -434,21 +443,27 @@ describe('Monitoring store actions', () => {
const metric = state.dashboard.panelGroups[0].panels[0].metrics[0]; const metric = state.dashboard.panelGroups[0].panels[0].metrics[0];
dispatch.mockResolvedValueOnce(); // fetchDeploymentsData dispatch.mockResolvedValueOnce(); // fetchDeploymentsData
dispatch.mockResolvedValueOnce(); // fetchVariableMetricLabelValues
// Mock having one out of four metrics failing // Mock having one out of four metrics failing
dispatch.mockRejectedValueOnce(new Error('Error fetching this metric')); dispatch.mockRejectedValueOnce(new Error('Error fetching this metric'));
dispatch.mockResolvedValue(); dispatch.mockResolvedValue();
fetchDashboardData({ state, commit, dispatch }) fetchDashboardData({ state, commit, dispatch })
.then(() => { .then(() => {
expect(dispatch).toHaveBeenCalledTimes(metricsDashboardPanelCount + 1); // plus 1 for deployments const defaultQueryParams = {
start_time: expect.any(String),
end_time: expect.any(String),
step: expect.any(Number),
};
expect(dispatch).toHaveBeenCalledTimes(metricsDashboardPanelCount + 2); // plus 1 for deployments
expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData'); expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData');
expect(dispatch).toHaveBeenCalledWith('fetchVariableMetricLabelValues', {
defaultQueryParams,
});
expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', { expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
metric, metric,
defaultQueryParams: { defaultQueryParams,
start_time: expect.any(String),
end_time: expect.any(String),
step: expect.any(Number),
},
}); });
expect(createFlash).toHaveBeenCalledTimes(1); expect(createFlash).toHaveBeenCalledTimes(1);
...@@ -1116,14 +1131,14 @@ describe('Monitoring store actions', () => { ...@@ -1116,14 +1131,14 @@ describe('Monitoring store actions', () => {
// Variables manipulation // Variables manipulation
describe('updateVariablesAndFetchData', () => { describe('updateVariablesAndFetchData', () => {
it('should commit UPDATE_VARIABLES mutation and fetch data', done => { it('should commit UPDATE_VARIABLE_VALUE mutation and fetch data', done => {
testAction( testAction(
updateVariablesAndFetchData, updateVariablesAndFetchData,
{ pod: 'POD' }, { pod: 'POD' },
state, state,
[ [
{ {
type: types.UPDATE_VARIABLES, type: types.UPDATE_VARIABLE_VALUE,
payload: { pod: 'POD' }, payload: { pod: 'POD' },
}, },
], ],
...@@ -1136,4 +1151,72 @@ describe('Monitoring store actions', () => { ...@@ -1136,4 +1151,72 @@ describe('Monitoring store actions', () => {
); );
}); });
}); });
describe('fetchVariableMetricLabelValues', () => {
const variable = {
type: 'metric_label_values',
options: {
prometheusEndpointPath: '/series',
label: 'job',
},
};
const defaultQueryParams = {
start_time: '2019-08-06T12:40:02.184Z',
end_time: '2019-08-06T20:40:02.184Z',
};
beforeEach(() => {
state = {
...state,
timeRange: defaultTimeRange,
variables: {
label1: variable,
},
};
});
it('should commit UPDATE_VARIABLE_METRIC_LABEL_VALUES mutation and fetch data', () => {
const data = [
{
__name__: 'up',
job: 'prometheus',
},
{
__name__: 'up',
job: 'POD',
},
];
mock.onGet('/series').reply(200, {
status: 'success',
data,
});
return testAction(
fetchVariableMetricLabelValues,
{ defaultQueryParams },
state,
[
{
type: types.UPDATE_VARIABLE_METRIC_LABEL_VALUES,
payload: { variable, label: 'job', data },
},
],
[],
);
});
it('should notify the user that dynamic options were not loaded', () => {
mock.onGet('/series').reply(500);
return testAction(fetchVariableMetricLabelValues, { defaultQueryParams }, state, [], []).then(
() => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith(
expect.stringContaining('error getting options for variable "label1"'),
);
},
);
});
});
}); });
...@@ -441,16 +441,57 @@ describe('Monitoring mutations', () => { ...@@ -441,16 +441,57 @@ describe('Monitoring mutations', () => {
}); });
}); });
describe('UPDATE_VARIABLES', () => { describe('UPDATE_VARIABLE_VALUE', () => {
afterEach(() => { afterEach(() => {
mutations[types.SET_VARIABLES](stateCopy, {}); mutations[types.SET_VARIABLES](stateCopy, {});
}); });
it('updates only the value of the variable in variables', () => { it('updates only the value of the variable in variables', () => {
mutations[types.SET_VARIABLES](stateCopy, { environment: { value: 'prod', type: 'text' } }); mutations[types.SET_VARIABLES](stateCopy, { environment: { value: 'prod', type: 'text' } });
mutations[types.UPDATE_VARIABLES](stateCopy, { key: 'environment', value: 'new prod' }); mutations[types.UPDATE_VARIABLE_VALUE](stateCopy, { key: 'environment', value: 'new prod' });
expect(stateCopy.variables).toEqual({ environment: { value: 'new prod', type: 'text' } }); expect(stateCopy.variables).toEqual({ environment: { value: 'new prod', type: 'text' } });
}); });
}); });
describe('UPDATE_VARIABLE_METRIC_LABEL_VALUES', () => {
it('updates options in a variable', () => {
const data = [
{
__name__: 'up',
job: 'prometheus',
env: 'prd',
},
{
__name__: 'up',
job: 'prometheus',
env: 'stg',
},
{
__name__: 'up',
job: 'node',
env: 'prod',
},
{
__name__: 'up',
job: 'node',
env: 'stg',
},
];
const variable = {
options: {},
};
mutations[types.UPDATE_VARIABLE_METRIC_LABEL_VALUES](stateCopy, {
variable,
label: 'job',
data,
});
expect(variable.options).toEqual({
values: [{ text: 'prometheus', value: 'prometheus' }, { text: 'node', value: 'node' }],
});
});
});
}); });
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