Commit 3be32027 authored by Eric Eastwood's avatar Eric Eastwood

Use dynamic variable list in scheduled pipelines and group/project CI secret variables

See https://gitlab.com/gitlab-org/gitlab-ce/issues/39118

Conflicts:
	app/views/ci/variables/_form.html.haml
	app/views/ci/variables/_table.html.haml
	ee/app/views/ci/variables/_environment_scope.html.haml
	spec/javascripts/ci_variable_list/ci_variable_list_ee_spec.js
	spec/javascripts/fixtures/projects.rb
parent 79570ce2
import _ from 'underscore';
import axios from '../lib/utils/axios_utils';
import { s__ } from '../locale';
import Flash from '../flash';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
import statusCodes from '../lib/utils/http_status';
import VariableList from './ci_variable_list';
function generateErrorBoxContent(errors) {
const errorList = [].concat(errors).map(errorString => `
<li>
${_.escape(errorString)}
</li>
`);
return `
<p>
${s__('CiVariable|Validation failed')}
</p>
<ul>
${errorList.join('')}
</ul>
`;
}
// Used for the variable list on CI/CD projects/groups settings page
export default class AjaxVariableList {
constructor({
container,
saveButton,
errorBox,
formField = 'variables',
saveEndpoint,
}) {
this.container = container;
this.saveButton = saveButton;
this.errorBox = errorBox;
this.saveEndpoint = saveEndpoint;
this.variableList = new VariableList({
container: this.container,
formField,
});
this.bindEvents();
this.variableList.init();
}
bindEvents() {
this.saveButton.addEventListener('click', this.onSaveClicked.bind(this));
}
onSaveClicked() {
const loadingIcon = this.saveButton.querySelector('.js-secret-variables-save-loading-icon');
loadingIcon.classList.toggle('hide', false);
this.errorBox.classList.toggle('hide', true);
// We use this to prevent a user from changing a key before we have a chance
// to match it up in `updateRowsWithPersistedVariables`
this.variableList.toggleEnableRow(false);
return axios.patch(this.saveEndpoint, {
variables_attributes: this.variableList.getAllData(),
}, {
// We want to be able to process the `res.data` from a 400 error response
// and print the validation messages such as duplicate variable keys
validateStatus: status => (
status >= statusCodes.OK &&
status < statusCodes.MULTIPLE_CHOICES
) ||
status === statusCodes.BAD_REQUEST,
})
.then((res) => {
loadingIcon.classList.toggle('hide', true);
this.variableList.toggleEnableRow(true);
if (res.status === statusCodes.OK && res.data) {
this.updateRowsWithPersistedVariables(res.data.variables);
} else if (res.status === statusCodes.BAD_REQUEST) {
// Validation failed
this.errorBox.innerHTML = generateErrorBoxContent(res.data);
this.errorBox.classList.toggle('hide', false);
}
})
.catch(() => {
loadingIcon.classList.toggle('hide', true);
this.variableList.toggleEnableRow(true);
Flash(s__('CiVariable|Error occured while saving variables'));
});
}
updateRowsWithPersistedVariables(persistedVariables = []) {
const persistedVariableMap = [].concat(persistedVariables).reduce((variableMap, variable) => ({
...variableMap,
[variable.key]: variable,
}), {});
this.container.querySelectorAll('.js-row').forEach((row) => {
// If we submitted a row that was destroyed, remove it so we don't try
// to destroy it again which would cause a BE error
const destroyInput = row.querySelector('.js-ci-variable-input-destroy');
if (convertPermissionToBoolean(destroyInput.value)) {
row.remove();
// Update the ID input so any future edits and `_destroy` will apply on the BE
} else {
const key = row.querySelector('.js-ci-variable-input-key').value;
const persistedVariable = persistedVariableMap[key];
if (persistedVariable) {
// eslint-disable-next-line no-param-reassign
row.querySelector('.js-ci-variable-input-id').value = persistedVariable.id;
row.setAttribute('data-is-persisted', 'true');
}
}
});
}
}
...@@ -11,7 +11,7 @@ function createEnvironmentItem(value) { ...@@ -11,7 +11,7 @@ function createEnvironmentItem(value) {
return { return {
title: value === '*' ? ALL_ENVIRONMENTS_STRING : value, title: value === '*' ? ALL_ENVIRONMENTS_STRING : value,
id: value, id: value,
text: value, text: value === '*' ? s__('CiVariable|* (All environments)') : value,
}; };
} }
...@@ -41,11 +41,11 @@ export default class VariableList { ...@@ -41,11 +41,11 @@ export default class VariableList {
selector: '.js-ci-variable-input-protected', selector: '.js-ci-variable-input-protected',
default: 'true', default: 'true',
}, },
environment: { environment_scope: {
// We can't use a `.js-` class here because // We can't use a `.js-` class here because
// gl_dropdown replaces the <input> and doesn't copy over the class // gl_dropdown replaces the <input> and doesn't copy over the class
// See https://gitlab.com/gitlab-org/gitlab-ce/issues/42458 // See https://gitlab.com/gitlab-org/gitlab-ce/issues/42458
selector: `input[name="${this.formField}[variables_attributes][][environment]"]`, selector: `input[name="${this.formField}[variables_attributes][][environment_scope]"]`,
default: '*', default: '*',
}, },
_destroy: { _destroy: {
...@@ -104,12 +104,15 @@ export default class VariableList { ...@@ -104,12 +104,15 @@ export default class VariableList {
setupToggleButtons($row[0]); setupToggleButtons($row[0]);
// Reset the resizable textarea
$row.find(this.inputMap.value.selector).css('height', '');
const $environmentSelect = $row.find('.js-variable-environment-toggle'); const $environmentSelect = $row.find('.js-variable-environment-toggle');
if ($environmentSelect.length) { if ($environmentSelect.length) {
const createItemDropdown = new CreateItemDropdown({ const createItemDropdown = new CreateItemDropdown({
$dropdown: $environmentSelect, $dropdown: $environmentSelect,
defaultToggleLabel: ALL_ENVIRONMENTS_STRING, defaultToggleLabel: ALL_ENVIRONMENTS_STRING,
fieldName: `${this.formField}[variables_attributes][][environment]`, fieldName: `${this.formField}[variables_attributes][][environment_scope]`,
getData: (term, callback) => callback(this.getEnvironmentValues()), getData: (term, callback) => callback(this.getEnvironmentValues()),
createNewItemFromValue: createEnvironmentItem, createNewItemFromValue: createEnvironmentItem,
onSelect: () => { onSelect: () => {
...@@ -117,7 +120,7 @@ export default class VariableList { ...@@ -117,7 +120,7 @@ export default class VariableList {
// so they have the new value we just picked // so they have the new value we just picked
this.refreshDropdownData(); this.refreshDropdownData();
$row.find(this.inputMap.environment.selector).trigger('trigger-change'); $row.find(this.inputMap.environment_scope.selector).trigger('trigger-change');
}, },
}); });
...@@ -143,7 +146,8 @@ export default class VariableList { ...@@ -143,7 +146,8 @@ export default class VariableList {
$row.after($rowClone); $row.after($rowClone);
} }
removeRow($row) { removeRow(row) {
const $row = $(row);
const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted')); const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted'));
if (isPersisted) { if (isPersisted) {
...@@ -155,6 +159,10 @@ export default class VariableList { ...@@ -155,6 +159,10 @@ export default class VariableList {
} else { } else {
$row.remove(); $row.remove();
} }
// Refresh the other dropdowns in the variable list
// so any value with the variable deleted is gone
this.refreshDropdownData();
} }
checkIfRowTouched($row) { checkIfRowTouched($row) {
...@@ -165,6 +173,11 @@ export default class VariableList { ...@@ -165,6 +173,11 @@ export default class VariableList {
}); });
} }
toggleEnableRow(isEnabled = true) {
this.$container.find(this.inputMap.key.selector).attr('disabled', !isEnabled);
this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled);
}
getAllData() { getAllData() {
// Ignore the last empty row because we don't want to try persist // Ignore the last empty row because we don't want to try persist
// a blank variable and run into validation problems. // a blank variable and run into validation problems.
...@@ -185,7 +198,7 @@ export default class VariableList { ...@@ -185,7 +198,7 @@ export default class VariableList {
} }
getEnvironmentValues() { getEnvironmentValues() {
const valueMap = this.$container.find(this.inputMap.environment.selector).toArray() const valueMap = this.$container.find(this.inputMap.environment_scope.selector).toArray()
.reduce((prevValueMap, envInput) => ({ .reduce((prevValueMap, envInput) => ({
...prevValueMap, ...prevValueMap,
[envInput.value]: envInput.value, [envInput.value]: envInput.value,
......
...@@ -18,3 +18,22 @@ Element.prototype.matches = Element.prototype.matches || ...@@ -18,3 +18,22 @@ Element.prototype.matches = Element.prototype.matches ||
while (i >= 0 && elms.item(i) !== this) { i -= 1; } while (i >= 0 && elms.item(i) !== this) { i -= 1; }
return i > -1; return i > -1;
}; };
// From the polyfill on MDN, https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove#Polyfill
((arr) => {
arr.forEach((item) => {
if (Object.prototype.hasOwnProperty.call(item, 'remove')) {
return;
}
Object.defineProperty(item, 'remove', {
configurable: true,
enumerable: true,
writable: true,
value: function remove() {
if (this.parentNode !== null) {
this.parentNode.removeChild(this);
}
},
});
});
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
...@@ -6,4 +6,6 @@ export default { ...@@ -6,4 +6,6 @@ export default {
ABORTED: 0, ABORTED: 0,
NO_CONTENT: 204, NO_CONTENT: 204,
OK: 200, OK: 200,
MULTIPLE_CHOICES: 300,
BAD_REQUEST: 400,
}; };
import SecretValues from '~/behaviors/secret_values'; import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
export default () => { export default () => {
const secretVariableTable = document.querySelector('.js-secret-variable-table'); const variableListEl = document.querySelector('.js-ci-variable-list-section');
if (secretVariableTable) { // eslint-disable-next-line no-new
const secretVariableTableValues = new SecretValues({ new AjaxVariableList({
container: secretVariableTable, container: variableListEl,
}); saveButton: variableListEl.querySelector('.js-secret-variables-save-button'),
secretVariableTableValues.init(); errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
} saveEndpoint: variableListEl.dataset.saveEndpoint,
});
}; };
import initSettingsPanels from '~/settings_panels'; import initSettingsPanels from '~/settings_panels';
import SecretValues from '~/behaviors/secret_values'; import SecretValues from '~/behaviors/secret_values';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
export default function () { export default function () {
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); initSettingsPanels();
const runnerToken = document.querySelector('.js-secret-runner-token'); const runnerToken = document.querySelector('.js-secret-runner-token');
if (runnerToken) { if (runnerToken) {
const runnerTokenSecretValue = new SecretValues({ const runnerTokenSecretValue = new SecretValues({
...@@ -12,11 +14,12 @@ export default function () { ...@@ -12,11 +14,12 @@ export default function () {
runnerTokenSecretValue.init(); runnerTokenSecretValue.init();
} }
const secretVariableTable = document.querySelector('.js-secret-variable-table'); const variableListEl = document.querySelector('.js-ci-variable-list-section');
if (secretVariableTable) { // eslint-disable-next-line no-new
const secretVariableTableValues = new SecretValues({ new AjaxVariableList({
container: secretVariableTable, container: variableListEl,
}); saveButton: variableListEl.querySelector('.js-secret-variables-save-button'),
secretVariableTableValues.init(); errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
} saveEndpoint: variableListEl.dataset.saveEndpoint,
});
} }
...@@ -8,7 +8,11 @@ ...@@ -8,7 +8,11 @@
.ci-variable-row { .ci-variable-row {
display: flex; display: flex;
align-items: flex-end; align-items: flex-start;
@media (max-width: $screen-xs-max) {
align-items: flex-end;
}
&:not(:last-child) { &:not(:last-child) {
margin-bottom: $gl-btn-padding; margin-bottom: $gl-btn-padding;
...@@ -41,6 +45,7 @@ ...@@ -41,6 +45,7 @@
.ci-variable-row-body { .ci-variable-row-body {
display: flex; display: flex;
align-items: flex-start;
width: 100%; width: 100%;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
...@@ -85,4 +90,8 @@ ...@@ -85,4 +90,8 @@
outline: none; outline: none;
color: $gl-text-color; color: $gl-text-color;
} }
&[disabled] {
color: $gl-text-color-disabled;
}
} }
%p.append-bottom-default = _('Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want.')
Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags.
You can use variables for passwords, secret keys, or whatever you want.
= form_for @variable, as: :variable, url: @variable.form_path do |f|
= form_errors(@variable)
.form-group
= f.label :key, "Key", class: "label-light"
= f.text_field :key, class: "form-control", placeholder: @variable.placeholder, required: true
.form-group
= f.label :value, "Value", class: "label-light"
= f.text_area :value, class: "form-control", placeholder: @variable.placeholder
.form-group
.checkbox
= f.label :protected do
= f.check_box :protected
%strong Protected
.help-block
This variable will be passed only to pipelines running on protected branches and tags
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'protected-secret-variables'), target: '_blank'
= f.submit btn_text, class: "btn btn-save"
.row.prepend-top-default.append-bottom-default - save_endpoint = local_assigns.fetch(:save_endpoint, nil)
.col-lg-12
%h5.prepend-top-0 .row
Add a variable .col-lg-12.js-ci-variable-list-section{ data: { save_endpoint: save_endpoint } }
= render "ci/variables/form", btn_text: "Add new variable" .hide.alert.alert-danger.js-ci-variable-error-box
%hr
%h5.prepend-top-0 %ul.ci-variable-list
Your variables (#{@variables.size}) - @variables.each.each do |variable|
- if @variables.empty? = render 'ci/variables/variable_row', form_field: 'variables', variable: variable
%p.settings-message.text-center.append-bottom-0 = render 'ci/variables/variable_row', form_field: 'variables'
No variables found, add one with the form above. .prepend-top-20
- else %button.btn.btn-success.js-secret-variables-save-button{ type: 'button' }
.js-secret-variable-table %span.hide.js-secret-variables-save-loading-icon
= render "ci/variables/table" = icon('spinner spin')
%button.btn.btn-info.js-secret-value-reveal-button{ data: { secret_reveal_status: 'false' } } = _('Save variables')
%button.btn.btn-info.btn-inverted.prepend-left-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: "#{@variables.size == 0}" } }
- if @variables.size == 0
= n_('Hide value', 'Hide values', @variables.size)
- else
= n_('Reveal value', 'Reveal values', @variables.size) = n_('Reveal value', 'Reveal values', @variables.size)
- page_title "Variables"
.row.prepend-top-default.append-bottom-default
.col-lg-3
= render "ci/variables/content"
.col-lg-9
%h4.prepend-top-0
Update variable
= render "ci/variables/form", btn_text: "Save variable"
.table-responsive.variables-table
%table.table
%colgroup
%col
%col
%col
%col{ width: 100 }
%thead
%th Key
%th Value
%th Protected
%th
%tbody
- @variables.each do |variable|
- if variable.id?
%tr
%td.variable-key= variable.key
%td.variable-value
%span.js-secret-value-placeholder
= '*' * 6
%span.hide.js-secret-value
= variable.value
%td.variable-protected= Gitlab::Utils.boolean_to_yes_no(variable.protected)
%td.variable-menu
= link_to variable.edit_path, class: "btn btn-transparent btn-variable-edit" do
%span.sr-only
Update
= icon("pencil")
= link_to variable.delete_path, class: "btn btn-transparent btn-variable-delete", method: :delete, data: { confirm: "Are you sure?" } do
%span.sr-only
Remove
= icon("trash")
- breadcrumb_title "CI / CD Settings" - breadcrumb_title "CI / CD Settings"
- page_title "CI / CD" - page_title "CI / CD"
= render 'ci/variables/index' %h4
= _('Secret variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
%p
= render "ci/variables/content"
= render 'ci/variables/index', save_endpoint: group_variables_path
...@@ -29,14 +29,14 @@ ...@@ -29,14 +29,14 @@
%section.settings.no-animate{ class: ('expanded' if expanded) } %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
Secret variables = _('Secret variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank' = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.js-settings-toggle %button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p.append-bottom-0
= render "ci/variables/content" = render "ci/variables/content"
.settings-content .settings-content
= render 'ci/variables/index' = render 'ci/variables/index', save_endpoint: project_variables_path(@project)
%section.settings.no-animate{ class: ('expanded' if expanded) } %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
......
---
title: Update CI/CD secret variables list to be dynamic and save without reloading
the page
merge_request: 4110
author:
type: added
...@@ -31,7 +31,7 @@ module QA ...@@ -31,7 +31,7 @@ module QA
page.fill_variable_key(key) page.fill_variable_key(key)
page.fill_variable_value(value) page.fill_variable_value(value)
page.add_variable page.save_variables
end end
end end
end end
......
...@@ -5,49 +5,40 @@ module QA ...@@ -5,49 +5,40 @@ module QA
class SecretVariables < Page::Base class SecretVariables < Page::Base
include Common include Common
view 'app/views/ci/variables/_table.html.haml' do view 'app/views/ci/variables/_variable_row.html.haml' do
element :variable_key, '.variable-key' element :variable_key, '.js-ci-variable-input-key'
element :variable_value, '.variable-value' element :variable_value, '.js-ci-variable-input-value'
end end
view 'app/views/ci/variables/_index.html.haml' do view 'app/views/ci/variables/_index.html.haml' do
element :add_new_variable, 'btn_text: "Add new variable"' element :save_variables, '.js-secret-variables-save-button'
end
view 'app/assets/javascripts/behaviors/secret_values.js' do
element :reveal_value, 'Reveal value'
element :hide_value, 'Hide value'
end end
def fill_variable_key(key) def fill_variable_key(key)
fill_in 'variable_key', with: key page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
page.find('.js-ci-variable-input-key').set(key)
end
end end
def fill_variable_value(value) def fill_variable_value(value)
fill_in 'variable_value', with: value page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
page.find('.js-ci-variable-input-value').set(value)
end
end end
def add_variable def save_variables
click_on 'Add new variable' click_button('Save variables')
end end
def variable_key def variable_key
page.find('.variable-key').text page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
end page.find('.js-ci-variable-input-key').value
def variable_value
reveal_value do
page.find('.variable-value').text
end end
end end
private def variable_value
page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
def reveal_value page.find('.js-ci-variable-input-value').value
click_button('Reveal value')
yield.tap do
click_button('Hide value')
end end
end end
end end
......
...@@ -3,76 +3,15 @@ require 'spec_helper' ...@@ -3,76 +3,15 @@ require 'spec_helper'
feature 'Group variables', :js do feature 'Group variables', :js do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:group) { create(:group) } let(:group) { create(:group) }
let!(:variable) { create(:ci_group_variable, key: 'test_key', value: 'test value', group: group) }
let(:page_path) { group_settings_ci_cd_path(group) }
background do background do
group.add_master(user) group.add_master(user)
gitlab_sign_in(user) gitlab_sign_in(user)
end
context 'when user creates a new variable' do
background do
visit group_settings_ci_cd_path(group)
fill_in 'variable_key', with: 'AAA'
fill_in 'variable_value', with: 'AAA123'
find(:css, "#variable_protected").set(true)
click_on 'Add new variable'
end
scenario 'user sees the created variable' do
page.within('.variables-table') do
expect(find(".variable-key")).to have_content('AAA')
expect(find(".variable-value")).to have_content('******')
expect(find(".variable-protected")).to have_content('Yes')
end
click_on 'Reveal value'
page.within('.variables-table') do
expect(find(".variable-value")).to have_content('AAA123')
end
end
end
context 'when user edits a variable' do
background do
create(:ci_group_variable, key: 'AAA', value: 'AAA123', protected: true,
group: group)
visit group_settings_ci_cd_path(group)
page.within('.variable-menu') do visit page_path
click_on 'Update'
end
fill_in 'variable_key', with: 'BBB'
fill_in 'variable_value', with: 'BBB123'
find(:css, "#variable_protected").set(false)
click_on 'Save variable'
end
scenario 'user sees the updated variable' do
page.within('.variables-table') do
expect(find(".variable-key")).to have_content('BBB')
expect(find(".variable-value")).to have_content('******')
expect(find(".variable-protected")).to have_content('No')
end
end
end end
context 'when user deletes a variable' do it_behaves_like 'variable list'
background do
create(:ci_group_variable, key: 'BBB', value: 'BBB123', protected: false,
group: group)
visit group_settings_ci_cd_path(group)
page.within('.variable-menu') do
page.accept_alert 'Are you sure?' do
click_on 'Remove'
end
end
end
scenario 'user does not see the deleted variable' do
expect(page).to have_no_css('.variables-table')
end
end
end end
require 'spec_helper'
describe 'Project variables', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') }
let(:page_path) { project_settings_ci_cd_path(project) }
before do
sign_in(user)
project.add_master(user)
project.variables << variable
visit page_path
end
it_behaves_like 'variable list'
end
require 'spec_helper'
describe 'Project variables', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') }
before do
sign_in(user)
project.add_master(user)
project.variables << variable
visit project_settings_ci_cd_path(project)
end
it 'shows list of variables' do
page.within('.variables-table') do
expect(page).to have_content(variable.key)
end
end
it 'adds new secret variable' do
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value')
click_button('Add new variable')
expect(page).to have_content('Variable was successfully created.')
page.within('.variables-table') do
expect(page).to have_content('key')
expect(page).to have_content('No')
end
end
it 'adds empty variable' do
fill_in('variable_key', with: 'new_key')
fill_in('variable_value', with: '')
click_button('Add new variable')
expect(page).to have_content('Variable was successfully created.')
page.within('.variables-table') do
expect(page).to have_content('new_key')
end
end
it 'adds new protected variable' do
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'value')
check('Protected')
click_button('Add new variable')
expect(page).to have_content('Variable was successfully created.')
page.within('.variables-table') do
expect(page).to have_content('key')
expect(page).to have_content('Yes')
end
end
it 'reveals and hides new variable' do
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value')
click_button('Add new variable')
page.within('.variables-table') do
expect(page).to have_content('key')
expect(page).to have_content('******')
end
click_button('Reveal values')
page.within('.variables-table') do
expect(page).to have_content('key')
expect(page).to have_content('key value')
end
click_button('Hide values')
page.within('.variables-table') do
expect(page).to have_content('key')
expect(page).to have_content('******')
end
end
it 'deletes variable' do
page.within('.variables-table') do
accept_confirm { click_on 'Remove' }
end
expect(page).not_to have_selector('variables-table')
end
it 'edits variable' do
page.within('.variables-table') do
click_on 'Update'
end
expect(page).to have_content('Update variable')
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value')
click_button('Save variable')
expect(page).to have_content('Variable was successfully updated.')
expect(project.variables(true).first.value).to eq('key value')
end
it 'edits variable with empty value' do
page.within('.variables-table') do
click_on 'Update'
end
expect(page).to have_content('Update variable')
fill_in('variable_value', with: '')
click_button('Save variable')
expect(page).to have_content('Variable was successfully updated.')
expect(project.variables(true).first.value).to eq('')
end
it 'edits variable to be protected' do
page.within('.variables-table') do
click_on 'Update'
end
expect(page).to have_content('Update variable')
check('Protected')
click_button('Save variable')
expect(page).to have_content('Variable was successfully updated.')
expect(project.variables(true).first).to be_protected
end
it 'edits variable to be unprotected' do
project.variables.first.update(protected: true)
page.within('.variables-table') do
click_on 'Update'
end
expect(page).to have_content('Update variable')
uncheck('Protected')
click_button('Save variable')
expect(page).to have_content('Variable was successfully updated.')
expect(project.variables(true).first).not_to be_protected
end
end
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import AjaxFormVariableList from '~/ci_variable_list/ajax_variable_list';
const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/variables';
describe('AjaxFormVariableList', () => {
preloadFixtures('projects/ci_cd_settings.html.raw');
preloadFixtures('projects/ci_cd_settings_with_variables.html.raw');
let container;
let saveButton;
let errorBox;
let mock;
let ajaxVariableList;
beforeEach(() => {
loadFixtures('projects/ci_cd_settings.html.raw');
container = document.querySelector('.js-ci-variable-list-section');
mock = new MockAdapter(axios);
const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section');
saveButton = ajaxVariableListEl.querySelector('.js-secret-variables-save-button');
errorBox = container.querySelector('.js-ci-variable-error-box');
ajaxVariableList = new AjaxFormVariableList({
container,
formField: 'variables',
saveButton,
errorBox,
saveEndpoint: container.dataset.saveEndpoint,
});
spyOn(ajaxVariableList, 'updateRowsWithPersistedVariables').and.callThrough();
spyOn(ajaxVariableList.variableList, 'toggleEnableRow').and.callThrough();
});
afterEach(() => {
mock.restore();
});
describe('onSaveClicked', () => {
it('shows loading spinner while waiting for the request', (done) => {
const loadingIcon = saveButton.querySelector('.js-secret-variables-save-loading-icon');
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => {
expect(loadingIcon.classList.contains('hide')).toEqual(false);
return [200, {}];
});
expect(loadingIcon.classList.contains('hide')).toEqual(true);
ajaxVariableList.onSaveClicked()
.then(() => {
expect(loadingIcon.classList.contains('hide')).toEqual(true);
})
.then(done)
.catch(done.fail);
});
it('calls `updateRowsWithPersistedVariables` with the persisted variables', (done) => {
const variablesResponse = [{ id: 1, key: 'foo', value: 'bar' }];
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, {
variables: variablesResponse,
});
ajaxVariableList.onSaveClicked()
.then(() => {
expect(ajaxVariableList.updateRowsWithPersistedVariables)
.toHaveBeenCalledWith(variablesResponse);
})
.then(done)
.catch(done.fail);
});
it('hides any previous error box', (done) => {
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200);
expect(errorBox.classList.contains('hide')).toEqual(true);
ajaxVariableList.onSaveClicked()
.then(() => {
expect(errorBox.classList.contains('hide')).toEqual(true);
})
.then(done)
.catch(done.fail);
});
it('disables remove buttons while waiting for the request', (done) => {
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => {
expect(ajaxVariableList.variableList.toggleEnableRow).toHaveBeenCalledWith(false);
return [200, {}];
});
ajaxVariableList.onSaveClicked()
.then(() => {
expect(ajaxVariableList.variableList.toggleEnableRow).toHaveBeenCalledWith(true);
})
.then(done)
.catch(done.fail);
});
it('shows error box with validation errors', (done) => {
const validationError = 'some validation error';
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(400, [
validationError,
]);
expect(errorBox.classList.contains('hide')).toEqual(true);
ajaxVariableList.onSaveClicked()
.then(() => {
expect(errorBox.classList.contains('hide')).toEqual(false);
expect(errorBox.textContent.trim().replace(/\n+\s+/m, ' ')).toEqual(`Validation failed ${validationError}`);
})
.then(done)
.catch(done.fail);
});
it('shows flash message when request fails', (done) => {
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(500);
expect(errorBox.classList.contains('hide')).toEqual(true);
ajaxVariableList.onSaveClicked()
.then(() => {
expect(errorBox.classList.contains('hide')).toEqual(true);
})
.then(done)
.catch(done.fail);
});
});
describe('updateRowsWithPersistedVariables', () => {
beforeEach(() => {
loadFixtures('projects/ci_cd_settings_with_variables.html.raw');
container = document.querySelector('.js-ci-variable-list-section');
const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section');
saveButton = ajaxVariableListEl.querySelector('.js-secret-variables-save-button');
errorBox = container.querySelector('.js-ci-variable-error-box');
ajaxVariableList = new AjaxFormVariableList({
container,
formField: 'variables',
saveButton,
errorBox,
saveEndpoint: container.dataset.saveEndpoint,
});
});
it('removes variable that was removed', () => {
expect(container.querySelectorAll('.js-row').length).toBe(3);
container.querySelector('.js-row-remove-button').click();
expect(container.querySelectorAll('.js-row').length).toBe(3);
ajaxVariableList.updateRowsWithPersistedVariables([]);
expect(container.querySelectorAll('.js-row').length).toBe(2);
});
it('updates new variable row with persisted ID', () => {
const row = container.querySelector('.js-row:last-child');
const idInput = row.querySelector('.js-ci-variable-input-id');
const keyInput = row.querySelector('.js-ci-variable-input-key');
const valueInput = row.querySelector('.js-ci-variable-input-value');
keyInput.value = 'foo';
keyInput.dispatchEvent(new Event('input'));
valueInput.value = 'bar';
valueInput.dispatchEvent(new Event('input'));
expect(idInput.value).toEqual('');
ajaxVariableList.updateRowsWithPersistedVariables([{
id: 3,
key: 'foo',
value: 'bar',
}]);
expect(idInput.value).toEqual('3');
expect(row.dataset.isPersisted).toEqual('true');
});
});
});
...@@ -4,6 +4,7 @@ import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper'; ...@@ -4,6 +4,7 @@ import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
describe('VariableList', () => { describe('VariableList', () => {
preloadFixtures('pipeline_schedules/edit.html.raw'); preloadFixtures('pipeline_schedules/edit.html.raw');
preloadFixtures('pipeline_schedules/edit_with_variables.html.raw'); preloadFixtures('pipeline_schedules/edit_with_variables.html.raw');
preloadFixtures('projects/ci_cd_settings.html.raw');
let $wrapper; let $wrapper;
let variableList; let variableList;
...@@ -105,37 +106,8 @@ describe('VariableList', () => { ...@@ -105,37 +106,8 @@ describe('VariableList', () => {
describe('with all inputs(key, value, protected)', () => { describe('with all inputs(key, value, protected)', () => {
beforeEach(() => { beforeEach(() => {
// This markup will be replaced with a fixture when we can render the loadFixtures('projects/ci_cd_settings.html.raw');
// CI/CD settings page with the new dynamic variable list in https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4110 $wrapper = $('.js-ci-variable-list-section');
$wrapper = $(`<form class="js-variable-list">
<ul>
<li class="js-row">
<div class="ci-variable-body-item">
<input class="js-ci-variable-input-key" name="variables[variables_attributes][][key]">
</div>
<div class="ci-variable-body-item">
<textarea class="js-ci-variable-input-value" name="variables[variables_attributes][][value]"></textarea>
</div>
<div class="ci-variable-body-item ci-variable-protected-item">
<button type="button" class="js-project-feature-toggle project-feature-toggle">
<input
type="hidden"
class="js-ci-variable-input-protected js-project-feature-toggle-input"
name="variables[variables_attributes][][protected]"
value="true"
/>
</button>
</div>
<button type="button" class="js-row-remove-button"></button>
</li>
</ul>
<button type="button" class="js-secret-value-reveal-button">
Reveal values
</button>
</form>`);
variableList = new VariableList({ variableList = new VariableList({
container: $wrapper, container: $wrapper,
...@@ -160,4 +132,51 @@ describe('VariableList', () => { ...@@ -160,4 +132,51 @@ describe('VariableList', () => {
.catch(done.fail); .catch(done.fail);
}); });
}); });
describe('toggleEnableRow method', () => {
beforeEach(() => {
loadFixtures('pipeline_schedules/edit_with_variables.html.raw');
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
container: $wrapper,
formField: 'variables',
});
variableList.init();
});
it('should disable all key inputs', () => {
expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3);
variableList.toggleEnableRow(false);
expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3);
});
it('should disable all remove buttons', () => {
expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3);
variableList.toggleEnableRow(false);
expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3);
});
it('should enable all remove buttons', () => {
variableList.toggleEnableRow(false);
expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3);
variableList.toggleEnableRow(true);
expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3);
});
it('should enable all key inputs', () => {
variableList.toggleEnableRow(false);
expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3);
variableList.toggleEnableRow(true);
expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3);
});
});
}); });
require 'spec_helper'
describe 'Groups (JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
let(:admin) { create(:admin) }
let(:group) { create(:group, name: 'frontend-fixtures-group' )}
render_views
before(:all) do
clean_frontend_fixtures('groups/')
end
before do
group.add_master(admin)
sign_in(admin)
end
describe Groups::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do
it 'groups/ci_cd_settings.html.raw' do |example|
get :show,
group_id: group
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
end
end
require 'spec_helper' require 'spec_helper'
describe ProjectsController, '(JavaScript fixtures)', type: :controller do describe 'Projects (JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers include JavaScriptFixturesHelpers
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, namespace: namespace, path: 'builds-project') } let(:project) { create(:project, namespace: namespace, path: 'builds-project') }
let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2') }
let!(:variable1) { create(:ci_variable, project: project_variable_populated) }
let!(:variable2) { create(:ci_variable, project: project_variable_populated) }
render_views render_views
...@@ -14,6 +17,9 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do ...@@ -14,6 +17,9 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do
end end
before do before do
# EE-specific start
# EE specific end
project.add_master(admin)
sign_in(admin) sign_in(admin)
end end
...@@ -21,12 +27,43 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do ...@@ -21,12 +27,43 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do
remove_repository(project) remove_repository(project)
end end
it 'projects/dashboard.html.raw' do |example| describe ProjectsController, '(JavaScript fixtures)', type: :controller do
get :show, it 'projects/dashboard.html.raw' do |example|
namespace_id: project.namespace.to_param, get :show,
id: project namespace_id: project.namespace.to_param,
id: project
expect(response).to be_success expect(response).to be_success
store_frontend_fixture(response, example.description) store_frontend_fixture(response, example.description)
end
it 'projects/edit.html.raw' do |example|
get :edit,
namespace_id: project.namespace.to_param,
id: project
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
end
describe Projects::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do
it 'projects/ci_cd_settings.html.raw' do |example|
get :show,
namespace_id: project.namespace.to_param,
project_id: project
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
it 'projects/ci_cd_settings_with_variables.html.raw' do |example|
get :show,
namespace_id: project_variable_populated.namespace.to_param,
project_id: project_variable_populated
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
end end
end end
shared_examples 'variable list' do
it 'shows list of variables' do
page.within('.js-ci-variable-list-section') do
expect(first('.js-ci-variable-input-key').value).to eq(variable.key)
end
end
it 'adds new secret variable' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('key')
find('.js-ci-variable-input-value').set('key value')
end
click_button('Save variables')
wait_for_requests
visit page_path
# We check the first row because it re-sorts to alphabetical order on refresh
page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
expect(find('.js-ci-variable-input-key').value).to eq('key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key value')
end
end
it 'adds empty variable' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('key')
find('.js-ci-variable-input-value').set('')
end
click_button('Save variables')
wait_for_requests
visit page_path
# We check the first row because it re-sorts to alphabetical order on refresh
page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
expect(find('.js-ci-variable-input-key').value).to eq('key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('')
end
end
it 'adds new unprotected variable' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('key')
find('.js-ci-variable-input-value').set('key value')
find('.ci-variable-protected-item .js-project-feature-toggle').click
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false')
end
click_button('Save variables')
wait_for_requests
visit page_path
# We check the first row because it re-sorts to alphabetical order on refresh
page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
expect(find('.js-ci-variable-input-key').value).to eq('key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key value')
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false')
end
end
it 'reveals and hides variables' do
page.within('.js-ci-variable-list-section') do
expect(first('.js-ci-variable-input-key').value).to eq(variable.key)
expect(first('.js-ci-variable-input-value', visible: false).value).to eq(variable.value)
expect(page).to have_content('*' * 20)
click_button('Reveal value')
expect(first('.js-ci-variable-input-key').value).to eq(variable.key)
expect(first('.js-ci-variable-input-value').value).to eq(variable.value)
expect(page).not_to have_content('*' * 20)
click_button('Hide value')
expect(first('.js-ci-variable-input-key').value).to eq(variable.key)
expect(first('.js-ci-variable-input-value', visible: false).value).to eq(variable.value)
expect(page).to have_content('*' * 20)
end
end
it 'deletes variable' do
page.within('.js-ci-variable-list-section') do
expect(page).to have_selector('.js-row', count: 2)
first('.js-row-remove-button').click
click_button('Save variables')
wait_for_requests
expect(page).to have_selector('.js-row', count: 1)
end
end
it 'edits variable' do
page.within('.js-ci-variable-list-section') do
click_button('Reveal value')
page.within('.js-row:nth-child(1)') do
find('.js-ci-variable-input-key').set('new_key')
find('.js-ci-variable-input-value').set('new_value')
end
click_button('Save variables')
wait_for_requests
visit page_path
page.within('.js-row:nth-child(1)') do
expect(find('.js-ci-variable-input-key').value).to eq('new_key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('new_value')
end
end
end
it 'edits variable with empty value' do
page.within('.js-ci-variable-list-section') do
click_button('Reveal value')
page.within('.js-row:nth-child(1)') do
find('.js-ci-variable-input-key').set('new_key')
find('.js-ci-variable-input-value').set('')
end
click_button('Save variables')
wait_for_requests
visit page_path
page.within('.js-row:nth-child(1)') do
expect(find('.js-ci-variable-input-key').value).to eq('new_key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('')
end
end
end
it 'edits variable to be protected' do
# Create the unprotected variable
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('unprotected_key')
find('.js-ci-variable-input-value').set('unprotected_value')
find('.ci-variable-protected-item .js-project-feature-toggle').click
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false')
end
click_button('Save variables')
wait_for_requests
visit page_path
# We check the first row because it re-sorts to alphabetical order on refresh
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
find('.ci-variable-protected-item .js-project-feature-toggle').click
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
end
click_button('Save variables')
wait_for_requests
visit page_path
# We check the first row because it re-sorts to alphabetical order on refresh
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
expect(find('.js-ci-variable-input-key').value).to eq('unprotected_key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('unprotected_value')
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
end
end
it 'edits variable to be unprotected' do
# Create the protected variable
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('protected_key')
find('.js-ci-variable-input-value').set('protected_value')
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
end
click_button('Save variables')
wait_for_requests
visit page_path
page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
find('.ci-variable-protected-item .js-project-feature-toggle').click
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false')
end
click_button('Save variables')
wait_for_requests
visit page_path
page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
expect(find('.js-ci-variable-input-key').value).to eq('protected_key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('protected_value')
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false')
end
end
it 'handles multiple edits and deletion in the middle' do
page.within('.js-ci-variable-list-section') do
# Create 2 variables
page.within('.js-row:last-child') do
find('.js-ci-variable-input-key').set('akey')
find('.js-ci-variable-input-value').set('akeyvalue')
end
page.within('.js-row:last-child') do
find('.js-ci-variable-input-key').set('zkey')
find('.js-ci-variable-input-value').set('zkeyvalue')
end
click_button('Save variables')
wait_for_requests
expect(page).to have_selector('.js-row', count: 4)
# Remove the `akey` variable
page.within('.js-row:nth-child(2)') do
first('.js-row-remove-button').click
end
# Add another variable
page.within('.js-row:last-child') do
find('.js-ci-variable-input-key').set('ckey')
find('.js-ci-variable-input-value').set('ckeyvalue')
end
click_button('Save variables')
wait_for_requests
visit page_path
# Expect to find 3 variables(4 rows) in alphbetical order
expect(page).to have_selector('.js-row', count: 4)
row_keys = all('.js-ci-variable-input-key')
expect(row_keys[0].value).to eq('ckey')
expect(row_keys[1].value).to eq('test_key')
expect(row_keys[2].value).to eq('zkey')
expect(row_keys[3].value).to eq('')
end
end
it 'shows validation error box about duplicate keys' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('samekey')
find('.js-ci-variable-input-value').set('value1')
end
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('samekey')
find('.js-ci-variable-input-value').set('value2')
end
click_button('Save variables')
wait_for_requests
# We check the first row because it re-sorts to alphabetical order on refresh
page.within('.js-ci-variable-list-section') do
expect(find('.js-ci-variable-error-box')).to have_content('Variables key has already been taken')
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment