Commit aae38d89 authored by Jake Burden's avatar Jake Burden Committed by Simon Knox

Add pagination to Environments Dashboard

Add GlPagination to Environments Dashboard
Updates state handling for page info
Add tests for pagination
parent e25d35f7
......@@ -8,6 +8,7 @@ import {
GlLink,
GlModal,
GlModalDirective,
GlPagination,
GlSprintf,
} from '@gitlab/ui';
import { s__ } from '~/locale';
......@@ -38,6 +39,7 @@ export default {
GlEmptyState,
GlLink,
GlModal,
GlPagination,
GlSprintf,
ProjectHeader,
ProjectSelector,
......@@ -79,7 +81,25 @@ export default {
'searchQuery',
'messages',
'pageInfo',
'projectsPage',
]),
currentPage: {
get() {
return this.projectsPage.pageInfo.page;
},
set(newPage) {
this.paginateDashboard(newPage);
},
},
projectsPerPage() {
return this.projectsPage.pageInfo.perPage;
},
totalProjects() {
return this.projectsPage.pageInfo.total;
},
shouldPaginate() {
return this.projectsPage.pageInfo.totalPages > 1;
},
isSearchingProjects() {
return this.searchCount > 0;
},
......@@ -87,6 +107,11 @@ export default {
return isEmpty(this.selectedProjects);
},
},
watch: {
currentPage: () => {
window.scrollTo(0, 0);
},
},
created() {
this.setProjectEndpoints({
list: this.listPath,
......@@ -105,6 +130,9 @@ export default {
'toggleSelectedProject',
'setSearchQuery',
'removeProject',
'clearProjectsEtagPoll',
'stopProjectsPolling',
'paginateDashboard',
]),
addProjects() {
this.addProjectsToDashboard();
......@@ -187,6 +215,15 @@ export default {
/>
</div>
</div>
<gl-pagination
v-if="shouldPaginate"
v-model="currentPage"
:per-page="projectsPerPage"
:total-items="totalProjects"
align="center"
class="gl-w-full gl-mt-3"
/>
</div>
<gl-dashboard-skeleton v-else-if="isLoadingProjects" />
......
......@@ -95,17 +95,23 @@ export const receiveAddProjectsToDashboardError = ({ state }) => {
);
};
export const fetchProjects = ({ state, dispatch }) => {
export const fetchProjects = ({ state, dispatch, commit }, page) => {
if (eTagPoll) return;
dispatch('requestProjects');
eTagPoll = new Poll({
resource: {
fetchProjects: () => axios.get(state.projectEndpoints.list),
fetchProjects: () => axios.get(state.projectEndpoints.list, { params: { page } }),
},
method: 'fetchProjects',
successCallback: ({ data }) => dispatch('receiveProjectsSuccess', data),
successCallback: response => {
const {
data: { projects },
headers,
} = response;
commit(types.RECEIVE_PROJECTS_SUCCESS, { projects, headers });
},
errorCallback: () => dispatch('receiveProjectsError'),
});
......@@ -126,10 +132,6 @@ export const requestProjects = ({ commit }) => {
commit(types.REQUEST_PROJECTS);
};
export const receiveProjectsSuccess = ({ commit }, data) => {
commit(types.RECEIVE_PROJECTS_SUCCESS, data.projects);
};
export const receiveProjectsError = ({ commit }) => {
commit(types.RECEIVE_PROJECTS_ERROR);
createFlash(__('Something went wrong, unable to get projects'));
......@@ -198,3 +200,11 @@ export const minimumQueryMessage = ({ commit }) => {
export const setProjects = ({ commit }, projects) => {
commit(types.SET_PROJECTS, projects);
};
export const paginateDashboard = ({ dispatch }, newPage) => {
return Promise.all([
dispatch('stopProjectsPolling'),
dispatch('clearProjectsEtagPoll'),
dispatch('fetchProjects', newPage),
]);
};
......@@ -52,7 +52,7 @@ export default {
[types.REQUEST_PROJECTS](state) {
state.isLoadingProjects = true;
},
[types.RECEIVE_PROJECTS_SUCCESS](state, projects) {
[types.RECEIVE_PROJECTS_SUCCESS](state, { projects, headers }) {
let projectIds = [];
if (AccessorUtilities.isLocalStorageAccessSafe()) {
projectIds = (localStorage.getItem(state.projectEndpoints.list) || '').split(',');
......@@ -65,6 +65,9 @@ export default {
if (AccessorUtilities.isLocalStorageAccessSafe()) {
localStorage.setItem(state.projectEndpoints.list, state.projects.map(p => p.id));
}
const pageInfo = parseIntPagination(normalizeHeaders(headers));
state.projectsPage.pageInfo = pageInfo;
},
[types.RECEIVE_PROJECTS_ERROR](state) {
state.projects = null;
......
......@@ -14,6 +14,15 @@ export default () => ({
},
projects: [],
projectSearchResults: [],
projectsPage: {
pageInfo: {
totalPages: 1,
totalResults: 0,
nextPage: 0,
prevPage: 0,
currentPage: 1,
},
},
selectedProjects: [],
messages: {
noResults: false,
......
---
title: Add pagination to Environments Dashboard
merge_request: 39637
author:
type: added
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlButton, GlEmptyState, GlModal, GlSprintf, GlLink } from '@gitlab/ui';
import { GlButton, GlEmptyState, GlModal, GlSprintf, GlLink, GlPagination } from '@gitlab/ui';
import createStore from 'ee/vue_shared/dashboards/store/index';
import state from 'ee/vue_shared/dashboards/store/state';
import component from 'ee/environments_dashboard/components/dashboard/dashboard.vue';
......@@ -55,6 +55,8 @@ describe('dashboard', () => {
store.replaceState(state());
});
const findPagination = () => wrapper.find(GlPagination);
it('should match the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
......@@ -67,6 +69,10 @@ describe('dashboard', () => {
expect(wrapper.find(GlEmptyState).exists()).toBe(true);
});
it('should not render pagination in empty state', () => {
expect(findPagination().exists()).toBe(false);
});
describe('page limits information message', () => {
let message;
......@@ -178,5 +184,21 @@ describe('dashboard', () => {
});
});
});
describe('pagination', () => {
const testPagination = async ({ totalPages }) => {
store.state.projectsPage.pageInfo.totalPages = totalPages;
const shouldRenderPagination = totalPages > 1;
await wrapper.vm.$nextTick();
expect(findPagination().exists()).toBe(shouldRenderPagination);
};
it('should not render the pagination component if there is only one page', () =>
testPagination({ totalPages: 1 }));
it('should render the pagination component if there are multiple pages', () =>
testPagination({ totalPages: 2 }));
});
});
});
......@@ -188,18 +188,36 @@ describe('actions', () => {
});
describe('fetchProjects', () => {
it('calls project list endpoint', () => {
const testListEndpoint = ({ page }) => {
store.state.projectEndpoints.list = mockListEndpoint;
mockAxios.onGet(mockListEndpoint).replyOnce(200);
mockAxios
.onGet(mockListEndpoint, {
params: {
page,
},
})
.replyOnce(200, { projects: mockProjects }, mockHeaders);
return testAction(
actions.fetchProjects,
null,
page,
store.state,
[],
[{ type: 'requestProjects' }, { type: 'receiveProjectsSuccess' }],
[
{
type: 'RECEIVE_PROJECTS_SUCCESS',
payload: {
headers: mockHeaders,
projects: mockProjects,
},
},
],
[{ type: 'requestProjects' }],
);
});
};
it('calls project list endpoint', () => testListEndpoint({ page: null }));
it('calls paginated project list endpoint', () => testListEndpoint({ page: 2 }));
it('handles response errors', () => {
store.state.projectEndpoints.list = mockListEndpoint;
......@@ -227,23 +245,6 @@ describe('actions', () => {
});
});
describe('receiveProjectsSuccess', () => {
it('sets projects from data on success', () => {
return testAction(
actions.receiveProjectsSuccess,
{ projects: mockProjects },
store.state,
[
{
type: types.RECEIVE_PROJECTS_SUCCESS,
payload: mockProjects,
},
],
[],
);
});
});
describe('receiveProjectsError', () => {
it('clears projects and alerts user of error', () => {
store.state.projects = mockProjects;
......@@ -515,4 +516,29 @@ describe('actions', () => {
);
});
});
describe('paginateDashboard', () => {
it('fetches a new page of projects', () => {
const newPage = 2;
return testAction(
actions.paginateDashboard,
newPage,
store.state,
[],
[
{
type: 'stopProjectsPolling',
},
{
type: 'clearProjectsEtagPoll',
},
{
type: 'fetchProjects',
payload: newPage,
},
],
);
});
});
});
......@@ -4,6 +4,7 @@ import * as types from 'ee/vue_shared/dashboards/store/mutation_types';
import { mockProjectData } from 'ee_jest/vue_shared/dashboards/mock_data';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
jest.mock('~/flash');
......@@ -121,14 +122,14 @@ describe('mutations', () => {
});
it('sets the project list and clears the loading status', () => {
mutations[types.RECEIVE_PROJECTS_SUCCESS](localState, projects);
mutations[types.RECEIVE_PROJECTS_SUCCESS](localState, { projects });
expect(localState.projects).toEqual(projects);
expect(localState.isLoadingProjects).toBe(false);
});
it('saves projects to localStorage', () => {
mutations[types.RECEIVE_PROJECTS_SUCCESS](localState, projects);
mutations[types.RECEIVE_PROJECTS_SUCCESS](localState, { projects });
expect(window.localStorage.setItem).toHaveBeenCalledWith(projectListEndpoint, projectIds);
});
......@@ -142,7 +143,7 @@ describe('mutations', () => {
});
const expectedOrder = [projects[2], projects[0], projects[1]];
mutations[types.RECEIVE_PROJECTS_SUCCESS](localState, projects);
mutations[types.RECEIVE_PROJECTS_SUCCESS](localState, { projects });
expect(localState.projects).toEqual(expectedOrder);
});
......@@ -156,10 +157,26 @@ describe('mutations', () => {
});
const expectedOrder = [projects[1], projects[2], projects[0]];
mutations[types.RECEIVE_PROJECTS_SUCCESS](localState, projects);
mutations[types.RECEIVE_PROJECTS_SUCCESS](localState, { projects });
expect(localState.projects).toEqual(expectedOrder);
});
it('sets dashbpard pagination state', () => {
const headers = {
'x-page': 1,
'x-per-page': 20,
'x-next-page': 2,
'x-total': 22,
'x-total-pages': 2,
'x-prev-page': null,
};
mutations[types.RECEIVE_PROJECTS_SUCCESS](localState, { projects, headers });
const expectedHeaders = parseIntPagination(normalizeHeaders(headers));
expect(localState.projectsPage.pageInfo).toEqual(expectedHeaders);
});
});
describe('RECEIVE_PROJECTS_ERROR', () => {
......
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