Commit d7244e72 authored by Brandon Labuschagne's avatar Brandon Labuschagne

Poll devops adoption table data

Only poll the data if there are pending rows
or if the data was not refreshed on the current
day.
parent 2dc5ffc1
...@@ -16,9 +16,11 @@ import { ...@@ -16,9 +16,11 @@ import {
MAX_SEGMENTS, MAX_SEGMENTS,
DATE_TIME_FORMAT, DATE_TIME_FORMAT,
DEVOPS_ADOPTION_SEGMENT_MODAL_ID, DEVOPS_ADOPTION_SEGMENT_MODAL_ID,
DEFAULT_POLLING_INTERVAL,
} from '../constants'; } from '../constants';
import devopsAdoptionSegmentsQuery from '../graphql/queries/devops_adoption_segments.query.graphql'; import devopsAdoptionSegmentsQuery from '../graphql/queries/devops_adoption_segments.query.graphql';
import getGroupsQuery from '../graphql/queries/get_groups.query.graphql'; import getGroupsQuery from '../graphql/queries/get_groups.query.graphql';
import { shouldPollTableData } from '../utils/helpers';
import DevopsAdoptionEmptyState from './devops_adoption_empty_state.vue'; import DevopsAdoptionEmptyState from './devops_adoption_empty_state.vue';
import DevopsAdoptionSegmentModal from './devops_adoption_segment_modal.vue'; import DevopsAdoptionSegmentModal from './devops_adoption_segment_modal.vue';
import DevopsAdoptionTable from './devops_adoption_table.vue'; import DevopsAdoptionTable from './devops_adoption_table.vue';
...@@ -48,6 +50,7 @@ export default { ...@@ -48,6 +50,7 @@ export default {
isLoadingGroups: false, isLoadingGroups: false,
requestCount: 0, requestCount: 0,
selectedSegment: null, selectedSegment: null,
openModal: false,
errors: { errors: {
[DEVOPS_ADOPTION_ERROR_KEYS.groups]: false, [DEVOPS_ADOPTION_ERROR_KEYS.groups]: false,
[DEVOPS_ADOPTION_ERROR_KEYS.segments]: false, [DEVOPS_ADOPTION_ERROR_KEYS.segments]: false,
...@@ -56,6 +59,7 @@ export default { ...@@ -56,6 +59,7 @@ export default {
nodes: [], nodes: [],
pageInfo: null, pageInfo: null,
}, },
pollingTableData: null,
}; };
}, },
apollo: { apollo: {
...@@ -98,7 +102,27 @@ export default { ...@@ -98,7 +102,27 @@ export default {
created() { created() {
this.fetchGroups(); this.fetchGroups();
}, },
beforeDestroy() {
clearInterval(this.pollingTableData);
},
methods: { methods: {
pollTableData() {
const shouldPoll = shouldPollTableData({
segments: this.devopsAdoptionSegments.nodes,
timestamp: this.devopsAdoptionSegments?.nodes[0]?.latestSnapshot?.recordedAt,
openModal: this.openModal,
});
if (shouldPoll) {
this.$apollo.queries.devopsAdoptionSegments.refetch();
}
},
trackModalOpenState(state) {
this.openModal = state;
},
startPollingTableData() {
this.pollingTableData = setInterval(this.pollTableData, DEFAULT_POLLING_INTERVAL);
},
handleError(key, error) { handleError(key, error) {
this.errors[key] = true; this.errors[key] = true;
Sentry.captureException(error); Sentry.captureException(error);
...@@ -126,6 +150,7 @@ export default { ...@@ -126,6 +150,7 @@ export default {
this.fetchGroups(pageInfo.nextPage); this.fetchGroups(pageInfo.nextPage);
} else { } else {
this.isLoadingGroups = false; this.isLoadingGroups = false;
this.startPollingTableData();
} }
}) })
.catch((error) => this.handleError(DEVOPS_ADOPTION_ERROR_KEYS.groups, error)); .catch((error) => this.handleError(DEVOPS_ADOPTION_ERROR_KEYS.groups, error));
...@@ -154,6 +179,7 @@ export default { ...@@ -154,6 +179,7 @@ export default {
:key="modalKey" :key="modalKey"
:groups="groups.nodes" :groups="groups.nodes"
:segment="selectedSegment" :segment="selectedSegment"
@trackModalOpenState="trackModalOpenState"
/> />
<div v-if="hasSegmentsData" class="gl-mt-3"> <div v-if="hasSegmentsData" class="gl-mt-3">
<div <div
...@@ -178,6 +204,7 @@ export default { ...@@ -178,6 +204,7 @@ export default {
:segments="devopsAdoptionSegments.nodes" :segments="devopsAdoptionSegments.nodes"
:selected-segment="selectedSegment" :selected-segment="selectedSegment"
@set-selected-segment="setSelectedSegment" @set-selected-segment="setSelectedSegment"
@trackModalOpenState="trackModalOpenState"
/> />
</div> </div>
<devops-adoption-empty-state <devops-adoption-empty-state
......
...@@ -96,6 +96,8 @@ export default { ...@@ -96,6 +96,8 @@ export default {
:action-primary="primaryOptions" :action-primary="primaryOptions"
:action-cancel="cancelOptions" :action-cancel="cancelOptions"
@primary.prevent="deleteSegment" @primary.prevent="deleteSegment"
@hide="$emit('trackModalOpenState', false)"
@show="$emit('trackModalOpenState', true)"
> >
<template #modal-title>{{ $options.i18n.title }}</template> <template #modal-title>{{ $options.i18n.title }}</template>
<gl-alert v-if="errors.length" variant="danger" class="gl-mb-3" @dismiss="clearErrors"> <gl-alert v-if="errors.length" variant="danger" class="gl-mb-3" @dismiss="clearErrors">
......
...@@ -120,6 +120,7 @@ export default { ...@@ -120,6 +120,7 @@ export default {
resetForm() { resetForm() {
this.selectedGroupId = null; this.selectedGroupId = null;
this.filter = ''; this.filter = '';
this.$emit('trackModalOpenState', false);
}, },
}, },
devopsSegmentModalId: DEVOPS_ADOPTION_SEGMENT_MODAL_ID, devopsSegmentModalId: DEVOPS_ADOPTION_SEGMENT_MODAL_ID,
...@@ -137,6 +138,7 @@ export default { ...@@ -137,6 +138,7 @@ export default {
@primary.prevent="primaryOptions.callback" @primary.prevent="primaryOptions.callback"
@canceled="cancelOptions.callback" @canceled="cancelOptions.callback"
@hide="resetForm" @hide="resetForm"
@show="$emit('trackModalOpenState', true)"
> >
<gl-alert v-if="errors.length" variant="danger" class="gl-mb-3" @dismiss="clearErrors"> <gl-alert v-if="errors.length" variant="danger" class="gl-mb-3" @dismiss="clearErrors">
{{ displayError }} {{ displayError }}
......
...@@ -225,6 +225,10 @@ export default { ...@@ -225,6 +225,10 @@ export default {
</div> </div>
</template> </template>
</gl-table> </gl-table>
<devops-adoption-delete-modal v-if="selectedSegment" :segment="selectedSegment" /> <devops-adoption-delete-modal
v-if="selectedSegment"
:segment="selectedSegment"
@trackModalOpenState="$emit('trackModalOpenState', $event)"
/>
</div> </div>
</template> </template>
import { s__, __, sprintf } from '~/locale'; import { s__, __, sprintf } from '~/locale';
export const DEFAULT_POLLING_INTERVAL = 30000;
export const MAX_SEGMENTS = 30; export const MAX_SEGMENTS = 30;
export const MAX_REQUEST_COUNT = 10; export const MAX_REQUEST_COUNT = 10;
......
import { isToday } from '~/lib/utils/datetime_utility';
/**
* A helper function which accepts the segments,
*
* @param {Object} params the segment data, timestamp and check for open modals
*
* @return {Boolean} a boolean to determine if table data should be polled
*/
export const shouldPollTableData = ({ segments, timestamp, openModal }) => {
if (openModal) {
return false;
} else if (!segments.length) {
return true;
}
const anyPendingSegments = segments.some((node) => node.latestSnapshot === null);
const dataNotRefreshedToday = !isToday(new Date(timestamp));
return anyPendingSegments || dataNotRefreshedToday;
};
...@@ -12,6 +12,7 @@ import { ...@@ -12,6 +12,7 @@ import {
DEVOPS_ADOPTION_STRINGS, DEVOPS_ADOPTION_STRINGS,
DEVOPS_ADOPTION_SEGMENT_MODAL_ID, DEVOPS_ADOPTION_SEGMENT_MODAL_ID,
MAX_SEGMENTS, MAX_SEGMENTS,
DEFAULT_POLLING_INTERVAL,
} from 'ee/analytics/devops_report/devops_adoption/constants'; } from 'ee/analytics/devops_report/devops_adoption/constants';
import devopsAdoptionSegments from 'ee/analytics/devops_report/devops_adoption/graphql/queries/devops_adoption_segments.query.graphql'; import devopsAdoptionSegments from 'ee/analytics/devops_report/devops_adoption/graphql/queries/devops_adoption_segments.query.graphql';
import getGroupsQuery from 'ee/analytics/devops_report/devops_adoption/graphql/queries/get_groups.query.graphql'; import getGroupsQuery from 'ee/analytics/devops_report/devops_adoption/graphql/queries/get_groups.query.graphql';
...@@ -446,5 +447,36 @@ describe('DevopsAdoptionApp', () => { ...@@ -446,5 +447,36 @@ describe('DevopsAdoptionApp', () => {
expect(Sentry.captureException.mock.calls[0][0].networkError).toBe(segmentsErrorMessage); expect(Sentry.captureException.mock.calls[0][0].networkError).toBe(segmentsErrorMessage);
}); });
}); });
describe('data polling', () => {
const mockIntervalId = 1234;
beforeEach(async () => {
jest.spyOn(window, 'setInterval').mockReturnValue(mockIntervalId);
jest.spyOn(window, 'clearInterval').mockImplementation();
wrapper = createComponent({
mockApollo: createMockApolloProvider({
groupsSpy: jest.fn().mockResolvedValueOnce({ ...initialResponse, pageInfo: null }),
}),
});
await waitForPromises();
});
it('sets pollTableData interval', () => {
expect(window.setInterval).toHaveBeenCalledWith(
wrapper.vm.pollTableData,
DEFAULT_POLLING_INTERVAL,
);
expect(wrapper.vm.pollingTableData).toBe(mockIntervalId);
});
it('clears pollTableData interval when destroying ', () => {
wrapper.vm.$destroy();
expect(window.clearInterval).toHaveBeenCalledWith(mockIntervalId);
});
});
}); });
}); });
...@@ -82,6 +82,21 @@ describe('DevopsAdoptionDeleteModal', () => { ...@@ -82,6 +82,21 @@ describe('DevopsAdoptionDeleteModal', () => {
}); });
}); });
describe.each`
state | action | expected
${'opening'} | ${'show'} | ${true}
${'closing'} | ${'hide'} | ${false}
`('$state the modal', ({ action, expected }) => {
beforeEach(() => {
createComponent();
findModal().vm.$emit(action);
});
it(`emits trackModalOpenState as ${expected}`, () => {
expect(wrapper.emitted('trackModalOpenState')).toStrictEqual([[expected]]);
});
});
describe('submitting the form', () => { describe('submitting the form', () => {
describe('while waiting for the mutation', () => { describe('while waiting for the mutation', () => {
beforeEach(() => createComponent({ mutationMock: mutateLoading })); beforeEach(() => createComponent({ mutationMock: mutateLoading }));
......
...@@ -159,6 +159,21 @@ describe('DevopsAdoptionSegmentModal', () => { ...@@ -159,6 +159,21 @@ describe('DevopsAdoptionSegmentModal', () => {
}); });
}); });
describe.each`
state | action | expected
${'opening'} | ${'show'} | ${true}
${'closing'} | ${'hide'} | ${false}
`('$state the modal', ({ action, expected }) => {
beforeEach(() => {
createComponent();
findModal().vm.$emit(action);
});
it(`emits trackModalOpenState as ${expected}`, () => {
expect(wrapper.emitted('trackModalOpenState')).toStrictEqual([[expected]]);
});
});
it.each` it.each`
selectedGroupId | disabled | values | state selectedGroupId | disabled | values | state
${null} | ${true} | ${'checkbox'} | ${'disables'} ${null} | ${true} | ${'checkbox'} | ${'disables'}
......
import { GlTable, GlButton, GlIcon } from '@gitlab/ui'; import { GlTable, GlButton, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import DevopsAdoptionDeleteModal from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_delete_modal.vue';
import DevopsAdoptionTable from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_table.vue'; import DevopsAdoptionTable from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_table.vue';
import DevopsAdoptionTableCellFlag from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_table_cell_flag.vue'; import DevopsAdoptionTableCellFlag from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_table_cell_flag.vue';
import { DEVOPS_ADOPTION_TABLE_TEST_IDS as TEST_IDS } from 'ee/analytics/devops_report/devops_adoption/constants'; import { DEVOPS_ADOPTION_TABLE_TEST_IDS as TEST_IDS } from 'ee/analytics/devops_report/devops_adoption/constants';
...@@ -15,6 +16,7 @@ describe('DevopsAdoptionTable', () => { ...@@ -15,6 +16,7 @@ describe('DevopsAdoptionTable', () => {
wrapper = mount(DevopsAdoptionTable, { wrapper = mount(DevopsAdoptionTable, {
propsData: { propsData: {
segments: devopsAdoptionSegmentsData.nodes, segments: devopsAdoptionSegmentsData.nodes,
selectedSegment: devopsAdoptionSegmentsData.nodes[0],
}, },
directives: { directives: {
GlTooltip: createMockDirective(), GlTooltip: createMockDirective(),
...@@ -45,6 +47,8 @@ describe('DevopsAdoptionTable', () => { ...@@ -45,6 +47,8 @@ describe('DevopsAdoptionTable', () => {
const findSortByLocalStorageSync = () => wrapper.findAll(LocalStorageSync).at(0); const findSortByLocalStorageSync = () => wrapper.findAll(LocalStorageSync).at(0);
const findSortDescLocalStorageSync = () => wrapper.findAll(LocalStorageSync).at(1); const findSortDescLocalStorageSync = () => wrapper.findAll(LocalStorageSync).at(1);
const findDeleteModal = () => wrapper.find(DevopsAdoptionDeleteModal);
describe('table headings', () => { describe('table headings', () => {
let headers; let headers;
...@@ -142,6 +146,14 @@ describe('DevopsAdoptionTable', () => { ...@@ -142,6 +146,14 @@ describe('DevopsAdoptionTable', () => {
}); });
}); });
describe('delete modal integration', () => {
it('re emits trackModalOpenState with the given value', async () => {
findDeleteModal().vm.$emit('trackModalOpenState', true);
expect(wrapper.emitted('trackModalOpenState')).toStrictEqual([[true]]);
});
});
describe('sorting', () => { describe('sorting', () => {
let headers; let headers;
......
import { shouldPollTableData } from 'ee/analytics/devops_report/devops_adoption/utils/helpers';
import { devopsAdoptionSegmentsData } from '../mock_data';
describe('shouldPollTableData', () => {
const { nodes: pendingData } = devopsAdoptionSegmentsData;
const comepleteData = [pendingData[0]];
const mockDate = '2020-07-06T00:00:00.000Z';
const previousDay = '2020-07-05T00:00:00.000Z';
it.each`
scenario | segments | timestamp | openModal | expected
${'no segment data'} | ${[]} | ${mockDate} | ${false} | ${true}
${'no timestamp'} | ${comepleteData} | ${null} | ${false} | ${true}
${'open modal'} | ${comepleteData} | ${mockDate} | ${true} | ${false}
${'segment data, timestamp is today, modal is closed'} | ${comepleteData} | ${mockDate} | ${false} | ${false}
${'segment data, timestamp is yesterday, modal is closed'} | ${comepleteData} | ${previousDay} | ${false} | ${true}
${'segment data, timestamp is today, modal is open'} | ${comepleteData} | ${mockDate} | ${true} | ${false}
${'pending segment data, timestamp is today, modal is closed'} | ${pendingData} | ${mockDate} | ${false} | ${true}
`('returns $expected when $scenario', ({ segments, timestamp, openModal, expected }) => {
expect(shouldPollTableData({ segments, timestamp, openModal })).toBe(expected);
});
});
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