Commit 101f51bb authored by Jiaan Louw's avatar Jiaan Louw Committed by Kushal Pandya

Add pagination to the compliance violations report

- Adds the `GlKeysetPagination` component.
- Adds pagination to the GraphQL query.
- Updates report spec.
parent d7537c74
<script> <script>
import { GlAlert, GlLoadingIcon, GlTable, GlLink } from '@gitlab/ui'; import { GlAlert, GlLoadingIcon, GlTable, GlLink, GlKeysetPagination } from '@gitlab/ui';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { thWidthClass, sortObjectToString, sortStringToObject } from '~/lib/utils/table_utility'; import { thWidthClass, sortObjectToString, sortStringToObject } from '~/lib/utils/table_utility';
...@@ -10,7 +10,7 @@ import { helpPagePath } from '~/helpers/help_page_helper'; ...@@ -10,7 +10,7 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue'; import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import complianceViolationsQuery from '../graphql/compliance_violations.query.graphql'; import complianceViolationsQuery from '../graphql/compliance_violations.query.graphql';
import { mapViolations } from '../graphql/mappers'; import { mapViolations } from '../graphql/mappers';
import { DEFAULT_SORT } from '../constants'; import { DEFAULT_SORT, GRAPHQL_PAGE_SIZE } from '../constants';
import { parseViolationsQueryFilter } from '../utils'; import { parseViolationsQueryFilter } from '../utils';
import MergeCommitsExportButton from './merge_requests/merge_commits_export_button.vue'; import MergeCommitsExportButton from './merge_requests/merge_commits_export_button.vue';
import MergeRequestDrawer from './drawer.vue'; import MergeRequestDrawer from './drawer.vue';
...@@ -24,6 +24,7 @@ export default { ...@@ -24,6 +24,7 @@ export default {
GlLoadingIcon, GlLoadingIcon,
GlTable, GlTable,
GlLink, GlLink,
GlKeysetPagination,
MergeCommitsExportButton, MergeCommitsExportButton,
MergeRequestDrawer, MergeRequestDrawer,
ViolationReason, ViolationReason,
...@@ -54,13 +55,20 @@ export default { ...@@ -54,13 +55,20 @@ export default {
return { return {
urlQuery: { ...this.defaultQuery }, urlQuery: { ...this.defaultQuery },
queryError: false, queryError: false,
violations: [], violations: {
list: [],
pageInfo: {},
},
showDrawer: false, showDrawer: false,
drawerMergeRequest: {}, drawerMergeRequest: {},
drawerProject: {}, drawerProject: {},
sortBy, sortBy,
sortDesc, sortDesc,
sortParam, sortParam,
paginationCursors: {
before: null,
after: null,
},
}; };
}, },
apollo: { apollo: {
...@@ -71,10 +79,16 @@ export default { ...@@ -71,10 +79,16 @@ export default {
fullPath: this.groupPath, fullPath: this.groupPath,
filter: parseViolationsQueryFilter(this.urlQuery), filter: parseViolationsQueryFilter(this.urlQuery),
sort: this.sortParam, sort: this.sortParam,
first: GRAPHQL_PAGE_SIZE,
...this.paginationCursors,
}; };
}, },
update(data) { update(data) {
return mapViolations(data?.group?.mergeRequestViolations?.nodes); const { nodes, pageInfo } = data?.group?.mergeRequestViolations || {};
return {
list: mapViolations(nodes),
pageInfo,
};
}, },
error(e) { error(e) {
Sentry.captureException(e); Sentry.captureException(e);
...@@ -89,6 +103,10 @@ export default { ...@@ -89,6 +103,10 @@ export default {
hasMergeCommitsCsvExportPath() { hasMergeCommitsCsvExportPath() {
return this.mergeCommitsCsvExportPath !== ''; return this.mergeCommitsCsvExportPath !== '';
}, },
showPagination() {
const { hasPreviousPage, hasNextPage } = this.violations.pageInfo || {};
return hasPreviousPage || hasNextPage;
},
}, },
methods: { methods: {
handleSortChanged(sortState) { handleSortChanged(sortState) {
...@@ -124,6 +142,18 @@ export default { ...@@ -124,6 +142,18 @@ export default {
...rest, ...rest,
}; };
}, },
loadPrevPage(startCursor) {
this.paginationCursors = {
before: startCursor,
after: null,
};
},
loadNextPage(endCursor) {
this.paginationCursors = {
before: null,
after: endCursor,
};
},
}, },
fields: [ fields: [
{ {
...@@ -159,6 +189,8 @@ export default { ...@@ -159,6 +189,8 @@ export default {
queryError: __('Retrieving the compliance report failed. Refresh the page and try again.'), queryError: __('Retrieving the compliance report failed. Refresh the page and try again.'),
noViolationsFound: s__('ComplianceReport|No violations found'), noViolationsFound: s__('ComplianceReport|No violations found'),
learnMore: __('Learn more.'), learnMore: __('Learn more.'),
prev: __('Prev'),
next: __('Next'),
}, },
documentationPath: helpPagePath('user/compliance/compliance_report/index.md', { documentationPath: helpPagePath('user/compliance/compliance_report/index.md', {
anchor: 'approval-status-and-separation-of-duties', anchor: 'approval-status-and-separation-of-duties',
...@@ -197,7 +229,7 @@ export default { ...@@ -197,7 +229,7 @@ export default {
<gl-table <gl-table
ref="table" ref="table"
:fields="$options.fields" :fields="$options.fields"
:items="violations" :items="violations.list"
:busy="isLoading" :busy="isLoading"
:empty-text="$options.i18n.noViolationsFound" :empty-text="$options.i18n.noViolationsFound"
:selectable="true" :selectable="true"
...@@ -228,9 +260,19 @@ export default { ...@@ -228,9 +260,19 @@ export default {
<time-ago-tooltip :time="mergeRequest.mergedAt" /> <time-ago-tooltip :time="mergeRequest.mergedAt" />
</template> </template>
<template #table-busy> <template #table-busy>
<gl-loading-icon size="lg" color="dark" class="mt-3" /> <gl-loading-icon size="lg" color="dark" class="gl-my-5" />
</template> </template>
</gl-table> </gl-table>
<div v-if="showPagination" class="gl-display-flex gl-justify-content-center">
<gl-keyset-pagination
v-bind="violations.pageInfo"
:disabled="isLoading"
:prev-text="$options.i18n.prev"
:next-text="$options.i18n.next"
@prev="loadPrevPage"
@next="loadNextPage"
/>
</div>
<merge-request-drawer <merge-request-drawer
:show-drawer="showDrawer" :show-drawer="showDrawer"
:merge-request="drawerMergeRequest" :merge-request="drawerMergeRequest"
......
...@@ -12,6 +12,8 @@ export const DRAWER_AVATAR_SIZE = 24; ...@@ -12,6 +12,8 @@ export const DRAWER_AVATAR_SIZE = 24;
export const DRAWER_MAXIMUM_AVATARS = 20; export const DRAWER_MAXIMUM_AVATARS = 20;
export const GRAPHQL_PAGE_SIZE = 20;
export const COMPLIANCE_DRAWER_CONTAINER_CLASS = '.content-wrapper'; export const COMPLIANCE_DRAWER_CONTAINER_CLASS = '.content-wrapper';
const VIOLATION_TYPE_APPROVED_BY_AUTHOR = 'approved_by_author'; const VIOLATION_TYPE_APPROVED_BY_AUTHOR = 'approved_by_author';
......
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
# TODO: Add the correct filter type once it has been added in https://gitlab.com/gitlab-org/gitlab/-/issues/347325 # TODO: Add the correct filter type once it has been added in https://gitlab.com/gitlab-org/gitlab/-/issues/347325
query getComplianceViolations($fullPath: ID!, $filter: Object, $sort: String) { query getComplianceViolations(
group(fullPath: $fullPath, filter: $filter, sort: $sort) @client { $fullPath: ID!
$filter: Object
$sort: String
$after: String
$before: String
$first: Int
) {
group(
fullPath: $fullPath
filter: $filter
sort: $sort
after: $after
before: $before
first: $first
) @client {
id id
mergeRequestViolations { mergeRequestViolations {
nodes { nodes {
...@@ -82,6 +98,9 @@ query getComplianceViolations($fullPath: ID!, $filter: Object, $sort: String) { ...@@ -82,6 +98,9 @@ query getComplianceViolations($fullPath: ID!, $filter: Object, $sort: String) {
} }
} }
} }
pageInfo {
...PageInfo
}
} }
} }
} }
...@@ -218,6 +218,13 @@ export default { ...@@ -218,6 +218,13 @@ export default {
}, },
}, },
], ],
pageInfo: {
__typename: 'PageInfo',
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'eyJpZCI6IjMzMjkwNjMzIn0',
endCursor: 'eyJpZCI6IjMzMjkwNjI5In0',
},
}, },
}; };
}, },
......
import { GlAlert, GlLoadingIcon, GlTable, GlLink } from '@gitlab/ui'; import { GlAlert, GlLoadingIcon, GlTable, GlLink, GlKeysetPagination } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
...@@ -20,7 +20,7 @@ import UrlSync from '~/vue_shared/components/url_sync.vue'; ...@@ -20,7 +20,7 @@ import UrlSync from '~/vue_shared/components/url_sync.vue';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import { sortObjectToString } from '~/lib/utils/table_utility'; import { sortObjectToString } from '~/lib/utils/table_utility';
import { parseViolationsQueryFilter } from 'ee/compliance_dashboard/utils'; import { parseViolationsQueryFilter } from 'ee/compliance_dashboard/utils';
import { DEFAULT_SORT } from 'ee/compliance_dashboard/constants'; import { DEFAULT_SORT, GRAPHQL_PAGE_SIZE } from 'ee/compliance_dashboard/constants';
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -44,6 +44,7 @@ describe('ComplianceReport component', () => { ...@@ -44,6 +44,7 @@ describe('ComplianceReport component', () => {
const findErrorMessage = () => wrapper.findComponent(GlAlert); const findErrorMessage = () => wrapper.findComponent(GlAlert);
const findViolationsTable = () => wrapper.findComponent(GlTable); const findViolationsTable = () => wrapper.findComponent(GlTable);
const findTableLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findTableLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPagination = () => wrapper.findComponent(GlKeysetPagination);
const findMergeRequestDrawer = () => wrapper.findComponent(MergeRequestDrawer); const findMergeRequestDrawer = () => wrapper.findComponent(MergeRequestDrawer);
const findMergeCommitsExportButton = () => wrapper.findComponent(MergeCommitsExportButton); const findMergeCommitsExportButton = () => wrapper.findComponent(MergeCommitsExportButton);
const findViolationReason = () => wrapper.findComponent(ViolationReason); const findViolationReason = () => wrapper.findComponent(ViolationReason);
...@@ -141,6 +142,9 @@ describe('ComplianceReport component', () => { ...@@ -141,6 +142,9 @@ describe('ComplianceReport component', () => {
fullPath: groupPath, fullPath: groupPath,
filter: parseViolationsQueryFilter(defaultQuery), filter: parseViolationsQueryFilter(defaultQuery),
sort: DEFAULT_SORT, sort: DEFAULT_SORT,
first: GRAPHQL_PAGE_SIZE,
after: null,
before: null,
}), }),
); );
}); });
...@@ -161,6 +165,9 @@ describe('ComplianceReport component', () => { ...@@ -161,6 +165,9 @@ describe('ComplianceReport component', () => {
fullPath: groupPath, fullPath: groupPath,
filter: parseViolationsQueryFilter(defaultQuery), filter: parseViolationsQueryFilter(defaultQuery),
sort, sort,
first: GRAPHQL_PAGE_SIZE,
after: null,
before: null,
}), }),
); );
}); });
...@@ -324,6 +331,10 @@ describe('ComplianceReport component', () => { ...@@ -324,6 +331,10 @@ describe('ComplianceReport component', () => {
expect(findTableLoadingIcon().exists()).toBe(true); expect(findTableLoadingIcon().exists()).toBe(true);
}); });
it('sets the pagination component to disabled', () => {
expect(findPagination().props('disabled')).toBe(true);
});
it('clears the project URL query param if the project array is empty', async () => { it('clears the project URL query param if the project array is empty', async () => {
await findViolationFilter().vm.$emit('filters-changed', { ...query, projectIds: [] }); await findViolationFilter().vm.$emit('filters-changed', { ...query, projectIds: [] });
...@@ -338,6 +349,9 @@ describe('ComplianceReport component', () => { ...@@ -338,6 +349,9 @@ describe('ComplianceReport component', () => {
fullPath: groupPath, fullPath: groupPath,
filter: parseViolationsQueryFilter(query), filter: parseViolationsQueryFilter(query),
sort: DEFAULT_SORT, sort: DEFAULT_SORT,
first: GRAPHQL_PAGE_SIZE,
after: null,
before: null,
}), }),
); );
}); });
...@@ -373,10 +387,73 @@ describe('ComplianceReport component', () => { ...@@ -373,10 +387,73 @@ describe('ComplianceReport component', () => {
fullPath: groupPath, fullPath: groupPath,
filter: parseViolationsQueryFilter(defaultQuery), filter: parseViolationsQueryFilter(defaultQuery),
sort: sortObjectToString(sortState), sort: sortObjectToString(sortState),
first: GRAPHQL_PAGE_SIZE,
after: null,
before: null,
}), }),
); );
}); });
}); });
describe('pagination', () => {
beforeEach(() => {
mockResolver = jest.fn().mockReturnValue(resolvers.Query.group());
wrapper = createComponent(mount);
return waitForPromises();
});
it('renders and configures the pagination', () => {
const pageInfo = stripTypenames(resolvers.Query.group().mergeRequestViolations.pageInfo);
expect(findPagination().props()).toMatchObject({
...pageInfo,
disabled: false,
});
});
it.each`
event | after | before
${'next'} | ${'foo'} | ${null}
${'prev'} | ${null} | ${'foo'}
`(
'fetches the $event page when the pagination emits "$event"',
async ({ event, after, before }) => {
await findPagination().vm.$emit(event, after ?? before);
await waitForPromises();
expect(mockResolver).toHaveBeenCalledTimes(2);
expect(mockResolver).toHaveBeenNthCalledWith(
2,
...expectApolloVariables({
fullPath: groupPath,
filter: parseViolationsQueryFilter(defaultQuery),
first: GRAPHQL_PAGE_SIZE,
sort: DEFAULT_SORT,
after,
before,
}),
);
},
);
describe('when there are no next or previous pages', () => {
beforeEach(() => {
const group = resolvers.Query.group();
group.mergeRequestViolations.pageInfo.hasNextPage = false;
group.mergeRequestViolations.pageInfo.hasPreviousPage = false;
mockResolver = () => jest.fn().mockReturnValue(group);
wrapper = createComponent(mount);
return waitForPromises();
});
it('does not render the pagination component', () => {
expect(findPagination().exists()).toBe(false);
});
});
});
}); });
describe('when there are no violations', () => { describe('when there are no violations', () => {
......
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