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 {
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