提交 d3a59e10 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 95ab36cd
<script>
import { GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui';
export default {
components: {
GlFormGroup,
GlDropdown,
GlDropdownItem,
},
props: {
name: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
value: {
type: String,
required: false,
default: '',
},
options: {
type: Array,
required: true,
},
},
computed: {
defaultText() {
const selectedOpt = this.options.find(opt => opt.value === this.value);
return selectedOpt?.text || this.value;
},
},
methods: {
onUpdate(value) {
this.$emit('onUpdate', this.name, value);
},
},
};
</script>
<template>
<gl-form-group :label="label">
<gl-dropdown toggle-class="dropdown-menu-toggle" :text="defaultText">
<gl-dropdown-item v-for="(opt, key) in options" :key="key" @click="onUpdate(opt.value)">{{
opt.text
}}</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
</template>
<script>
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
export default {
components: {
GlFormGroup,
GlFormInput,
},
props: {
name: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
value: {
type: String,
required: true,
},
},
methods: {
onUpdate(event) {
this.$emit('onUpdate', this.name, event.target.value);
},
},
};
</script>
<template>
<gl-form-group :label="label">
<gl-form-input
:value="value"
:name="name"
@keyup.native.enter="onUpdate"
@blur.native="onUpdate"
/>
</gl-form-group>
</template>
<script>
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
import CustomVariable from './variables/custom_variable.vue';
import TextVariable from './variables/text_variable.vue';
import { setPromCustomVariablesFromUrl } from '../utils';
export default {
components: {
GlFormGroup,
GlFormInput,
CustomVariable,
TextVariable,
},
computed: {
...mapState('monitoringDashboard', ['promVariables']),
},
methods: {
...mapActions('monitoringDashboard', ['fetchDashboardData', 'setVariableValues']),
refreshDashboard(event) {
const { name, value } = event.target;
if (this.promVariables[name] !== value) {
const changedVariable = { [name]: value };
this.setVariableValues(changedVariable);
updateHistory({
url: mergeUrlParams(this.promVariables, window.location.href),
title: document.title,
});
...mapActions('monitoringDashboard', ['fetchDashboardData', 'updateVariableValues']),
refreshDashboard(variable, value) {
if (this.promVariables[variable].value !== value) {
const changedVariable = { key: variable, value };
// update the Vuex store
this.updateVariableValues(changedVariable);
// the below calls can ideally be moved out of the
// component and into the actions and let the
// mutation respond directly.
// This can be further investigate in
// https://gitlab.com/gitlab-org/gitlab/-/issues/217713
setPromCustomVariablesFromUrl(this.promVariables);
// fetch data
this.fetchDashboardData();
}
},
variableComponent(type) {
const types = {
text: TextVariable,
custom: CustomVariable,
};
return types[type] || TextVariable;
},
},
};
</script>
<template>
<div ref="variablesSection" class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section">
<div v-for="(val, key) in promVariables" :key="key" class="mb-1 pr-2 d-flex d-sm-block">
<gl-form-group :label="key" class="mb-0 flex-grow-1">
<gl-form-input
:value="val"
:name="key"
@keyup.native.enter="refreshDashboard"
@blur.native="refreshDashboard"
/>
</gl-form-group>
<div v-for="(variable, key) in promVariables" :key="key" class="mb-1 pr-2 d-flex d-sm-block">
<component
:is="variableComponent(variable.type)"
class="mb-0 flex-grow-1"
:label="variable.label"
:value="variable.value"
:name="key"
:options="variable.options"
@onUpdate="refreshDashboard"
/>
</div>
</div>
</template>
......@@ -4,7 +4,6 @@ import Dashboard from '~/monitoring/components/dashboard.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { getParameterValues } from '~/lib/utils/url_utility';
import store from './stores';
import { promCustomVariablesFromUrl } from './utils';
Vue.use(GlToast);
......@@ -14,8 +13,6 @@ export default (props = {}) => {
if (el && el.dataset) {
const [currentDashboard] = getParameterValues('dashboard');
store.dispatch('monitoringDashboard/setVariableValues', promCustomVariablesFromUrl());
// eslint-disable-next-line no-new
new Vue({
el,
......
......@@ -3,6 +3,8 @@ import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { parseTemplatingVariables } from './variable_mapping';
import { mergeURLVariables } from '../utils';
import {
gqClient,
parseEnvironmentsResponse,
......@@ -159,6 +161,7 @@ export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response
commit(types.SET_ALL_DASHBOARDS, all_dashboards);
commit(types.RECEIVE_METRICS_DASHBOARD_SUCCESS, dashboard);
commit(types.SET_VARIABLES, mergeURLVariables(parseTemplatingVariables(dashboard.templating)));
commit(types.SET_ENDPOINTS, convertObjectPropsToCamelCase(metrics_data));
return dispatch('fetchDashboardData');
......@@ -413,7 +416,7 @@ export const duplicateSystemDashboard = ({ state }, payload) => {
// Variables manipulation
export const setVariableValues = ({ commit }, updatedVariable) => {
export const updateVariableValues = ({ commit }, updatedVariable) => {
commit(types.UPDATE_VARIABLE_VALUES, updatedVariable);
};
......
import { flatMap } from 'lodash';
import { removePrefixFromLabels } from './utils';
import { NOT_IN_DB_PREFIX } from '../constants';
const metricsIdsInPanel = panel =>
......@@ -123,10 +122,7 @@ export const filteredEnvironments = state =>
*/
export const getCustomVariablesArray = state =>
flatMap(state.promVariables, (val, key) => [
encodeURIComponent(removePrefixFromLabels(key)),
encodeURIComponent(val),
]);
flatMap(state.promVariables, (variable, key) => [key, variable.value]);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -191,11 +191,10 @@ export default {
[types.SET_VARIABLES](state, variables) {
state.promVariables = variables;
},
[types.UPDATE_VARIABLE_VALUES](state, newVariable) {
Object.keys(newVariable).forEach(key => {
if (Object.prototype.hasOwnProperty.call(state.promVariables, key)) {
state.promVariables[key] = newVariable[key];
}
[types.UPDATE_VARIABLE_VALUES](state, updatedVariable) {
Object.assign(state.promVariables[updatedVariable.key], {
...state.promVariables[updatedVariable.key],
value: updatedVariable.value,
});
},
};
......@@ -2,7 +2,7 @@ import { slugify } from '~/lib/utils/text_utility';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { NOT_IN_DB_PREFIX, VARIABLE_PREFIX } from '../constants';
import { NOT_IN_DB_PREFIX } from '../constants';
export const gqClient = createGqClient(
{},
......@@ -229,25 +229,3 @@ export const normalizeQueryResult = timeSeries => {
return normalizedResult;
};
/**
* Variable labels are used as names for the dropdowns and also
* as URL params. Prefixing the name reduces the risk of
* collision with other URL params
*
* @param {String} label label for the template variable
* @returns {String}
*/
export const addPrefixToLabels = label => `${VARIABLE_PREFIX}${label}`;
/**
* Before the templating variables are passed to the backend the
* prefix needs to be removed.
*
* This method removes the prefix at the beginning of the string.
*
* @param {String} label label to remove prefix from
* @returns {String}
*/
export const removePrefixFromLabels = label =>
(label || '').replace(new RegExp(`^${VARIABLE_PREFIX}`), '');
import { isString } from 'lodash';
import { addPrefixToLabels } from './utils';
import { VARIABLE_TYPES } from '../constants';
/**
......@@ -56,15 +55,20 @@ const normalizeCustomVariableOptions = ({ default: defaultOpt = false, text, val
* Custom advanced variables are rendered as dropdown elements in the dashboard
* header. This method parses advanced custom variables.
*
* The default value is the option with default set to true or the first option
* if none of the options have default prop true.
*
* @param {Object} advVariable advance custom variable
* @returns {Object}
*/
const customAdvancedVariableParser = advVariable => {
const options = advVariable?.options?.values ?? [];
const options = (advVariable?.options?.values ?? []).map(normalizeCustomVariableOptions);
const defaultOpt = options.find(opt => opt.default === true) || options[0];
return {
type: VARIABLE_TYPES.custom,
label: advVariable.label,
options: options.map(normalizeCustomVariableOptions),
value: defaultOpt?.value,
options,
};
};
......@@ -83,6 +87,9 @@ const parseSimpleCustomOptions = opt => ({ text: opt, value: opt });
*
* Simple custom variables do not have labels so its set to null here.
*
* The default value is set to the first option as the user cannot
* set a default value for this format
*
* @param {Array} customVariable array of options
* @returns {Object}
*/
......@@ -90,6 +97,7 @@ const customSimpleVariableParser = simpleVar => {
const options = (simpleVar || []).map(parseSimpleCustomOptions);
return {
type: VARIABLE_TYPES.custom,
value: options[0].value,
label: null,
options: options.map(normalizeCustomVariableOptions),
};
......@@ -150,7 +158,7 @@ export const parseTemplatingVariables = ({ variables = {} } = {}) =>
if (parsedVar) {
acc[key] = {
...parsedVar,
label: addPrefixToLabels(parsedVar.label || key),
label: parsedVar.label || key,
};
}
return acc;
......
import { pickBy } from 'lodash';
import { queryToObject, mergeUrlParams, removeParams } from '~/lib/utils/url_utility';
import { pickBy, mapKeys } from 'lodash';
import {
queryToObject,
mergeUrlParams,
removeParams,
updateHistory,
} from '~/lib/utils/url_utility';
import {
timeRangeParamNames,
timeRangeFromParams,
......@@ -122,6 +127,44 @@ export const timeRangeFromUrl = (search = window.location.search) => {
return timeRangeFromParams(params);
};
/**
* Variable labels are used as names for the dropdowns and also
* as URL params. Prefixing the name reduces the risk of
* collision with other URL params
*
* @param {String} label label for the template variable
* @returns {String}
*/
export const addPrefixToLabel = label => `${VARIABLE_PREFIX}${label}`;
/**
* Before the templating variables are passed to the backend the
* prefix needs to be removed.
*
* This method removes the prefix at the beginning of the string.
*
* @param {String} label label to remove prefix from
* @returns {String}
*/
export const removePrefixFromLabel = label =>
(label || '').replace(new RegExp(`^${VARIABLE_PREFIX}`), '');
/**
* Convert parsed template variables to an object
* with just keys and values. Prepare the promVariables
* to be added to the URL. Keys of the object will
* have a prefix so that these params can be
* differentiated from other URL params.
*
* @param {Object} variables
* @returns {Object}
*/
export const convertVariablesForURL = variables =>
Object.keys(variables || {}).reduce((acc, key) => {
acc[addPrefixToLabel(key)] = variables[key]?.value;
return acc;
}, {});
/**
* User-defined variables from the URL are extracted. The variables
* begin with a constant prefix so that it doesn't collide with
......@@ -131,8 +174,30 @@ export const timeRangeFromUrl = (search = window.location.search) => {
* @returns {Object} The custom variables defined by the user in the URL
*/
export const promCustomVariablesFromUrl = (search = window.location.search) =>
pickBy(queryToObject(search), (val, key) => key.startsWith(VARIABLE_PREFIX));
export const getPromCustomVariablesFromUrl = (search = window.location.search) => {
const params = queryToObject(search);
// pick the params with variable prefix
const paramsWithVars = pickBy(params, (val, key) => key.startsWith(VARIABLE_PREFIX));
// remove the prefix before storing in the Vuex store
return mapKeys(paramsWithVars, (val, key) => removePrefixFromLabel(key));
};
/**
* Update the URL with promVariables. This usually get triggered when
* the user interacts with the dynamic input elements in the monitoring
* dashboard header.
*
* @param {Object} promVariables user defined variables
*/
export const setPromCustomVariablesFromUrl = promVariables => {
// prep the variables to append to URL
const parsedVariables = convertVariablesForURL(promVariables);
// update the URL
updateHistory({
url: mergeUrlParams(parsedVariables, window.location.href),
title: document.title,
});
};
/**
* Returns a URL with no time range based on the current URL.
......@@ -280,4 +345,39 @@ export const barChartsDataParser = (data = []) =>
{},
);
/**
* Custom variables are defined in the dashboard yml file
* and their values can be passed through the URL.
*
* On component load, this method merges variables data
* from the yml file with URL data to store in the Vuex store.
* Not all params coming from the URL need to be stored. Only
* the ones that have a corresponding variable defined in the
* yml file.
*
* This ensures that there is always a single source of truth
* for variables
*
* This method can be improved further. See the below issue
* https://gitlab.com/gitlab-org/gitlab/-/issues/217713
*
* @param {Object} varsFromYML template variables from yml file
* @returns {Object}
*/
export const mergeURLVariables = (varsFromYML = {}) => {
const varsFromURL = getPromCustomVariablesFromUrl();
const variables = {};
Object.keys(varsFromYML).forEach(key => {
if (Object.prototype.hasOwnProperty.call(varsFromURL, key)) {
variables[key] = {
...varsFromYML[key],
value: varsFromURL[key],
};
} else {
variables[key] = varsFromYML[key];
}
});
return variables;
};
export default {};
......@@ -242,7 +242,7 @@ export default {
</li>
<li v-if="renderAddToTreeDropdown" class="breadcrumb-item">
<gl-dropdown toggle-class="add-to-tree qa-add-to-tree ml-1">
<template slot="button-content">
<template #button-content>
<span class="sr-only">{{ __('Add to tree') }}</span>
<icon name="plus" :size="16" class="float-left" />
<icon name="chevron-down" :size="16" class="float-left" />
......
......@@ -20,9 +20,6 @@ module Boards
skip_before_action :authenticate_user!, only: [:index]
before_action :validate_id_list, only: [:bulk_move]
before_action :can_move_issues?, only: [:bulk_move]
before_action do
push_frontend_feature_flag(:not_issuable_queries, board.group, default_enabled: true)
end
def index
list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params)
......
......@@ -10,6 +10,9 @@ module BoardsActions
before_action :boards, only: :index
before_action :board, only: :show
before_action :push_wip_limits, only: [:index, :show]
before_action do
push_frontend_feature_flag(:not_issuable_queries, parent, default_enabled: true)
end
end
def index
......
......@@ -4,7 +4,7 @@ module PerformanceMonitoring
class PrometheusDashboard
include ActiveModel::Model
attr_accessor :dashboard, :panel_groups, :path, :environment, :priority
attr_accessor :dashboard, :panel_groups, :path, :environment, :priority, :templating
validates :dashboard, presence: true
validates :panel_groups, presence: true
......
---
title: Conan registry is accessible using deploy tokens
merge_request: 31114
author:
type: added
---
title: Render dropdown and text elements based on variables defined in monitoring
dashboard yml file
merge_request: 31524
author:
type: added
---
title: Re-enable negative filters for Boards
merge_request: 32348
author:
type: fixed
---
title: Update deprecated slot syntax in ./app/assets/javascripts/repository/components/breadcrumbs.vue
merge_request: 32017
author: Gilang Gumilar
type: changed
......@@ -141,8 +141,8 @@ module.exports = {
output: {
path: path.join(ROOT_PATH, 'public/assets/webpack'),
publicPath: '/assets/webpack/',
filename: IS_PRODUCTION ? '[name].[chunkhash:8].bundle.js' : '[name].bundle.js',
chunkFilename: IS_PRODUCTION ? '[name].[chunkhash:8].chunk.js' : '[name].chunk.js',
filename: IS_PRODUCTION ? '[name].[contenthash:8].bundle.js' : '[name].bundle.js',
chunkFilename: IS_PRODUCTION ? '[name].[contenthash:8].chunk.js' : '[name].chunk.js',
globalObject: 'this', // allow HMR and web workers to play nice
},
......@@ -191,7 +191,7 @@ module.exports = {
test: /icons\.svg$/,
loader: 'file-loader',
options: {
name: '[name].[hash:8].[ext]',
name: '[name].[contenthash:8].[ext]',
},
},
{
......@@ -210,7 +210,7 @@ module.exports = {
{
loader: 'worker-loader',
options: {
name: '[name].[hash:8].worker.js',
name: '[name].[contenthash:8].worker.js',
inline: IS_DEV_SERVER,
},
},
......@@ -222,7 +222,7 @@ module.exports = {
exclude: /node_modules/,
loader: 'file-loader',
options: {
name: '[name].[hash:8].[ext]',
name: '[name].[contenthash:8].[ext]',
},
},
{
......@@ -232,7 +232,7 @@ module.exports = {
{
loader: 'css-loader',
options: {
name: '[name].[hash:8].[ext]',
name: '[name].[contenthash:8].[ext]',
},
},
],
......@@ -242,13 +242,15 @@ module.exports = {
include: /node_modules\/katex\/dist\/fonts/,
loader: 'file-loader',
options: {
name: '[name].[hash:8].[ext]',
name: '[name].[contenthash:8].[ext]',
},
},
],
},
optimization: {
// Replace 'hashed' with 'deterministic' in webpack 5
moduleIds: 'hashed',
runtimeChunk: 'single',
splitChunks: {
maxInitialRequests: 4,
......
......@@ -108,14 +108,19 @@ conan search Hello* --all --remote=gitlab
## Authenticating to the GitLab Conan Repository
You will need to generate a [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `api` for repository authentication.
You will need a personal access token or deploy token.
For repository authentication:
- You can generate a [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `api`.
- You can generate a [deploy token](./../../project/deploy_tokens/index.md) with the scope set to `read_package_registry`, `write_package_registry`, or both.
### Adding a Conan user to the GitLab remote
Once you have a personal access token and have [set your Conan remote](#adding-the-gitlab-package-registry-as-a-conan-remote), you can associate the token with the remote so you do not have to explicitly add them to each Conan command you run:
```shell
conan user <gitlab-username> -r gitlab -p <personal_access_token>
conan user <gitlab_username or deploy_token_username> -r gitlab -p <personal_access_token or deploy_token>
```
Note: **Note**
......@@ -130,7 +135,7 @@ Alternatively, you could explicitly include your credentials in any given comman
For example:
```shell
CONAN_LOGIN_USERNAME=<gitlab-username> CONAN_PASSWORD=<personal_access_token> conan upload Hello/0.1@my-group+my-project/beta --all --remote=gitlab
CONAN_LOGIN_USERNAME=<gitlab_username or deploy_token_username> CONAN_PASSWORD=<personal_access_token or deploy_token> conan upload Hello/0.1@my-group+my-project/beta --all --remote=gitlab
```
### Setting a default remote to your project (optional)
......@@ -148,7 +153,7 @@ This functionality is best suited for when you want to consume or install packag
The rest of the example commands in this documentation assume that you have added a Conan user with your credentials to the `gitlab` remote and will not include the explicit credentials or remote option, but be aware that any of the commands could be run without having added a user or default remote:
```shell
`CONAN_LOGIN_USERNAME=<gitlab-username> CONAN_PASSWORD=<personal_access_token> <conan command> --remote=gitlab
`CONAN_LOGIN_USERNAME=<gitlab_username or deploy_token_username> CONAN_PASSWORD=<personal_access_token or deploy_token> <conan command> --remote=gitlab
```
## Uploading a package
......
......@@ -137,6 +137,49 @@ describe 'Issue Boards', :js do
expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 0)
end
context 'search list negation queries' do
context 'with the NOT queries feature flag disabled' do
before do
stub_feature_flags(not_issuable_queries: false)
visit project_board_path(project, board)
end
it 'does not have the != option' do
find('.filtered-search').set('label:')
wait_for_requests
within('#js-dropdown-operator') do
tokens = all(:css, 'li.filter-dropdown-item')
expect(tokens.count).to eq(1)
button = tokens[0].find('button')
expect(button).to have_content('=')
expect(button).not_to have_content('!=')
end
end
end
context 'with the NOT queries feature flag enabled' do
before do
stub_feature_flags(not_issuable_queries: true)
visit project_board_path(project, board)
end
it 'does not have the != option' do
find('.filtered-search').set('label:')
wait_for_requests
within('#js-dropdown-operator') do
tokens = all(:css, 'li.filter-dropdown-item')
expect(tokens.count).to eq(2)
button = tokens[0].find('button')
expect(button).to have_content('=')
button = tokens[1].find('button')
expect(button).to have_content('!=')
end
end
end
end
it 'allows user to delete board' do
page.within(find('.board:nth-child(2)')) do
accept_confirm { find('.board-delete').click }
......
import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import CustomVariable from '~/monitoring/components/variables/custom_variable.vue';
describe('Custom variable component', () => {
let wrapper;
const propsData = {
name: 'env',
label: 'Select environment',
value: 'Production',
options: [{ text: 'Production', value: 'prod' }, { text: 'Canary', value: 'canary' }],
};
const createShallowWrapper = () => {
wrapper = shallowMount(CustomVariable, {
propsData,
});
};
const findDropdown = () => wrapper.find(GlDropdown);
const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
it('renders dropdown element when all necessary props are passed', () => {
createShallowWrapper();
expect(findDropdown()).toExist();
});
it('renders dropdown element with a text', () => {
createShallowWrapper();
expect(findDropdown().attributes('text')).toBe(propsData.value);
});
it('renders all the dropdown items', () => {
createShallowWrapper();
expect(findDropdownItems()).toHaveLength(propsData.options.length);
});
it('changing dropdown items triggers update', () => {
createShallowWrapper();
jest.spyOn(wrapper.vm, '$emit');
findDropdownItems()
.at(1)
.vm.$emit('click');
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'env', 'canary');
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlFormInput } from '@gitlab/ui';
import TextVariable from '~/monitoring/components/variables/text_variable.vue';
describe('Text variable component', () => {
let wrapper;
const propsData = {
name: 'pod',
label: 'Select pod',
value: 'test-pod',
};
const createShallowWrapper = () => {
wrapper = shallowMount(TextVariable, {
propsData,
});
};
const findInput = () => wrapper.find(GlFormInput);
it('renders a text input when all props are passed', () => {
createShallowWrapper();
expect(findInput()).toExist();
});
it('always has a default value', () => {
createShallowWrapper();
return wrapper.vm.$nextTick(() => {
expect(findInput().attributes('value')).toBe(propsData.value);
});
});
it('triggers keyup enter', () => {
createShallowWrapper();
jest.spyOn(wrapper.vm, '$emit');
findInput().element.value = 'prod-pod';
findInput().trigger('input');
findInput().trigger('keyup.enter');
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'pod', 'prod-pod');
});
});
it('triggers blur enter', () => {
createShallowWrapper();
jest.spyOn(wrapper.vm, '$emit');
findInput().element.value = 'canary-pod';
findInput().trigger('input');
findInput().trigger('blur');
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'pod', 'canary-pod');
});
});
});
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlFormInput } from '@gitlab/ui';
import VariablesSection from '~/monitoring/components/variables_section.vue';
import CustomVariable from '~/monitoring/components/variables/custom_variable.vue';
import TextVariable from '~/monitoring/components/variables/text_variable.vue';
import { updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
import { createStore } from '~/monitoring/stores';
import { convertVariablesForURL } from '~/monitoring/utils';
import * as types from '~/monitoring/stores/mutation_types';
import { mockTemplatingDataResponses } from '../mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
updateHistory: jest.fn(),
......@@ -15,8 +18,9 @@ describe('Metrics dashboard/variables section component', () => {
let store;
let wrapper;
const sampleVariables = {
'var-label1': 'pod',
'var-label2': 'main',
label1: mockTemplatingDataResponses.simpleText.simpleText,
label2: mockTemplatingDataResponses.advText.advText,
label3: mockTemplatingDataResponses.simpleCustom.simpleCustom,
};
const createShallowWrapper = () => {
......@@ -25,8 +29,8 @@ describe('Metrics dashboard/variables section component', () => {
});
};
const findAllFormInputs = () => wrapper.findAll(GlFormInput);
const getInputAt = i => findAllFormInputs().at(i);
const findTextInput = () => wrapper.findAll(TextVariable);
const findCustomInput = () => wrapper.findAll(CustomVariable);
beforeEach(() => {
store = createStore();
......@@ -36,9 +40,9 @@ describe('Metrics dashboard/variables section component', () => {
it('does not show the variables section', () => {
createShallowWrapper();
const allInputs = findAllFormInputs();
const allInputs = findTextInput().length + findCustomInput().length;
expect(allInputs).toHaveLength(0);
expect(allInputs).toBe(0);
});
it('shows the variables section', () => {
......@@ -46,15 +50,15 @@ describe('Metrics dashboard/variables section component', () => {
wrapper.vm.$store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, sampleVariables);
return wrapper.vm.$nextTick(() => {
const allInputs = findAllFormInputs();
const allInputs = findTextInput().length + findCustomInput().length;
expect(allInputs).toHaveLength(Object.keys(sampleVariables).length);
expect(allInputs).toBe(Object.keys(sampleVariables).length);
});
});
describe('when changing the variable inputs', () => {
const fetchDashboardData = jest.fn();
const setVariableValues = jest.fn();
const updateVariableValues = jest.fn();
beforeEach(() => {
store = new Vuex.Store({
......@@ -67,7 +71,7 @@ describe('Metrics dashboard/variables section component', () => {
},
actions: {
fetchDashboardData,
setVariableValues,
updateVariableValues,
},
},
},
......@@ -76,39 +80,44 @@ describe('Metrics dashboard/variables section component', () => {
createShallowWrapper();
});
it('merges the url params and refreshes the dashboard when a form input is blurred', () => {
const firstInput = getInputAt(0);
it('merges the url params and refreshes the dashboard when a text-based variables inputs are updated', () => {
const firstInput = findTextInput().at(0);
firstInput.element.value = 'POD';
firstInput.vm.$emit('input');
firstInput.trigger('blur');
firstInput.vm.$emit('onUpdate', 'label1', 'test');
expect(setVariableValues).toHaveBeenCalled();
expect(mergeUrlParams).toHaveBeenCalledWith(sampleVariables, window.location.href);
expect(updateHistory).toHaveBeenCalled();
expect(fetchDashboardData).toHaveBeenCalled();
return wrapper.vm.$nextTick(() => {
expect(updateVariableValues).toHaveBeenCalled();
expect(mergeUrlParams).toHaveBeenCalledWith(
convertVariablesForURL(sampleVariables),
window.location.href,
);
expect(updateHistory).toHaveBeenCalled();
expect(fetchDashboardData).toHaveBeenCalled();
});
});
it('merges the url params and refreshes the dashboard when a form input has received an enter key press', () => {
const firstInput = getInputAt(0);
it('merges the url params and refreshes the dashboard when a custom-based variables inputs are updated', () => {
const firstInput = findCustomInput().at(0);
firstInput.element.value = 'POD';
firstInput.vm.$emit('input');
firstInput.trigger('keyup.enter');
firstInput.vm.$emit('onUpdate', 'label1', 'test');
expect(setVariableValues).toHaveBeenCalled();
expect(mergeUrlParams).toHaveBeenCalledWith(sampleVariables, window.location.href);
expect(updateHistory).toHaveBeenCalled();
expect(fetchDashboardData).toHaveBeenCalled();
return wrapper.vm.$nextTick(() => {
expect(updateVariableValues).toHaveBeenCalled();
expect(mergeUrlParams).toHaveBeenCalledWith(
convertVariablesForURL(sampleVariables),
window.location.href,
);
expect(updateHistory).toHaveBeenCalled();
expect(fetchDashboardData).toHaveBeenCalled();
});
});
it('does not merge the url params and refreshes the dashboard if the value entered is not different that is what currently stored', () => {
const firstInput = getInputAt(0);
const firstInput = findTextInput().at(0);
firstInput.vm.$emit('input');
firstInput.trigger('keyup.enter');
firstInput.vm.$emit('onUpdate', 'label1', 'Simple text');
expect(setVariableValues).not.toHaveBeenCalled();
expect(updateVariableValues).not.toHaveBeenCalled();
expect(mergeUrlParams).not.toHaveBeenCalled();
expect(updateHistory).not.toHaveBeenCalled();
expect(fetchDashboardData).not.toHaveBeenCalled();
......
......@@ -642,7 +642,7 @@ const generateMockTemplatingData = data => {
const responseForSimpleTextVariable = {
simpleText: {
label: 'var-simpleText',
label: 'simpleText',
type: 'text',
value: 'Simple text',
},
......@@ -650,7 +650,7 @@ const responseForSimpleTextVariable = {
const responseForAdvTextVariable = {
advText: {
label: 'var-Variable 4',
label: 'Variable 4',
type: 'text',
value: 'default',
},
......@@ -658,7 +658,8 @@ const responseForAdvTextVariable = {
const responseForSimpleCustomVariable = {
simpleCustom: {
label: 'var-simpleCustom',
label: 'simpleCustom',
value: 'value1',
options: [
{
default: false,
......@@ -682,7 +683,7 @@ const responseForSimpleCustomVariable = {
const responseForAdvancedCustomVariableWithoutOptions = {
advCustomWithoutOpts: {
label: 'var-advCustomWithoutOpts',
label: 'advCustomWithoutOpts',
options: [],
type: 'custom',
},
......@@ -690,7 +691,8 @@ const responseForAdvancedCustomVariableWithoutOptions = {
const responseForAdvancedCustomVariableWithoutLabel = {
advCustomWithoutLabel: {
label: 'var-advCustomWithoutLabel',
label: 'advCustomWithoutLabel',
value: 'value2',
options: [
{
default: false,
......@@ -710,7 +712,8 @@ const responseForAdvancedCustomVariableWithoutLabel = {
const responseForAdvancedCustomVariable = {
...responseForSimpleCustomVariable,
advCustomNormal: {
label: 'var-Advanced Var',
label: 'Advanced Var',
value: 'value2',
options: [
{
default: false,
......
......@@ -26,7 +26,7 @@ import {
clearExpandedPanel,
setGettingStartedEmptyState,
duplicateSystemDashboard,
setVariableValues,
updateVariableValues,
} from '~/monitoring/stores/actions';
import {
gqClient,
......@@ -40,6 +40,7 @@ import {
deploymentData,
environmentData,
annotationsData,
mockTemplatingData,
dashboardGitResponse,
mockDashboardsErrorResponse,
} from '../mock_data';
......@@ -442,14 +443,14 @@ describe('Monitoring store actions', () => {
});
});
describe('setVariableValues', () => {
describe('updateVariableValues', () => {
let mockedState;
beforeEach(() => {
mockedState = storeState();
});
it('should commit UPDATE_VARIABLE_VALUES mutation', done => {
testAction(
setVariableValues,
updateVariableValues,
{ pod: 'POD' },
mockedState,
[
......@@ -574,6 +575,33 @@ describe('Monitoring store actions', () => {
);
expect(dispatch).toHaveBeenCalledWith('fetchDashboardData');
});
it('stores templating variables', () => {
const response = {
...metricsDashboardResponse.dashboard,
...mockTemplatingData.allVariableTypes.dashboard,
};
receiveMetricsDashboardSuccess(
{ state, commit, dispatch },
{
response: {
...metricsDashboardResponse,
dashboard: {
...metricsDashboardResponse.dashboard,
...mockTemplatingData.allVariableTypes.dashboard,
},
},
},
);
expect(commit).toHaveBeenCalledWith(
types.RECEIVE_METRICS_DASHBOARD_SUCCESS,
response,
);
});
it('sets the dashboards loaded from the repository', () => {
const params = {};
const response = metricsDashboardResponse;
......
......@@ -3,7 +3,12 @@ import * as getters from '~/monitoring/stores/getters';
import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import { metricStates } from '~/monitoring/constants';
import { environmentData, metricsResult, dashboardGitResponse } from '../mock_data';
import {
environmentData,
metricsResult,
dashboardGitResponse,
mockTemplatingDataResponses,
} from '../mock_data';
import {
metricsDashboardPayload,
metricResultStatus,
......@@ -326,10 +331,6 @@ describe('Monitoring store Getters', () => {
describe('getCustomVariablesArray', () => {
let state;
const sampleVariables = {
'var-label1': 'pod',
'var-label2': 'env',
};
beforeEach(() => {
state = {
......@@ -337,11 +338,20 @@ describe('Monitoring store Getters', () => {
};
});
it('transforms the promVariables object to an array in the [variable, variable_value] format', () => {
mutations[types.SET_VARIABLES](state, sampleVariables);
it('transforms the promVariables object to an array in the [variable, variable_value] format for all variable types', () => {
mutations[types.SET_VARIABLES](state, mockTemplatingDataResponses.allVariableTypes);
const variablesArray = getters.getCustomVariablesArray(state);
expect(variablesArray).toEqual(['label1', 'pod', 'label2', 'env']);
expect(variablesArray).toEqual([
'simpleText',
'Simple text',
'advText',
'default',
'simpleCustom',
'value1',
'advCustomNormal',
'value2',
]);
});
it('transforms the promVariables object to an empty array when no keys are present', () => {
......
......@@ -427,18 +427,11 @@ describe('Monitoring mutations', () => {
mutations[types.SET_VARIABLES](stateCopy, {});
});
it('ignores updates that are not already in promVariables', () => {
mutations[types.SET_VARIABLES](stateCopy, { environment: 'prod' });
mutations[types.UPDATE_VARIABLE_VALUES](stateCopy, { pod: 'new pod' });
it('updates only the value of the variable in promVariables', () => {
mutations[types.SET_VARIABLES](stateCopy, { environment: { value: 'prod', type: 'text' } });
mutations[types.UPDATE_VARIABLE_VALUES](stateCopy, { key: 'environment', value: 'new prod' });
expect(stateCopy.promVariables).toEqual({ environment: 'prod' });
});
it('only updates existing variables', () => {
mutations[types.SET_VARIABLES](stateCopy, { pod: 'POD' });
mutations[types.UPDATE_VARIABLE_VALUES](stateCopy, { pod: 'new pod' });
expect(stateCopy.promVariables).toEqual({ pod: 'new pod' });
expect(stateCopy.promVariables).toEqual({ environment: { value: 'new prod', type: 'text' } });
});
});
});
......@@ -5,7 +5,6 @@ import {
parseAnnotationsResponse,
removeLeadingSlash,
mapToDashboardViewModel,
removePrefixFromLabels,
} from '~/monitoring/stores/utils';
import { annotationsData } from '../mock_data';
import { NOT_IN_DB_PREFIX } from '~/monitoring/constants';
......@@ -420,24 +419,3 @@ describe('removeLeadingSlash', () => {
});
});
});
describe('removePrefixFromLabels', () => {
it.each`
input | expected
${undefined} | ${''}
${null} | ${''}
${''} | ${''}
${' '} | ${' '}
${'pod-1'} | ${'pod-1'}
${'pod-var-1'} | ${'pod-var-1'}
${'pod-1-var'} | ${'pod-1-var'}
${'podvar--1'} | ${'podvar--1'}
${'povar-d-1'} | ${'povar-d-1'}
${'var-pod-1'} | ${'pod-1'}
${'var-var-pod-1'} | ${'var-pod-1'}
${'varvar-pod-1'} | ${'varvar-pod-1'}
${'var-pod-1-var-'} | ${'pod-1-var-'}
`('removePrefixFromLabels returns $expected with input $input', ({ input, expected }) => {
expect(removePrefixFromLabels(input)).toEqual(expected);
});
});
......@@ -169,8 +169,8 @@ describe('monitoring/utils', () => {
});
});
describe('promCustomVariablesFromUrl', () => {
const { promCustomVariablesFromUrl } = monitoringUtils;
describe('getPromCustomVariablesFromUrl', () => {
const { getPromCustomVariablesFromUrl } = monitoringUtils;
beforeEach(() => {
jest.spyOn(urlUtils, 'queryToObject');
......@@ -195,7 +195,7 @@ describe('monitoring/utils', () => {
'var-pod': 'POD',
});
expect(promCustomVariablesFromUrl()).toEqual(expect.objectContaining({ 'var-pod': 'POD' }));
expect(getPromCustomVariablesFromUrl()).toEqual(expect.objectContaining({ pod: 'POD' }));
});
it('returns an empty object when no custom variables are present', () => {
......@@ -203,7 +203,7 @@ describe('monitoring/utils', () => {
dashboard: '.gitlab/dashboards/custom_dashboard.yml',
});
expect(promCustomVariablesFromUrl()).toStrictEqual({});
expect(getPromCustomVariablesFromUrl()).toStrictEqual({});
});
});
......@@ -398,4 +398,108 @@ describe('monitoring/utils', () => {
});
});
});
describe('removePrefixFromLabel', () => {
it.each`
input | expected
${undefined} | ${''}
${null} | ${''}
${''} | ${''}
${' '} | ${' '}
${'pod-1'} | ${'pod-1'}
${'pod-var-1'} | ${'pod-var-1'}
${'pod-1-var'} | ${'pod-1-var'}
${'podvar--1'} | ${'podvar--1'}
${'povar-d-1'} | ${'povar-d-1'}
${'var-pod-1'} | ${'pod-1'}
${'var-var-pod-1'} | ${'var-pod-1'}
${'varvar-pod-1'} | ${'varvar-pod-1'}
${'var-pod-1-var-'} | ${'pod-1-var-'}
`('removePrefixFromLabel returns $expected with input $input', ({ input, expected }) => {
expect(monitoringUtils.removePrefixFromLabel(input)).toEqual(expected);
});
});
describe('mergeURLVariables', () => {
beforeEach(() => {
jest.spyOn(urlUtils, 'queryToObject');
});
afterEach(() => {
urlUtils.queryToObject.mockRestore();
});
it('returns empty object if variables are not defined in yml or URL', () => {
urlUtils.queryToObject.mockReturnValueOnce({});
expect(monitoringUtils.mergeURLVariables({})).toEqual({});
});
it('returns empty object if variables are defined in URL but not in yml', () => {
urlUtils.queryToObject.mockReturnValueOnce({
'var-env': 'one',
'var-instance': 'localhost',
});
expect(monitoringUtils.mergeURLVariables({})).toEqual({});
});
it('returns yml variables if variables defined in yml but not in the URL', () => {
urlUtils.queryToObject.mockReturnValueOnce({});
const params = {
env: 'one',
instance: 'localhost',
};
expect(monitoringUtils.mergeURLVariables(params)).toEqual(params);
});
it('returns yml variables if variables defined in URL do not match with yml variables', () => {
const urlParams = {
'var-env': 'one',
'var-instance': 'localhost',
};
const ymlParams = {
pod: { value: 'one' },
service: { value: 'database' },
};
urlUtils.queryToObject.mockReturnValueOnce(urlParams);
expect(monitoringUtils.mergeURLVariables(ymlParams)).toEqual(ymlParams);
});
it('returns merged yml and URL variables if there is some match', () => {
const urlParams = {
'var-env': 'one',
'var-instance': 'localhost:8080',
};
const ymlParams = {
instance: { value: 'localhost' },
service: { value: 'database' },
};
const merged = {
instance: { value: 'localhost:8080' },
service: { value: 'database' },
};
urlUtils.queryToObject.mockReturnValueOnce(urlParams);
expect(monitoringUtils.mergeURLVariables(ymlParams)).toEqual(merged);
});
});
describe('convertVariablesForURL', () => {
it.each`
input | expected
${undefined} | ${{}}
${null} | ${{}}
${{}} | ${{}}
${{ env: { value: 'prod' } }} | ${{ 'var-env': 'prod' }}
${{ 'var-env': { value: 'prod' } }} | ${{ 'var-var-env': 'prod' }}
`('convertVariablesForURL returns $expected with input $input', ({ input, expected }) => {
expect(monitoringUtils.convertVariablesForURL(input)).toEqual(expected);
});
});
});
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册