From 3be32027b6c543287b94b5be34bf53039d86f88c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 16 Jan 2018 15:13:14 -0600 Subject: [PATCH] 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 --- .../ci_variable_list/ajax_variable_list.js | 116 ++++++++ .../ci_variable_list/ci_variable_list.js | 27 +- .../javascripts/commons/polyfills/element.js | 19 ++ .../javascripts/lib/utils/http_status.js | 2 + .../pages/groups/settings/ci_cd/show/index.js | 17 +- .../projects/settings/ci_cd/show/index.js | 17 +- .../framework/ci_variable_list.scss | 11 +- app/views/ci/variables/_content.html.haml | 4 +- app/views/ci/variables/_form.html.haml | 19 -- app/views/ci/variables/_index.html.haml | 34 ++- app/views/ci/variables/_show.html.haml | 9 - app/views/ci/variables/_table.html.haml | 32 --- .../groups/settings/ci_cd/show.html.haml | 9 +- app/views/groups/variables/show.html.haml | 1 - .../projects/settings/ci_cd/show.html.haml | 8 +- app/views/projects/variables/show.html.haml | 1 - .../39118-dynamic-pipeline-variables-fe.yml | 6 + doc/ci/variables/img/secret_variables.png | Bin 15658 -> 32886 bytes qa/qa/factory/resource/secret_variable.rb | 2 +- .../page/project/settings/secret_variables.rb | 43 ++- spec/features/group_variables_spec.rb | 69 +---- spec/features/project_variables_spec.rb | 18 ++ spec/features/variables_spec.rb | 145 ---------- .../ajax_variable_list_spec.js | 189 ++++++++++++ .../ci_variable_list/ci_variable_list_spec.js | 81 ++++-- spec/javascripts/fixtures/groups.rb | 29 ++ spec/javascripts/fixtures/projects.rb | 51 +++- .../features/variable_list_shared_examples.rb | 269 ++++++++++++++++++ 28 files changed, 845 insertions(+), 383 deletions(-) create mode 100644 app/assets/javascripts/ci_variable_list/ajax_variable_list.js delete mode 100644 app/views/ci/variables/_form.html.haml delete mode 100644 app/views/ci/variables/_show.html.haml delete mode 100644 app/views/ci/variables/_table.html.haml delete mode 100644 app/views/groups/variables/show.html.haml delete mode 100644 app/views/projects/variables/show.html.haml create mode 100644 changelogs/unreleased-ee/39118-dynamic-pipeline-variables-fe.yml create mode 100644 spec/features/project_variables_spec.rb delete mode 100644 spec/features/variables_spec.rb create mode 100644 spec/javascripts/ci_variable_list/ajax_variable_list_spec.js create mode 100644 spec/javascripts/fixtures/groups.rb create mode 100644 spec/support/features/variable_list_shared_examples.rb diff --git a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js new file mode 100644 index 00000000000..76f93e5c6bd --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js @@ -0,0 +1,116 @@ +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 => ` +
  • + ${_.escape(errorString)} +
  • + `); + + return ` +

    + ${s__('CiVariable|Validation failed')} +

    + + `; +} + +// 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'); + } + } + }); + } +} diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js index e46478ddb98..d91789c2192 100644 --- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js @@ -11,7 +11,7 @@ function createEnvironmentItem(value) { return { title: value === '*' ? ALL_ENVIRONMENTS_STRING : value, id: value, - text: value, + text: value === '*' ? s__('CiVariable|* (All environments)') : value, }; } @@ -41,11 +41,11 @@ export default class VariableList { selector: '.js-ci-variable-input-protected', default: 'true', }, - environment: { + environment_scope: { // We can't use a `.js-` class here because // gl_dropdown replaces the and doesn't copy over the class // 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: '*', }, _destroy: { @@ -104,12 +104,15 @@ export default class VariableList { setupToggleButtons($row[0]); + // Reset the resizable textarea + $row.find(this.inputMap.value.selector).css('height', ''); + const $environmentSelect = $row.find('.js-variable-environment-toggle'); if ($environmentSelect.length) { const createItemDropdown = new CreateItemDropdown({ $dropdown: $environmentSelect, defaultToggleLabel: ALL_ENVIRONMENTS_STRING, - fieldName: `${this.formField}[variables_attributes][][environment]`, + fieldName: `${this.formField}[variables_attributes][][environment_scope]`, getData: (term, callback) => callback(this.getEnvironmentValues()), createNewItemFromValue: createEnvironmentItem, onSelect: () => { @@ -117,7 +120,7 @@ export default class VariableList { // so they have the new value we just picked 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 { $row.after($rowClone); } - removeRow($row) { + removeRow(row) { + const $row = $(row); const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted')); if (isPersisted) { @@ -155,6 +159,10 @@ export default class VariableList { } else { $row.remove(); } + + // Refresh the other dropdowns in the variable list + // so any value with the variable deleted is gone + this.refreshDropdownData(); } checkIfRowTouched($row) { @@ -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() { // Ignore the last empty row because we don't want to try persist // a blank variable and run into validation problems. @@ -185,7 +198,7 @@ export default class VariableList { } 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) => ({ ...prevValueMap, [envInput.value]: envInput.value, diff --git a/app/assets/javascripts/commons/polyfills/element.js b/app/assets/javascripts/commons/polyfills/element.js index 9a1f73bf2ac..b593bde6aa2 100644 --- a/app/assets/javascripts/commons/polyfills/element.js +++ b/app/assets/javascripts/commons/polyfills/element.js @@ -18,3 +18,22 @@ Element.prototype.matches = Element.prototype.matches || while (i >= 0 && elms.item(i) !== this) { 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]); diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js index 625e53ee9de..bb151929431 100644 --- a/app/assets/javascripts/lib/utils/http_status.js +++ b/app/assets/javascripts/lib/utils/http_status.js @@ -6,4 +6,6 @@ export default { ABORTED: 0, NO_CONTENT: 204, OK: 200, + MULTIPLE_CHOICES: 300, + BAD_REQUEST: 400, }; diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js index f26c7360fbe..ad79f7e09ac 100644 --- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js @@ -1,11 +1,12 @@ -import SecretValues from '~/behaviors/secret_values'; +import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; export default () => { - const secretVariableTable = document.querySelector('.js-secret-variable-table'); - if (secretVariableTable) { - const secretVariableTableValues = new SecretValues({ - container: secretVariableTable, - }); - secretVariableTableValues.init(); - } + const variableListEl = document.querySelector('.js-ci-variable-list-section'); + // eslint-disable-next-line no-new + new AjaxVariableList({ + container: variableListEl, + saveButton: variableListEl.querySelector('.js-secret-variables-save-button'), + errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), + saveEndpoint: variableListEl.dataset.saveEndpoint, + }); }; diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js index 18dc1dc03a5..a563d0f9961 100644 --- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -1,9 +1,11 @@ import initSettingsPanels from '~/settings_panels'; import SecretValues from '~/behaviors/secret_values'; +import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; export default function () { // Initialize expandable settings panels initSettingsPanels(); + const runnerToken = document.querySelector('.js-secret-runner-token'); if (runnerToken) { const runnerTokenSecretValue = new SecretValues({ @@ -12,11 +14,12 @@ export default function () { runnerTokenSecretValue.init(); } - const secretVariableTable = document.querySelector('.js-secret-variable-table'); - if (secretVariableTable) { - const secretVariableTableValues = new SecretValues({ - container: secretVariableTable, - }); - secretVariableTableValues.init(); - } + const variableListEl = document.querySelector('.js-ci-variable-list-section'); + // eslint-disable-next-line no-new + new AjaxVariableList({ + container: variableListEl, + saveButton: variableListEl.querySelector('.js-secret-variables-save-button'), + errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), + saveEndpoint: variableListEl.dataset.saveEndpoint, + }); } diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss index 8f654ab363c..ccd36af071f 100644 --- a/app/assets/stylesheets/framework/ci_variable_list.scss +++ b/app/assets/stylesheets/framework/ci_variable_list.scss @@ -8,7 +8,11 @@ .ci-variable-row { display: flex; - align-items: flex-end; + align-items: flex-start; + + @media (max-width: $screen-xs-max) { + align-items: flex-end; + } &:not(:last-child) { margin-bottom: $gl-btn-padding; @@ -41,6 +45,7 @@ .ci-variable-row-body { display: flex; + align-items: flex-start; width: 100%; @media (max-width: $screen-xs-max) { @@ -85,4 +90,8 @@ outline: none; color: $gl-text-color; } + + &[disabled] { + color: $gl-text-color-disabled; + } } diff --git a/app/views/ci/variables/_content.html.haml b/app/views/ci/variables/_content.html.haml index fbfe3e56588..d355e7799df 100644 --- a/app/views/ci/variables/_content.html.haml +++ b/app/views/ci/variables/_content.html.haml @@ -1,3 +1 @@ -%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.') diff --git a/app/views/ci/variables/_form.html.haml b/app/views/ci/variables/_form.html.haml deleted file mode 100644 index eebd0955c80..00000000000 --- a/app/views/ci/variables/_form.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -= 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" diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml index 6e399fc7392..e402801a776 100644 --- a/app/views/ci/variables/_index.html.haml +++ b/app/views/ci/variables/_index.html.haml @@ -1,16 +1,20 @@ -.row.prepend-top-default.append-bottom-default - .col-lg-12 - %h5.prepend-top-0 - Add a variable - = render "ci/variables/form", btn_text: "Add new variable" - %hr - %h5.prepend-top-0 - Your variables (#{@variables.size}) - - if @variables.empty? - %p.settings-message.text-center.append-bottom-0 - No variables found, add one with the form above. - - else - .js-secret-variable-table - = render "ci/variables/table" - %button.btn.btn-info.js-secret-value-reveal-button{ data: { secret_reveal_status: 'false' } } +- save_endpoint = local_assigns.fetch(:save_endpoint, nil) + +.row + .col-lg-12.js-ci-variable-list-section{ data: { save_endpoint: save_endpoint } } + .hide.alert.alert-danger.js-ci-variable-error-box + + %ul.ci-variable-list + - @variables.each.each do |variable| + = render 'ci/variables/variable_row', form_field: 'variables', variable: variable + = render 'ci/variables/variable_row', form_field: 'variables' + .prepend-top-20 + %button.btn.btn-success.js-secret-variables-save-button{ type: 'button' } + %span.hide.js-secret-variables-save-loading-icon + = icon('spinner spin') + = _('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) diff --git a/app/views/ci/variables/_show.html.haml b/app/views/ci/variables/_show.html.haml deleted file mode 100644 index 6d75ae96124..00000000000 --- a/app/views/ci/variables/_show.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- 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" diff --git a/app/views/ci/variables/_table.html.haml b/app/views/ci/variables/_table.html.haml deleted file mode 100644 index 2298930d0c7..00000000000 --- a/app/views/ci/variables/_table.html.haml +++ /dev/null @@ -1,32 +0,0 @@ -.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") diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml index 472da2a6a72..dd82922ec55 100644 --- a/app/views/groups/settings/ci_cd/show.html.haml +++ b/app/views/groups/settings/ci_cd/show.html.haml @@ -1,4 +1,11 @@ - breadcrumb_title "CI / CD Settings" - 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 diff --git a/app/views/groups/variables/show.html.haml b/app/views/groups/variables/show.html.haml deleted file mode 100644 index df533952b76..00000000000 --- a/app/views/groups/variables/show.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render 'ci/variables/show' diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 664a4554692..756f31f91d9 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -29,14 +29,14 @@ %section.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 - Secret variables - = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank' + = _('Secret variables') + = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer' %button.btn.js-settings-toggle = expanded ? 'Collapse' : 'Expand' - %p + %p.append-bottom-0 = render "ci/variables/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) } .settings-header diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml deleted file mode 100644 index df533952b76..00000000000 --- a/app/views/projects/variables/show.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render 'ci/variables/show' diff --git a/changelogs/unreleased-ee/39118-dynamic-pipeline-variables-fe.yml b/changelogs/unreleased-ee/39118-dynamic-pipeline-variables-fe.yml new file mode 100644 index 00000000000..a38b447e345 --- /dev/null +++ b/changelogs/unreleased-ee/39118-dynamic-pipeline-variables-fe.yml @@ -0,0 +1,6 @@ +--- +title: Update CI/CD secret variables list to be dynamic and save without reloading + the page +merge_request: 4110 +author: +type: added diff --git a/doc/ci/variables/img/secret_variables.png b/doc/ci/variables/img/secret_variables.png index f70935069d9604009d5e0458b63343da0dbf7e8c..3c1aa361dc2b38759fa6619dcd2c8ffc788b8929 100644 GIT binary patch literal 32886 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 literal 15658 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 { + 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'); + }); + }); +}); diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js index 0170ab458d4..6ab7b50e035 100644 --- a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js @@ -4,6 +4,7 @@ import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper'; describe('VariableList', () => { preloadFixtures('pipeline_schedules/edit.html.raw'); preloadFixtures('pipeline_schedules/edit_with_variables.html.raw'); + preloadFixtures('projects/ci_cd_settings.html.raw'); let $wrapper; let variableList; @@ -105,37 +106,8 @@ describe('VariableList', () => { describe('with all inputs(key, value, protected)', () => { beforeEach(() => { - // This markup will be replaced with a fixture when we can render the - // CI/CD settings page with the new dynamic variable list in https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4110 - $wrapper = $(`
    -
      -
    • -
      - -
      - -
      - -
      - -
      - -
      - - -
    • -
    - -
    `); + loadFixtures('projects/ci_cd_settings.html.raw'); + $wrapper = $('.js-ci-variable-list-section'); variableList = new VariableList({ container: $wrapper, @@ -160,4 +132,51 @@ describe('VariableList', () => { .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); + }); + }); }); diff --git a/spec/javascripts/fixtures/groups.rb b/spec/javascripts/fixtures/groups.rb new file mode 100644 index 00000000000..35be52fbf97 --- /dev/null +++ b/spec/javascripts/fixtures/groups.rb @@ -0,0 +1,29 @@ +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 diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb index 2a100e7fab5..b344b389241 100644 --- a/spec/javascripts/fixtures/projects.rb +++ b/spec/javascripts/fixtures/projects.rb @@ -1,11 +1,14 @@ require 'spec_helper' -describe ProjectsController, '(JavaScript fixtures)', type: :controller do +describe 'Projects (JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers let(:admin) { create(:admin) } let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} 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 @@ -14,6 +17,9 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do end before do + # EE-specific start + # EE specific end + project.add_master(admin) sign_in(admin) end @@ -21,12 +27,43 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do remove_repository(project) end - it 'projects/dashboard.html.raw' do |example| - get :show, - namespace_id: project.namespace.to_param, - id: project + describe ProjectsController, '(JavaScript fixtures)', type: :controller do + it 'projects/dashboard.html.raw' do |example| + get :show, + namespace_id: project.namespace.to_param, + id: project - expect(response).to be_success - store_frontend_fixture(response, example.description) + expect(response).to be_success + 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 diff --git a/spec/support/features/variable_list_shared_examples.rb b/spec/support/features/variable_list_shared_examples.rb new file mode 100644 index 00000000000..ea4e8386eff --- /dev/null +++ b/spec/support/features/variable_list_shared_examples.rb @@ -0,0 +1,269 @@ +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 -- GitLab