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 {
MAX_SEGMENTS,
DATE_TIME_FORMAT,
DEVOPS_ADOPTION_SEGMENT_MODAL_ID,
DEFAULT_POLLING_INTERVAL,
} from '../constants';
import devopsAdoptionSegmentsQuery from '../graphql/queries/devops_adoption_segments.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 DevopsAdoptionSegmentModal from './devops_adoption_segment_modal.vue';
import DevopsAdoptionTable from './devops_adoption_table.vue';
......@@ -48,6 +50,7 @@ export default {
isLoadingGroups: false,
requestCount: 0,
selectedSegment: null,
openModal: false,
errors: {
[DEVOPS_ADOPTION_ERROR_KEYS.groups]: false,
[DEVOPS_ADOPTION_ERROR_KEYS.segments]: false,
......@@ -56,6 +59,7 @@ export default {
nodes: [],
pageInfo: null,
},
pollingTableData: null,
};
},
apollo: {
......@@ -98,7 +102,27 @@ export default {
created() {
this.fetchGroups();
},
beforeDestroy() {
clearInterval(this.pollingTableData);
},
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) {
this.errors[key] = true;
Sentry.captureException(error);
......@@ -126,6 +150,7 @@ export default {
this.fetchGroups(pageInfo.nextPage);
} else {
this.isLoadingGroups = false;
this.startPollingTableData();
}
})
.catch((error) => this.handleError(DEVOPS_ADOPTION_ERROR_KEYS.groups, error));
......@@ -154,6 +179,7 @@ export default {
:key="modalKey"
:groups="groups.nodes"
:segment="selectedSegment"
@trackModalOpenState="trackModalOpenState"
/>
<div v-if="hasSegmentsData" class="gl-mt-3">
<div
......@@ -178,6 +204,7 @@ export default {
:segments="devopsAdoptionSegments.nodes"
:selected-segment="selectedSegment"
@set-selected-segment="setSelectedSegment"
@trackModalOpenState="trackModalOpenState"
/>
</div>
<devops-adoption-empty-state
......
......@@ -96,6 +96,8 @@ export default {
:action-primary="primaryOptions"
:action-cancel="cancelOptions"
@primary.prevent="deleteSegment"
@hide="$emit('trackModalOpenState', false)"
@show="$emit('trackModalOpenState', true)"
>
<template #modal-title>{{ $options.i18n.title }}</template>
<gl-alert v-if="errors.length" variant="danger" class="gl-mb-3" @dismiss="clearErrors">
......
......@@ -120,6 +120,7 @@ export default {
resetForm() {
this.selectedGroupId = null;
this.filter = '';
this.$emit('trackModalOpenState', false);
},
},
devopsSegmentModalId: DEVOPS_ADOPTION_SEGMENT_MODAL_ID,
......@@ -137,6 +138,7 @@ export default {
@primary.prevent="primaryOptions.callback"
@canceled="cancelOptions.callback"
@hide="resetForm"
@show="$emit('trackModalOpenState', true)"
>
<gl-alert v-if="errors.length" variant="danger" class="gl-mb-3" @dismiss="clearErrors">
{{ displayError }}
......
......@@ -225,6 +225,10 @@ export default {
</div>
</template>
</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>
</template>
import { s__, __, sprintf } from '~/locale';
export const DEFAULT_POLLING_INTERVAL = 30000;
export const MAX_SEGMENTS = 30;
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 {
DEVOPS_ADOPTION_STRINGS,
DEVOPS_ADOPTION_SEGMENT_MODAL_ID,
MAX_SEGMENTS,
DEFAULT_POLLING_INTERVAL,
} 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 getGroupsQuery from 'ee/analytics/devops_report/devops_adoption/graphql/queries/get_groups.query.graphql';
......@@ -446,5 +447,36 @@ describe('DevopsAdoptionApp', () => {
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', () => {
});
});
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('while waiting for the mutation', () => {
beforeEach(() => createComponent({ mutationMock: mutateLoading }));
......
......@@ -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`
selectedGroupId | disabled | values | state
${null} | ${true} | ${'checkbox'} | ${'disables'}
......
import { GlTable, GlButton, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
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 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';
......@@ -15,6 +16,7 @@ describe('DevopsAdoptionTable', () => {
wrapper = mount(DevopsAdoptionTable, {
propsData: {
segments: devopsAdoptionSegmentsData.nodes,
selectedSegment: devopsAdoptionSegmentsData.nodes[0],
},
directives: {
GlTooltip: createMockDirective(),
......@@ -45,6 +47,8 @@ describe('DevopsAdoptionTable', () => {
const findSortByLocalStorageSync = () => wrapper.findAll(LocalStorageSync).at(0);
const findSortDescLocalStorageSync = () => wrapper.findAll(LocalStorageSync).at(1);
const findDeleteModal = () => wrapper.find(DevopsAdoptionDeleteModal);
describe('table headings', () => {
let headers;
......@@ -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', () => {
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