Commit 333db852 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch '208470_04-package-file-graphql-pagination' into 'master'

Geo Package Files - GraphQL Pagination

Closes #208470

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