From d0e5bafa305b7bb481cb3fe527bbbc0f5b09ade3 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 22 Sep 2017 11:46:55 +0100 Subject: Adds pagination Adds specs --- app/assets/javascripts/registry/components/app.vue | 22 ++- .../registry/components/collapsible_container.vue | 171 +++++++++++---------- app/assets/javascripts/registry/index.js | 3 + app/assets/javascripts/registry/stores/actions.js | 15 +- .../javascripts/registry/stores/mutations.js | 12 +- .../projects/registry/repositories/index.html.haml | 4 +- spec/javascripts/registry/components/app_spec.js | 122 +++++++++++++++ .../components/collapsible_container_spec.js | 19 +++ spec/javascripts/registry/stores/actions_spec.js | 2 +- spec/javascripts/registry/stores/mock_data.js | 8 +- spec/javascripts/registry/stores/mutations_spec.js | 28 +++- 11 files changed, 303 insertions(+), 103 deletions(-) diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index c4d66382850..7a3e61b64cd 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -12,7 +12,7 @@ props: { endpoint: { type: String, - required: true + required: true, }, }, store, @@ -37,8 +37,8 @@ ]), fetchRegistryList(repo) { - this.fetchList(repo) - .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY)) + this.fetchList({ repo }) + .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY)); }, deleteRegistry(repo, registry) { @@ -53,9 +53,14 @@ .catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); }, - showError(message){ - Flash(__(errorMessages[message])); - } + showError(message) { + Flash(this.__(errorMessages[message])); + }, + + onPageChange(repo, page) { + this.fetchList({ repo, page }) + .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY)); + }, }, created() { this.setMainEndpoint(this.endpoint); @@ -63,7 +68,7 @@ mounted() { this.fetchRepos() .catch(() => this.showError(errorMessagesTypes.FETCH_REPOS)); - } + }, }; diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index 739e48b93f2..2d46eb8270e 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -1,6 +1,7 @@ @@ -101,82 +112,88 @@ v-else-if="!repo.isLoading && isOpen" class="container-image-tags"> - - - - - - - - - - - - - - - - - - - - - -
{{__("Tag")}}{{__("Tag ID")}}{{__("Size")}}{{__("Created")}}
- - {{item.tag}} - - - - - {{item.shortRevision}} - - - -
- \- -
-
- -
- \- -
-
- -
+
diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js index ad76dfd333b..d8edff73f72 100644 --- a/app/assets/javascripts/registry/index.js +++ b/app/assets/javascripts/registry/index.js @@ -1,5 +1,8 @@ import Vue from 'vue'; import registryApp from './components/app.vue'; +import Translate from '../vue_shared/translate'; + +Vue.use(Translate); document.addEventListener('DOMContentLoaded', () => new Vue({ el: '#js-vue-registry-images', diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js index c86e40a1a28..d980e98e7b1 100644 --- a/app/assets/javascripts/registry/stores/actions.js +++ b/app/assets/javascripts/registry/stores/actions.js @@ -15,14 +15,17 @@ export const fetchRepos = ({ commit, state }) => { }); }; -export const fetchList = ({ commit }, list) => { - commit(types.TOGGLE_REGISTRY_LIST_LOADING, list); +export const fetchList = ({ commit }, { repo, page }) => { + commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); - return Vue.http.get(list.path) - .then(res => res.json()) + return Vue.http.get(repo.tagsPath, { params: { page } }) .then((response) => { - commit(types.TOGGLE_REGISTRY_LIST_LOADING, list); - commit(types.SET_REGISTRY_LIST, list, response); + const headers = response.headers; + + return response.json().then((resp) => { + commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); + commit(types.SET_REGISTRY_LIST, { repo, resp, headers }); + }); }); }; diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js index 0e69d2bed1b..e40382e7afc 100644 --- a/app/assets/javascripts/registry/stores/mutations.js +++ b/app/assets/javascripts/registry/stores/mutations.js @@ -1,4 +1,5 @@ import * as types from './mutation_types'; +import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils'; export default { @@ -15,7 +16,7 @@ export default { isLoading: false, list: [], location: el.location, - name: el.name, + name: el.path, tagsPath: el.tags_path, })), }); @@ -25,10 +26,15 @@ export default { Object.assign(state, { isLoading: !state.isLoading }); }, - [types.SET_REGISTRY_LIST](state, repo, list) { + [types.SET_REGISTRY_LIST](state, { repo, resp, headers }) { const listToUpdate = state.repos.find(el => el.id === repo.id); - listToUpdate.list = list.map(element => ({ + const normalizedHeaders = normalizeHeaders(headers); + const pagination = parseIntPagination(normalizedHeaders); + + listToUpdate.pagination = pagination; + + listToUpdate.list = resp.map(element => ({ tag: element.name, revision: element.revision, shortRevision: element.short_revision, diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index 9bf5eb03cb0..ca78ecf1226 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -18,7 +18,7 @@ = _('How to use the Container Registry') .panel-body %p - = _('First log in to GitLab’s Container Registry using your GitLab username and password. If you have') + = _('First log in to GitLab’s Container Registry using your GitLab username and password. If you have').html_safe = link_to _('2FA enabled'), help_page_path('user/profile/account/two_factor_authentication'), target: '_blank' = _('you need to use a') = succeed ':' do @@ -27,7 +27,7 @@ docker login #{Gitlab.config.registry.host_port} %br %p - = _('Once you log in, you’re free to create and upload a container image using the common') + = _('Once you log in, you’re free to create and upload a container image using the common').html_safe %code = _('build') = _('and') diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js index e69de29bb2d..2d4bc010a00 100644 --- a/spec/javascripts/registry/components/app_spec.js +++ b/spec/javascripts/registry/components/app_spec.js @@ -0,0 +1,122 @@ +import Vue from 'vue'; +import registry from '~/registry/components/app.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; +import { reposServerResponse } from '../stores/mock_data'; + +describe('Registry List', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(registry); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('with data', () => { + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify(reposServerResponse), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + vm = mountComponent(Component, { endpoint: 'foo' }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('should render a list of repos', (done) => { + setTimeout(() => { + expect(vm.$store.state.repos.length).toEqual(reposServerResponse.length); + + Vue.nextTick(() => { + expect( + vm.$el.querySelectorAll('.container-image').length, + ).toEqual(reposServerResponse.length); + done(); + }); + }, 0); + }); + + describe('delete repository', () => { + it('should be possible to delete a repo', (done) => { + setTimeout(() => { + Vue.nextTick(() => { + expect(vm.$el.querySelector('.container-image-head .js-remove-repo')).toBeDefined(); + done(); + }); + }, 0); + }); + }); + + describe('toggle repository', () => { + it('should open the container', (done) => { + setTimeout(() => { + Vue.nextTick(() => { + vm.$el.querySelector('.container-image a').click(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-toggle-repo i').className).toEqual('fa fa-chevron-up'); + done(); + }); + }); + }, 0); + }); + }); + }); + + describe('without data', () => { + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + vm = mountComponent(Component, { endpoint: 'foo' }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('should render empty message', (done) => { + setTimeout(() => { + expect( + vm.$el.querySelector('p').textContent.trim(), + ).toEqual('No container images stored for this project. Add one by following the instructions above.'); + done(); + }, 0); + }); + }); + + describe('while loading data', () => { + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify(reposServerResponse), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + vm = mountComponent(Component, { endpoint: 'foo' }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('should render a loading spinner', (done) => { + Vue.nextTick(() => { + expect(vm.$el.querySelector('.fa-spinner')).not.toBe(null); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/registry/components/collapsible_container_spec.js b/spec/javascripts/registry/components/collapsible_container_spec.js index b9372f48965..a659f08e250 100644 --- a/spec/javascripts/registry/components/collapsible_container_spec.js +++ b/spec/javascripts/registry/components/collapsible_container_spec.js @@ -30,6 +30,14 @@ describe('collapsible registry container', () => { location: 'location', name: 'foo', tagsPath: 'path', + pagination: { + perPage: 5, + page: 1, + total: 13, + totalPages: 1, + nextPage: null, + previousPage: null, + }, }; vm = mountComponent(Component, { repo: mockData }); }); @@ -108,5 +116,16 @@ describe('collapsible registry container', () => { done(); }); }); + + describe('pagination', () => { + it('should be possible to change the page', (done) => { + vm.$el.querySelector('.js-toggle-repo').click(); + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.gl-pagination')).toBeDefined(); + done(); + }); + }); + }); }); }); diff --git a/spec/javascripts/registry/stores/actions_spec.js b/spec/javascripts/registry/stores/actions_spec.js index d835ea04622..3c53956510d 100644 --- a/spec/javascripts/registry/stores/actions_spec.js +++ b/spec/javascripts/registry/stores/actions_spec.js @@ -59,7 +59,7 @@ describe('Actions Registry Store', () => { it('should set received list', (done) => { mockedState.repos = parsedReposServerResponse; - testAction(actions.fetchList, mockedState.repos[1], mockedState, [ + testAction(actions.fetchList, { repo: mockedState.repos[1] }, mockedState, [ { type: types.TOGGLE_REGISTRY_LIST_LOADING }, { type: types.SET_REGISTRY_LIST, payload: registryServerResponse }, ], done); diff --git a/spec/javascripts/registry/stores/mock_data.js b/spec/javascripts/registry/stores/mock_data.js index 80f7c51426a..07c51503b7f 100644 --- a/spec/javascripts/registry/stores/mock_data.js +++ b/spec/javascripts/registry/stores/mock_data.js @@ -9,14 +9,14 @@ export const reposServerResponse = [ destroy_path: 'path', id: '123', location: 'location', - name: 'foo', + path: 'foo', tags_path: 'tags_path', }, { destroy_path: 'path_', id: '456', location: 'location_', - name: 'bar', + path: 'bar', tags_path: 'tags_path_', }, ]; @@ -50,7 +50,7 @@ export const parsedReposServerResponse = [ isLoading: false, list: [], location: reposServerResponse[0].location, - name: reposServerResponse[0].name, + name: reposServerResponse[0].path, tagsPath: reposServerResponse[0].tags_path, }, { @@ -60,7 +60,7 @@ export const parsedReposServerResponse = [ isLoading: false, list: [], location: reposServerResponse[1].location, - name: reposServerResponse[1].name, + name: reposServerResponse[1].path, tagsPath: reposServerResponse[1].tags_path, }, ]; diff --git a/spec/javascripts/registry/stores/mutations_spec.js b/spec/javascripts/registry/stores/mutations_spec.js index 7fae19f3656..82c0d8c7f07 100644 --- a/spec/javascripts/registry/stores/mutations_spec.js +++ b/spec/javascripts/registry/stores/mutations_spec.js @@ -39,16 +39,40 @@ describe('Mutations Registry Store', () => { describe('SET_REGISTRY_LIST', () => { it('should set a list of registries in a specific repository', () => { mutations[types.SET_REPOS_LIST](mockState, reposServerResponse); - mutations[types.SET_REGISTRY_LIST](mockState, mockState.repos[0], registryServerResponse); + mutations[types.SET_REGISTRY_LIST](mockState, { + repo: mockState.repos[0], + resp: registryServerResponse, + headers: { + 'x-per-page': 2, + 'x-page': 1, + 'x-total': 10, + }, + }); expect(mockState.repos[0].list).toEqual(parsedRegistryServerResponse); + expect(mockState.repos[0].pagination).toEqual({ + perPage: 2, + page: 1, + total: 10, + totalPages: NaN, + nextPage: NaN, + previousPage: NaN, + }); }); }); describe('TOGGLE_REGISTRY_LIST_LOADING', () => { it('should toggle isLoading property for a specific repository', () => { mutations[types.SET_REPOS_LIST](mockState, reposServerResponse); - mutations[types.SET_REGISTRY_LIST](mockState, mockState.repos[0], registryServerResponse); + mutations[types.SET_REGISTRY_LIST](mockState, { + repo: mockState.repos[0], + resp: registryServerResponse, + headers: { + 'x-per-page': 2, + 'x-page': 1, + 'x-total': 10, + }, + }); mutations[types.TOGGLE_REGISTRY_LIST_LOADING](mockState, mockState.repos[0]); expect(mockState.repos[0].isLoading).toEqual(true); -- cgit v1.2.1