Commit a68a1849 authored by Miguel Rincon's avatar Miguel Rincon

Refetch runners list data after runner is updated

This change updates data refresh mechanism for the runners UI, when
a runner is deleted or paused/unpaused.

Total count of runners should stay up to date as users interact with
the runner data, and remove some ambiguity as to when data is refreshed.

Changelog: fixed
parent 3dbb300d
<script> <script>
import { GlBadge, GlLink } from '@gitlab/ui'; import { GlBadge, GlLink } from '@gitlab/ui';
import { createAlert } from '~/flash'; import { createAlert } from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility'; import { updateHistory } from '~/lib/utils/url_utility';
import { formatNumber } from '~/locale'; import { formatNumber } from '~/locale';
import { fetchPolicies } from '~/lib/graphql';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue'; import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
...@@ -37,7 +37,7 @@ import { captureException } from '../sentry_utils'; ...@@ -37,7 +37,7 @@ import { captureException } from '../sentry_utils';
const runnersCountSmartQuery = { const runnersCountSmartQuery = {
query: runnersAdminCountQuery, query: runnersAdminCountQuery,
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, fetchPolicy: fetchPolicies.NETWORK_ONLY,
update(data) { update(data) {
return data?.runners?.count; return data?.runners?.count;
}, },
...@@ -78,10 +78,7 @@ export default { ...@@ -78,10 +78,7 @@ export default {
apollo: { apollo: {
runners: { runners: {
query: runnersAdminQuery, query: runnersAdminQuery,
// Runners can be updated by users directly in this list. fetchPolicy: fetchPolicies.NETWORK_ONLY,
// A "cache and network" policy prevents outdated filtered
// results.
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
variables() { variables() {
return this.variables; return this.variables;
}, },
...@@ -224,9 +221,19 @@ export default { ...@@ -224,9 +221,19 @@ export default {
} }
return ''; return '';
}, },
refetchFilteredCounts() {
this.$apollo.queries.allRunnersCount.refetch();
this.$apollo.queries.instanceRunnersCount.refetch();
this.$apollo.queries.groupRunnersCount.refetch();
this.$apollo.queries.projectRunnersCount.refetch();
},
onToggledPaused() {
// When a runner is Paused, the tab count can
// become stale, refetch outdated counts.
this.refetchFilteredCounts();
},
onDeleted({ message }) { onDeleted({ message }) {
this.$root.$toast?.show(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 });
...@@ -289,6 +296,7 @@ export default { ...@@ -289,6 +296,7 @@ export default {
<runner-actions-cell <runner-actions-cell
:runner="runner" :runner="runner"
:edit-url="runner.editAdminUrl" :edit-url="runner.editAdminUrl"
@toggledPaused="onToggledPaused"
@deleted="onDeleted" @deleted="onDeleted"
/> />
</template> </template>
......
...@@ -23,7 +23,7 @@ export default { ...@@ -23,7 +23,7 @@ export default {
required: false, required: false,
}, },
}, },
emits: ['deleted'], emits: ['toggledPaused', 'deleted'],
computed: { computed: {
canUpdate() { canUpdate() {
return this.runner.userPermissions?.updateRunner; return this.runner.userPermissions?.updateRunner;
...@@ -33,6 +33,9 @@ export default { ...@@ -33,6 +33,9 @@ export default {
}, },
}, },
methods: { methods: {
onToggledPaused() {
this.$emit('toggledPaused');
},
onDeleted(value) { onDeleted(value) {
this.$emit('deleted', value); this.$emit('deleted', value);
}, },
...@@ -43,7 +46,12 @@ export default { ...@@ -43,7 +46,12 @@ export default {
<template> <template>
<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"
@toggledPaused="onToggledPaused"
/>
<runner-delete-button <runner-delete-button
:disabled="!canDelete" :disabled="!canDelete"
:runner="runner" :runner="runner"
......
...@@ -126,6 +126,11 @@ export default { ...@@ -126,6 +126,11 @@ export default {
id: this.runner.id, id: this.runner.id,
}, },
}, },
update: (cache) => {
// Remove deleted runner from the cache
const cacheId = cache.identify(this.runner);
cache.evict({ id: cacheId });
},
}); });
if (errors && errors.length) { if (errors && errors.length) {
throw new Error(errors.join(' ')); throw new Error(errors.join(' '));
......
...@@ -24,6 +24,7 @@ export default { ...@@ -24,6 +24,7 @@ export default {
default: false, default: false,
}, },
}, },
emits: ['toggledPaused'],
data() { data() {
return { return {
updating: false, updating: false,
...@@ -83,6 +84,7 @@ export default { ...@@ -83,6 +84,7 @@ export default {
if (errors && errors.length) { if (errors && errors.length) {
throw new Error(errors.join(' ')); throw new Error(errors.join(' '));
} }
this.$emit('toggledPaused');
} catch (e) { } catch (e) {
this.onError(e); this.onError(e);
} finally { } finally {
......
<script> <script>
import { GlBadge, GlLink } from '@gitlab/ui'; import { GlBadge, GlLink } from '@gitlab/ui';
import { createAlert } from '~/flash'; import { createAlert } from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility'; import { updateHistory } from '~/lib/utils/url_utility';
import { formatNumber } from '~/locale'; import { formatNumber } from '~/locale';
import { fetchPolicies } from '~/lib/graphql';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue'; import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
...@@ -35,7 +35,7 @@ import { captureException } from '../sentry_utils'; ...@@ -35,7 +35,7 @@ import { captureException } from '../sentry_utils';
const runnersCountSmartQuery = { const runnersCountSmartQuery = {
query: groupRunnersCountQuery, query: groupRunnersCountQuery,
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, fetchPolicy: fetchPolicies.NETWORK_ONLY,
update(data) { update(data) {
return data?.group?.runners?.count; return data?.group?.runners?.count;
}, },
...@@ -85,10 +85,7 @@ export default { ...@@ -85,10 +85,7 @@ export default {
apollo: { apollo: {
runners: { runners: {
query: groupRunnersQuery, query: groupRunnersQuery,
// Runners can be updated by users directly in this list. fetchPolicy: fetchPolicies.NETWORK_ONLY,
// A "cache and network" policy prevents outdated filtered
// results.
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
variables() { variables() {
return this.variables; return this.variables;
}, },
...@@ -241,9 +238,18 @@ export default { ...@@ -241,9 +238,18 @@ export default {
editUrl(runner) { editUrl(runner) {
return this.runners.urlsById[runner.id]?.edit; return this.runners.urlsById[runner.id]?.edit;
}, },
refetchFilteredCounts() {
this.$apollo.queries.allRunnersCount.refetch();
this.$apollo.queries.groupRunnersCount.refetch();
this.$apollo.queries.projectRunnersCount.refetch();
},
onToggledPaused() {
// When a runner is Paused, the tab count can
// become stale, refetch outdated counts.
this.refetchFilteredCounts();
},
onDeleted({ message }) { onDeleted({ message }) {
this.$root.$toast?.show(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 });
...@@ -302,7 +308,12 @@ export default { ...@@ -302,7 +308,12 @@ 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)" @deleted="onDeleted" /> <runner-actions-cell
:runner="runner"
:edit-url="editUrl(runner)"
@toggledPaused="onToggledPaused"
@deleted="onDeleted"
/>
</template> </template>
</runner-list> </runner-list>
<runner-pagination <runner-pagination
......
...@@ -235,9 +235,11 @@ describe('AdminRunnersApp', () => { ...@@ -235,9 +235,11 @@ describe('AdminRunnersApp', () => {
const mockRunner = runnersData.data.runners.nodes[0]; const mockRunner = runnersData.data.runners.nodes[0];
const { id: graphqlId, shortSha } = mockRunner; const { id: graphqlId, shortSha } = mockRunner;
const id = getIdFromGraphQLId(graphqlId); const id = getIdFromGraphQLId(graphqlId);
const COUNT_QUERIES = 7; // Smart queries that display a filtered count of runners
const FILTERED_COUNT_QUERIES = 4; // Smart queries that display a count of runners in tabs
beforeEach(async () => { beforeEach(async () => {
mockRunnersQuery.mockClear(); mockRunnersCountQuery.mockClear();
createComponent({ mountFn: mountExtended }); createComponent({ mountFn: mountExtended });
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show'); showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
...@@ -252,12 +254,18 @@ describe('AdminRunnersApp', () => { ...@@ -252,12 +254,18 @@ describe('AdminRunnersApp', () => {
expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${id}`); 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 () => { it('When runner is paused or unpaused, some data is refetched', async () => {
expect(mockRunnersQuery).toHaveBeenCalledTimes(1); expect(mockRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES);
findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' }); findRunnerActionsCell().vm.$emit('toggledPaused');
expect(mockRunnersQuery).toHaveBeenCalledTimes(2); expect(mockRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES + FILTERED_COUNT_QUERIES);
expect(showToast).toHaveBeenCalledTimes(0);
});
it('When runner is deleted, data is refetched and a toast message is shown', async () => {
findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(showToast).toHaveBeenCalledTimes(1); expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Runner deleted'); expect(showToast).toHaveBeenCalledWith('Runner deleted');
......
...@@ -100,6 +100,16 @@ describe('RunnerActionsCell', () => { ...@@ -100,6 +100,16 @@ describe('RunnerActionsCell', () => {
expect(findDeleteBtn().props('runner')).toEqual(mockRunner); expect(findDeleteBtn().props('runner')).toEqual(mockRunner);
}); });
it('Emits toggledPaused events', () => {
createComponent();
expect(wrapper.emitted('toggledPaused')).toBe(undefined);
findRunnerPauseBtn().vm.$emit('toggledPaused');
expect(wrapper.emitted('toggledPaused')).toHaveLength(1);
});
it('Emits delete events', () => { it('Emits delete events', () => {
const value = { name: 'Runner' }; const value = { name: 'Runner' };
......
...@@ -146,6 +146,10 @@ describe('RunnerPauseButton', () => { ...@@ -146,6 +146,10 @@ describe('RunnerPauseButton', () => {
it('The button does not have a loading state', () => { it('The button does not have a loading state', () => {
expect(findBtn().props('loading')).toBe(false); expect(findBtn().props('loading')).toBe(false);
}); });
it('The button emits toggledPaused', () => {
expect(wrapper.emitted('toggledPaused')).toHaveLength(1);
});
}); });
describe('When update fails', () => { describe('When update fails', () => {
......
...@@ -193,9 +193,11 @@ describe('GroupRunnersApp', () => { ...@@ -193,9 +193,11 @@ describe('GroupRunnersApp', () => {
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);
const COUNT_QUERIES = 6; // Smart queries that display a filtered count of runners
const FILTERED_COUNT_QUERIES = 3; // Smart queries that display a count of runners in tabs
beforeEach(async () => { beforeEach(async () => {
mockGroupRunnersQuery.mockClear(); mockGroupRunnersCountQuery.mockClear();
createComponent({ mountFn: mountExtended }); createComponent({ mountFn: mountExtended });
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show'); showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
...@@ -219,12 +221,20 @@ describe('GroupRunnersApp', () => { ...@@ -219,12 +221,20 @@ describe('GroupRunnersApp', () => {
}); });
}); });
it('When runner is deleted, data is refetched and a toast is shown', async () => { it('When runner is paused or unpaused, some data is refetched', async () => {
expect(mockGroupRunnersQuery).toHaveBeenCalledTimes(1); expect(mockGroupRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES);
findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' }); findRunnerActionsCell().vm.$emit('toggledPaused');
expect(mockGroupRunnersCountQuery).toHaveBeenCalledTimes(
COUNT_QUERIES + FILTERED_COUNT_QUERIES,
);
expect(mockGroupRunnersQuery).toHaveBeenCalledTimes(2); expect(showToast).toHaveBeenCalledTimes(0);
});
it('When runner is deleted, data is refetched and a toast message is shown', async () => {
findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(showToast).toHaveBeenCalledTimes(1); expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Runner deleted'); expect(showToast).toHaveBeenCalledWith('Runner deleted');
......
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