diff options
-rw-r--r-- | app/assets/javascripts/environments/components/environment_actions.vue | 37 | ||||
-rw-r--r-- | app/assets/javascripts/environments/components/environment_item.vue | 53 | ||||
-rw-r--r-- | app/assets/stylesheets/pages/environments.scss | 7 | ||||
-rw-r--r-- | app/models/ci/build.rb | 6 | ||||
-rw-r--r-- | app/models/deployment.rb | 6 | ||||
-rw-r--r-- | app/serializers/deployment_entity.rb | 1 | ||||
-rw-r--r-- | changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml | 5 | ||||
-rw-r--r-- | spec/features/projects/environments/environments_spec.rb | 67 | ||||
-rw-r--r-- | spec/javascripts/environments/environment_actions_spec.js | 126 | ||||
-rw-r--r-- | spec/models/ci/build_spec.rb | 46 | ||||
-rw-r--r-- | spec/models/deployment_spec.rb | 16 | ||||
-rw-r--r-- | spec/serializers/deployment_entity_spec.rb | 22 |
12 files changed, 303 insertions, 89 deletions
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index 2bc168a6b02..2d51a13d8a0 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -1,4 +1,6 @@ <script> +import { s__, sprintf } from '~/locale'; +import { formatTime } from '~/lib/utils/datetime_utility'; import Icon from '~/vue_shared/components/icon.vue'; import eventHub from '../event_hub'; import tooltip from '../../vue_shared/directives/tooltip'; @@ -28,10 +30,24 @@ export default { }, }, methods: { - onClickAction(endpoint) { + onClickAction(action) { + if (action.scheduledAt) { + const confirmationMessage = sprintf( + s__( + "DelayedJobs|Are you sure you want to run %{jobName} immediately? This job will run automatically after it's timer finishes.", + ), + { jobName: action.name }, + ); + // https://gitlab.com/gitlab-org/gitlab-ce/issues/52156 + // eslint-disable-next-line no-alert + if (!window.confirm(confirmationMessage)) { + return; + } + } + this.isLoading = true; - eventHub.$emit('postAction', { endpoint }); + eventHub.$emit('postAction', { endpoint: action.playPath }); }, isActionDisabled(action) { @@ -41,6 +57,11 @@ export default { return !action.playable; }, + + remainingTime(action) { + const remainingMilliseconds = new Date(action.scheduledAt).getTime() - Date.now(); + return formatTime(Math.max(0, remainingMilliseconds)); + }, }, }; </script> @@ -54,7 +75,7 @@ export default { :aria-label="title" :disabled="isLoading" type="button" - class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container" + class="dropdown btn btn-default dropdown-new js-environment-actions-dropdown" data-container="body" data-toggle="dropdown" > @@ -75,12 +96,16 @@ export default { :class="{ disabled: isActionDisabled(action) }" :disabled="isActionDisabled(action)" type="button" - class="js-manual-action-link no-btn btn" - @click="onClickAction(action.play_path)" + class="js-manual-action-link no-btn btn d-flex align-items-center" + @click="onClickAction(action)" > - <span> + <span class="flex-fill"> {{ action.name }} </span> + <span v-if="action.scheduledAt"> + <icon name="clock" /> + {{ remainingTime(action) }} + </span> </button> </li> </ul> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index bb9c139727e..f2fb0074ea1 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -13,6 +13,7 @@ import TerminalButtonComponent from './environment_terminal_button.vue'; import MonitoringButtonComponent from './environment_monitoring.vue'; import CommitComponent from '../../vue_shared/components/commit.vue'; import eventHub from '../event_hub'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; /** * Envrionment Item Component @@ -74,21 +75,6 @@ export default { }, /** - * Verifies is the given environment has manual actions. - * Used to verify if we should render them or nor. - * - * @returns {Boolean|Undefined} - */ - hasManualActions() { - return ( - this.model && - this.model.last_deployment && - this.model.last_deployment.manual_actions && - this.model.last_deployment.manual_actions.length > 0 - ); - }, - - /** * Checkes whether the environment is protected. * (`is_protected` currently only set in EE) * @@ -154,23 +140,24 @@ export default { return ''; }, - /** - * Returns the manual actions with the name parsed. - * - * @returns {Array.<Object>|Undefined} - */ - manualActions() { - if (this.hasManualActions) { - return this.model.last_deployment.manual_actions.map(action => { - const parsedAction = { - name: humanize(action.name), - play_path: action.play_path, - playable: action.playable, - }; - return parsedAction; - }); + actions() { + if (!this.model || !this.model.last_deployment) { + return []; + } + + const { manualActions, scheduledActions } = convertObjectPropsToCamelCase( + this.model.last_deployment, + { deep: true }, + ); + let combinedActions = []; + if (this.canCreateDeployment) { + combinedActions = combinedActions.concat(manualActions || []); } - return []; + combinedActions = combinedActions.concat(scheduledActions || []); + return combinedActions.map(action => ({ + ...action, + name: humanize(action.name), + })); }, /** @@ -619,8 +606,8 @@ export default { /> <actions-component - v-if="hasManualActions && canCreateDeployment" - :actions="manualActions" + v-if="actions.length > 0" + :actions="actions" /> <terminal-button-component diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 19a36061c45..347fcad771a 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -44,11 +44,6 @@ margin: 0; } - .icon-play { - height: 13px; - width: 12px; - } - .external-url, .dropdown-new { color: $gl-text-color-secondary; @@ -366,7 +361,7 @@ } .arrow-shadow { - content: ""; + content: ''; position: absolute; width: 7px; height: 7px; diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index cdfe8175a42..f244ffc1c3a 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -245,10 +245,14 @@ module Ci .fabricate! end - def other_actions + def other_manual_actions pipeline.manual_actions.where.not(name: name) end + def other_scheduled_actions + pipeline.scheduled_actions.where.not(name: name) + end + def pages_generator? Gitlab.config.pages.enabled && self.name == 'pages' diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 62dc0f2cbeb..f1bce1a1daf 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -55,7 +55,11 @@ class Deployment < ActiveRecord::Base end def manual_actions - @manual_actions ||= deployable.try(:other_actions) + @manual_actions ||= deployable.try(:other_manual_actions) + end + + def scheduled_actions + @scheduled_actions ||= deployable.try(:other_scheduled_actions) end def includes_commit?(commit) diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb index 344148a1fb7..aa1d9e6292c 100644 --- a/app/serializers/deployment_entity.rb +++ b/app/serializers/deployment_entity.rb @@ -25,4 +25,5 @@ class DeploymentEntity < Grape::Entity expose :commit, using: CommitEntity expose :deployable, using: JobEntity expose :manual_actions, using: JobEntity + expose :scheduled_actions, using: JobEntity end diff --git a/changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml b/changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml new file mode 100644 index 00000000000..c89af78d989 --- /dev/null +++ b/changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml @@ -0,0 +1,5 @@ +--- +title: Add the Play button for delayed jobs in environment page +merge_request: 22106 +author: +type: added diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 917ba495f01..22d0187ac81 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -162,7 +162,7 @@ describe 'Environments page', :js do end it 'shows a play button' do - find('.js-dropdown-play-icon-container').click + find('.js-environment-actions-dropdown').click expect(page).to have_content(action.name.humanize) end @@ -170,7 +170,7 @@ describe 'Environments page', :js do it 'allows to play a manual action', :js do expect(action).to be_manual - find('.js-dropdown-play-icon-container').click + find('.js-environment-actions-dropdown').click expect(page).to have_content(action.name.humanize) expect { find('.js-manual-action-link').click } @@ -260,6 +260,69 @@ describe 'Environments page', :js do end end end + + context 'when there is a delayed job' do + let!(:pipeline) { create(:ci_pipeline, project: project) } + let!(:build) { create(:ci_build, pipeline: pipeline) } + + let!(:delayed_job) do + create(:ci_build, :scheduled, + pipeline: pipeline, + name: 'delayed job', + stage: 'test', + commands: 'test') + end + + let!(:deployment) do + create(:deployment, + environment: environment, + deployable: build, + sha: project.commit.id) + end + + before do + visit_environments(project) + end + + it 'has a dropdown for actionable jobs' do + expect(page).to have_selector('.dropdown-new.btn.btn-default .ic-play') + end + + it "has link to the delayed job's action" do + find('.js-environment-actions-dropdown').click + + expect(page).to have_button('Delayed job') + expect(page).to have_content(/\d{2}:\d{2}:\d{2}/) + end + + context 'when delayed job is expired already' do + let!(:delayed_job) do + create(:ci_build, :expired_scheduled, + pipeline: pipeline, + name: 'delayed job', + stage: 'test', + commands: 'test') + end + + it "shows 00:00:00 as the remaining time" do + find('.js-environment-actions-dropdown').click + + expect(page).to have_content("00:00:00") + end + end + + context 'when user played a delayed job immediately' do + before do + find('.js-environment-actions-dropdown').click + page.accept_confirm { click_button('Delayed job') } + wait_for_requests + end + + it 'enqueues the delayed job', :js do + expect(delayed_job.reload).to be_pending + end + end + end end end diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js index 223153d4e31..787df757d32 100644 --- a/spec/javascripts/environments/environment_actions_spec.js +++ b/spec/javascripts/environments/environment_actions_spec.js @@ -1,15 +1,19 @@ import Vue from 'vue'; -import actionsComp from '~/environments/components/environment_actions.vue'; +import eventHub from '~/environments/event_hub'; +import EnvironmentActions from '~/environments/components/environment_actions.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { TEST_HOST } from 'spec/test_constants'; -describe('Actions Component', () => { - let ActionsComponent; - let actionsMock; - let component; +describe('EnvironmentActions Component', () => { + const Component = Vue.extend(EnvironmentActions); + let vm; - beforeEach(() => { - ActionsComponent = Vue.extend(actionsComp); + afterEach(() => { + vm.$destroy(); + }); - actionsMock = [ + describe('manual actions', () => { + const actions = [ { name: 'bar', play_path: 'https://gitlab.com/play', @@ -25,43 +29,89 @@ describe('Actions Component', () => { }, ]; - component = new ActionsComponent({ - propsData: { - actions: actionsMock, - }, - }).$mount(); - }); + beforeEach(() => { + vm = mountComponent(Component, { actions }); + }); + + it('should render a dropdown button with icon and title attribute', () => { + expect(vm.$el.querySelector('.fa-caret-down')).toBeDefined(); + expect(vm.$el.querySelector('.dropdown-new').getAttribute('data-original-title')).toEqual( + 'Deploy to...', + ); - describe('computed', () => { - it('title', () => { - expect(component.title).toEqual('Deploy to...'); + expect(vm.$el.querySelector('.dropdown-new').getAttribute('aria-label')).toEqual( + 'Deploy to...', + ); }); - }); - it('should render a dropdown button with icon and title attribute', () => { - expect(component.$el.querySelector('.fa-caret-down')).toBeDefined(); - expect( - component.$el.querySelector('.dropdown-new').getAttribute('data-original-title'), - ).toEqual('Deploy to...'); + it('should render a dropdown with the provided list of actions', () => { + expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(actions.length); + }); - expect(component.$el.querySelector('.dropdown-new').getAttribute('aria-label')).toEqual( - 'Deploy to...', - ); - }); + it("should render a disabled action when it's not playable", () => { + expect( + vm.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'), + ).toEqual('disabled'); - it('should render a dropdown with the provided list of actions', () => { - expect(component.$el.querySelectorAll('.dropdown-menu li').length).toEqual(actionsMock.length); + expect( + vm.$el.querySelector('.dropdown-menu li:last-child button').classList.contains('disabled'), + ).toEqual(true); + }); }); - it("should render a disabled action when it's not playable", () => { - expect( - component.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'), - ).toEqual('disabled'); + describe('scheduled jobs', () => { + const scheduledJobAction = { + name: 'scheduled action', + playPath: `${TEST_HOST}/scheduled/job/action`, + playable: true, + scheduledAt: '2063-04-05T00:42:00Z', + }; + const expiredJobAction = { + name: 'expired action', + playPath: `${TEST_HOST}/expired/job/action`, + playable: true, + scheduledAt: '2018-10-05T08:23:00Z', + }; + const findDropdownItem = action => { + const buttons = vm.$el.querySelectorAll('.dropdown-menu li button'); + return Array.prototype.find.call(buttons, element => + element.innerText.trim().startsWith(action.name), + ); + }; + + beforeEach(() => { + spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime()); + vm = mountComponent(Component, { actions: [scheduledJobAction, expiredJobAction] }); + }); + + it('emits postAction event after confirming', () => { + const emitSpy = jasmine.createSpy('emit'); + eventHub.$on('postAction', emitSpy); + spyOn(window, 'confirm').and.callFake(() => true); + + findDropdownItem(scheduledJobAction).click(); + + expect(window.confirm).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath }); + }); + + it('does not emit postAction event if confirmation is cancelled', () => { + const emitSpy = jasmine.createSpy('emit'); + eventHub.$on('postAction', emitSpy); + spyOn(window, 'confirm').and.callFake(() => false); + + findDropdownItem(scheduledJobAction).click(); - expect( - component.$el - .querySelector('.dropdown-menu li:last-child button') - .classList.contains('disabled'), - ).toEqual(true); + expect(window.confirm).toHaveBeenCalled(); + expect(emitSpy).not.toHaveBeenCalled(); + }); + + it('displays the remaining time in the dropdown', () => { + expect(findDropdownItem(scheduledJobAction)).toContainText('24:00:00'); + }); + + it('displays 00:00:00 for expired jobs in the dropdown', () => { + expect(findDropdownItem(expiredJobAction)).toContainText('00:00:00'); + }); }); }); diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index a046541031e..df0851c1b6c 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1523,11 +1523,11 @@ describe Ci::Build do end end - describe '#other_actions' do + describe '#other_manual_actions' do let(:build) { create(:ci_build, :manual, pipeline: pipeline) } let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') } - subject { build.other_actions } + subject { build.other_manual_actions } before do project.add_developer(user) @@ -1558,6 +1558,48 @@ describe Ci::Build do end end + describe '#other_scheduled_actions' do + let(:build) { create(:ci_build, :scheduled, pipeline: pipeline) } + + subject { build.other_scheduled_actions } + + before do + project.add_developer(user) + end + + context "when other build's status is success" do + let!(:other_build) { create(:ci_build, :schedulable, :success, pipeline: pipeline, name: 'other action') } + + it 'returns other actions' do + is_expected.to contain_exactly(other_build) + end + end + + context "when other build's status is failed" do + let!(:other_build) { create(:ci_build, :schedulable, :failed, pipeline: pipeline, name: 'other action') } + + it 'returns other actions' do + is_expected.to contain_exactly(other_build) + end + end + + context "when other build's status is running" do + let!(:other_build) { create(:ci_build, :schedulable, :running, pipeline: pipeline, name: 'other action') } + + it 'does not return other actions' do + is_expected.to be_empty + end + end + + context "when other build's status is scheduled" do + let!(:other_build) { create(:ci_build, :scheduled, pipeline: pipeline, name: 'other action') } + + it 'does not return other actions' do + is_expected.to contain_exactly(other_build) + end + end + end + describe '#persisted_environment' do let!(:environment) do create(:environment, project: project, name: "foo-#{project.default_branch}") diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index b8364e0cf88..146d35122f7 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -16,6 +16,22 @@ describe Deployment do it { is_expected.to validate_presence_of(:ref) } it { is_expected.to validate_presence_of(:sha) } + describe '#scheduled_actions' do + subject { deployment.scheduled_actions } + + let(:project) { create(:project, :repository) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, :success, pipeline: pipeline) } + let(:deployment) { create(:deployment, deployable: build) } + + it 'delegates to other_scheduled_actions' do + expect_any_instance_of(Ci::Build) + .to receive(:other_scheduled_actions) + + subject + end + end + describe 'modules' do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb index 522c92ce295..8793a762f9d 100644 --- a/spec/serializers/deployment_entity_spec.rb +++ b/spec/serializers/deployment_entity_spec.rb @@ -22,4 +22,26 @@ describe DeploymentEntity do it 'exposes creation date' do expect(subject).to include(:created_at) end + + describe 'scheduled_actions' do + let(:project) { create(:project, :repository) } + let(:pipeline) { create(:ci_pipeline, project: project, user: user) } + let(:build) { create(:ci_build, :success, pipeline: pipeline) } + let(:deployment) { create(:deployment, deployable: build) } + + context 'when the same pipeline has a scheduled action' do + let(:other_build) { create(:ci_build, :schedulable, :success, pipeline: pipeline, name: 'other build') } + let!(:other_deployment) { create(:deployment, deployable: other_build) } + + it 'returns other scheduled actions' do + expect(subject[:scheduled_actions][0][:name]).to eq 'other build' + end + end + + context 'when the same pipeline does not have a scheduled action' do + it 'does not return other actions' do + expect(subject[:scheduled_actions]).to be_empty + end + end + end end |