Commit eb50675e authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '329658-add-delete-action-to-runner' into 'master'

Add Delete runner action to Runners Vue UI

See merge request gitlab-org/gitlab!63176
parents 76080438 42af780b
<script>
import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import { __, s__ } from '~/locale';
import deleteRunnerMutation from '~/runner/graphql/delete_runner.mutation.graphql';
import updateRunnerMutation from '~/runner/graphql/update_runner.mutation.graphql';
const i18n = {
I18N_EDIT: __('Edit'),
I18N_PAUSE: __('Pause'),
I18N_RESUME: __('Resume'),
I18N_REMOVE: __('Remove'),
I18N_REMOVE_CONFIRMATION: s__('Runners|Are you sure you want to delete this runner?'),
};
export default {
......@@ -27,6 +30,7 @@ export default {
data() {
return {
updating: false,
deleting: false,
};
},
computed: {
......@@ -46,12 +50,16 @@ export default {
toggleActiveTitle() {
if (this.updating) {
// Prevent a "sticky" tooltip: If this button is disabled,
// mouseout listeners will not run and the tooltip will
// stay stuck on the button.
// mouseout listeners don't run leaving the tooltip stuck
return '';
}
return this.isActive ? i18n.I18N_PAUSE : i18n.I18N_RESUME;
},
deleteTitle() {
// Prevent a "sticky" tooltip: If element gets removed,
// mouseout listeners don't run and leaving the tooltip stuck
return this.deleting ? '' : i18n.I18N_REMOVE;
},
},
methods: {
async onToggleActive() {
......@@ -87,6 +95,39 @@ export default {
}
},
async onDelete() {
// TODO Replace confirmation with gl-modal
// eslint-disable-next-line no-alert
if (!window.confirm(i18n.I18N_REMOVE_CONFIRMATION)) {
return;
}
this.deleting = true;
try {
const {
data: {
runnerDelete: { errors },
},
} = await this.$apollo.mutate({
mutation: deleteRunnerMutation,
variables: {
input: {
id: this.runner.id,
},
},
awaitRefetchQueries: true,
refetchQueries: ['getRunners'],
});
if (errors && errors.length) {
this.onError(new Error(errors[0]));
}
} catch (e) {
this.onError(e);
} finally {
this.deleting = false;
}
},
onError(error) {
// TODO Render errors when "delete" action is done
// `active` toggle would not fail due to user input.
......@@ -116,6 +157,15 @@ export default {
data-testid="toggle-active-runner"
@click="onToggleActive"
/>
<!-- TODO add delete action to update runners -->
<gl-button
v-gl-tooltip.hover.viewport
:title="deleteTitle"
:aria-label="deleteTitle"
icon="close"
:loading="deleting"
variant="danger"
data-testid="delete-runner"
@click="onDelete"
/>
</gl-button-group>
</template>
mutation runnerDelete($input: RunnerDeleteInput!) {
runnerDelete(input: $input) {
errors
}
}
......@@ -28342,6 +28342,9 @@ msgstr ""
msgid "Runners|Architecture"
msgstr ""
msgid "Runners|Are you sure you want to delete this runner?"
msgstr ""
msgid "Runners|Can run untagged jobs"
msgstr ""
......
......@@ -2,16 +2,21 @@ import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.vue';
import deleteRunnerMutation from '~/runner/graphql/delete_runner.mutation.graphql';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
import updateRunnerMutation from '~/runner/graphql/update_runner.mutation.graphql';
const mockId = '1';
const getRunnersQueryName = getRunnersQuery.definitions[0].name.value;
describe('RunnerTypeCell', () => {
let wrapper;
let mutate;
const findEditBtn = () => wrapper.findByTestId('edit-runner');
const findToggleActiveBtn = () => wrapper.findByTestId('toggle-active-runner');
const findDeleteBtn = () => wrapper.findByTestId('delete-runner');
const createComponent = ({ active = true } = {}, options) => {
wrapper = extendedWrapper(
......@@ -78,6 +83,11 @@ describe('RunnerTypeCell', () => {
await findToggleActiveBtn().vm.$emit('click');
expect(findToggleActiveBtn().props('loading')).toBe(true);
});
it(`After the ${icon} button is clicked, stale tooltip is removed`, async () => {
await findToggleActiveBtn().vm.$emit('click');
expect(findToggleActiveBtn().attributes('title')).toBe('');
expect(findToggleActiveBtn().attributes('aria-label')).toBe('');
});
......@@ -106,4 +116,86 @@ describe('RunnerTypeCell', () => {
});
});
});
describe('When the user clicks a runner', () => {
beforeEach(() => {
createComponent();
mutate.mockResolvedValue({
data: {
runnerDelete: {
runner: {
id: `gid://gitlab/Ci::Runner/1`,
__typename: 'CiRunner',
},
},
},
});
jest.spyOn(window, 'confirm');
});
describe('When the user confirms deletion', () => {
beforeEach(async () => {
window.confirm.mockReturnValue(true);
await findDeleteBtn().vm.$emit('click');
});
it('The user sees a confirmation alert', async () => {
expect(window.confirm).toHaveBeenCalledTimes(1);
expect(window.confirm).toHaveBeenCalledWith(expect.any(String));
});
it('The delete mutation is called correctly', () => {
expect(mutate).toHaveBeenCalledTimes(1);
expect(mutate).toHaveBeenCalledWith({
mutation: deleteRunnerMutation,
variables: {
input: {
id: `gid://gitlab/Ci::Runner/${mockId}`,
},
},
awaitRefetchQueries: true,
refetchQueries: [getRunnersQueryName],
});
});
it('The delete button does not have a loading state', () => {
expect(findDeleteBtn().props('loading')).toBe(false);
expect(findDeleteBtn().attributes('title')).toBe('Remove');
});
it('After the delete button is clicked, loading state is shown', async () => {
await findDeleteBtn().vm.$emit('click');
expect(findDeleteBtn().props('loading')).toBe(true);
});
it('After the delete button is clicked, stale tooltip is removed', async () => {
await findDeleteBtn().vm.$emit('click');
expect(findDeleteBtn().attributes('title')).toBe('');
});
});
describe('When the user does not confirm deletion', () => {
beforeEach(async () => {
window.confirm.mockReturnValue(false);
await findDeleteBtn().vm.$emit('click');
});
it('The user sees a confirmation alert', () => {
expect(window.confirm).toHaveBeenCalledTimes(1);
});
it('The delete mutation is not called', () => {
expect(mutate).toHaveBeenCalledTimes(0);
});
it('The delete button does not have a loading state', () => {
expect(findDeleteBtn().props('loading')).toBe(false);
expect(findDeleteBtn().attributes('title')).toBe('Remove');
});
});
});
});
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