Commit 08e1047a authored by Simon Knox's avatar Simon Knox

Merge branch '352887-refetch-on-delete' into 'master'

Use events to refetch runners data

See merge request gitlab-org/gitlab!81163
parents da9f22f9 bc82f520
...@@ -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