summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-06-24 03:08:44 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-06-24 03:08:44 +0000
commit4870899d6cec693b58acbef91636e1310160fa28 (patch)
tree305628a2a8436e77b0c3972f8053df771a89843c
parent082b24b03bbb9dca7edf1341aa7b0a51c9aeb18b (diff)
downloadgitlab-ce-4870899d6cec693b58acbef91636e1310160fa28.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue27
-rw-r--r--app/assets/javascripts/monitoring/components/variables/dropdown_field.vue (renamed from app/assets/javascripts/monitoring/components/variables/custom_variable.vue)17
-rw-r--r--app/assets/javascripts/monitoring/components/variables/text_field.vue (renamed from app/assets/javascripts/monitoring/components/variables/text_variable.vue)0
-rw-r--r--app/assets/javascripts/monitoring/components/variables_section.vue22
-rw-r--r--app/assets/javascripts/monitoring/constants.js1
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js38
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js17
-rw-r--r--app/assets/javascripts/monitoring/stores/variable_mapping.js113
-rw-r--r--app/models/repository.rb2
-rw-r--r--changelogs/unreleased/214539-fe-fetch-dynamic-variable-options.yml5
-rw-r--r--doc/development/geo/framework.md7
-rw-r--r--doc/development/telemetry/usage_ping.md10
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js6
-rw-r--r--spec/frontend/monitoring/components/variables/dropdown_field_spec.js (renamed from spec/frontend/monitoring/components/variables/custom_variable_spec.js)31
-rw-r--r--spec/frontend/monitoring/components/variables/text_field_spec.js (renamed from spec/frontend/monitoring/components/variables/text_variable_spec.js)4
-rw-r--r--spec/frontend/monitoring/components/variables_section_spec.js37
-rw-r--r--spec/frontend/monitoring/mock_data.js146
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js101
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js45
-rw-r--r--spec/frontend/monitoring/store/variable_mapping_spec.js235
-rw-r--r--spec/models/repository_spec.rb23
23 files changed, 678 insertions, 218 deletions
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index b6f53ee0d69..3fc3dcf6d01 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -2,7 +2,7 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import createFlash from '~/flash';
import {
- GlDeprecatedButton,
+ GlButton,
GlFormInput,
GlLink,
GlLoadingIcon,
@@ -33,7 +33,7 @@ const SENTRY_TIMEOUT = 10000;
export default {
components: {
- GlDeprecatedButton,
+ GlButton,
GlFormInput,
GlLink,
GlLoadingIcon,
@@ -279,22 +279,24 @@ export default {
</div>
<div class="error-details-actions">
<div class="d-inline-flex bv-d-sm-down-none">
- <gl-deprecated-button
+ <gl-button
:loading="updatingIgnoreStatus"
data-testid="update-ignore-status-btn"
@click="onIgnoreStatusUpdate"
>
{{ ignoreBtnLabel }}
- </gl-deprecated-button>
- <gl-deprecated-button
- class="btn-outline-info ml-2"
+ </gl-button>
+ <gl-button
+ class="ml-2"
+ category="secondary"
+ variant="info"
:loading="updatingResolveStatus"
data-testid="update-resolve-status-btn"
@click="onResolveStatusUpdate"
>
{{ resolveBtnLabel }}
- </gl-deprecated-button>
- <gl-deprecated-button
+ </gl-button>
+ <gl-button
v-if="error.gitlabIssuePath"
class="ml-2"
data-testid="view_issue_button"
@@ -302,7 +304,7 @@ export default {
variant="success"
>
{{ __('View issue') }}
- </gl-deprecated-button>
+ </gl-button>
<form
ref="sentryIssueForm"
:action="projectIssuesPath"
@@ -317,15 +319,16 @@ export default {
name="issue[sentry_issue_attributes][sentry_issue_identifier]"
/>
<gl-form-input :value="csrfToken" class="hidden" name="authenticity_token" />
- <gl-deprecated-button
+ <gl-button
v-if="!error.gitlabIssuePath"
- class="btn-success"
+ category="primary"
+ variant="success"
:loading="issueCreationInProgress"
data-qa-selector="create_issue_button"
@click="createIssue"
>
{{ __('Create issue') }}
- </gl-deprecated-button>
+ </gl-button>
</form>
</div>
<gl-dropdown
diff --git a/app/assets/javascripts/monitoring/components/variables/custom_variable.vue b/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue
index 0ac7c0b80df..d79b8284a65 100644
--- a/app/assets/javascripts/monitoring/components/variables/custom_variable.vue
+++ b/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue
@@ -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>
diff --git a/app/assets/javascripts/monitoring/components/variables/text_variable.vue b/app/assets/javascripts/monitoring/components/variables/text_field.vue
index ce0d19760e2..ce0d19760e2 100644
--- a/app/assets/javascripts/monitoring/components/variables/text_variable.vue
+++ b/app/assets/javascripts/monitoring/components/variables/text_field.vue
diff --git a/app/assets/javascripts/monitoring/components/variables_section.vue b/app/assets/javascripts/monitoring/components/variables_section.vue
index 3d1d111d5b3..9d3159dfb6e 100644
--- a/app/assets/javascripts/monitoring/components/variables_section.vue
+++ b/app/assets/javascripts/monitoring/components/variables_section.vue
@@ -1,13 +1,14 @@
<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"
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index 50330046c99..7cebe5cb11d 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -230,6 +230,7 @@ export const OPERATORS = {
export const VARIABLE_TYPES = {
custom: 'custom',
text: 'text',
+ metric_label_values: 'metric_label_values',
};
/**
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 6527a799759..aaa7865f30a 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -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 () => {};
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index 17563f966f5..d43065749c1 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -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';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 08e6f00b866..53c8029e46b 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -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 });
+ },
};
diff --git a/app/assets/javascripts/monitoring/stores/variable_mapping.js b/app/assets/javascripts/monitoring/stores/variable_mapping.js
index c0a8150063b..0b268402992 100644
--- a/app/assets/javascripts/monitoring/stores/variable_mapping.js
+++ b/app/assets/javascripts/monitoring/stores/variable_mapping.js
@@ -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 {};
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 838b850ddcb..aef11936ca0 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1113,7 +1113,7 @@ class Repository
def project
if repo_type.snippet?
container.project
- else
+ elsif container.is_a?(Project)
container
end
end
diff --git a/changelogs/unreleased/214539-fe-fetch-dynamic-variable-options.yml b/changelogs/unreleased/214539-fe-fetch-dynamic-variable-options.yml
new file mode 100644
index 00000000000..44c49d2c3e3
--- /dev/null
+++ b/changelogs/unreleased/214539-fe-fetch-dynamic-variable-options.yml
@@ -0,0 +1,5 @@
+---
+title: Fetch metrics dashboard templating variable options using a Prometheus query
+merge_request: 34607
+author:
+type: added
diff --git a/doc/development/geo/framework.md b/doc/development/geo/framework.md
index 40190108b7a..debd9978c62 100644
--- a/doc/development/geo/framework.md
+++ b/doc/development/geo/framework.md
@@ -511,6 +511,13 @@ Widgets should now be verified by Geo!
Individual widget synchronization and verification data should now be available
via the GraphQL API!
+1. Take care of replicating "update" events. Geo Framework does not currently support
+ replicating "update" events because all entities added to the framework, by this time,
+ are immutable. If this is the case
+ for the entity you're going to add, please follow [https://gitlab.com/gitlab-org/gitlab/-/issues/118743]
+ and [https://gitlab.com/gitlab-org/gitlab/-/issues/118745] as examples to add the new event type.
+ Please also remove this notice when you've added it.
+
#### Admin UI
To do: This should be done as part of
diff --git a/doc/development/telemetry/usage_ping.md b/doc/development/telemetry/usage_ping.md
index 1c7b9f46bf4..30fc7e192d2 100644
--- a/doc/development/telemetry/usage_ping.md
+++ b/doc/development/telemetry/usage_ping.md
@@ -326,7 +326,15 @@ When adding, changing, or updating metrics, please update the [Usage Statistics
Check if new metrics need to be added to the Versions Application. See `usage_data` [schema](https://gitlab.com/gitlab-services/version-gitlab-com/-/blob/master/db/schema.rb#L147) and usage data [parameters accepted](https://gitlab.com/gitlab-services/version-gitlab-com/-/blob/master/app/services/usage_ping.rb). Any metrics added under the `counts` key are saved in the `counts` column.
-### 6. Ask for a Telemetry Review
+### 6. Add the feature label
+
+Add the `feature` label to the Merge Request for new Usage Ping metrics. These are user-facing changes and are part of expanding the Usage Ping feature.
+
+### 7. Add a changelog file
+
+Ensure you comply with the [Changelog entries guide](../changelog.md).
+
+### 8. Ask for a Telemetry Review
On GitLab.com, we have DangerBot setup to monitor Telemetry related files and DangerBot will recommend a Telemetry review. Mention `@gitlab-org/growth/telemetry/engineers` in your MR for a review.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b837fd415fe..e791fd771d7 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index f10102ef799..6124602e038 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -3,7 +3,7 @@ import Vuex from 'vuex';
import { __ } from '~/locale';
import createFlash from '~/flash';
import {
- GlDeprecatedButton,
+ GlButton,
GlLoadingIcon,
GlLink,
GlBadge,
@@ -52,7 +52,7 @@ describe('ErrorDetails', () => {
function mountComponent() {
wrapper = shallowMount(ErrorDetails, {
- stubs: { GlDeprecatedButton, GlSprintf },
+ stubs: { GlButton, GlSprintf },
localVue,
store,
mocks,
@@ -195,7 +195,7 @@ describe('ErrorDetails', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find(Stacktrace).exists()).toBe(false);
expect(wrapper.find(GlBadge).exists()).toBe(false);
- expect(wrapper.findAll(GlDeprecatedButton).length).toBe(3);
+ expect(wrapper.findAll(GlButton)).toHaveLength(3);
});
describe('unsafe chars for culprit field', () => {
diff --git a/spec/frontend/monitoring/components/variables/custom_variable_spec.js b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
index 5a2b26219b6..623125d18f1 100644
--- a/spec/frontend/monitoring/components/variables/custom_variable_spec.js
+++ b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
@@ -1,18 +1,25 @@
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', () => {
diff --git a/spec/frontend/monitoring/components/variables/text_variable_spec.js b/spec/frontend/monitoring/components/variables/text_field_spec.js
index f01584ae8bc..68bfb8ec695 100644
--- a/spec/frontend/monitoring/components/variables/text_variable_spec.js
+++ b/spec/frontend/monitoring/components/variables/text_field_spec.js
@@ -1,6 +1,6 @@
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,
});
};
diff --git a/spec/frontend/monitoring/components/variables_section_spec.js b/spec/frontend/monitoring/components/variables_section_spec.js
index fd814e81c8f..9fb93a18e0b 100644
--- a/spec/frontend/monitoring/components/variables_section_spec.js
+++ b/spec/frontend/monitoring/components/variables_section_spec.js
@@ -1,8 +1,8 @@
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');
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index 0f443838985..6a0a3eeb5ae 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -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,
};
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index ac925222924..5af13c0987a 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -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"'),
+ );
+ },
+ );
+ });
+ });
});
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index 111ee69f4e6..fb5e6156daf 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -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' }],
+ });
+ });
+ });
});
diff --git a/spec/frontend/monitoring/store/variable_mapping_spec.js b/spec/frontend/monitoring/store/variable_mapping_spec.js
index 5164ed1b54b..390cb2d8eac 100644
--- a/spec/frontend/monitoring/store/variable_mapping_spec.js
+++ b/spec/frontend/monitoring/store/variable_mapping_spec.js
@@ -1,94 +1,185 @@
-import { parseTemplatingVariables, mergeURLVariables } from '~/monitoring/stores/variable_mapping';
+import {
+ parseTemplatingVariables,
+ mergeURLVariables,
+ optionsFromSeriesData,
+} from '~/monitoring/stores/variable_mapping';
import * as urlUtils from '~/lib/utils/url_utility';
import { mockTemplatingData, mockTemplatingDataResponses } from '../mock_data';
-describe('parseTemplatingVariables', () => {
- it.each`
- case | input | expected
- ${'Returns empty object for no dashboard input'} | ${{}} | ${{}}
- ${'Returns empty object for empty dashboard input'} | ${{ dashboard: {} }} | ${{}}
- ${'Returns empty object for empty templating prop'} | ${mockTemplatingData.emptyTemplatingProp} | ${{}}
- ${'Returns empty object for empty variables prop'} | ${mockTemplatingData.emptyVariablesProp} | ${{}}
- ${'Returns parsed object for simple text variable'} | ${mockTemplatingData.simpleText} | ${mockTemplatingDataResponses.simpleText}
- ${'Returns parsed object for advanced text variable'} | ${mockTemplatingData.advText} | ${mockTemplatingDataResponses.advText}
- ${'Returns parsed object for simple custom variable'} | ${mockTemplatingData.simpleCustom} | ${mockTemplatingDataResponses.simpleCustom}
- ${'Returns parsed object for advanced custom variable without options'} | ${mockTemplatingData.advCustomWithoutOpts} | ${mockTemplatingDataResponses.advCustomWithoutOpts}
- ${'Returns parsed object for advanced custom variable for option without text'} | ${mockTemplatingData.advCustomWithoutOptText} | ${mockTemplatingDataResponses.advCustomWithoutOptText}
- ${'Returns parsed object for advanced custom variable without type'} | ${mockTemplatingData.advCustomWithoutType} | ${{}}
- ${'Returns parsed object for advanced custom variable without label'} | ${mockTemplatingData.advCustomWithoutLabel} | ${mockTemplatingDataResponses.advCustomWithoutLabel}
- ${'Returns parsed object for simple and advanced custom variables'} | ${mockTemplatingData.simpleAndAdv} | ${mockTemplatingDataResponses.simpleAndAdv}
- ${'Returns parsed object for all variable types'} | ${mockTemplatingData.allVariableTypes} | ${mockTemplatingDataResponses.allVariableTypes}
- `('$case', ({ input, expected }) => {
- expect(parseTemplatingVariables(input?.dashboard?.templating)).toEqual(expected);
+describe('Monitoring variable mapping', () => {
+ describe('parseTemplatingVariables', () => {
+ it.each`
+ case | input | expected
+ ${'Returns empty object for no dashboard input'} | ${{}} | ${{}}
+ ${'Returns empty object for empty dashboard input'} | ${{ dashboard: {} }} | ${{}}
+ ${'Returns empty object for empty templating prop'} | ${mockTemplatingData.emptyTemplatingProp} | ${{}}
+ ${'Returns empty object for empty variables prop'} | ${mockTemplatingData.emptyVariablesProp} | ${{}}
+ ${'Returns parsed object for simple text variable'} | ${mockTemplatingData.simpleText} | ${mockTemplatingDataResponses.simpleText}
+ ${'Returns parsed object for advanced text variable'} | ${mockTemplatingData.advText} | ${mockTemplatingDataResponses.advText}
+ ${'Returns parsed object for simple custom variable'} | ${mockTemplatingData.simpleCustom} | ${mockTemplatingDataResponses.simpleCustom}
+ ${'Returns parsed object for advanced custom variable without options'} | ${mockTemplatingData.advCustomWithoutOpts} | ${mockTemplatingDataResponses.advCustomWithoutOpts}
+ ${'Returns parsed object for advanced custom variable for option without text'} | ${mockTemplatingData.advCustomWithoutOptText} | ${mockTemplatingDataResponses.advCustomWithoutOptText}
+ ${'Returns parsed object for advanced custom variable without type'} | ${mockTemplatingData.advCustomWithoutType} | ${{}}
+ ${'Returns parsed object for advanced custom variable without label'} | ${mockTemplatingData.advCustomWithoutLabel} | ${mockTemplatingDataResponses.advCustomWithoutLabel}
+ ${'Returns parsed object for simple and advanced custom variables'} | ${mockTemplatingData.simpleAndAdv} | ${mockTemplatingDataResponses.simpleAndAdv}
+ ${'Returns parsed object for metricLabelValues'} | ${mockTemplatingData.metricLabelValues} | ${mockTemplatingDataResponses.metricLabelValues}
+ ${'Returns parsed object for all variable types'} | ${mockTemplatingData.allVariableTypes} | ${mockTemplatingDataResponses.allVariableTypes}
+ `('$case', ({ input, expected }) => {
+ expect(parseTemplatingVariables(input?.dashboard?.templating)).toEqual(expected);
+ });
});
-});
-describe('mergeURLVariables', () => {
- beforeEach(() => {
- jest.spyOn(urlUtils, 'queryToObject');
- });
+ describe('mergeURLVariables', () => {
+ beforeEach(() => {
+ jest.spyOn(urlUtils, 'queryToObject');
+ });
- afterEach(() => {
- urlUtils.queryToObject.mockRestore();
- });
+ afterEach(() => {
+ urlUtils.queryToObject.mockRestore();
+ });
- it('returns empty object if variables are not defined in yml or URL', () => {
- urlUtils.queryToObject.mockReturnValueOnce({});
+ it('returns empty object if variables are not defined in yml or URL', () => {
+ urlUtils.queryToObject.mockReturnValueOnce({});
- expect(mergeURLVariables({})).toEqual({});
- });
+ expect(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',
+ });
- it('returns empty object if variables are defined in URL but not in yml', () => {
- urlUtils.queryToObject.mockReturnValueOnce({
- 'var-env': 'one',
- 'var-instance': 'localhost',
+ expect(mergeURLVariables({})).toEqual({});
});
- expect(mergeURLVariables({})).toEqual({});
- });
+ it('returns yml variables if variables defined in yml but not in the URL', () => {
+ urlUtils.queryToObject.mockReturnValueOnce({});
- it('returns yml variables if variables defined in yml but not in the URL', () => {
- urlUtils.queryToObject.mockReturnValueOnce({});
+ const params = {
+ env: 'one',
+ instance: 'localhost',
+ };
- const params = {
- env: 'one',
- instance: 'localhost',
- };
+ expect(mergeURLVariables(params)).toEqual(params);
+ });
- expect(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(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);
- 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(mergeURLVariables(ymlParams)).toEqual(ymlParams);
+ expect(mergeURLVariables(ymlParams)).toEqual(merged);
+ });
});
- 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' },
- };
+ describe('optionsFromSeriesData', () => {
+ it('fetches the label values from missing data', () => {
+ expect(optionsFromSeriesData({ label: 'job' })).toEqual([]);
+ });
- const merged = {
- instance: { value: 'localhost:8080' },
- service: { value: 'database' },
- };
+ it('fetches the label values from a simple series', () => {
+ const data = [
+ {
+ __name__: 'up',
+ job: 'job1',
+ },
+ {
+ __name__: 'up',
+ job: 'job2',
+ },
+ ];
+
+ expect(optionsFromSeriesData({ label: 'job', data })).toEqual([
+ { text: 'job1', value: 'job1' },
+ { text: 'job2', value: 'job2' },
+ ]);
+ });
- urlUtils.queryToObject.mockReturnValueOnce(urlParams);
+ it('fetches the label values from multiple series', () => {
+ const data = [
+ {
+ __name__: 'up',
+ job: 'job1',
+ instance: 'host1',
+ },
+ {
+ __name__: 'up',
+ job: 'job2',
+ instance: 'host1',
+ },
+ {
+ __name__: 'up',
+ job: 'job1',
+ instance: 'host2',
+ },
+ {
+ __name__: 'up',
+ job: 'job2',
+ instance: 'host2',
+ },
+ ];
+
+ expect(optionsFromSeriesData({ label: '__name__', data })).toEqual([
+ { text: 'up', value: 'up' },
+ ]);
+
+ expect(optionsFromSeriesData({ label: 'job', data })).toEqual([
+ { text: 'job1', value: 'job1' },
+ { text: 'job2', value: 'job2' },
+ ]);
+
+ expect(optionsFromSeriesData({ label: 'instance', data })).toEqual([
+ { text: 'host1', value: 'host1' },
+ { text: 'host2', value: 'host2' },
+ ]);
+ });
- expect(mergeURLVariables(ymlParams)).toEqual(merged);
+ it('fetches the label values from a series with missing values', () => {
+ const data = [
+ {
+ __name__: 'up',
+ job: 'job1',
+ },
+ {
+ __name__: 'up',
+ job: 'job2',
+ },
+ {
+ __name__: 'up',
+ },
+ ];
+
+ expect(optionsFromSeriesData({ label: 'job', data })).toEqual([
+ { text: 'job1', value: 'job1' },
+ { text: 'job2', value: 'job2' },
+ ]);
+ });
});
});
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index e2a148165ab..a9021e5771c 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -2895,6 +2895,29 @@ describe Repository do
end
end
+ describe '#project' do
+ it 'returns the project for a project snippet' do
+ snippet = create(:project_snippet)
+
+ expect(snippet.repository.project).to be(snippet.project)
+ end
+
+ it 'returns nil for a personal snippet' do
+ snippet = create(:personal_snippet)
+
+ expect(snippet.repository.project).to be_nil
+ end
+
+ it 'returns the container if it is a project' do
+ expect(repository.project).to be(project)
+ end
+
+ it 'returns nil if the container is not a project' do
+ expect(repository).to receive(:container).and_return(Group.new)
+ expect(repository.project).to be_nil
+ end
+ end
+
describe '#submodule_links' do
it 'returns an instance of Gitlab::SubmoduleLinks' do
expect(repository.submodule_links).to be_a(Gitlab::SubmoduleLinks)