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