Commit dee17694 authored by Savas Vedova's avatar Savas Vedova

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

Add Pause runner action to Runners Vue UI

See merge request gitlab-org/gitlab!62707
parents 362e0f54 6a41cb1f
<script>
import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import updateRunnerMutation from '~/runner/graphql/update_runner.mutation.graphql';
const i18n = {
I18N_EDIT: __('Edit'),
I18N_PAUSE: __('Pause'),
I18N_RESUME: __('Resume'),
};
export default {
components: {
GlButton,
GlButtonGroup,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
runner: {
type: Object,
required: true,
},
},
data() {
return {
updating: false,
};
},
computed: {
runnerNumericalId() {
return getIdFromGraphQLId(this.runner.id);
},
runnerUrl() {
// TODO implement using webUrl from the API
return `${gon.gitlab_url || ''}/admin/runners/${this.runnerNumericalId}`;
},
isActive() {
return this.runner.active;
},
toggleActiveIcon() {
return this.isActive ? 'pause' : 'play';
},
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.
return '';
}
return this.isActive ? i18n.I18N_PAUSE : i18n.I18N_RESUME;
},
},
methods: {
async onToggleActive() {
this.updating = true;
// TODO In HAML iteration we had a confirmation modal via:
// data-confirm="_('Are you sure?')"
// this may not have to ported, this is an easily reversible operation
try {
const toggledActive = !this.runner.active;
const {
data: {
runnerUpdate: { errors },
},
} = await this.$apollo.mutate({
mutation: updateRunnerMutation,
variables: {
input: {
id: this.runner.id,
active: toggledActive,
},
},
});
if (errors && errors.length) {
this.onError(new Error(errors[0]));
}
} catch (e) {
this.onError(e);
} finally {
this.updating = false;
}
},
onError(error) {
// TODO Render errors when "delete" action is done
// `active` toggle would not fail due to user input.
throw error;
},
},
i18n,
};
</script>
<template>
<gl-button-group>
<gl-button
v-gl-tooltip.hover.viewport
:title="$options.i18n.I18N_EDIT"
:aria-label="$options.i18n.I18N_EDIT"
icon="pencil"
:href="runnerUrl"
data-testid="edit-runner"
/>
<gl-button
v-gl-tooltip.hover.viewport
:title="toggleActiveTitle"
:aria-label="toggleActiveTitle"
:icon="toggleActiveIcon"
:loading="updating"
data-testid="toggle-active-runner"
@click="onToggleActive"
/>
<!-- TODO add delete action to update runners -->
</gl-button-group>
</template>
......@@ -3,6 +3,7 @@ import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { formatNumber, sprintf, __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import RunnerActionsCell from './cells/runner_actions_cell.vue';
import RunnerNameCell from './cells/runner_name_cell.vue';
import RunnerTypeCell from './cells/runner_type_cell.vue';
import RunnerTags from './runner_tags.vue';
......@@ -32,6 +33,7 @@ export default {
GlTable,
GlSkeletonLoader,
TimeAgo,
RunnerActionsCell,
RunnerNameCell,
RunnerTags,
RunnerTypeCell,
......@@ -132,8 +134,8 @@ export default {
<template v-else>{{ __('Never') }}</template>
</template>
<template #cell(actions)>
<!-- TODO add actions to update runners -->
<template #cell(actions)="{ item }">
<runner-actions-cell :runner="item" />
</template>
</gl-table>
</div>
......
#import "~/runner/graphql/runner_node.fragment.graphql"
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query getRunners(
......@@ -19,17 +20,7 @@ query getRunners(
sort: $sort
) {
nodes {
id
description
runnerType
shortSha
version
revision
ipAddress
active
locked
tagList
contactedAt
...RunnerNode
}
pageInfo {
...PageInfo
......
fragment RunnerNode on CiRunner {
id
description
runnerType
shortSha
version
revision
ipAddress
active
locked
tagList
contactedAt
}
#import "~/runner/graphql/runner_node.fragment.graphql"
mutation runnerUpdate($input: RunnerUpdateInput!) {
runnerUpdate(input: $input) {
runner {
...RunnerNode
}
errors
}
}
<script>
import * as Sentry from '@sentry/browser';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
......@@ -43,6 +44,10 @@ export default {
apollo: {
runners: {
query: getRunnersQuery,
// Runners can be updated by users directly in this list.
// A "cache and network" policy prevents outdated filtered
// results.
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
variables() {
return this.variables;
},
......
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 updateRunnerMutation from '~/runner/graphql/update_runner.mutation.graphql';
const mockId = '1';
describe('RunnerTypeCell', () => {
let wrapper;
let mutate;
const findEditBtn = () => wrapper.findByTestId('edit-runner');
const findToggleActiveBtn = () => wrapper.findByTestId('toggle-active-runner');
const createComponent = ({ active = true } = {}, options) => {
wrapper = extendedWrapper(
shallowMount(RunnerActionCell, {
propsData: {
runner: {
id: `gid://gitlab/Ci::Runner/${mockId}`,
active,
},
},
mocks: {
$apollo: {
mutate,
},
},
...options,
}),
);
};
beforeEach(() => {
mutate = jest.fn();
});
afterEach(() => {
mutate.mockReset();
wrapper.destroy();
});
it('Displays the runner edit link with the correct href', () => {
createComponent();
expect(findEditBtn().attributes('href')).toBe('/admin/runners/1');
});
describe.each`
state | label | icon | isActive | newActiveValue
${'active'} | ${'Pause'} | ${'pause'} | ${true} | ${false}
${'paused'} | ${'Resume'} | ${'play'} | ${false} | ${true}
`('When the runner is $state', ({ label, icon, isActive, newActiveValue }) => {
beforeEach(() => {
mutate.mockResolvedValue({
data: {
runnerUpdate: {
runner: {
id: `gid://gitlab/Ci::Runner/1`,
__typename: 'CiRunner',
},
},
},
});
createComponent({ active: isActive });
});
it(`Displays a ${icon} button`, () => {
expect(findToggleActiveBtn().props('loading')).toBe(false);
expect(findToggleActiveBtn().props('icon')).toBe(icon);
expect(findToggleActiveBtn().attributes('title')).toBe(label);
expect(findToggleActiveBtn().attributes('aria-label')).toBe(label);
});
it(`After clicking the ${icon} button, the button has a loading state`, async () => {
await findToggleActiveBtn().vm.$emit('click');
expect(findToggleActiveBtn().props('loading')).toBe(true);
expect(findToggleActiveBtn().attributes('title')).toBe('');
expect(findToggleActiveBtn().attributes('aria-label')).toBe('');
});
describe(`When clicking on the ${icon} button`, () => {
beforeEach(async () => {
await findToggleActiveBtn().vm.$emit('click');
await waitForPromises();
});
it(`The apollo mutation to set active to ${newActiveValue} is called`, () => {
expect(mutate).toHaveBeenCalledTimes(1);
expect(mutate).toHaveBeenCalledWith({
mutation: updateRunnerMutation,
variables: {
input: {
id: `gid://gitlab/Ci::Runner/${mockId}`,
active: newActiveValue,
},
},
});
});
it('The button does not have a loading state', () => {
expect(findToggleActiveBtn().props('loading')).toBe(false);
});
});
});
});
......@@ -17,7 +17,7 @@ describe('RunnerList', () => {
const findHeaders = () => wrapper.findAll('th');
const findRows = () => wrapper.findAll('[data-testid^="runner-row-"]');
const findCell = ({ row = 0, fieldKey }) =>
findRows().at(row).find(`[data-testid="td-${fieldKey}"]`);
extendedWrapper(findRows().at(row).find(`[data-testid="td-${fieldKey}"]`));
const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
wrapper = extendedWrapper(
......@@ -93,7 +93,12 @@ describe('RunnerList', () => {
expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('');
expect(findCell({ fieldKey: 'tagList' }).text()).toBe('');
expect(findCell({ fieldKey: 'contactedAt' }).text()).toEqual(expect.any(String));
expect(findCell({ fieldKey: 'actions' }).text()).toBe('');
// Actions
const actions = findCell({ fieldKey: 'actions' });
expect(actions.findByTestId('edit-runner').exists()).toBe(true);
expect(actions.findByTestId('toggle-active-runner').exists()).toBe(true);
});
it('Links to the runner page', () => {
......
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