2018-01-26 06:25:26 +00:00
|
|
|
import $ from 'jquery';
|
2018-11-21 15:13:04 +00:00
|
|
|
import { parseBoolean } from '../lib/utils/common_utils';
|
2018-01-26 06:25:26 +00:00
|
|
|
import { s__ } from '../locale';
|
|
|
|
import setupToggleButtons from '../toggle_buttons';
|
|
|
|
import CreateItemDropdown from '../create_item_dropdown';
|
|
|
|
import SecretValues from '../behaviors/secret_values';
|
|
|
|
|
|
|
|
const ALL_ENVIRONMENTS_STRING = s__('CiVariable|All environments');
|
|
|
|
|
|
|
|
function createEnvironmentItem(value) {
|
|
|
|
return {
|
|
|
|
title: value === '*' ? ALL_ENVIRONMENTS_STRING : value,
|
|
|
|
id: value,
|
2018-01-16 21:13:14 +00:00
|
|
|
text: value === '*' ? s__('CiVariable|* (All environments)') : value,
|
2018-01-26 06:25:26 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export default class VariableList {
|
2018-10-24 19:17:03 +00:00
|
|
|
constructor({ container, formField }) {
|
2018-01-26 06:25:26 +00:00
|
|
|
this.$container = $(container);
|
|
|
|
this.formField = formField;
|
|
|
|
this.environmentDropdownMap = new WeakMap();
|
|
|
|
|
|
|
|
this.inputMap = {
|
|
|
|
id: {
|
|
|
|
selector: '.js-ci-variable-input-id',
|
|
|
|
default: '',
|
|
|
|
},
|
2018-03-22 11:08:16 +00:00
|
|
|
key: {
|
2018-01-26 06:25:26 +00:00
|
|
|
selector: '.js-ci-variable-input-key',
|
|
|
|
default: '',
|
|
|
|
},
|
2018-03-17 11:37:48 +00:00
|
|
|
secret_value: {
|
2018-01-26 06:25:26 +00:00
|
|
|
selector: '.js-ci-variable-input-value',
|
|
|
|
default: '',
|
|
|
|
},
|
|
|
|
protected: {
|
|
|
|
selector: '.js-ci-variable-input-protected',
|
2018-10-31 19:12:22 +00:00
|
|
|
// use `attr` instead of `data` as we don't want the value to be
|
|
|
|
// converted. we need the value as a string.
|
|
|
|
default: $('.js-ci-variable-input-protected').attr('data-default'),
|
2018-01-26 06:25:26 +00:00
|
|
|
},
|
2019-03-27 19:13:55 +00:00
|
|
|
masked: {
|
|
|
|
selector: '.js-ci-variable-input-masked',
|
|
|
|
// use `attr` instead of `data` as we don't want the value to be
|
|
|
|
// converted. we need the value as a string.
|
|
|
|
default: $('.js-ci-variable-input-masked').attr('data-default'),
|
|
|
|
},
|
2018-01-16 21:13:14 +00:00
|
|
|
environment_scope: {
|
2018-01-26 06:25:26 +00:00
|
|
|
// We can't use a `.js-` class here because
|
|
|
|
// gl_dropdown replaces the <input> and doesn't copy over the class
|
|
|
|
// See https://gitlab.com/gitlab-org/gitlab-ce/issues/42458
|
2018-01-16 21:13:14 +00:00
|
|
|
selector: `input[name="${this.formField}[variables_attributes][][environment_scope]"]`,
|
2018-01-26 06:25:26 +00:00
|
|
|
default: '*',
|
|
|
|
},
|
|
|
|
_destroy: {
|
|
|
|
selector: '.js-ci-variable-input-destroy',
|
|
|
|
default: '',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
this.secretValues = new SecretValues({
|
|
|
|
container: this.$container[0],
|
|
|
|
valueSelector: '.js-row:not(:last-child) .js-secret-value',
|
|
|
|
placeholderSelector: '.js-row:not(:last-child) .js-secret-value-placeholder',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
init() {
|
|
|
|
this.bindEvents();
|
|
|
|
this.secretValues.init();
|
|
|
|
}
|
|
|
|
|
|
|
|
bindEvents() {
|
|
|
|
this.$container.find('.js-row').each((index, rowEl) => {
|
|
|
|
this.initRow(rowEl);
|
|
|
|
});
|
|
|
|
|
2018-10-24 19:17:03 +00:00
|
|
|
this.$container.on('click', '.js-row-remove-button', e => {
|
2018-01-26 06:25:26 +00:00
|
|
|
e.preventDefault();
|
|
|
|
this.removeRow($(e.currentTarget).closest('.js-row'));
|
|
|
|
});
|
|
|
|
|
|
|
|
const inputSelector = Object.keys(this.inputMap)
|
|
|
|
.map(name => this.inputMap[name].selector)
|
|
|
|
.join(',');
|
|
|
|
|
|
|
|
// Remove any empty rows except the last row
|
2018-10-24 19:17:03 +00:00
|
|
|
this.$container.on('blur', inputSelector, e => {
|
2018-01-26 06:25:26 +00:00
|
|
|
const $row = $(e.currentTarget).closest('.js-row');
|
|
|
|
|
|
|
|
if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) {
|
|
|
|
this.removeRow($row);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2019-03-27 19:13:55 +00:00
|
|
|
this.$container.on('input trigger-change', inputSelector, e => {
|
|
|
|
// Always make sure there is an empty last row
|
2018-01-26 06:25:26 +00:00
|
|
|
const $lastRow = this.$container.find('.js-row').last();
|
|
|
|
|
|
|
|
if (this.checkIfRowTouched($lastRow)) {
|
|
|
|
this.insertRow($lastRow);
|
|
|
|
}
|
2019-03-27 19:13:55 +00:00
|
|
|
|
|
|
|
// If masked, validate value against regex
|
|
|
|
this.validateMaskability($(e.currentTarget).closest('.js-row'));
|
2018-01-26 06:25:26 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
initRow(rowEl) {
|
|
|
|
const $row = $(rowEl);
|
|
|
|
|
|
|
|
setupToggleButtons($row[0]);
|
|
|
|
|
2018-01-16 21:13:14 +00:00
|
|
|
// Reset the resizable textarea
|
2018-03-17 11:37:48 +00:00
|
|
|
$row.find(this.inputMap.secret_value.selector).css('height', '');
|
2018-01-16 21:13:14 +00:00
|
|
|
|
2018-01-26 06:25:26 +00:00
|
|
|
const $environmentSelect = $row.find('.js-variable-environment-toggle');
|
|
|
|
if ($environmentSelect.length) {
|
|
|
|
const createItemDropdown = new CreateItemDropdown({
|
|
|
|
$dropdown: $environmentSelect,
|
|
|
|
defaultToggleLabel: ALL_ENVIRONMENTS_STRING,
|
2018-01-16 21:13:14 +00:00
|
|
|
fieldName: `${this.formField}[variables_attributes][][environment_scope]`,
|
2018-01-26 06:25:26 +00:00
|
|
|
getData: (term, callback) => callback(this.getEnvironmentValues()),
|
|
|
|
createNewItemFromValue: createEnvironmentItem,
|
|
|
|
onSelect: () => {
|
|
|
|
// Refresh the other dropdowns in the variable list
|
|
|
|
// so they have the new value we just picked
|
|
|
|
this.refreshDropdownData();
|
|
|
|
|
2018-01-16 21:13:14 +00:00
|
|
|
$row.find(this.inputMap.environment_scope.selector).trigger('trigger-change');
|
2018-01-26 06:25:26 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
// Clear out any data that might have been left-over from the row clone
|
|
|
|
createItemDropdown.clearDropdown();
|
|
|
|
|
|
|
|
this.environmentDropdownMap.set($row[0], createItemDropdown);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
insertRow($row) {
|
|
|
|
const $rowClone = $row.clone();
|
|
|
|
$rowClone.removeAttr('data-is-persisted');
|
|
|
|
|
|
|
|
// Reset the inputs to their defaults
|
2018-10-24 19:17:03 +00:00
|
|
|
Object.keys(this.inputMap).forEach(name => {
|
2018-01-26 06:25:26 +00:00
|
|
|
const entry = this.inputMap[name];
|
|
|
|
$rowClone.find(entry.selector).val(entry.default);
|
|
|
|
});
|
|
|
|
|
2018-05-22 22:12:41 +00:00
|
|
|
// Close any dropdowns
|
2018-05-22 22:34:43 +00:00
|
|
|
$rowClone.find('.dropdown-menu.show').each((index, $dropdown) => {
|
2018-05-22 22:12:41 +00:00
|
|
|
$dropdown.classList.remove('show');
|
|
|
|
});
|
|
|
|
|
2018-01-26 06:25:26 +00:00
|
|
|
this.initRow($rowClone);
|
|
|
|
|
|
|
|
$row.after($rowClone);
|
|
|
|
}
|
|
|
|
|
2018-01-16 21:13:14 +00:00
|
|
|
removeRow(row) {
|
|
|
|
const $row = $(row);
|
2018-11-21 15:13:04 +00:00
|
|
|
const isPersisted = parseBoolean($row.attr('data-is-persisted'));
|
2018-01-26 06:25:26 +00:00
|
|
|
|
|
|
|
if (isPersisted) {
|
|
|
|
$row.hide();
|
|
|
|
$row
|
|
|
|
// eslint-disable-next-line no-underscore-dangle
|
|
|
|
.find(this.inputMap._destroy.selector)
|
|
|
|
.val(true);
|
|
|
|
} else {
|
|
|
|
$row.remove();
|
|
|
|
}
|
2018-01-16 21:13:14 +00:00
|
|
|
|
|
|
|
// Refresh the other dropdowns in the variable list
|
|
|
|
// so any value with the variable deleted is gone
|
|
|
|
this.refreshDropdownData();
|
2018-01-26 06:25:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
checkIfRowTouched($row) {
|
2018-10-24 19:17:03 +00:00
|
|
|
return Object.keys(this.inputMap).some(name => {
|
2019-03-27 19:13:55 +00:00
|
|
|
// Row should not qualify as touched if only switches have been touched
|
|
|
|
if (['protected', 'masked'].includes(name)) return false;
|
|
|
|
|
2018-01-26 06:25:26 +00:00
|
|
|
const entry = this.inputMap[name];
|
|
|
|
const $el = $row.find(entry.selector);
|
|
|
|
return $el.length && $el.val() !== entry.default;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-03-27 19:13:55 +00:00
|
|
|
validateMaskability($row) {
|
|
|
|
const invalidInputClass = 'gl-field-error-outline';
|
|
|
|
|
|
|
|
const maskableRegex = /^\w{8,}$/; // Eight or more alphanumeric characters plus underscores
|
|
|
|
const variableValue = $row.find(this.inputMap.secret_value.selector).val();
|
|
|
|
const isValueMaskable = maskableRegex.test(variableValue) || variableValue === '';
|
|
|
|
const isMaskedChecked = $row.find(this.inputMap.masked.selector).val() === 'true';
|
|
|
|
|
|
|
|
// Show a validation error if the user wants to mask an unmaskable variable value
|
|
|
|
$row
|
|
|
|
.find(this.inputMap.secret_value.selector)
|
|
|
|
.toggleClass(invalidInputClass, isMaskedChecked && !isValueMaskable);
|
|
|
|
$row
|
|
|
|
.find('.js-secret-value-placeholder')
|
|
|
|
.toggleClass(invalidInputClass, isMaskedChecked && !isValueMaskable);
|
|
|
|
$row.find('.masking-validation-error').toggle(isMaskedChecked && !isValueMaskable);
|
|
|
|
}
|
|
|
|
|
2018-01-16 21:13:14 +00:00
|
|
|
toggleEnableRow(isEnabled = true) {
|
2018-03-22 11:08:16 +00:00
|
|
|
this.$container.find(this.inputMap.key.selector).attr('disabled', !isEnabled);
|
2018-01-16 21:13:14 +00:00
|
|
|
this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled);
|
|
|
|
}
|
|
|
|
|
2018-02-09 19:09:33 +00:00
|
|
|
hideValues() {
|
|
|
|
this.secretValues.updateDom(false);
|
|
|
|
}
|
|
|
|
|
2018-01-26 06:25:26 +00:00
|
|
|
getAllData() {
|
|
|
|
// Ignore the last empty row because we don't want to try persist
|
|
|
|
// a blank variable and run into validation problems.
|
2018-10-24 19:17:03 +00:00
|
|
|
const validRows = this.$container
|
|
|
|
.find('.js-row')
|
|
|
|
.toArray()
|
|
|
|
.slice(0, -1);
|
2018-01-26 06:25:26 +00:00
|
|
|
|
2018-10-24 19:17:03 +00:00
|
|
|
return validRows.map(rowEl => {
|
2018-01-26 06:25:26 +00:00
|
|
|
const resultant = {};
|
2018-10-24 19:17:03 +00:00
|
|
|
Object.keys(this.inputMap).forEach(name => {
|
2018-01-26 06:25:26 +00:00
|
|
|
const entry = this.inputMap[name];
|
|
|
|
const $input = $(rowEl).find(entry.selector);
|
|
|
|
if ($input.length) {
|
|
|
|
resultant[name] = $input.val();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return resultant;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
getEnvironmentValues() {
|
2018-10-24 19:17:03 +00:00
|
|
|
const valueMap = this.$container
|
|
|
|
.find(this.inputMap.environment_scope.selector)
|
|
|
|
.toArray()
|
|
|
|
.reduce(
|
|
|
|
(prevValueMap, envInput) => ({
|
|
|
|
...prevValueMap,
|
|
|
|
[envInput.value]: envInput.value,
|
|
|
|
}),
|
|
|
|
{},
|
|
|
|
);
|
2018-01-26 06:25:26 +00:00
|
|
|
|
|
|
|
return Object.keys(valueMap).map(createEnvironmentItem);
|
|
|
|
}
|
|
|
|
|
|
|
|
refreshDropdownData() {
|
|
|
|
this.$container.find('.js-row').each((index, rowEl) => {
|
|
|
|
const environmentDropdown = this.environmentDropdownMap.get(rowEl);
|
|
|
|
if (environmentDropdown) {
|
|
|
|
environmentDropdown.refreshData();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|