Commit bad8a8ac authored by Zack Cuddy's avatar Zack Cuddy Committed by Nicolò Maria Mezzopera

Geo Package Files - GraphQL Pagination

This splits off of:
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/32872

This is an attempt at MVC to avoid a
massive MR.

This MR hooks up the separate logic
for GraphQL pagination.
parent b6226a4f
<script>
import { mapState, mapActions } from 'vuex';
import { GlPagination } from '@gitlab/ui';
import { PREV, NEXT } from '../constants';
import GeoReplicableItem from './geo_replicable_item.vue';
export default {
......@@ -16,12 +17,27 @@ export default {
return this.paginationData.page;
},
set(newVal) {
let action;
if (this.useGraphQl) {
action = this.page > newVal ? PREV : NEXT;
}
this.setPage(newVal);
this.fetchReplicableItems();
this.fetchReplicableItems(action);
},
},
showRestfulPagination() {
return !this.useGraphQl && this.paginationData.total > 0;
paginationProps() {
if (!this.useGraphQl) {
return {
perPage: this.paginationData.perPage,
totalItems: this.paginationData.total,
};
}
return {
prevPage: this.paginationData.hasPreviousPage ? this.page - 1 : null,
nextPage: this.paginationData.hasNextPage ? this.page + 1 : null,
};
},
},
methods: {
......@@ -45,12 +61,6 @@ export default {
:last-verified="item.lastVerifiedAt"
:last-checked="item.lastCheckedAt"
/>
<gl-pagination
v-if="showRestfulPagination"
v-model="page"
:per-page="paginationData.perPage"
:total-items="paginationData.total"
align="center"
/>
<gl-pagination v-model="page" v-bind="paginationProps" align="center" />
</section>
</template>
......@@ -47,3 +47,5 @@ export const ACTION_TYPES = {
export const PREV = 'prev';
export const NEXT = 'next';
export const DEFAULT_PAGE_SIZE = 20;
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query($before: String!, $after: String!) {
query($first: Int, $last: Int, $before: String!, $after: String!) {
geoNode {
packageFileRegistries(first: 20, before: $before, after: $after) {
packageFileRegistries(first: $first, last: $last, before: $before, after: $after) {
pageInfo {
...PageInfo
}
......
......@@ -10,7 +10,7 @@ import {
import packageFilesQuery from '../graphql/package_files.query.graphql';
import { gqClient } from '../utils';
import * as types from './mutation_types';
import { FILTER_STATES, PREV, NEXT } from '../constants';
import { FILTER_STATES, PREV, NEXT, DEFAULT_PAGE_SIZE } from '../constants';
// Fetch Replicable Items
export const requestReplicableItems = ({ commit }) => commit(types.REQUEST_REPLICABLE_ITEMS);
......@@ -37,8 +37,14 @@ export const fetchReplicableItemsGraphQl = ({ state, dispatch }, direction) => {
let before = '';
let after = '';
// If we are going backwards we want the last 20, otherwise get the first 20.
let first = DEFAULT_PAGE_SIZE;
let last = null;
if (direction === PREV) {
before = state.paginationData.startCursor;
first = null;
last = DEFAULT_PAGE_SIZE;
} else if (direction === NEXT) {
after = state.paginationData.endCursor;
}
......@@ -46,12 +52,15 @@ export const fetchReplicableItemsGraphQl = ({ state, dispatch }, direction) => {
gqClient
.query({
query: packageFilesQuery,
variables: { before, after },
variables: { first, last, before, after },
})
.then(res => {
const registries = res.data.geoNode.packageFileRegistries;
const data = registries.edges.map(e => e.node);
const pagination = registries.pageInfo;
const pagination = {
...registries.pageInfo,
page: state.paginationData.page,
};
dispatch('receiveReplicableItemsSuccess', { data, pagination });
})
......
import { FILTER_STATES } from '../constants';
import { FILTER_STATES, DEFAULT_PAGE_SIZE } from '../constants';
const createState = ({ replicableType, useGraphQl }) => ({
replicableType,
......@@ -14,7 +14,7 @@ const createState = ({ replicableType, useGraphQl }) => ({
endCursor: '',
// RESTful
total: 0,
perPage: 0,
perPage: DEFAULT_PAGE_SIZE,
page: 1,
},
......
import initGeoReplicable from 'ee/geo_replicable';
if (gon?.features?.geoSelfServiceFramework) {
document.addEventListener('DOMContentLoaded', initGeoReplicable);
}
document.addEventListener('DOMContentLoaded', initGeoReplicable);
......@@ -2,9 +2,6 @@
class Admin::Geo::PackageFilesController < Admin::Geo::ApplicationController
before_action :check_license!
before_action do
push_frontend_feature_flag(:geo_self_service_framework)
end
def index
end
......
......@@ -18,7 +18,6 @@
= link_to admin_geo_designs_path, title: _('Designs') do
%span
= _('Designs')
- if Feature.enabled?(:geo_self_service_framework)
= nav_link(path: 'admin/geo/package_files#index', html_options: { class: 'gl-pr-2' }) do
= link_to admin_geo_package_files_path, title: _('Package Files') do
%span
......
---
title: Geo Package Files UI
merge_request: 34004
author:
type: added
......@@ -48,18 +48,5 @@ RSpec.describe 'admin Geo Replication Nav', :js, :geo do
it_behaves_like 'active sidebar link', 'Package Files' do
let(:path) { admin_geo_package_files_path }
end
context 'when geo_self_service_framework feature is disabled' do
before do
stub_feature_flags(geo_self_service_framework: false)
visit admin_geo_projects_path
wait_for_requests
end
it 'does not render navigational element' do
expect(page).not_to have_selector("a[title=\"Package Files\"]")
end
end
end
end
import Vuex from 'vuex';
import { createLocalVue, mount } from '@vue/test-utils';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlPagination } from '@gitlab/ui';
import createStore from 'ee/geo_replicable/store';
import initStore from 'ee/geo_replicable/store';
import * as types from 'ee/geo_replicable/store/mutation_types';
import GeoReplicable from 'ee/geo_replicable/components/geo_replicable.vue';
import GeoReplicableItem from 'ee/geo_replicable/components/geo_replicable_item.vue';
import { MOCK_BASIC_FETCH_DATA_MAP, MOCK_REPLICABLE_TYPE } from '../mock_data';
import {
MOCK_BASIC_FETCH_DATA_MAP,
MOCK_REPLICABLE_TYPE,
MOCK_GRAPHQL_PAGINATION_DATA,
MOCK_RESTFUL_PAGINATION_DATA,
} from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('GeoReplicable', () => {
let wrapper;
let store;
const actionSpies = {
setPage: jest.fn(),
fetchReplicableItems: jest.fn(),
const createStore = options => {
store = initStore({ replicableType: MOCK_REPLICABLE_TYPE, useGraphQl: false, ...options });
jest.spyOn(store, 'dispatch').mockImplementation();
};
const createComponent = () => {
wrapper = mount(GeoReplicable, {
wrapper = shallowMount(GeoReplicable, {
localVue,
store: createStore({ replicableType: MOCK_REPLICABLE_TYPE, useGraphQl: false }),
methods: {
...actionSpies,
},
store,
});
};
afterEach(() => {
wrapper.destroy();
store = null;
});
const findGeoReplicableContainer = () => wrapper.find('section');
......@@ -37,6 +43,11 @@ describe('GeoReplicable', () => {
describe('template', () => {
beforeEach(() => {
createStore();
store.commit(types.RECEIVE_REPLICABLE_ITEMS_SUCCESS, {
data: MOCK_BASIC_FETCH_DATA_MAP,
pagination: MOCK_RESTFUL_PAGINATION_DATA,
});
createComponent();
});
......@@ -44,72 +55,61 @@ describe('GeoReplicable', () => {
expect(findGeoReplicableContainer().exists()).toBe(true);
});
describe('when useGraphQl is false', () => {
describe('GlPagination', () => {
describe('when perPage >= total', () => {
beforeEach(() => {
wrapper.vm.$store.state.paginationData.perPage = 2;
wrapper.vm.$store.state.paginationData.total = 1;
});
it('is hidden', () => {
expect(findGlPagination().isEmpty()).toBe(true);
});
});
describe('GeoReplicableItem', () => {
it('renders an instance for each replicableItem in the store', () => {
const replicableItemWrappers = findGeoReplicableItem();
const replicableItems = [...store.state.replicableItems];
describe('when perPage < total', () => {
beforeEach(() => {
wrapper.vm.$store.state.paginationData.perPage = 1;
wrapper.vm.$store.state.paginationData.total = 2;
for (let i = 0; i < replicableItemWrappers.length; i += 1) {
expect(replicableItemWrappers.at(i).props().projectId).toBe(replicableItems[i].projectId);
}
});
it('renders', () => {
expect(findGlPagination().html()).not.toBeUndefined();
});
});
describe('GlPagination', () => {
describe('when useGraphQl is false', () => {
it('renders always', () => {
createStore({ useGraphQl: false });
createComponent();
expect(findGlPagination().exists()).toBe(true);
});
});
describe('when useGraphQl is true', () => {
beforeEach(() => {
it('renders always', () => {
createStore({ useGraphQl: true });
createComponent();
wrapper.vm.$store.state.useGraphQl = true;
expect(findGlPagination().exists()).toBe(true);
});
it('does not render GlPagination', () => {
expect(findGlPagination().exists()).toBeFalsy();
});
});
describe('GeoReplicableItem', () => {
describe.each`
useGraphQl | currentPage | newPage | action
${false} | ${1} | ${2} | ${undefined}
${false} | ${2} | ${1} | ${undefined}
${true} | ${1} | ${2} | ${'next'}
${true} | ${2} | ${1} | ${'prev'}
`(`changing the page`, ({ useGraphQl, currentPage, newPage, action }) => {
describe(`when useGraphQl is ${useGraphQl}`, () => {
describe(`from ${currentPage} to ${newPage}`, () => {
beforeEach(() => {
wrapper.vm.$store.state.replicableItems = MOCK_BASIC_FETCH_DATA_MAP;
});
it('renders an instance for each replicableItem in the store', () => {
const replicableItemWrappers = findGeoReplicableItem();
const replicableItems = [...wrapper.vm.$store.state.replicableItems];
for (let i = 0; i < replicableItemWrappers.length; i += 1) {
expect(replicableItemWrappers.at(i).props().projectId).toBe(replicableItems[i].projectId);
}
});
});
createStore({ useGraphQl });
store.commit(types.RECEIVE_REPLICABLE_ITEMS_SUCCESS, {
data: MOCK_BASIC_FETCH_DATA_MAP,
pagination: { ...MOCK_GRAPHQL_PAGINATION_DATA, page: currentPage },
});
describe('changing the page', () => {
describe('when useGraphQl is false', () => {
beforeEach(() => {
createComponent();
wrapper.vm.page = 2;
findGlPagination().vm.$emit(GlPagination.model.event, newPage);
});
it('should call setPage', () => {
expect(actionSpies.setPage).toHaveBeenCalledWith(2);
it(`should call setPage with ${newPage}`, () => {
expect(store.dispatch).toHaveBeenCalledWith('setPage', newPage);
});
it('should call fetchReplicableItems', () => {
expect(actionSpies.fetchReplicableItems).toHaveBeenCalled();
it(`should call fetchReplicableItems with ${action}`, () => {
expect(store.dispatch).toHaveBeenCalledWith('fetchReplicableItems', action);
});
});
});
});
......
......@@ -6,7 +6,7 @@ import Api from 'ee/api';
import * as actions from 'ee/geo_replicable/store/actions';
import * as types from 'ee/geo_replicable/store/mutation_types';
import createState from 'ee/geo_replicable/store/state';
import { ACTION_TYPES, PREV, NEXT } from 'ee/geo_replicable/constants';
import { ACTION_TYPES, PREV, NEXT, DEFAULT_PAGE_SIZE } from 'ee/geo_replicable/constants';
import { gqClient } from 'ee/geo_replicable/utils';
import packageFilesQuery from 'ee/geo_replicable/graphql/package_files.query.graphql';
import {
......@@ -121,6 +121,7 @@ describe('GeoReplicable Store Actions', () => {
data: MOCK_BASIC_GRAPHQL_QUERY_RESPONSE,
});
state.paginationData = MOCK_GRAPHQL_PAGINATION_DATA;
state.paginationData.page = 1;
});
describe('with no direction set', () => {
......@@ -128,7 +129,7 @@ describe('GeoReplicable Store Actions', () => {
const registries = MOCK_BASIC_GRAPHQL_QUERY_RESPONSE.geoNode?.packageFileRegistries;
const data = registries.edges.map(e => e.node);
it('should call gqClient with no before/after variables', () => {
it('should call gqClient with no before/after variables as well as a first variable but no last variable', () => {
testAction(
actions.fetchReplicableItemsGraphQl,
direction,
......@@ -143,7 +144,7 @@ describe('GeoReplicable Store Actions', () => {
() => {
expect(gqClient.query).toHaveBeenCalledWith({
query: packageFilesQuery,
variables: { before: '', after: '' },
variables: { before: '', after: '', first: DEFAULT_PAGE_SIZE, last: null },
});
},
);
......@@ -155,7 +156,7 @@ describe('GeoReplicable Store Actions', () => {
const registries = MOCK_BASIC_GRAPHQL_QUERY_RESPONSE.geoNode?.packageFileRegistries;
const data = registries.edges.map(e => e.node);
it('should call gqClient with after variable but no before variable', () => {
it('should call gqClient with after variable but no before variable as well as a first variable but no last variable', () => {
testAction(
actions.fetchReplicableItemsGraphQl,
direction,
......@@ -170,7 +171,12 @@ describe('GeoReplicable Store Actions', () => {
() => {
expect(gqClient.query).toHaveBeenCalledWith({
query: packageFilesQuery,
variables: { before: '', after: MOCK_GRAPHQL_PAGINATION_DATA.endCursor },
variables: {
before: '',
after: MOCK_GRAPHQL_PAGINATION_DATA.endCursor,
first: DEFAULT_PAGE_SIZE,
last: null,
},
});
},
);
......@@ -182,7 +188,7 @@ describe('GeoReplicable Store Actions', () => {
const registries = MOCK_BASIC_GRAPHQL_QUERY_RESPONSE.geoNode?.packageFileRegistries;
const data = registries.edges.map(e => e.node);
it('should call gqClient with before variable but no after variable', () => {
it('should call gqClient with before variable but no after variable as well as a last variable but no first variable', () => {
testAction(
actions.fetchReplicableItemsGraphQl,
direction,
......@@ -197,7 +203,12 @@ describe('GeoReplicable Store Actions', () => {
() => {
expect(gqClient.query).toHaveBeenCalledWith({
query: packageFilesQuery,
variables: { before: MOCK_GRAPHQL_PAGINATION_DATA.startCursor, after: '' },
variables: {
before: MOCK_GRAPHQL_PAGINATION_DATA.startCursor,
after: '',
first: null,
last: DEFAULT_PAGE_SIZE,
},
});
},
);
......
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