summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFilipa Lacerda <filipa@gitlab.com>2017-09-22 11:46:55 +0100
committerFilipa Lacerda <filipa@gitlab.com>2017-09-22 12:14:59 +0100
commitd0e5bafa305b7bb481cb3fe527bbbc0f5b09ade3 (patch)
tree6021d373889a389afd4e4d3a4d14820e8053ca97
parent07b0d933b523b22464c72e0dd85bc413f455b72f (diff)
downloadgitlab-ce-d0e5bafa305b7bb481cb3fe527bbbc0f5b09ade3.tar.gz
Adds pagination
Adds specs
-rw-r--r--app/assets/javascripts/registry/components/app.vue22
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue171
-rw-r--r--app/assets/javascripts/registry/index.js3
-rw-r--r--app/assets/javascripts/registry/stores/actions.js15
-rw-r--r--app/assets/javascripts/registry/stores/mutations.js12
-rw-r--r--app/views/projects/registry/repositories/index.html.haml4
-rw-r--r--spec/javascripts/registry/components/app_spec.js122
-rw-r--r--spec/javascripts/registry/components/collapsible_container_spec.js19
-rw-r--r--spec/javascripts/registry/stores/actions_spec.js2
-rw-r--r--spec/javascripts/registry/stores/mock_data.js8
-rw-r--r--spec/javascripts/registry/stores/mutations_spec.js28
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));
- }
+ },
};
</script>
<template>
@@ -81,10 +86,11 @@
@fetchRegistryList="fetchRegistryList"
@deleteRepository="deleteRepository"
@deleteRegistry="deleteRegistry"
+ @pageChange="onPageChange"
/>
<p v-else-if="!isLoading && !repos.length">
- {{__("No container images stored for this project. Add one by following the instructions above")}}
+ {{__("No container images stored for this project. Add one by following the instructions above.")}}
</p>
</div>
</template>
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 @@
<script>
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ import tablePagination from '../../vue_shared/components/table_pagination.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago';
@@ -15,6 +16,7 @@
components: {
clipboardButton,
loadingIcon,
+ tablePagination,
},
mixins: [
timeagoMixin,
@@ -27,6 +29,11 @@
isOpen: false,
};
},
+ computed: {
+ shouldRenderPagination() {
+ return this.repo.pagination.total > this.repo.pagination.perPage;
+ },
+ },
methods: {
layers(item) {
const pluralize = gl.text.pluralize('layer', item.layers);
@@ -41,12 +48,16 @@
},
handleDeleteRepository() {
- this.$emit('deleteRepository', this.repo)
+ this.$emit('deleteRepository', this.repo);
},
handleDeleteRegistry(registry) {
this.$emit('deleteRegistry', this.repo, registry);
},
+
+ onPageChange(pageNumber) {
+ this.$emit('pageChange', this.repo, pageNumber);
+ },
},
};
</script>
@@ -101,82 +112,88 @@
v-else-if="!repo.isLoading && isOpen"
class="container-image-tags">
- <table
- class="table tags"
- v-if="repo.list.length">
- <thead>
- <tr>
- <th>{{__("Tag")}}</th>
- <th>{{__("Tag ID")}}</th>
- <th>{{__("Size")}}</th>
- <th>{{__("Created")}}</th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- <tr
- v-for="(item, i) in repo.list"
- :key="i">
- <td>
-
- {{item.tag}}
-
- <clipboard-button
- v-if="item.location"
- :title="item.location"
- :text="__(`docker pull ${item.location}`)"
- />
- </td>
- <td>
- <span
- v-tooltip
- :title="item.revision"
- data-placement="bottom">
- {{item.shortRevision}}
- </span>
- </td>
- <td>
- <template v-if="item.size">
- {{item.size}}
- &middot;
- {{layers(item)}}
- </template>
- <div
- v-else
- class="light">
- \-
- </div>
- </td>
-
- <td>
- <template v-if="item.createdAt">
- {{timeFormated(item.createdAt)}}
- </template>
- <div
- v-else
- class="light">
- \-
- </div>
- </td>
-
- <td class="content">
- <button
- v-if="item.canDelete"
- type="button"
- class="js-delete-registry btn btn-remove hidden-xs pull-right"
- :title="__('Remove tag')"
- data-container="body"
- v-tooltip
- @click="handleDeleteRegistry(item)">
- <i
- class="fa fa-trash"
- aria-hidden="true">
- </i>
- </button>
- </td>
- </tr>
- </tbody>
- </table>
+ <template v-if="repo.list.length">
+ <table class="table tags">
+ <thead>
+ <tr>
+ <th>{{__("Tag")}}</th>
+ <th>{{__("Tag ID")}}</th>
+ <th>{{__("Size")}}</th>
+ <th>{{__("Created")}}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr
+ v-for="(item, i) in repo.list"
+ :key="i">
+ <td>
+
+ {{item.tag}}
+
+ <clipboard-button
+ v-if="item.location"
+ :title="item.location"
+ :text="__(`docker pull ${item.location}`)"
+ />
+ </td>
+ <td>
+ <span
+ v-tooltip
+ :title="item.revision"
+ data-placement="bottom">
+ {{item.shortRevision}}
+ </span>
+ </td>
+ <td>
+ <template v-if="item.size">
+ {{item.size}}
+ &middot;
+ {{layers(item)}}
+ </template>
+ <div
+ v-else
+ class="light">
+ \-
+ </div>
+ </td>
+
+ <td>
+ <template v-if="item.createdAt">
+ {{timeFormated(item.createdAt)}}
+ </template>
+ <div
+ v-else
+ class="light">
+ \-
+ </div>
+ </td>
+
+ <td class="content">
+ <button
+ v-if="item.canDelete"
+ type="button"
+ class="js-delete-registry btn btn-remove hidden-xs pull-right"
+ :title="__('Remove tag')"
+ data-container="body"
+ v-tooltip
+ @click="handleDeleteRegistry(item)">
+ <i
+ class="fa fa-trash"
+ aria-hidden="true">
+ </i>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table-pagination
+ v-if="shouldRenderPagination"
+ :change="onPageChange"
+ :page-info="repo.pagination"
+ />
+ </template>
<div
v-else
class="nothing-here-block">
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&rsquo;s Container Registry using your GitLab username and password. If you have')
+ = _('First log in to GitLab&rsquo;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&rsquo;re free to create and upload a container image using the common')
+ = _('Once you log in, you&rsquo;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);