Commit 3e4c5ee1 authored by Paul Slaughter's avatar Paul Slaughter

Merge branch 'eread/migrate-env-dropdown' into 'master'

Migrate environment dropdown

See merge request gitlab-org/gitlab!46193
parents 0a176bc4 1a9eee3d
<script> <script>
import { GlButton, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import { formatTime } from '~/lib/utils/datetime_utility'; import { formatTime } from '~/lib/utils/datetime_utility';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
...@@ -9,7 +9,8 @@ export default { ...@@ -9,7 +9,8 @@ export default {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
components: { components: {
GlButton, GlDropdown,
GlDropdownItem,
GlIcon, GlIcon,
GlLoadingIcon, GlLoadingIcon,
}, },
...@@ -35,7 +36,7 @@ export default { ...@@ -35,7 +36,7 @@ export default {
if (action.scheduledAt) { if (action.scheduledAt) {
const confirmationMessage = sprintf( const confirmationMessage = sprintf(
s__( s__(
"DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.", 'DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after its timer finishes.',
), ),
{ jobName: action.name }, { jobName: action.name },
); );
...@@ -67,40 +68,32 @@ export default { ...@@ -67,40 +68,32 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="btn-group" role="group"> <gl-dropdown
<gl-button v-gl-tooltip
v-gl-tooltip :title="title"
:title="title" :aria-label="title"
:aria-label="title" :disabled="isLoading"
:disabled="isLoading" right
class="dropdown dropdown-new js-environment-actions-dropdown" data-container="body"
data-container="body" data-testid="environment-actions-button"
data-toggle="dropdown" >
data-testid="environment-actions-button" <template #button-content>
<gl-icon name="play" />
<gl-icon name="chevron-down" />
<gl-loading-icon v-if="isLoading" />
</template>
<gl-dropdown-item
v-for="(action, i) in actions"
:key="i"
:disabled="isActionDisabled(action)"
data-testid="manual-action-link"
@click="onClickAction(action)"
> >
<span> <span class="gl-flex-fill-1">{{ action.name }}</span>
<gl-icon name="play" /> <span v-if="action.scheduledAt" class="gl-text-gray-500 float-right">
<gl-icon name="chevron-down" /> <gl-icon name="clock" />
<gl-loading-icon v-if="isLoading" /> {{ remainingTime(action) }}
</span> </span>
</gl-button> </gl-dropdown-item>
</gl-dropdown>
<ul class="dropdown-menu dropdown-menu-right">
<li v-for="(action, i) in actions" :key="i" class="gl-display-flex">
<gl-button
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
variant="link"
class="js-manual-action-link gl-flex-fill-1"
@click="onClickAction(action)"
>
<span class="gl-flex-fill-1">{{ action.name }}</span>
<span v-if="action.scheduledAt" class="text-secondary float-right">
<gl-icon name="clock" />
{{ remainingTime(action) }}
</span>
</gl-button>
</li>
</ul>
</div>
</template> </template>
...@@ -32,7 +32,7 @@ export default { ...@@ -32,7 +32,7 @@ export default {
if (action.scheduled_at) { if (action.scheduled_at) {
const confirmationMessage = sprintf( const confirmationMessage = sprintf(
s__( s__(
"DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.", 'DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after its timer finishes.',
), ),
{ jobName: action.name }, { jobName: action.name },
); );
......
...@@ -15,6 +15,14 @@ RSpec.describe 'Environments page', :js do ...@@ -15,6 +15,14 @@ RSpec.describe 'Environments page', :js do
sign_in(user) sign_in(user)
end end
def action_link_selector
'[data-testid="manual-action-link"]'
end
def actions_button_selector
'[data-testid="environment-actions-button"]'
end
context 'when an environment is protected and user has access to it' do context 'when an environment is protected and user has access to it' do
before do before do
create(:protected_environment, create(:protected_environment,
...@@ -45,10 +53,9 @@ RSpec.describe 'Environments page', :js do ...@@ -45,10 +53,9 @@ RSpec.describe 'Environments page', :js do
end end
it 'shows an enabled play button' do it 'shows an enabled play button' do
find('.js-environment-actions-dropdown').click find(actions_button_selector).click
play_button = %q{button.js-manual-action-link}
expect(page).to have_selector(play_button) expect(page).to have_selector(action_link_selector)
end end
it 'shows a stop button' do it 'shows a stop button' do
...@@ -129,8 +136,8 @@ RSpec.describe 'Environments page', :js do ...@@ -129,8 +136,8 @@ RSpec.describe 'Environments page', :js do
end end
it 'show a disabled play button' do it 'show a disabled play button' do
find('.js-environment-actions-dropdown').click find(actions_button_selector).click
disabled_play_button = %q{button.js-manual-action-link.disabled} disabled_play_button = %Q{#{action_link_selector}[disabled="disabled"]}
expect(page).to have_selector(disabled_play_button) expect(page).to have_selector(disabled_play_button)
end end
......
...@@ -8749,7 +8749,7 @@ msgstr "" ...@@ -8749,7 +8749,7 @@ msgstr ""
msgid "Delayed Project Deletion (%{adjourned_deletion})" msgid "Delayed Project Deletion (%{adjourned_deletion})"
msgstr "" msgstr ""
msgid "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes." msgid "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after its timer finishes."
msgstr "" msgstr ""
msgid "DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after it's timer finishes." msgid "DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after it's timer finishes."
......
...@@ -12,8 +12,16 @@ RSpec.describe 'Environments page', :js do ...@@ -12,8 +12,16 @@ RSpec.describe 'Environments page', :js do
sign_in(user) sign_in(user)
end end
def actions_button_selector
'[data-testid="environment-actions-button"]'
end
def action_link_selector
'[data-testid="manual-action-link"]'
end
def stop_button_selector def stop_button_selector
%q{button[title="Stop environment"]} 'button[title="Stop environment"]'
end end
describe 'page tabs' do describe 'page tabs' do
...@@ -187,18 +195,17 @@ RSpec.describe 'Environments page', :js do ...@@ -187,18 +195,17 @@ RSpec.describe 'Environments page', :js do
end end
it 'shows a play button' do it 'shows a play button' do
find('.js-environment-actions-dropdown').click find(actions_button_selector).click
expect(page).to have_content(action.name) expect(page).to have_content(action.name)
end end
it 'allows to play a manual action', :js do it 'allows to play a manual action', :js do
expect(action).to be_manual expect(action).to be_manual
find('.js-environment-actions-dropdown').click find(actions_button_selector).click
expect(page).to have_content(action.name) expect(page).to have_content(action.name)
expect { find('.js-manual-action-link').click } expect { find(action_link_selector).click }
.not_to change { Ci::Pipeline.count } .not_to change { Ci::Pipeline.count }
end end
...@@ -301,11 +308,11 @@ RSpec.describe 'Environments page', :js do ...@@ -301,11 +308,11 @@ RSpec.describe 'Environments page', :js do
end end
it 'has a dropdown for actionable jobs' do it 'has a dropdown for actionable jobs' do
expect(page).to have_selector('.dropdown-new.btn.btn-default [data-testid="play-icon"]') expect(page).to have_selector("#{actions_button_selector} [data-testid=\"play-icon\"]")
end end
it "has link to the delayed job's action" do it "has link to the delayed job's action" do
find('.js-environment-actions-dropdown').click find(actions_button_selector).click
expect(page).to have_button('delayed job') expect(page).to have_button('delayed job')
expect(page).to have_content(/\d{2}:\d{2}:\d{2}/) expect(page).to have_content(/\d{2}:\d{2}:\d{2}/)
...@@ -320,7 +327,7 @@ RSpec.describe 'Environments page', :js do ...@@ -320,7 +327,7 @@ RSpec.describe 'Environments page', :js do
end end
it "shows 00:00:00 as the remaining time" do it "shows 00:00:00 as the remaining time" do
find('.js-environment-actions-dropdown').click find(actions_button_selector).click
expect(page).to have_content("00:00:00") expect(page).to have_content("00:00:00")
end end
...@@ -328,8 +335,8 @@ RSpec.describe 'Environments page', :js do ...@@ -328,8 +335,8 @@ RSpec.describe 'Environments page', :js do
context 'when user played a delayed job immediately' do context 'when user played a delayed job immediately' do
before do before do
find('.js-environment-actions-dropdown').click find(actions_button_selector).click
page.accept_confirm { click_button('delayed job') } accept_confirm { find(action_link_selector).click }
wait_for_requests wait_for_requests
end end
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import eventHub from '~/environments/event_hub'; import eventHub from '~/environments/event_hub';
import EnvironmentActions from '~/environments/components/environment_actions.vue'; import EnvironmentActions from '~/environments/components/environment_actions.vue';
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',
};
describe('EnvironmentActions Component', () => { describe('EnvironmentActions Component', () => {
let vm; let wrapper;
const findEnvironmentActionsButton = () => vm.find('[data-testid="environment-actions-button"]'); const findEnvironmentActionsButton = () =>
wrapper.find('[data-testid="environment-actions-button"]');
beforeEach(() => { function createComponent(props, { mountFn = shallowMount } = {}) {
vm = shallowMount(EnvironmentActions, { wrapper = mountFn(EnvironmentActions, {
propsData: { actions: [] }, propsData: { actions: [], ...props },
directives: { directives: {
GlTooltip: createMockDirective(), GlTooltip: createMockDirective(),
}, },
}); });
}); }
function createComponentWithScheduledJobs(opts = {}) {
return createComponent({ actions: [scheduledJobAction, expiredJobAction] }, opts);
}
const findDropdownItem = action => {
const buttons = wrapper.findAll(GlDropdownItem);
return buttons.filter(button => button.text().startsWith(action.name)).at(0);
};
afterEach(() => { afterEach(() => {
vm.destroy(); wrapper.destroy();
wrapper = null;
}); });
it('should render a dropdown button with 2 icons', () => { it('should render a dropdown button with 2 icons', () => {
expect(vm.find('.dropdown-new').findAll(GlIcon).length).toBe(2); createComponent({}, { mountFn: mount });
expect(wrapper.find(GlDropdown).findAll(GlIcon).length).toBe(2);
}); });
it('should render a dropdown button with aria-label description', () => { it('should render a dropdown button with aria-label description', () => {
expect(vm.find('.dropdown-new').attributes('aria-label')).toEqual('Deploy to...'); createComponent();
expect(wrapper.find(GlDropdown).attributes('aria-label')).toBe('Deploy to...');
}); });
it('should render a tooltip', () => { it('should render a tooltip', () => {
createComponent();
const tooltip = getBinding(findEnvironmentActionsButton().element, 'gl-tooltip'); const tooltip = getBinding(findEnvironmentActionsButton().element, 'gl-tooltip');
expect(tooltip).toBeDefined(); expect(tooltip).toBeDefined();
}); });
describe('is loading', () => {
beforeEach(() => {
vm.setData({ isLoading: true });
});
it('should render a dropdown button with a loading icon', () => {
expect(vm.findAll(GlLoadingIcon).length).toBe(1);
});
});
describe('manual actions', () => { describe('manual actions', () => {
const actions = [ const actions = [
{ {
...@@ -64,68 +82,71 @@ describe('EnvironmentActions Component', () => { ...@@ -64,68 +82,71 @@ describe('EnvironmentActions Component', () => {
]; ];
beforeEach(() => { beforeEach(() => {
vm.setProps({ actions }); createComponent({ actions });
}); });
it('should render a dropdown with the provided list of actions', () => { it('should render a dropdown with the provided list of actions', () => {
expect(vm.findAll('.dropdown-menu li').length).toEqual(actions.length); expect(wrapper.findAll(GlDropdownItem)).toHaveLength(actions.length);
}); });
it("should render a disabled action when it's not playable", () => { it("should render a disabled action when it's not playable", () => {
expect(vm.find('.dropdown-menu li:last-child gl-button-stub').props('disabled')).toBe(true); const dropdownItems = wrapper.findAll(GlDropdownItem);
const lastDropdownItem = dropdownItems.at(dropdownItems.length - 1);
expect(lastDropdownItem.attributes('disabled')).toBe('true');
}); });
}); });
describe('scheduled jobs', () => { describe('scheduled jobs', () => {
const scheduledJobAction = { let emitSpy;
name: 'scheduled action',
playPath: `${TEST_HOST}/scheduled/job/action`, const clickAndConfirm = async ({ confirm = true } = {}) => {
playable: true, jest.spyOn(window, 'confirm').mockImplementation(() => confirm);
scheduledAt: '2063-04-05T00:42:00Z',
}; findDropdownItem(scheduledJobAction).vm.$emit('click');
const expiredJobAction = { await wrapper.vm.$nextTick();
name: 'expired action',
playPath: `${TEST_HOST}/expired/job/action`,
playable: true,
scheduledAt: '2018-10-05T08:23:00Z',
};
const findDropdownItem = action => {
const buttons = vm.findAll('.dropdown-menu li gl-button-stub');
return buttons.filter(button => button.text().startsWith(action.name)).at(0);
}; };
beforeEach(() => { beforeEach(() => {
emitSpy = jest.fn();
eventHub.$on('postAction', emitSpy);
jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime()); jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
vm.setProps({ actions: [scheduledJobAction, expiredJobAction] });
}); });
it('emits postAction event after confirming', () => { describe('when postAction event is confirmed', () => {
const emitSpy = jest.fn(); beforeEach(async () => {
eventHub.$on('postAction', emitSpy); createComponentWithScheduledJobs({ mountFn: mount });
jest.spyOn(window, 'confirm').mockImplementation(() => true); clickAndConfirm();
});
findDropdownItem(scheduledJobAction).vm.$emit('click'); it('emits postAction event', () => {
expect(window.confirm).toHaveBeenCalled();
expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath });
});
expect(window.confirm).toHaveBeenCalled(); it('should render a dropdown button with a loading icon', () => {
expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath }); expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
});
}); });
it('does not emit postAction event if confirmation is cancelled', () => { describe('when postAction event is denied', () => {
const emitSpy = jest.fn(); beforeEach(() => {
eventHub.$on('postAction', emitSpy); createComponentWithScheduledJobs({ mountFn: mount });
jest.spyOn(window, 'confirm').mockImplementation(() => false); clickAndConfirm({ confirm: false });
});
findDropdownItem(scheduledJobAction).vm.$emit('click');
expect(window.confirm).toHaveBeenCalled(); it('does not emit postAction event if confirmation is cancelled', () => {
expect(emitSpy).not.toHaveBeenCalled(); expect(window.confirm).toHaveBeenCalled();
expect(emitSpy).not.toHaveBeenCalled();
});
}); });
it('displays the remaining time in the dropdown', () => { it('displays the remaining time in the dropdown', () => {
createComponentWithScheduledJobs();
expect(findDropdownItem(scheduledJobAction).text()).toContain('24:00:00'); expect(findDropdownItem(scheduledJobAction).text()).toContain('24:00:00');
}); });
it('displays 00:00:00 for expired jobs in the dropdown', () => { it('displays 00:00:00 for expired jobs in the dropdown', () => {
createComponentWithScheduledJobs();
expect(findDropdownItem(expiredJobAction).text()).toContain('00:00:00'); expect(findDropdownItem(expiredJobAction).text()).toContain('00:00:00');
}); });
}); });
......
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