Commit b66bf91f authored by Simon Knox's avatar Simon Knox

Merge branch 'add-pagination-frontend-to-environments-dashboard' into 'master'

Add pagination frontend to Environments dashboard

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