Commit bc82f520 authored by Miguel Rincon's avatar Miguel Rincon

Use events to refetch runners data

This change refactors the way runner data is refetched when one of the
runners is deleted.

Instead of using a global refetch in the mutation, each app is made
responsible for refetching. This will give us more flexibility when
responding to delete events.
parent fc897527
......@@ -224,6 +224,10 @@ export default {
}
return '';
},
onDeleted({ message }) {
this.$root.$toast?.show(message);
this.$apollo.queries.runners.refetch();
},
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
......@@ -282,7 +286,11 @@ export default {
</gl-link>
</template>
<template #runner-actions-cell="{ runner }">
<runner-actions-cell :runner="runner" :edit-url="runner.editAdminUrl" />
<runner-actions-cell
:runner="runner"
:edit-url="runner.editAdminUrl"
@deleted="onDeleted"
/>
</template>
</runner-list>
<runner-pagination
......
......@@ -23,6 +23,7 @@ export default {
required: false,
},
},
emits: ['deleted'],
computed: {
canUpdate() {
return this.runner.userPermissions?.updateRunner;
......@@ -31,6 +32,11 @@ export default {
return this.runner.userPermissions?.deleteRunner;
},
},
methods: {
onDeleted(value) {
this.$emit('deleted', value);
},
},
};
</script>
......@@ -38,6 +44,6 @@ export default {
<gl-button-group>
<runner-edit-button v-if="canUpdate && editUrl" :href="editUrl" />
<runner-pause-button v-if="canUpdate" :runner="runner" :compact="true" />
<runner-delete-button v-if="canDelete" :runner="runner" :compact="true" />
<runner-delete-button v-if="canDelete" :runner="runner" :compact="true" @deleted="onDeleted" />
</gl-button-group>
</template>
......@@ -2,14 +2,12 @@
import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import runnerDeleteMutation from '~/runner/graphql/shared/runner_delete.mutation.graphql';
import { createAlert } from '~/flash';
import { s__, sprintf } from '~/locale';
import { sprintf } from '~/locale';
import { captureException } from '~/runner/sentry_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { I18N_DELETE_RUNNER } from '../constants';
import { I18N_DELETE_RUNNER, I18N_DELETED_TOAST } from '../constants';
import RunnerDeleteModal from './runner_delete_modal.vue';
const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted');
export default {
name: 'RunnerDeleteButton',
components: {
......@@ -34,6 +32,7 @@ export default {
default: false,
},
},
emits: ['deleted'],
data() {
return {
deleting: false,
......@@ -102,12 +101,13 @@ export default {
id: this.runner.id,
},
},
refetchQueries: ['getRunners', 'getGroupRunners'],
});
if (errors && errors.length) {
throw new Error(errors.join(' '));
} else {
this.$root.$toast?.show(sprintf(I18N_DELETED_TOAST, { name: this.runnerName }));
this.$emit('deleted', {
message: sprintf(I18N_DELETED_TOAST, { name: this.runnerName }),
});
}
} catch (e) {
this.deleting = false;
......
......@@ -40,6 +40,7 @@ export const I18N_EDIT = __('Edit');
export const I18N_PAUSE = __('Pause');
export const I18N_RESUME = __('Resume');
export const I18N_DELETE_RUNNER = s__('Runners|Delete runner');
export const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted');
export const I18N_LOCKED_RUNNER_DESCRIPTION = s__('Runners|You cannot assign to other projects');
export const I18N_PAUSED_RUNNER_DESCRIPTION = s__('Runners|Not available to run jobs');
......
......@@ -16,13 +16,13 @@ import RunnerActionsCell from '../components/cells/runner_actions_cell.vue';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import {
I18N_FETCH_ERROR,
GROUP_FILTERED_SEARCH_NAMESPACE,
GROUP_TYPE,
PROJECT_TYPE,
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_STALE,
I18N_FETCH_ERROR,
} from '../constants';
import groupRunnersQuery from '../graphql/list/group_runners.query.graphql';
import groupRunnersCountQuery from '../graphql/list/group_runners_count.query.graphql';
......@@ -241,6 +241,10 @@ export default {
editUrl(runner) {
return this.runners.urlsById[runner.id]?.edit;
},
onDeleted({ message }) {
this.$root.$toast?.show(message);
this.$apollo.queries.runners.refetch();
},
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
......@@ -298,7 +302,7 @@ export default {
</gl-link>
</template>
<template #runner-actions-cell="{ runner }">
<runner-actions-cell :runner="runner" :edit-url="editUrl(runner)" />
<runner-actions-cell :runner="runner" :edit-url="editUrl(runner)" @deleted="onDeleted" />
</template>
</runner-list>
<runner-pagination
......
import Vue from 'vue';
import { GlLink } from '@gitlab/ui';
import { GlToast, GlLink } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
......@@ -18,8 +18,8 @@ import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
import {
......@@ -52,6 +52,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
}));
Vue.use(VueApollo);
Vue.use(GlToast);
describe('AdminRunnersApp', () => {
let wrapper;
......@@ -59,6 +60,7 @@ describe('AdminRunnersApp', () => {
let mockRunnersCountQuery;
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
......@@ -95,6 +97,7 @@ describe('AdminRunnersApp', () => {
afterEach(() => {
mockRunnersQuery.mockReset();
mockRunnersCountQuery.mockReset();
wrapper.destroy();
});
......@@ -228,6 +231,41 @@ describe('AdminRunnersApp', () => {
]);
});
describe('Single runner row', () => {
let showToast;
const mockRunner = runnersData.data.runners.nodes[0];
const { id: graphqlId, shortSha } = mockRunner;
const id = getIdFromGraphQLId(graphqlId);
beforeEach(async () => {
mockRunnersQuery.mockClear();
createComponent({ mountFn: mountExtended });
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
await waitForPromises();
});
it('Links to the runner page', async () => {
const runnerLink = wrapper.find('tr [data-testid="td-summary"]').find(GlLink);
expect(runnerLink.text()).toBe(`#${id} (${shortSha})`);
expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${id}`);
});
it('When runner is deleted, data is refetched and a toast message is shown', async () => {
expect(mockRunnersQuery).toHaveBeenCalledTimes(1);
findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(mockRunnersQuery).toHaveBeenCalledTimes(2);
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Runner deleted');
});
});
describe('when a filter is preselected', () => {
beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
......
......@@ -92,6 +92,18 @@ describe('RunnerActionsCell', () => {
expect(findDeleteBtn().props('compact')).toBe(true);
});
it('Emits delete events', () => {
const value = { name: 'Runner' };
createComponent();
expect(wrapper.emitted('deleted')).toBe(undefined);
findDeleteBtn().vm.$emit('deleted', value);
expect(wrapper.emitted('deleted')).toEqual([[value]]);
});
it('Does not render the runner delete button when user cannot delete', () => {
createComponent({
runner: {
......
import Vue from 'vue';
import { GlButton, GlToast } from '@gitlab/ui';
import { GlButton } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
......@@ -19,7 +19,6 @@ const mockRunner = runnersData.data.runners.nodes[0];
const mockRunnerId = getIdFromGraphQLId(mockRunner.id);
Vue.use(VueApollo);
Vue.use(GlToast);
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
......@@ -27,7 +26,6 @@ jest.mock('~/runner/sentry_utils');
describe('RunnerDeleteButton', () => {
let wrapper;
let runnerDeleteHandler;
let showToast;
const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
const getModal = () => getBinding(wrapper.element, 'gl-modal').value;
......@@ -52,8 +50,6 @@ describe('RunnerDeleteButton', () => {
GlModal: createMockDirective(),
},
});
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show').mockImplementation(() => {});
};
const clickOkAndWait = async () => {
......@@ -128,7 +124,7 @@ describe('RunnerDeleteButton', () => {
await clickOkAndWait();
});
it('The mutation to delete is called', async () => {
it('The mutation to delete is called', () => {
expect(runnerDeleteHandler).toHaveBeenCalledTimes(1);
expect(runnerDeleteHandler).toHaveBeenCalledWith({
input: {
......@@ -137,8 +133,12 @@ describe('RunnerDeleteButton', () => {
});
});
it('The user is notified', async () => {
expect(showToast).toHaveBeenCalledTimes(1);
it('The user can be notified with an event', () => {
const deleted = wrapper.emitted('deleted');
expect(deleted).toHaveLength(1);
expect(deleted[0][0].message).toMatch(`#${mockRunnerId}`);
expect(deleted[0][0].message).toMatch(`${mockRunner.shortSha}`);
});
});
......
import Vue, { nextTick } from 'vue';
import { GlButton, GlLink } from '@gitlab/ui';
import { GlButton, GlLink, GlToast } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
......@@ -17,6 +17,7 @@ import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
......@@ -40,6 +41,7 @@ import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered
import { groupRunnersData, groupRunnersDataPaginated, groupRunnersCountData } from '../mock_data';
Vue.use(VueApollo);
Vue.use(GlToast);
const mockGroupFullPath = 'group1';
const mockRegistrationToken = 'AABBCC';
......@@ -59,6 +61,7 @@ describe('GroupRunnersApp', () => {
let mockGroupRunnersCountQuery;
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
......@@ -187,12 +190,17 @@ describe('GroupRunnersApp', () => {
});
describe('Single runner row', () => {
let showToast;
const { webUrl, editUrl, node } = mockGroupRunnersEdges[0];
const { id: graphqlId, shortSha } = node;
const id = getIdFromGraphQLId(graphqlId);
beforeEach(async () => {
mockGroupRunnersQuery.mockClear();
createComponent({ mountFn: mountExtended });
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
await waitForPromises();
});
......@@ -212,6 +220,17 @@ describe('GroupRunnersApp', () => {
href: editUrl,
});
});
it('When runner is deleted, data is refetched and a toast is shown', async () => {
expect(mockGroupRunnersQuery).toHaveBeenCalledTimes(1);
findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(mockGroupRunnersQuery).toHaveBeenCalledTimes(2);
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Runner deleted');
});
});
describe('when a filter is preselected', () => {
......
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