Skip to content
Snippets Groups Projects
Commit d49817d1 authored by Marius Bobin's avatar Marius Bobin :two:
Browse files

Merge branch '393846-remove-pipeline-dropdown-actions-ff' into 'master'

Removing FF lazy_load_pipeline_dropdown_actions

See merge request !117442



Merged-by: default avatarMarius Bobin <mbobin@gitlab.com>
Approved-by: default avatarValery Sizov <valery@gitlab.com>
Approved-by: Simon Knox's avatarSimon Knox <simon@gitlab.com>
Approved-by: default avatarPayton Burdette <pburdette@gitlab.com>
Approved-by: default avatarMarius Bobin <mbobin@gitlab.com>
Reviewed-by: default avatarPayton Burdette <pburdette@gitlab.com>
Reviewed-by: default avatarMax Fan <mfan@gitlab.com>
Co-authored-by: default avatarMax Fan <mfan@gitlab.com>
parents 800f22e8 40df9091
No related branches found
No related tags found
2 merge requests!118700Remove refactor_vulnerability_filters feature flag,!117442Removing FF lazy_load_pipeline_dropdown_actions
Pipeline #842045115 passed
<script>
import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
import Tracking from '~/tracking';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub';
import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL, TRACKING_CATEGORIES } from '../../constants';
import PipelineMultiActions from './pipeline_multi_actions.vue';
import PipelinesManualActions from './pipelines_manual_actions.vue';
import PipelinesManualActionsLegacy from './pipelines_manual_actions_legacy.vue';
export default {
BUTTON_TOOLTIP_RETRY,
......@@ -19,9 +17,8 @@ export default {
GlButton,
PipelineMultiActions,
PipelinesManualActions,
PipelinesManualActionsLegacy,
},
mixins: [Tracking.mixin(), glFeatureFlagsMixin()],
mixins: [Tracking.mixin()],
props: {
pipeline: {
type: Object,
......@@ -39,21 +36,11 @@ export default {
};
},
computed: {
shouldLazyLoadActions() {
return this.glFeatures.lazyLoadPipelineDropdownActions;
},
hasActions() {
return (
this.pipeline?.details?.has_manual_actions || this.pipeline?.details?.has_scheduled_actions
);
},
actions() {
if (!this.pipeline || !this.pipeline.details) {
return [];
}
const { details } = this.pipeline;
return [...(details.manual_actions || []), ...(details.scheduled_actions || [])];
},
isCancelling() {
return this.cancelingPipeline === this.pipeline.id;
},
......@@ -86,12 +73,7 @@ export default {
<template>
<div class="gl-text-right">
<div class="btn-group">
<pipelines-manual-actions v-if="hasActions && shouldLazyLoadActions" :iid="pipeline.iid" />
<pipelines-manual-actions-legacy
v-if="actions.length > 0 && !shouldLazyLoadActions"
:actions="actions"
/>
<pipelines-manual-actions v-if="hasActions" :iid="pipeline.iid" />
<gl-button
v-if="pipeline.flags.retryable"
......
<script>
import { GlDropdown, GlDropdownItem, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { s__, __, sprintf } from '~/locale';
import Tracking from '~/tracking';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import eventHub from '../../event_hub';
import { TRACKING_CATEGORIES } from '../../constants';
export default {
name: 'PipelinesManualActionsLegacy',
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
GlCountdown,
GlDropdown,
GlDropdownItem,
GlIcon,
},
mixins: [Tracking.mixin()],
props: {
actions: {
type: Array,
required: true,
},
},
data() {
return {
isLoading: false,
};
},
methods: {
async onClickAction(action) {
if (action.scheduled_at) {
const confirmationMessage = sprintf(
s__(
'DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after its timer finishes.',
),
{ jobName: action.name },
);
const confirmed = await confirmAction(confirmationMessage);
if (!confirmed) {
return;
}
}
this.isLoading = true;
/**
* Ideally, the component would not make an api call directly.
* However, in order to use the eventhub and know when to
* toggle back the `isLoading` property we'd need an ID
* to track the request with a wacther - since this component
* is rendered at least 20 times in the same page, moving the
* api call directly here is the most performant solution
*/
axios
.post(`${action.path}.json`)
.then(() => {
this.isLoading = false;
eventHub.$emit('updateTable');
})
.catch(() => {
this.isLoading = false;
createAlert({ message: __('An error occurred while making the request.') });
});
},
isActionDisabled(action) {
if (action.playable === undefined) {
return false;
}
return !action.playable;
},
trackClick() {
this.track('click_manual_actions', { label: TRACKING_CATEGORIES.table });
},
},
};
</script>
<template>
<gl-dropdown
v-gl-tooltip
:title="__('Run manual or delayed jobs')"
:loading="isLoading"
data-testid="pipelines-manual-actions-dropdown"
right
lazy
icon="play"
@shown="trackClick"
>
<gl-dropdown-item
v-for="action in actions"
:key="action.path"
:disabled="isActionDisabled(action)"
@click="onClickAction(action)"
>
<div class="gl-display-flex gl-justify-content-space-between gl-flex-wrap">
{{ action.name }}
<span v-if="action.scheduled_at">
<gl-icon name="clock" />
<gl-countdown :end-date-string="action.scheduled_at" />
</span>
</div>
</gl-dropdown-item>
</gl-dropdown>
</template>
......@@ -22,7 +22,6 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action :ensure_pipeline, only: [:show, :downloadable_artifacts]
before_action :reject_if_build_artifacts_size_refreshing!, only: [:destroy]
before_action :push_frontend_feature_flags, only: [:index]
# Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596
before_action :redirect_for_legacy_scope_filter, only: [:index], if: -> { request.format.html? }
......@@ -234,7 +233,7 @@ def serialize_pipelines
@pipelines,
disable_coverage: true,
preload: true,
disable_manual_and_scheduled_actions: Feature.enabled?(:lazy_load_pipeline_dropdown_actions, @project)
disable_manual_and_scheduled_actions: true
)
end
......@@ -358,10 +357,6 @@ def tracking_namespace_source
def tracking_project_source
project
end
def push_frontend_feature_flags
push_frontend_feature_flag(:lazy_load_pipeline_dropdown_actions, @project)
end
end
Projects::PipelinesController.prepend_mod_with('Projects::PipelinesController')
---
name: lazy_load_pipeline_dropdown_actions
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114490
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/393846
milestone: '15.10'
type: development
group: group::pipeline execution
default_enabled: false
......@@ -199,22 +199,6 @@
check_pipeline_response(returned: 6, all: 6)
end
end
context "with lazy_load_pipeline_dropdown_actions feature flag disabled" do
before do
stub_feature_flags(lazy_load_pipeline_dropdown_actions: false)
end
it 'returns manual and scheduled actions' do
get_pipelines_index_json
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('pipeline')
expect(json_response.dig('pipelines', 0, 'details')).to include('manual_actions')
expect(json_response.dig('pipelines', 0, 'details')).to include('scheduled_actions')
end
end
end
def get_pipelines_index_html(params = {})
......
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue';
import PipelinesManualActionsLegacy from '~/pipelines/components/pipelines_list/pipelines_manual_actions_legacy.vue';
import PipelineMultiActions from '~/pipelines/components/pipelines_list/pipeline_multi_actions.vue';
import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue';
import eventHub from '~/pipelines/event_hub';
......@@ -15,15 +14,6 @@ describe('Pipeline operations', () => {
details: {
has_manual_actions: true,
has_scheduled_actions: false,
manual_actions: [
{
name: 'dont-interrupt-me',
path: '/root/ci-project/-/jobs/3974323562/play',
playable: true,
scheduled: false,
},
],
scheduled_actions: [],
},
flags: {
retryable: true,
......@@ -34,20 +24,14 @@ describe('Pipeline operations', () => {
},
};
const createComponent = (props = defaultProps, flagState = true) => {
const createComponent = (props = defaultProps) => {
wrapper = shallowMountExtended(PipelineOperations, {
provide: {
glFeatures: {
lazyLoadPipelineDropdownActions: flagState,
},
},
propsData: {
...props,
},
});
};
const findLegacyManualActions = () => wrapper.findComponent(PipelinesManualActionsLegacy);
const findManualActions = () => wrapper.findComponent(PipelinesManualActions);
const findMultiActions = () => wrapper.findComponent(PipelineMultiActions);
const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button');
......@@ -57,14 +41,6 @@ describe('Pipeline operations', () => {
createComponent();
expect(findManualActions().exists()).toBe(true);
expect(findLegacyManualActions().exists()).toBe(false);
});
it('should display legacy pipeline manual actions', () => {
createComponent(defaultProps, false);
expect(findLegacyManualActions().exists()).toBe(true);
expect(findManualActions().exists()).toBe(false);
});
it('should display pipeline multi actions', () => {
......
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import PipelinesManualActionsLegacy from '~/pipelines/components/pipelines_list/pipelines_manual_actions_legacy.vue';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import { TRACKING_CATEGORIES } from '~/pipelines/constants';
jest.mock('~/alert');
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
describe('Pipelines Actions dropdown', () => {
let wrapper;
let mock;
const createComponent = (props, mountFn = shallowMount) => {
wrapper = mountFn(PipelinesManualActionsLegacy, {
propsData: {
...props,
},
});
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findAllCountdowns = () => wrapper.findAllComponents(GlCountdown);
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
confirmAction.mockReset();
});
describe('manual actions', () => {
const mockActions = [
{
name: 'stop_review',
path: `${TEST_HOST}/root/review-app/builds/1893/play`,
},
{
name: 'foo',
path: `${TEST_HOST}/disabled/pipeline/action`,
playable: false,
},
];
beforeEach(() => {
createComponent({ actions: mockActions });
});
it('renders a dropdown with the provided actions', () => {
expect(findAllDropdownItems()).toHaveLength(mockActions.length);
});
it("renders a disabled action when it's not playable", () => {
expect(findAllDropdownItems().at(1).attributes('disabled')).toBe('true');
});
describe('on click', () => {
it('makes a request and toggles the loading state', async () => {
mock.onPost(mockActions.path).reply(HTTP_STATUS_OK);
findAllDropdownItems().at(0).vm.$emit('click');
await nextTick();
expect(findDropdown().props('loading')).toBe(true);
await waitForPromises();
expect(findDropdown().props('loading')).toBe(false);
});
it('makes a failed request and toggles the loading state', async () => {
mock.onPost(mockActions.path).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
findAllDropdownItems().at(0).vm.$emit('click');
await nextTick();
expect(findDropdown().props('loading')).toBe(true);
await waitForPromises();
expect(findDropdown().props('loading')).toBe(false);
expect(createAlert).toHaveBeenCalledTimes(1);
});
});
describe('tracking', () => {
afterEach(() => {
unmockTracking();
});
it('tracks manual actions click', () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
findDropdown().vm.$emit('shown');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_manual_actions', {
label: TRACKING_CATEGORIES.table,
});
});
});
});
describe('scheduled jobs', () => {
const scheduledJobAction = {
name: 'scheduled action',
path: `${TEST_HOST}/scheduled/job/action`,
playable: true,
scheduled_at: '2063-04-05T00:42:00Z',
};
const expiredJobAction = {
name: 'expired action',
path: `${TEST_HOST}/expired/job/action`,
playable: true,
scheduled_at: '2018-10-05T08:23:00Z',
};
beforeEach(() => {
jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
createComponent({ actions: [scheduledJobAction, expiredJobAction] });
});
it('makes post request after confirming', async () => {
mock.onPost(scheduledJobAction.path).reply(HTTP_STATUS_OK);
confirmAction.mockResolvedValueOnce(true);
findAllDropdownItems().at(0).vm.$emit('click');
expect(confirmAction).toHaveBeenCalled();
await waitForPromises();
expect(mock.history.post).toHaveLength(1);
});
it('does not make post request if confirmation is cancelled', async () => {
mock.onPost(scheduledJobAction.path).reply(HTTP_STATUS_OK);
confirmAction.mockResolvedValueOnce(false);
findAllDropdownItems().at(0).vm.$emit('click');
expect(confirmAction).toHaveBeenCalled();
await waitForPromises();
expect(mock.history.post).toHaveLength(0);
});
it('displays the remaining time in the dropdown', () => {
expect(findAllCountdowns().at(0).props('endDateString')).toBe(
scheduledJobAction.scheduled_at,
);
});
it('displays 00:00:00 for expired jobs in the dropdown', () => {
expect(findAllCountdowns().at(1).props('endDateString')).toBe(expiredJobAction.scheduled_at);
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment