Commit ac722682 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '214539-fe-fetch-dynamic-variable-options' into 'master'

Fetch variable options from the Prometheus API to populate dropdown

See merge request gitlab-org/gitlab!34607
parents 9ffbb6ac 2c4dbd41
......@@ -22,13 +22,13 @@ export default {
default: '',
},
options: {
type: Array,
type: Object,
required: true,
},
},
computed: {
defaultText() {
const selectedOpt = this.options.find(opt => opt.value === this.value);
text() {
const selectedOpt = this.options.values?.find(opt => opt.value === this.value);
return selectedOpt?.text || this.value;
},
},
......@@ -41,10 +41,13 @@ export default {
</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 toggle-class="dropdown-menu-toggle" :text="text || s__('Metrics|Select a value')">
<gl-dropdown-item
v-for="val in options.values"
:key="val.value"
@click="onUpdate(val.value)"
>{{ val.text }}</gl-dropdown-item
>
</gl-dropdown>
</gl-form-group>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import CustomVariable from './variables/custom_variable.vue';
import TextVariable from './variables/text_variable.vue';
import DropdownField from './variables/dropdown_field.vue';
import TextField from './variables/text_field.vue';
import { setCustomVariablesFromUrl } from '../utils';
import { VARIABLE_TYPES } from '../constants';
export default {
components: {
CustomVariable,
TextVariable,
DropdownField,
TextField,
},
computed: {
...mapState('monitoringDashboard', ['variables']),
......@@ -27,12 +28,11 @@ export default {
setCustomVariablesFromUrl(this.variables);
}
},
variableComponent(type) {
const types = {
text: TextVariable,
custom: CustomVariable,
};
return types[type] || TextVariable;
variableField(type) {
if (type === VARIABLE_TYPES.custom || type === VARIABLE_TYPES.metric_label_values) {
return DropdownField;
}
return TextField;
},
},
};
......@@ -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 v-for="(variable, key) in variables" :key="key" class="mb-1 pr-2 d-flex d-sm-block">
<component
:is="variableComponent(variable.type)"
:is="variableField(variable.type)"
class="mb-0 flex-grow-1"
:label="variable.label"
:value="variable.value"
......
......@@ -230,6 +230,7 @@ export const OPERATORS = {
export const VARIABLE_TYPES = {
custom: 'custom',
text: 'text',
metric_label_values: 'metric_label_values',
};
/**
......
......@@ -21,6 +21,7 @@ import {
PROMETHEUS_TIMEOUT,
ENVIRONMENT_AVAILABLE_STATE,
DEFAULT_DASHBOARD_PATH,
VARIABLE_TYPES,
} from '../constants';
function prometheusMetricQueryParams(timeRange) {
......@@ -191,8 +192,12 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => {
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);
dispatch('fetchVariableMetricLabelValues', { defaultQueryParams });
const promises = [];
state.dashboard.panelGroups.forEach(group => {
group.panels.forEach(panel => {
......@@ -466,10 +471,41 @@ export const duplicateSystemDashboard = ({ state }, payload) => {
// Variables manipulation
export const updateVariablesAndFetchData = ({ commit, dispatch }, updatedVariable) => {
commit(types.UPDATE_VARIABLES, updatedVariable);
commit(types.UPDATE_VARIABLE_VALUE, updatedVariable);
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
export default () => {};
......@@ -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_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE';
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 RECEIVE_DASHBOARD_STARRING_SUCCESS = 'RECEIVE_DASHBOARD_STARRING_SUCCESS';
......
......@@ -2,9 +2,10 @@ import Vue from 'vue';
import { pick } from 'lodash';
import * as types from './mutation_types';
import { mapToDashboardViewModel, normalizeQueryResponseData } from './utils';
import httpStatusCodes from '~/lib/utils/http_status';
import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
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
......@@ -205,10 +206,16 @@ export default {
[types.SET_VARIABLES](state, variables) {
state.variables = variables;
},
[types.UPDATE_VARIABLES](state, updatedVariable) {
Object.assign(state.variables[updatedVariable.key], {
...state.variables[updatedVariable.key],
value: updatedVariable.value,
[types.UPDATE_VARIABLE_VALUE](state, { key, value }) {
Object.assign(state.variables[key], {
...state.variables[key],
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 => ({
* @param {Object} custom variable option
* @returns {Object} normalized custom variable options
*/
const normalizeCustomVariableOptions = ({ default: defaultOpt = false, text, value }) => ({
const normalizeVariableValues = ({ default: defaultOpt = false, text, value }) => ({
default: defaultOpt,
text: text || value,
value,
......@@ -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
* if none of the options have default prop true.
*
* @param {Object} advVariable advance custom variable
* @param {Object} advVariable advanced custom variable
* @returns {Object}
*/
const customAdvancedVariableParser = advVariable => {
const options = (advVariable?.options?.values ?? []).map(normalizeCustomVariableOptions);
const defaultOpt = options.find(opt => opt.default === true) || options[0];
const values = (advVariable?.options?.values ?? []).map(normalizeVariableValues);
const defaultValue = values.find(opt => opt.default === true) || values[0];
return {
type: VARIABLE_TYPES.custom,
label: advVariable.label,
value: defaultOpt?.value,
options,
value: defaultValue?.value,
options: {
values,
},
};
};
......@@ -80,7 +82,7 @@ const customAdvancedVariableParser = advVariable => {
* @param {String} opt option from simple custom variable
* @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
......@@ -95,12 +97,28 @@ const parseSimpleCustomOptions = opt => ({ text: opt, value: opt });
* @returns {Object}
*/
const customSimpleVariableParser = simpleVar => {
const options = (simpleVar || []).map(parseSimpleCustomOptions);
const values = (simpleVar || []).map(parseSimpleCustomValues);
return {
type: VARIABLE_TYPES.custom,
value: options[0].value,
value: values[0].value,
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);
* @return {Function} parser method
*/
const getVariableParser = variable => {
if (isSimpleCustomVariable(variable)) {
if (isString(variable)) {
return textSimpleVariableParser;
} else if (isSimpleCustomVariable(variable)) {
return customSimpleVariableParser;
} else if (variable.type === VARIABLE_TYPES.custom) {
return customAdvancedVariableParser;
} else if (variable.type === VARIABLE_TYPES.text) {
return textAdvancedVariableParser;
} else if (isString(variable)) {
return textSimpleVariableParser;
} else if (variable.type === VARIABLE_TYPES.custom) {
return customAdvancedVariableParser;
} else if (variable.type === VARIABLE_TYPES.metric_label_values) {
return metricLabelValuesVariableParser;
}
return () => null;
};
......@@ -200,4 +220,67 @@ export const mergeURLVariables = (varsFromYML = {}) => {
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 {};
---
title: Fetch metrics dashboard templating variable options using a Prometheus query
merge_request: 34607
author:
type: added
......@@ -14308,6 +14308,9 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
msgid "Metrics|Select a value"
msgstr ""
msgid "Metrics|Star dashboard"
msgstr ""
......@@ -14335,6 +14338,9 @@ msgstr ""
msgid "Metrics|There was an error getting environments information."
msgstr ""
msgid "Metrics|There was an error getting options for variable \"%{name}\"."
msgstr ""
msgid "Metrics|There was an error trying to validate your query"
msgstr ""
......
import { shallowMount } from '@vue/test-utils';
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', () => {
let wrapper;
const propsData = {
const defaultProps = {
name: 'env',
label: 'Select environment',
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, {
propsData,
const createShallowWrapper = props => {
wrapper = shallowMount(DropdownField, {
propsData: {
...defaultProps,
...props,
},
});
};
......@@ -22,19 +29,25 @@ describe('Custom variable component', () => {
it('renders dropdown element when all necessary props are passed', () => {
createShallowWrapper();
expect(findDropdown()).toExist();
expect(findDropdown().exists()).toBe(true);
});
it('renders dropdown element with a text', () => {
createShallowWrapper();
expect(findDropdown().attributes('text')).toBe(propsData.value);
expect(findDropdown().attributes('text')).toBe(defaultProps.value);
});
it('renders all the dropdown items', () => {
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', () => {
......
import { shallowMount } from '@vue/test-utils';
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', () => {
let wrapper;
......@@ -10,7 +10,7 @@ describe('Text variable component', () => {
value: 'test-pod',
};
const createShallowWrapper = () => {
wrapper = shallowMount(TextVariable, {
wrapper = shallowMount(TextField, {
propsData,
});
};
......
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
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 DropdownField from '~/monitoring/components/variables/dropdown_field.vue';
import TextField from '~/monitoring/components/variables/text_field.vue';
import { updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
import { createStore } from '~/monitoring/stores';
import { convertVariablesForURL } from '~/monitoring/utils';
......@@ -21,6 +21,7 @@ describe('Metrics dashboard/variables section component', () => {
label1: mockTemplatingDataResponses.simpleText.simpleText,
label2: mockTemplatingDataResponses.advText.advText,
label3: mockTemplatingDataResponses.simpleCustom.simpleCustom,
label4: mockTemplatingDataResponses.metricLabelValues.simple,
};
const createShallowWrapper = () => {
......@@ -29,8 +30,8 @@ describe('Metrics dashboard/variables section component', () => {
});
};
const findTextInput = () => wrapper.findAll(TextVariable);
const findCustomInput = () => wrapper.findAll(CustomVariable);
const findTextInputs = () => wrapper.findAll(TextField);
const findCustomInputs = () => wrapper.findAll(DropdownField);
beforeEach(() => {
store = createStore();
......@@ -40,20 +41,30 @@ describe('Metrics dashboard/variables section component', () => {
it('does not show the variables section', () => {
createShallowWrapper();
const allInputs = findTextInput().length + findCustomInput().length;
const allInputs = findTextInputs().length + findCustomInputs().length;
expect(allInputs).toBe(0);
});
it('shows the variables section', () => {
createShallowWrapper();
store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, sampleVariables);
describe('when variables are set', () => {
beforeEach(() => {
createShallowWrapper();
store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, sampleVariables);
return wrapper.vm.$nextTick;
});
return wrapper.vm.$nextTick(() => {
const allInputs = findTextInput().length + findCustomInput().length;
it('shows the variables section', () => {
const allInputs = findTextInputs().length + findCustomInputs().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', () => {
......@@ -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', () => {
const firstInput = findTextInput().at(0);
const firstInput = findTextInputs().at(0);
firstInput.vm.$emit('onUpdate', 'label1', 'test');
......@@ -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', () => {
const firstInput = findCustomInput().at(0);
const firstInput = findCustomInputs().at(0);
firstInput.vm.$emit('onUpdate', 'label1', 'test');
......@@ -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', () => {
const firstInput = findTextInput().at(0);
const firstInput = findTextInputs().at(0);
firstInput.vm.$emit('onUpdate', 'label1', 'Simple text');
......
......@@ -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 => {
......@@ -754,23 +765,25 @@ const responseForSimpleCustomVariable = {
simpleCustom: {
label: 'simpleCustom',
value: 'value1',
options: [
{
default: false,
text: 'value1',
value: 'value1',
},
{
default: false,
text: 'value2',
value: 'value2',
},
{
default: false,
text: 'value3',
value: 'value3',
},
],
options: {
values: [
{
default: false,
text: 'value1',
value: 'value1',
},
{
default: false,
text: 'value2',
value: 'value2',
},
{
default: false,
text: 'value3',
value: 'value3',
},
],
},
type: 'custom',
},
};
......@@ -778,7 +791,9 @@ const responseForSimpleCustomVariable = {
const responseForAdvancedCustomVariableWithoutOptions = {
advCustomWithoutOpts: {
label: 'advCustomWithoutOpts',
options: [],
options: {
values: [],
},
type: 'custom',
},
};
......@@ -787,18 +802,20 @@ const responseForAdvancedCustomVariableWithoutLabel = {
advCustomWithoutLabel: {
label: 'advCustomWithoutLabel',
value: 'value2',
options: [
{
default: false,
text: 'Var 1 Option 1',
value: 'value1',
},
{
default: true,
text: 'Var 1 Option 2',
value: 'value2',
},
],
options: {
values: [
{
default: false,
text: 'Var 1 Option 1',
value: 'value1',
},
{
default: true,
text: 'Var 1 Option 2',
value: 'value2',
},
],
},
type: 'custom',
},
};
......@@ -807,39 +824,56 @@ const responseForAdvancedCustomVariableWithoutOptText = {
advCustomWithoutOptText: {
label: 'Options without text',
value: 'value2',
options: [
{
default: false,
text: 'value1',
value: 'value1',
},
{
default: true,
text: 'value2',
value: 'value2',
},
],
options: {
values: [
{
default: false,
text: 'value1',
value: 'value1',
},
{
default: true,
text: 'value2',
value: 'value2',
},
],
},
type: 'custom',
},
};
const responseForMetricLabelValues = {
simple: {
label: 'Metric Label Values',
type: 'metric_label_values',
value: null,
options: {
prometheusEndpointPath: '/series',
label: 'backend',
values: [],
},
},
};
const responseForAdvancedCustomVariable = {
...responseForSimpleCustomVariable,
advCustomNormal: {
label: 'Advanced Var',
value: 'value2',
options: [
{
default: false,
text: 'Var 1 Option 1',
value: 'value1',
},
{
default: true,
text: 'Var 1 Option 2',
value: 'value2',
},
],
options: {
values: [
{
default: false,
text: 'Var 1 Option 1',
value: 'value1',
},
{
default: true,
text: 'Var 1 Option 2',
value: 'value2',
},
],
},
type: 'custom',
},
};
......@@ -873,6 +907,9 @@ export const mockTemplatingData = {
simpleCustom: templatingVariableTypes.custom.simple,
advCustomNormal: templatingVariableTypes.custom.advanced.normal,
}),
metricLabelValues: generateMockTemplatingData({
simple: templatingVariableTypes.metricLabelValues.simple,
}),
allVariableTypes: generateMockTemplatingData({
simpleText: templatingVariableTypes.text.simple,
advText: templatingVariableTypes.text.advanced,
......@@ -893,4 +930,5 @@ export const mockTemplatingDataResponses = {
advCustomWithoutOptText: responseForAdvancedCustomVariableWithoutOptText,
simpleAndAdv: responseForAdvancedCustomVariable,
allVariableTypes: responsesForAllVariableTypes,
metricLabelValues: responseForMetricLabelValues,
};
......@@ -29,6 +29,7 @@ import {
toggleStarredValue,
duplicateSystemDashboard,
updateVariablesAndFetchData,
fetchVariableMetricLabelValues,
} from '~/monitoring/stores/actions';
import {
gqClient,
......@@ -384,14 +385,22 @@ describe('Monitoring store actions', () => {
value: 0,
},
);
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenCalledTimes(2);
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();
done();
})
.catch(done.fail);
});
it('dispatches fetchPrometheusMetric for each panel query', done => {
state.dashboard.panelGroups = convertObjectPropsToCamelCase(
metricsDashboardResponse.dashboard.panel_groups,
......@@ -434,21 +443,27 @@ describe('Monitoring store actions', () => {
const metric = state.dashboard.panelGroups[0].panels[0].metrics[0];
dispatch.mockResolvedValueOnce(); // fetchDeploymentsData
dispatch.mockResolvedValueOnce(); // fetchVariableMetricLabelValues
// Mock having one out of four metrics failing
dispatch.mockRejectedValueOnce(new Error('Error fetching this metric'));
dispatch.mockResolvedValue();
fetchDashboardData({ state, commit, dispatch })
.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('fetchVariableMetricLabelValues', {
defaultQueryParams,
});
expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
metric,
defaultQueryParams: {
start_time: expect.any(String),
end_time: expect.any(String),
step: expect.any(Number),
},
defaultQueryParams,
});
expect(createFlash).toHaveBeenCalledTimes(1);
......@@ -1116,14 +1131,14 @@ describe('Monitoring store actions', () => {
// Variables manipulation
describe('updateVariablesAndFetchData', () => {
it('should commit UPDATE_VARIABLES mutation and fetch data', done => {
it('should commit UPDATE_VARIABLE_VALUE mutation and fetch data', done => {
testAction(
updateVariablesAndFetchData,
{ pod: 'POD' },
state,
[
{
type: types.UPDATE_VARIABLES,
type: types.UPDATE_VARIABLE_VALUE,
payload: { pod: 'POD' },
},
],
......@@ -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', () => {
});
});
describe('UPDATE_VARIABLES', () => {
describe('UPDATE_VARIABLE_VALUE', () => {
afterEach(() => {
mutations[types.SET_VARIABLES](stateCopy, {});
});
it('updates only the value of the variable in variables', () => {
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' } });
});
});
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