Commit f6e9762d authored by Kev's avatar Kev Committed by Jose Ivan Vargas

Allow tags as target of pipeline scheduled

Changelog: changed
parent 6f86346d
import $ from 'jquery';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class TargetBranchDropdown {
constructor() {
this.$dropdown = $('.js-target-branch-dropdown');
this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text');
this.$input = $('#schedule_ref');
this.initDefaultBranch();
this.initDropdown();
}
initDropdown() {
initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.formatBranchesList(),
filterable: true,
selectable: true,
toggleLabel: (item) => item.name,
search: {
fields: ['name'],
},
clicked: (cfg) => this.updateInputValue(cfg),
text: (item) => item.name,
});
this.setDropdownToggle();
}
formatBranchesList() {
return this.$dropdown.data('data').map((val) => ({ name: val }));
}
setDropdownToggle() {
const initialValue = this.$input.val();
this.$dropdownToggle.text(initialValue);
}
initDefaultBranch() {
const initialValue = this.$input.val();
const defaultBranch = this.$dropdown.data('defaultBranch');
if (!initialValue) {
this.$input.val(defaultBranch);
}
}
updateInputValue({ selectedObj, e }) {
e.preventDefault();
this.$input.val(selectedObj.name);
gl.pipelineScheduleFieldErrors.updateFormValidityState();
}
}
import $ from 'jquery';
import Vue from 'vue';
import { __ } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
import setupNativeFormVariableList from '../../../../ci_variable_list/native_form_variable_list';
import GlFieldErrors from '../../../../gl_field_errors';
import Translate from '../../../../vue_shared/translate';
import intervalPatternInput from './components/interval_pattern_input.vue';
import TargetBranchDropdown from './components/target_branch_dropdown';
import TimezoneDropdown from './components/timezone_dropdown';
Vue.use(Translate);
......@@ -30,6 +32,52 @@ function initIntervalPatternInput() {
});
}
function getEnabledRefTypes() {
const refTypes = [REF_TYPE_BRANCHES];
if (gon.features.pipelineSchedulesWithTags) {
refTypes.push(REF_TYPE_TAGS);
}
return refTypes;
}
function initTargetRefDropdown() {
const $refField = document.getElementById('schedule_ref');
const el = document.querySelector('.js-target-ref-dropdown');
const { projectId, defaultBranch } = el.dataset;
if (!$refField.value) {
$refField.value = defaultBranch;
}
const refDropdown = new Vue({
el,
render(h) {
return h(RefSelector, {
props: {
enabledRefTypes: getEnabledRefTypes(),
projectId,
value: $refField.value,
useSymbolicRefNames: true,
translations: {
dropdownHeader: gon.features.pipelineSchedulesWithTags
? __('Select target branch or tag')
: __('Select target branch'),
},
},
class: 'gl-w-full',
});
},
});
refDropdown.$children[0].$on('input', (newRef) => {
$refField.value = newRef;
});
return refDropdown;
}
export default () => {
/* Most of the form is written in haml, but for fields with more complex behaviors,
* you should mount individual Vue components here. If at some point components need
......@@ -48,9 +96,10 @@ export default () => {
gl.pipelineScheduleFieldErrors.updateFormValidityState();
},
});
gl.targetBranchDropdown = new TargetBranchDropdown();
gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement);
initTargetRefDropdown();
setupNativeFormVariableList({
container: $('.js-ci-variable-list-section'),
formField: 'schedule',
......
......@@ -58,6 +58,11 @@ export default {
required: false,
default: () => ({}),
},
useSymbolicRefNames: {
type: Boolean,
required: false,
default: false,
},
/** The validation state of this component. */
state: {
......@@ -121,8 +126,15 @@ export default {
query: this.lastQuery,
};
},
selectedRefForDisplay() {
if (this.useSymbolicRefNames && this.selectedRef) {
return this.selectedRef.replace(/^refs\/(tags|heads)\//, '');
}
return this.selectedRef;
},
buttonText() {
return this.selectedRef || this.i18n.noRefSelected;
return this.selectedRefForDisplay || this.i18n.noRefSelected;
},
},
watch: {
......@@ -164,9 +176,20 @@ export default {
},
{ immediate: true },
);
this.$watch(
'useSymbolicRefNames',
() => this.setUseSymbolicRefNames(this.useSymbolicRefNames),
{ immediate: true },
);
},
methods: {
...mapActions(['setEnabledRefTypes', 'setProjectId', 'setSelectedRef']),
...mapActions([
'setEnabledRefTypes',
'setUseSymbolicRefNames',
'setProjectId',
'setSelectedRef',
]),
...mapActions({ storeSearch: 'search' }),
focusSearchBox() {
this.$refs.searchBox.$el.querySelector('input').focus();
......
......@@ -5,6 +5,9 @@ import * as types from './mutation_types';
export const setEnabledRefTypes = ({ commit }, refTypes) =>
commit(types.SET_ENABLED_REF_TYPES, refTypes);
export const setUseSymbolicRefNames = ({ commit }, useSymbolicRefNames) =>
commit(types.SET_USE_SYMBOLIC_REF_NAMES, useSymbolicRefNames);
export const setProjectId = ({ commit }, projectId) => commit(types.SET_PROJECT_ID, projectId);
export const setSelectedRef = ({ commit }, selectedRef) =>
......
export const SET_ENABLED_REF_TYPES = 'SET_ENABLED_REF_TYPES';
export const SET_USE_SYMBOLIC_REF_NAMES = 'SET_USE_SYMBOLIC_REF_NAMES';
export const SET_PROJECT_ID = 'SET_PROJECT_ID';
export const SET_SELECTED_REF = 'SET_SELECTED_REF';
......
......@@ -7,6 +7,9 @@ export default {
[types.SET_ENABLED_REF_TYPES](state, refTypes) {
state.enabledRefTypes = refTypes;
},
[types.SET_USE_SYMBOLIC_REF_NAMES](state, useSymbolicRefNames) {
state.useSymbolicRefNames = useSymbolicRefNames;
},
[types.SET_PROJECT_ID](state, projectId) {
state.projectId = projectId;
},
......@@ -28,6 +31,7 @@ export default {
state.matches.branches = {
list: convertObjectPropsToCamelCase(response.data).map((b) => ({
name: b.name,
value: state.useSymbolicRefNames ? `refs/heads/${b.name}` : undefined,
default: b.default,
})),
totalCount: parseInt(response.headers[X_TOTAL_HEADER], 10),
......@@ -46,6 +50,7 @@ export default {
state.matches.tags = {
list: convertObjectPropsToCamelCase(response.data).map((b) => ({
name: b.name,
value: state.useSymbolicRefNames ? `refs/tags/${b.name}` : undefined,
})),
totalCount: parseInt(response.headers[X_TOTAL_HEADER], 10),
error: null,
......
......@@ -10,6 +10,10 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
before_action :authorize_update_pipeline_schedule!, except: [:index, :new, :create, :play]
before_action :authorize_admin_pipeline_schedule!, only: [:destroy]
before_action do
push_frontend_feature_flag(:pipeline_schedules_with_tags, @project, default_enabled: :yaml)
end
feature_category :continuous_integration
# rubocop: disable CodeReuse/ActiveRecord
......
......@@ -66,6 +66,18 @@ module Ci
project.actual_limits.limit_for(:ci_daily_pipeline_schedule_triggers)
end
def ref_for_display
return unless ref.present?
ref.gsub(%r{^refs/(heads|tags)/}, '')
end
def for_tag?
return false unless ref.present?
ref.start_with? 'refs/tags/'
end
private
def worker_cron_expression
......
......@@ -15,8 +15,9 @@
= f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true
.form-group.row
.col-md-9
= f.label :ref, _('Target Branch'), class: 'label-bold'
= dropdown_tag(_("Select target branch"), options: { toggle_class: 'gl-button btn btn-default js-target-branch-dropdown w-100', dropdown_class: 'git-revision-dropdown w-100', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } )
= f.label :ref, Feature.enabled?(:pipeline_schedules_with_tags) ? _('Target branch or tag') : _('Target branch'), class: 'label-bold'
%div{ data: { testid: 'schedule-target-ref' } }
.js-target-ref-dropdown{ data: { project_id: @project.id, default_branch: @project.default_branch } }
= f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true
.form-group.row.js-ci-variable-list-section
.col-md-9
......
......@@ -3,9 +3,12 @@
%td
= pipeline_schedule.description
%td.branch-name-cell
- if pipeline_schedule.for_tag?
= sprite_icon('tag', size: 12)
- else
= sprite_icon('fork', size: 12)
- if pipeline_schedule.ref.present?
= link_to pipeline_schedule.ref, project_ref_path(@project, pipeline_schedule.ref), class: "ref-name"
= link_to pipeline_schedule.ref_for_display, project_ref_path(@project, pipeline_schedule.ref_for_display), class: "ref-name"
%td
- if pipeline_schedule.last_pipeline
.status-icon-container{ class: "ci-status-icon-#{pipeline_schedule.last_pipeline.status}" }
......
---
name: pipeline_schedules_with_tags
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81476
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354421
milestone: '14.9'
type: development
group: group::pipeline execution
default_enabled: false
......@@ -33241,6 +33241,9 @@ msgstr ""
msgid "Select target branch"
msgstr ""
msgid "Select target branch or tag"
msgstr ""
msgid "Select timezone"
msgstr ""
......@@ -35993,6 +35996,9 @@ msgstr ""
msgid "Target branch"
msgstr ""
msgid "Target branch or tag"
msgstr ""
msgid "Target-Branch"
msgstr ""
......
......@@ -135,8 +135,8 @@ RSpec.describe 'Pipeline Schedules', :js do
end
it 'shows the pipeline schedule with default ref' do
page.within('.js-target-branch-dropdown') do
expect(first('.dropdown-toggle-text').text).to eq('master')
page.within('[data-testid="schedule-target-ref"]') do
expect(first('.gl-new-dropdown-button-text').text).to eq('master')
end
end
end
......@@ -148,8 +148,8 @@ RSpec.describe 'Pipeline Schedules', :js do
end
it 'shows the pipeline schedule with default ref' do
page.within('.js-target-branch-dropdown') do
expect(first('.dropdown-toggle-text').text).to eq('master')
page.within('[data-testid="schedule-target-ref"]') do
expect(first('.gl-new-dropdown-button-text').text).to eq('master')
end
end
end
......@@ -293,8 +293,8 @@ RSpec.describe 'Pipeline Schedules', :js do
end
def select_target_branch
find('.js-target-branch-dropdown').click
click_link 'master'
find('[data-testid="schedule-target-ref"] .dropdown-toggle').click
click_button 'master'
end
def save_pipeline_schedule
......
......@@ -10,30 +10,37 @@ Object {
Object {
"default": false,
"name": "add_images_and_changes",
"value": undefined,
},
Object {
"default": false,
"name": "conflict-contains-conflict-markers",
"value": undefined,
},
Object {
"default": false,
"name": "deleted-image-test",
"value": undefined,
},
Object {
"default": false,
"name": "diff-files-image-to-symlink",
"value": undefined,
},
Object {
"default": false,
"name": "diff-files-symlink-to-image",
"value": undefined,
},
Object {
"default": false,
"name": "markdown",
"value": undefined,
},
Object {
"default": true,
"name": "master",
"value": undefined,
},
],
"totalCount": 123,
......@@ -54,12 +61,15 @@ Object {
"list": Array [
Object {
"name": "v1.1.1",
"value": undefined,
},
Object {
"name": "v1.1.0",
"value": undefined,
},
Object {
"name": "v1.0.0",
"value": undefined,
},
],
"totalCount": 456,
......
......@@ -48,6 +48,14 @@ describe('Ref selector Vuex store mutations', () => {
});
});
describe(`${types.SET_USE_SYMBOLIC_REF_NAMES}`, () => {
it('sets useSymbolicRefNames on the state', () => {
mutations[types.SET_USE_SYMBOLIC_REF_NAMES](state, true);
expect(state.useSymbolicRefNames).toBe(true);
});
});
describe(`${types.SET_PROJECT_ID}`, () => {
it('updates the project ID', () => {
const newProjectId = '4';
......
......@@ -228,6 +228,66 @@ RSpec.describe Ci::PipelineSchedule do
end
end
describe '#for_tag?' do
context 'when the target is a tag' do
before do
subject.ref = 'refs/tags/v1.0'
end
it { expect(subject.for_tag?).to eq(true) }
end
context 'when the target is a branch' do
before do
subject.ref = 'refs/heads/main'
end
it { expect(subject.for_tag?).to eq(false) }
end
context 'when there is no ref' do
before do
subject.ref = nil
end
it { expect(subject.for_tag?).to eq(false) }
end
end
describe '#ref_for_display' do
context 'when the target is a tag' do
before do
subject.ref = 'refs/tags/v1.0'
end
it { expect(subject.ref_for_display).to eq('v1.0') }
end
context 'when the target is a branch' do
before do
subject.ref = 'refs/heads/main'
end
it { expect(subject.ref_for_display).to eq('main') }
end
context 'when the ref is ambiguous' do
before do
subject.ref = 'release-2.8'
end
it { expect(subject.ref_for_display).to eq('release-2.8') }
end
context 'when there is no ref' do
before do
subject.ref = nil
end
it { expect(subject.ref_for_display).to eq(nil) }
end
end
context 'loose foreign key on ci_pipeline_schedules.project_id' do
it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:project) }
......
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