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 {
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