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>
import { GlAlert, GlLoadingIcon, GlTable, GlLink } from '@gitlab/ui';
import { GlAlert, GlLoadingIcon, GlTable, GlLink, GlKeysetPagination } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { s__, __ } from '~/locale';
import { thWidthClass, sortObjectToString, sortStringToObject } from '~/lib/utils/table_utility';
......@@ -10,7 +10,7 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import complianceViolationsQuery from '../graphql/compliance_violations.query.graphql';
import { mapViolations } from '../graphql/mappers';
import { DEFAULT_SORT } from '../constants';
import { DEFAULT_SORT, GRAPHQL_PAGE_SIZE } from '../constants';
import { parseViolationsQueryFilter } from '../utils';
import MergeCommitsExportButton from './merge_requests/merge_commits_export_button.vue';
import MergeRequestDrawer from './drawer.vue';
......@@ -24,6 +24,7 @@ export default {
GlLoadingIcon,
GlTable,
GlLink,
GlKeysetPagination,
MergeCommitsExportButton,
MergeRequestDrawer,
ViolationReason,
......@@ -54,13 +55,20 @@ export default {
return {
urlQuery: { ...this.defaultQuery },
queryError: false,
violations: [],
violations: {
list: [],
pageInfo: {},
},
showDrawer: false,
drawerMergeRequest: {},
drawerProject: {},
sortBy,
sortDesc,
sortParam,
paginationCursors: {
before: null,
after: null,
},
};
},
apollo: {
......@@ -71,10 +79,16 @@ export default {
fullPath: this.groupPath,
filter: parseViolationsQueryFilter(this.urlQuery),
sort: this.sortParam,
first: GRAPHQL_PAGE_SIZE,
...this.paginationCursors,
};
},
update(data) {
return mapViolations(data?.group?.mergeRequestViolations?.nodes);
const { nodes, pageInfo } = data?.group?.mergeRequestViolations || {};
return {
list: mapViolations(nodes),
pageInfo,
};
},
error(e) {
Sentry.captureException(e);
......@@ -89,6 +103,10 @@ export default {
hasMergeCommitsCsvExportPath() {
return this.mergeCommitsCsvExportPath !== '';
},
showPagination() {
const { hasPreviousPage, hasNextPage } = this.violations.pageInfo || {};
return hasPreviousPage || hasNextPage;
},
},
methods: {
handleSortChanged(sortState) {
......@@ -124,6 +142,18 @@ export default {
...rest,
};
},
loadPrevPage(startCursor) {
this.paginationCursors = {
before: startCursor,
after: null,
};
},
loadNextPage(endCursor) {
this.paginationCursors = {
before: null,
after: endCursor,
};
},
},
fields: [
{
......@@ -159,6 +189,8 @@ export default {
queryError: __('Retrieving the compliance report failed. Refresh the page and try again.'),
noViolationsFound: s__('ComplianceReport|No violations found'),
learnMore: __('Learn more.'),
prev: __('Prev'),
next: __('Next'),
},
documentationPath: helpPagePath('user/compliance/compliance_report/index.md', {
anchor: 'approval-status-and-separation-of-duties',
......@@ -197,7 +229,7 @@ export default {
<gl-table
ref="table"
:fields="$options.fields"
:items="violations"
:items="violations.list"
:busy="isLoading"
:empty-text="$options.i18n.noViolationsFound"
:selectable="true"
......@@ -228,9 +260,19 @@ export default {
<time-ago-tooltip :time="mergeRequest.mergedAt" />
</template>
<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>
</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
:show-drawer="showDrawer"
:merge-request="drawerMergeRequest"
......
......@@ -12,6 +12,8 @@ export const DRAWER_AVATAR_SIZE = 24;
export const DRAWER_MAXIMUM_AVATARS = 20;
export const GRAPHQL_PAGE_SIZE = 20;
export const COMPLIANCE_DRAWER_CONTAINER_CLASS = '.content-wrapper';
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
query getComplianceViolations($fullPath: ID!, $filter: Object, $sort: String) {
group(fullPath: $fullPath, filter: $filter, sort: $sort) @client {
query getComplianceViolations(
$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
mergeRequestViolations {
nodes {
......@@ -82,6 +98,9 @@ query getComplianceViolations($fullPath: ID!, $filter: Object, $sort: String) {
}
}
}
pageInfo {
...PageInfo
}
}
}
}
......@@ -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 VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
......@@ -20,7 +20,7 @@ import UrlSync from '~/vue_shared/components/url_sync.vue';
import { stubComponent } from 'helpers/stub_component';
import { sortObjectToString } from '~/lib/utils/table_utility';
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);
......@@ -44,6 +44,7 @@ describe('ComplianceReport component', () => {
const findErrorMessage = () => wrapper.findComponent(GlAlert);
const findViolationsTable = () => wrapper.findComponent(GlTable);
const findTableLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPagination = () => wrapper.findComponent(GlKeysetPagination);
const findMergeRequestDrawer = () => wrapper.findComponent(MergeRequestDrawer);
const findMergeCommitsExportButton = () => wrapper.findComponent(MergeCommitsExportButton);
const findViolationReason = () => wrapper.findComponent(ViolationReason);
......@@ -141,6 +142,9 @@ describe('ComplianceReport component', () => {
fullPath: groupPath,
filter: parseViolationsQueryFilter(defaultQuery),
sort: DEFAULT_SORT,
first: GRAPHQL_PAGE_SIZE,
after: null,
before: null,
}),
);
});
......@@ -161,6 +165,9 @@ describe('ComplianceReport component', () => {
fullPath: groupPath,
filter: parseViolationsQueryFilter(defaultQuery),
sort,
first: GRAPHQL_PAGE_SIZE,
after: null,
before: null,
}),
);
});
......@@ -324,6 +331,10 @@ describe('ComplianceReport component', () => {
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 () => {
await findViolationFilter().vm.$emit('filters-changed', { ...query, projectIds: [] });
......@@ -338,6 +349,9 @@ describe('ComplianceReport component', () => {
fullPath: groupPath,
filter: parseViolationsQueryFilter(query),
sort: DEFAULT_SORT,
first: GRAPHQL_PAGE_SIZE,
after: null,
before: null,
}),
);
});
......@@ -373,10 +387,73 @@ describe('ComplianceReport component', () => {
fullPath: groupPath,
filter: parseViolationsQueryFilter(defaultQuery),
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', () => {
......
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