Commit 5576214d authored by Eric Eastwood's avatar Eric Eastwood Committed by Shinya Maeda

Schedule pipelines with variables

Fix https://gitlab.com/gitlab-org/gitlab-ce/issues/32568
parent d7cd3c36
...@@ -3,6 +3,7 @@ import Translate from '../vue_shared/translate'; ...@@ -3,6 +3,7 @@ import Translate from '../vue_shared/translate';
import intervalPatternInput from './components/interval_pattern_input.vue'; import intervalPatternInput from './components/interval_pattern_input.vue';
import TimezoneDropdown from './components/timezone_dropdown'; import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown'; import TargetBranchDropdown from './components/target_branch_dropdown';
import { setupPipelineVariableList } from './setup_pipeline_variable_list';
Vue.use(Translate); Vue.use(Translate);
...@@ -39,4 +40,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -39,4 +40,6 @@ document.addEventListener('DOMContentLoaded', () => {
gl.timezoneDropdown = new TimezoneDropdown(); gl.timezoneDropdown = new TimezoneDropdown();
gl.targetBranchDropdown = new TargetBranchDropdown(); gl.targetBranchDropdown = new TargetBranchDropdown();
gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement); gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement);
setupPipelineVariableList($('.js-pipeline-variable-list'));
}); });
function insertRow($row) {
const $rowClone = $row.clone();
$rowClone.removeAttr('data-is-persisted');
$rowClone.find('input, textarea').val('');
$row.after($rowClone);
}
function removeRow($row) {
const isPersisted = gl.utils.convertPermissionToBoolean($row.attr('data-is-persisted'));
if (isPersisted) {
$row.hide();
$row
.find('.js-destroy-input')
.val(1);
} else {
$row.remove();
}
}
function checkIfRowTouched($row) {
return $row.find('.js-user-input').toArray().some(el => $(el).val().length > 0);
}
function setupPipelineVariableList(parent = document) {
const $parent = $(parent);
$parent.on('click', '.js-row-remove-button', (e) => {
const $row = $(e.currentTarget).closest('.js-row');
removeRow($row);
e.preventDefault();
});
// Remove any empty rows except the last r
$parent.on('blur', '.js-user-input', (e) => {
const $row = $(e.currentTarget).closest('.js-row');
const isTouched = checkIfRowTouched($row);
if ($row.is(':not(:last-child)') && !isTouched) {
removeRow($row);
}
});
// Always make sure there is an empty last row
$parent.on('input', '.js-user-input', () => {
const $lastRow = $parent.find('.js-row').last();
const isTouched = checkIfRowTouched($lastRow);
if (isTouched) {
insertRow($lastRow);
}
});
// Clear out the empty last row so it
// doesn't get submitted and throw validation errors
$parent.closest('form').on('submit', () => {
const $lastRow = $parent.find('.js-row').last();
const isTouched = checkIfRowTouched($lastRow);
if (!isTouched) {
$lastRow.find('input, textarea').attr('name', '');
}
});
}
export {
setupPipelineVariableList,
insertRow,
removeRow,
};
...@@ -574,6 +574,12 @@ $stage-hover-bg: #eaf3fc; ...@@ -574,6 +574,12 @@ $stage-hover-bg: #eaf3fc;
$stage-hover-border: #d1e7fc; $stage-hover-border: #d1e7fc;
$action-icon-color: #d6d6d6; $action-icon-color: #d6d6d6;
/*
Pipeline Schedules
*/
$pipeline-variable-remove-button-width: calc(1em + #{2 * $gl-padding});
/* /*
Filtered Search Filtered Search
*/ */
......
...@@ -74,3 +74,65 @@ ...@@ -74,3 +74,65 @@
margin-right: 3px; margin-right: 3px;
} }
} }
.pipeline-variable-list {
margin-left: 0;
margin-bottom: 0;
padding-left: 0;
}
.pipeline-variable-row {
display: flex;
&:not(:last-child) {
margin-bottom: $gl-btn-padding;
}
@media (max-width: $screen-xs-max) {
flex-wrap: wrap;
}
&:last-child {
& > .pipeline-variable-row-remove-button {
display: none;
}
& > .pipeline-variable-value-input {
margin-right: $pipeline-variable-remove-button-width;
}
}
}
.pipeline-variable-key-input {
margin-right: $gl-btn-padding;
@media (max-width: $screen-xs-max) {
margin-right: $pipeline-variable-remove-button-width;
margin-bottom: $gl-btn-padding;
}
}
.pipeline-variable-value-input {
@media (max-width: $screen-xs-max) {
flex: 1;
}
}
.pipeline-variable-row-remove-button {
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
width: $pipeline-variable-remove-button-width;
padding: 0;
background: transparent;
border: 0;
color: $gl-text-color-secondary;
@include transition(color);
&:hover,
&:focus {
outline: none;
color: $gl-text-color;
}
}
...@@ -22,6 +22,15 @@ ...@@ -22,6 +22,15 @@
= f.label :ref, _('Target Branch'), class: 'label-light' = f.label :ref, _('Target Branch'), class: 'label-light'
= dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) = dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } )
= f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true = f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true
.form-group
.col-md-9
%label.label-light
#{ _('Variables') }
%ul.js-pipeline-variable-list.pipeline-variable-list
- if @schedule.variables.present?
- @schedule.variables.each_with_index do |variable, i|
= render 'variable_row', id: variable.id, key: variable.key, value: variable.value
= render 'variable_row'
.form-group .form-group
.col-md-9 .col-md-9
= f.label :active, s_('PipelineSchedules|Activated'), class: 'label-light' = f.label :active, s_('PipelineSchedules|Activated'), class: 'label-light'
......
- id = local_assigns.fetch(:id, nil)
- key = local_assigns.fetch(:key, "")
- value = local_assigns.fetch(:value, "")
%li.js-row.pipeline-variable-row{ data: { is_persisted: "#{!id.nil?}" } }
%input{ type: "hidden", name: "schedule[variables_attributes][][id]", value: id }
%input.js-destroy-input{ type: "hidden", name: "schedule[variables_attributes][][_destroy]" }
%input.js-user-input.pipeline-variable-key-input.form-control{ type: "text",
name: "schedule[variables_attributes][][key]",
value: key,
placeholder: _('Input variable key') }
%textarea.js-user-input.pipeline-variable-value-input.form-control{ rows: 1,
name: "schedule[variables_attributes][][value]",
placeholder: _('Input variable value') }
= value
%button.js-row-remove-button.pipeline-variable-row-remove-button{ 'aria-label': _('Remove variable row') }
%i.fa.fa-minus-circle{ 'aria-hidden': "true" }
...@@ -98,6 +98,15 @@ feature 'Pipeline Schedules', :feature do ...@@ -98,6 +98,15 @@ feature 'Pipeline Schedules', :feature do
expect(page).to have_content('This field is required') expect(page).to have_content('This field is required')
end end
it 'sets a variable' do
fill_in_schedule_form
fill_in_variable
save_pipeline_schedule
expect(Ci::PipelineSchedule.last.job_variables).to eq([{ key: 'foo', value: 'bar', public: false }])
end
end end
describe 'PATCH /projects/pipelines_schedules/:id/edit', js: true do describe 'PATCH /projects/pipelines_schedules/:id/edit', js: true do
...@@ -120,6 +129,14 @@ feature 'Pipeline Schedules', :feature do ...@@ -120,6 +129,14 @@ feature 'Pipeline Schedules', :feature do
expect(page).to have_content('my brand new description') expect(page).to have_content('my brand new description')
end end
it 'adds a new variable' do
fill_in_variable
save_pipeline_schedule
expect(Ci::PipelineSchedule.last.job_variables).to eq([{ key: 'foo', value: 'bar', public: false }])
end
context 'when ref is nil' do context 'when ref is nil' do
before do before do
pipeline_schedule.update_attribute(:ref, nil) pipeline_schedule.update_attribute(:ref, nil)
...@@ -132,6 +149,40 @@ feature 'Pipeline Schedules', :feature do ...@@ -132,6 +149,40 @@ feature 'Pipeline Schedules', :feature do
end end
end end
end end
context 'when variables already exist' do
before do
create(:ci_pipeline_schedule_variable, key: 'some_key', value: 'some_value', pipeline_schedule: pipeline_schedule)
edit_pipeline_schedule
end
it 'edits existing variable' do
expect(first('[name="schedule[variables_attributes][][key]"]').value).to eq('some_key')
expect(first('[name="schedule[variables_attributes][][value]"]').value).to eq('some_value')
fill_in_variable
save_pipeline_schedule
expect(Ci::PipelineSchedule.last.job_variables).to eq([{ key: 'foo', value: 'bar', public: false }])
end
it 'removes an existing variable' do
remove_variable
save_pipeline_schedule
expect(Ci::PipelineSchedule.last.job_variables).to eq([])
end
it 'adds another variable' do
fill_in_variable(1)
save_pipeline_schedule
expect(Ci::PipelineSchedule.last.job_variables).to eq([
{ key: 'some_key', value: 'some_value', public: false },
{ key: 'foo', value: 'bar', public: false }
])
end
end
end end
def visit_new_pipeline_schedule def visit_new_pipeline_schedule
...@@ -160,6 +211,15 @@ feature 'Pipeline Schedules', :feature do ...@@ -160,6 +211,15 @@ feature 'Pipeline Schedules', :feature do
click_button 'Save pipeline schedule' click_button 'Save pipeline schedule'
end end
def fill_in_variable(index = 0)
all('[name="schedule[variables_attributes][][key]"]')[index].set('foo')
all('[name="schedule[variables_attributes][][value]"]')[index].set('bar')
end
def remove_variable
first('.js-pipeline-variable-list .js-row-remove-button').click
end
def fill_in_schedule_form def fill_in_schedule_form
fill_in 'schedule_description', with: 'my fancy description' fill_in 'schedule_description', with: 'my fancy description'
fill_in 'schedule_cron', with: '* 1 2 3 4' fill_in 'schedule_cron', with: '* 1 2 3 4'
......
import {
setupPipelineVariableList,
insertRow,
removeRow,
} from '~/pipeline_schedules/setup_pipeline_variable_list';
describe('Pipeline Variable List', () => {
let $markup;
describe('insertRow', () => {
it('should insert another row', () => {
$markup = $(`<div>
<li class="js-row">
<input>
<textarea></textarea>
</li>
</div>`);
insertRow($markup.find('.js-row'));
expect($markup.find('.js-row').length).toBe(2);
});
it('should clear `data-is-persisted` on cloned row', () => {
$markup = $(`<div>
<li class="js-row" data-is-persisted="true"></li>
</div>`);
insertRow($markup.find('.js-row'));
const $lastRow = $markup.find('.js-row').last();
expect($lastRow.attr('data-is-persisted')).toBe(undefined);
});
it('should clear inputs on cloned row', () => {
$markup = $(`<div>
<li class="js-row">
<input value="foo">
<textarea>bar</textarea>
</li>
</div>`);
insertRow($markup.find('.js-row'));
const $lastRow = $markup.find('.js-row').last();
expect($lastRow.find('input').val()).toBe('');
expect($lastRow.find('textarea').val()).toBe('');
});
});
describe('removeRow', () => {
it('should remove dynamic row', () => {
$markup = $(`<div>
<li class="js-row">
<input>
<textarea></textarea>
</li>
</div>`);
removeRow($markup.find('.js-row'));
expect($markup.find('.js-row').length).toBe(0);
});
it('should hide and mark to destroy with already persisted rows', () => {
$markup = $(`<div>
<li class="js-row" data-is-persisted="true">
<input class="js-destroy-input">
</li>
</div>`);
const $row = $markup.find('.js-row');
removeRow($row);
expect($row.find('.js-destroy-input').val()).toBe('1');
expect($markup.find('.js-row').length).toBe(1);
});
});
describe('setupPipelineVariableList', () => {
beforeEach(() => {
$markup = $(`<form>
<li class="js-row">
<input class="js-user-input" name="schedule[variables_attributes][][key]">
<textarea class="js-user-input" name="schedule[variables_attributes][][value]"></textarea>
<button class="js-row-remove-button"></button>
<button class="js-row-add-button"></button>
</li>
</form>`);
setupPipelineVariableList($markup);
});
it('should remove the row when clicking the remove button', () => {
$markup.find('.js-row-remove-button').trigger('click');
expect($markup.find('.js-row').length).toBe(0);
});
it('should add another row when editing the last rows key input', () => {
const $row = $markup.find('.js-row');
$row.find('input.js-user-input')
.val('foo')
.trigger('input');
expect($markup.find('.js-row').length).toBe(2);
});
it('should add another row when editing the last rows value textarea', () => {
const $row = $markup.find('.js-row');
$row.find('textarea.js-user-input')
.val('foo')
.trigger('input');
expect($markup.find('.js-row').length).toBe(2);
});
it('should remove empty row after blurring', () => {
const $row = $markup.find('.js-row');
$row.find('input.js-user-input')
.val('foo')
.trigger('input');
expect($markup.find('.js-row').length).toBe(2);
$row.find('input.js-user-input')
.val('')
.trigger('input')
.trigger('blur');
expect($markup.find('.js-row').length).toBe(1);
});
it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => {
const $row = $markup.find('.js-row');
expect($row.find('input').attr('name')).toBe('schedule[variables_attributes][][key]');
expect($row.find('textarea').attr('name')).toBe('schedule[variables_attributes][][value]');
$markup.filter('form').submit();
expect($row.find('input').attr('name')).toBe('');
expect($row.find('textarea').attr('name')).toBe('');
});
});
});
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