Commit 8fe57fb5 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch 'jivavnl-add-pagination-jobs-refactor' into 'master'

Add pagination to the jobs table refactor

See merge request gitlab-org/gitlab!62338
parents 4b88d6a1 f34c24a7
export const GRAPHQL_PAGE_SIZE = 30;
export const initialPaginationState = {
currentPage: 1,
prevPageCursor: '',
nextPageCursor: '',
first: GRAPHQL_PAGE_SIZE,
last: null,
};
query getJobs($fullPath: ID!, $statuses: [CiJobStatus!]) { query getJobs(
$fullPath: ID!
$first: Int
$last: Int
$after: String
$before: String
$statuses: [CiJobStatus!]
) {
project(fullPath: $fullPath) { project(fullPath: $fullPath) {
jobs(first: 20, statuses: $statuses) { jobs(after: $after, before: $before, first: $first, last: $last, statuses: $statuses) {
pageInfo { pageInfo {
endCursor endCursor
hasNextPage hasNextPage
......
<script> <script>
import { GlAlert, GlSkeletonLoader } from '@gitlab/ui'; import { GlAlert, GlPagination, GlSkeletonLoader } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { GRAPHQL_PAGE_SIZE, initialPaginationState } from './constants';
import GetJobs from './graphql/queries/get_jobs.query.graphql'; import GetJobs from './graphql/queries/get_jobs.query.graphql';
import JobsTable from './jobs_table.vue'; import JobsTable from './jobs_table.vue';
import JobsTableEmptyState from './jobs_table_empty_state.vue'; import JobsTableEmptyState from './jobs_table_empty_state.vue';
...@@ -12,6 +13,7 @@ export default { ...@@ -12,6 +13,7 @@ export default {
}, },
components: { components: {
GlAlert, GlAlert,
GlPagination,
GlSkeletonLoader, GlSkeletonLoader,
JobsTable, JobsTable,
JobsTableEmptyState, JobsTableEmptyState,
...@@ -28,10 +30,18 @@ export default { ...@@ -28,10 +30,18 @@ export default {
variables() { variables() {
return { return {
fullPath: this.fullPath, fullPath: this.fullPath,
first: this.pagination.first,
last: this.pagination.last,
after: this.pagination.nextPageCursor,
before: this.pagination.prevPageCursor,
}; };
}, },
update({ project }) { update(data) {
return project?.jobs?.nodes || []; const { jobs: { nodes: list = [], pageInfo = {} } = {} } = data.project || {};
return {
list,
pageInfo,
};
}, },
error() { error() {
this.hasError = true; this.hasError = true;
...@@ -40,10 +50,11 @@ export default { ...@@ -40,10 +50,11 @@ export default {
}, },
data() { data() {
return { return {
jobs: null, jobs: {},
hasError: false, hasError: false,
isAlertDismissed: false, isAlertDismissed: false,
scope: null, scope: null,
pagination: initialPaginationState,
}; };
}, },
computed: { computed: {
...@@ -51,7 +62,16 @@ export default { ...@@ -51,7 +62,16 @@ export default {
return this.hasError && !this.isAlertDismissed; return this.hasError && !this.isAlertDismissed;
}, },
showEmptyState() { showEmptyState() {
return this.jobs.length === 0 && !this.scope; return this.jobs.list.length === 0 && !this.scope;
},
prevPage() {
return Math.max(this.pagination.currentPage - 1, 0);
},
nextPage() {
return this.jobs.pageInfo?.hasNextPage ? this.pagination.currentPage + 1 : null;
},
showPaginationControls() {
return Boolean(this.prevPage || this.nextPage) && !this.$apollo.loading;
}, },
}, },
methods: { methods: {
...@@ -60,6 +80,24 @@ export default { ...@@ -60,6 +80,24 @@ export default {
this.$apollo.queries.jobs.refetch({ statuses: scope }); this.$apollo.queries.jobs.refetch({ statuses: scope });
}, },
handlePageChange(page) {
const { startCursor, endCursor } = this.jobs.pageInfo;
if (page > this.pagination.currentPage) {
this.pagination = {
...initialPaginationState,
nextPageCursor: endCursor,
currentPage: page,
};
} else {
this.pagination = {
last: GRAPHQL_PAGE_SIZE,
first: null,
prevPageCursor: startCursor,
currentPage: page,
};
}
},
}, },
}; };
</script> </script>
...@@ -97,6 +135,16 @@ export default { ...@@ -97,6 +135,16 @@ export default {
<jobs-table-empty-state v-else-if="showEmptyState" /> <jobs-table-empty-state v-else-if="showEmptyState" />
<jobs-table v-else :jobs="jobs" /> <jobs-table v-else :jobs="jobs.list" />
<gl-pagination
v-if="showPaginationControls"
:value="pagination.currentPage"
:prev-page="prevPage"
:next-page="nextPage"
align="center"
class="gl-mt-3"
@input="handlePageChange"
/>
</div> </div>
</template> </template>
import { GlSkeletonLoader, GlAlert, GlEmptyState } from '@gitlab/ui'; import { GlSkeletonLoader, GlAlert, GlEmptyState, GlPagination } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils'; import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
...@@ -25,6 +25,10 @@ describe('Job table app', () => { ...@@ -25,6 +25,10 @@ describe('Job table app', () => {
const findTabs = () => wrapper.findComponent(JobsTableTabs); const findTabs = () => wrapper.findComponent(JobsTableTabs);
const findAlert = () => wrapper.findComponent(GlAlert); const findAlert = () => wrapper.findComponent(GlAlert);
const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findPagination = () => wrapper.findComponent(GlPagination);
const findPrevious = () => findPagination().findAll('.page-item').at(0);
const findNext = () => findPagination().findAll('.page-item').at(1);
const createMockApolloProvider = (handler) => { const createMockApolloProvider = (handler) => {
const requestHandlers = [[getJobsQuery, handler]]; const requestHandlers = [[getJobsQuery, handler]];
...@@ -32,8 +36,17 @@ describe('Job table app', () => { ...@@ -32,8 +36,17 @@ describe('Job table app', () => {
return createMockApollo(requestHandlers); return createMockApollo(requestHandlers);
}; };
const createComponent = (handler = successHandler, mountFn = shallowMount) => { const createComponent = ({
handler = successHandler,
mountFn = shallowMount,
data = {},
} = {}) => {
wrapper = mountFn(JobsTableApp, { wrapper = mountFn(JobsTableApp, {
data() {
return {
...data,
};
},
provide: { provide: {
projectPath, projectPath,
}, },
...@@ -52,6 +65,7 @@ describe('Job table app', () => { ...@@ -52,6 +65,7 @@ describe('Job table app', () => {
expect(findSkeletonLoader().exists()).toBe(true); expect(findSkeletonLoader().exists()).toBe(true);
expect(findTable().exists()).toBe(false); expect(findTable().exists()).toBe(false);
expect(findPagination().exists()).toBe(false);
}); });
}); });
...@@ -65,9 +79,10 @@ describe('Job table app', () => { ...@@ -65,9 +79,10 @@ describe('Job table app', () => {
it('should display the jobs table with data', () => { it('should display the jobs table with data', () => {
expect(findTable().exists()).toBe(true); expect(findTable().exists()).toBe(true);
expect(findSkeletonLoader().exists()).toBe(false); expect(findSkeletonLoader().exists()).toBe(false);
expect(findPagination().exists()).toBe(true);
}); });
it('should retfech jobs query on fetchJobsByStatus event', async () => { it('should refetch jobs query on fetchJobsByStatus event', async () => {
jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn()); jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0); expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
...@@ -78,9 +93,72 @@ describe('Job table app', () => { ...@@ -78,9 +93,72 @@ describe('Job table app', () => {
}); });
}); });
describe('pagination', () => {
it('should disable the next page button on the last page', async () => {
createComponent({
handler: successHandler,
mountFn: mount,
data: {
pagination: {
currentPage: 3,
},
jobs: {
pageInfo: {
hasPreviousPage: true,
startCursor: 'abc',
endCursor: 'bcd',
},
},
},
});
await wrapper.vm.$nextTick();
wrapper.setData({
jobs: {
pageInfo: {
hasNextPage: false,
},
},
});
await wrapper.vm.$nextTick();
expect(findPrevious().exists()).toBe(true);
expect(findNext().exists()).toBe(true);
expect(findNext().classes('disabled')).toBe(true);
});
it('should disable the previous page button on the first page', async () => {
createComponent({
handler: successHandler,
mountFn: mount,
data: {
pagination: {
currentPage: 1,
},
jobs: {
pageInfo: {
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'abc',
endCursor: 'bcd',
},
},
},
});
await wrapper.vm.$nextTick();
expect(findPrevious().exists()).toBe(true);
expect(findPrevious().classes('disabled')).toBe(true);
expect(findNext().exists()).toBe(true);
});
});
describe('error state', () => { describe('error state', () => {
it('should show an alert if there is an error fetching the data', async () => { it('should show an alert if there is an error fetching the data', async () => {
createComponent(failedHandler); createComponent({ handler: failedHandler });
await waitForPromises(); await waitForPromises();
...@@ -90,7 +168,7 @@ describe('Job table app', () => { ...@@ -90,7 +168,7 @@ describe('Job table app', () => {
describe('empty state', () => { describe('empty state', () => {
it('should display empty state if there are no jobs and tab scope is null', async () => { it('should display empty state if there are no jobs and tab scope is null', async () => {
createComponent(emptyHandler, mount); createComponent({ handler: emptyHandler, mountFn: mount });
await waitForPromises(); await waitForPromises();
...@@ -99,7 +177,7 @@ describe('Job table app', () => { ...@@ -99,7 +177,7 @@ describe('Job table app', () => {
}); });
it('should not display empty state if there are jobs and tab scope is not null', async () => { it('should not display empty state if there are jobs and tab scope is not null', async () => {
createComponent(successHandler, mount); createComponent({ handler: successHandler, mountFn: mount });
await waitForPromises(); await waitForPromises();
......
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