Commit df354525 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '299605-devops-adoption-lazy-loading-refreshing-group-data' into 'master'

[DevOps Adoption] Lazy loading / refreshing group data

See merge request gitlab-org/gitlab!54922
parents cb722271 d7244e72
...@@ -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