Commit d0e5bafa authored by Filipa Lacerda's avatar Filipa Lacerda

Adds pagination

Adds specs
parent 07b0d933
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
props: { props: {
endpoint: { endpoint: {
type: String, type: String,
required: true required: true,
}, },
}, },
store, store,
...@@ -37,8 +37,8 @@ ...@@ -37,8 +37,8 @@
]), ]),
fetchRegistryList(repo) { fetchRegistryList(repo) {
this.fetchList(repo) this.fetchList({ repo })
.catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY)) .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
}, },
deleteRegistry(repo, registry) { deleteRegistry(repo, registry) {
...@@ -53,9 +53,14 @@ ...@@ -53,9 +53,14 @@
.catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); .catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
}, },
showError(message){ showError(message) {
Flash(__(errorMessages[message])); Flash(this.__(errorMessages[message]));
} },
onPageChange(repo, page) {
this.fetchList({ repo, page })
.catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
},
}, },
created() { created() {
this.setMainEndpoint(this.endpoint); this.setMainEndpoint(this.endpoint);
...@@ -63,7 +68,7 @@ ...@@ -63,7 +68,7 @@
mounted() { mounted() {
this.fetchRepos() this.fetchRepos()
.catch(() => this.showError(errorMessagesTypes.FETCH_REPOS)); .catch(() => this.showError(errorMessagesTypes.FETCH_REPOS));
} },
}; };
</script> </script>
<template> <template>
...@@ -81,10 +86,11 @@ ...@@ -81,10 +86,11 @@
@fetchRegistryList="fetchRegistryList" @fetchRegistryList="fetchRegistryList"
@deleteRepository="deleteRepository" @deleteRepository="deleteRepository"
@deleteRegistry="deleteRegistry" @deleteRegistry="deleteRegistry"
@pageChange="onPageChange"
/> />
<p v-else-if="!isLoading && !repos.length"> <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> </p>
</div> </div>
</template> </template>
<script> <script>
import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.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 tooltip from '../../vue_shared/directives/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago'; import timeagoMixin from '../../vue_shared/mixins/timeago';
...@@ -15,6 +16,7 @@ ...@@ -15,6 +16,7 @@
components: { components: {
clipboardButton, clipboardButton,
loadingIcon, loadingIcon,
tablePagination,
}, },
mixins: [ mixins: [
timeagoMixin, timeagoMixin,
...@@ -27,6 +29,11 @@ ...@@ -27,6 +29,11 @@
isOpen: false, isOpen: false,
}; };
}, },
computed: {
shouldRenderPagination() {
return this.repo.pagination.total > this.repo.pagination.perPage;
},
},
methods: { methods: {
layers(item) { layers(item) {
const pluralize = gl.text.pluralize('layer', item.layers); const pluralize = gl.text.pluralize('layer', item.layers);
...@@ -41,12 +48,16 @@ ...@@ -41,12 +48,16 @@
}, },
handleDeleteRepository() { handleDeleteRepository() {
this.$emit('deleteRepository', this.repo) this.$emit('deleteRepository', this.repo);
}, },
handleDeleteRegistry(registry) { handleDeleteRegistry(registry) {
this.$emit('deleteRegistry', this.repo, registry); this.$emit('deleteRegistry', this.repo, registry);
}, },
onPageChange(pageNumber) {
this.$emit('pageChange', this.repo, pageNumber);
},
}, },
}; };
</script> </script>
...@@ -101,82 +112,88 @@ ...@@ -101,82 +112,88 @@
v-else-if="!repo.isLoading && isOpen" v-else-if="!repo.isLoading && isOpen"
class="container-image-tags"> class="container-image-tags">
<table <template v-if="repo.list.length">
class="table tags" <table class="table tags">
v-if="repo.list.length"> <thead>
<thead> <tr>
<tr> <th>{{__("Tag")}}</th>
<th>{{__("Tag")}}</th> <th>{{__("Tag ID")}}</th>
<th>{{__("Tag ID")}}</th> <th>{{__("Size")}}</th>
<th>{{__("Size")}}</th> <th>{{__("Created")}}</th>
<th>{{__("Created")}}</th> <th></th>
<th></th> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> <tr
<tr v-for="(item, i) in repo.list"
v-for="(item, i) in repo.list" :key="i">
:key="i"> <td>
<td>
{{item.tag}}
{{item.tag}}
<clipboard-button
<clipboard-button v-if="item.location"
v-if="item.location" :title="item.location"
:title="item.location" :text="__(`docker pull ${item.location}`)"
:text="__(`docker pull ${item.location}`)" />
/> </td>
</td> <td>
<td> <span
<span v-tooltip
v-tooltip :title="item.revision"
:title="item.revision" data-placement="bottom">
data-placement="bottom"> {{item.shortRevision}}
{{item.shortRevision}} </span>
</span> </td>
</td> <td>
<td> <template v-if="item.size">
<template v-if="item.size"> {{item.size}}
{{item.size}} &middot;
&middot; {{layers(item)}}
{{layers(item)}} </template>
</template> <div
<div v-else
v-else class="light">
class="light"> \-
\- </div>
</div> </td>
</td>
<td>
<td> <template v-if="item.createdAt">
<template v-if="item.createdAt"> {{timeFormated(item.createdAt)}}
{{timeFormated(item.createdAt)}} </template>
</template> <div
<div v-else
v-else class="light">
class="light"> \-
\- </div>
</div> </td>
</td>
<td class="content">
<td class="content"> <button
<button v-if="item.canDelete"
v-if="item.canDelete" type="button"
type="button" class="js-delete-registry btn btn-remove hidden-xs pull-right"
class="js-delete-registry btn btn-remove hidden-xs pull-right" :title="__('Remove tag')"
:title="__('Remove tag')" data-container="body"
data-container="body" v-tooltip
v-tooltip @click="handleDeleteRegistry(item)">
@click="handleDeleteRegistry(item)"> <i
<i class="fa fa-trash"
class="fa fa-trash" aria-hidden="true">
aria-hidden="true"> </i>
</i> </button>
</button> </td>
</td> </tr>
</tr> </tbody>
</tbody> </table>
</table>
<table-pagination
v-if="shouldRenderPagination"
:change="onPageChange"
:page-info="repo.pagination"
/>
</template>
<div <div
v-else v-else
class="nothing-here-block"> class="nothing-here-block">
......
import Vue from 'vue'; import Vue from 'vue';
import registryApp from './components/app.vue'; import registryApp from './components/app.vue';
import Translate from '../vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#js-vue-registry-images', el: '#js-vue-registry-images',
......
...@@ -15,14 +15,17 @@ export const fetchRepos = ({ commit, state }) => { ...@@ -15,14 +15,17 @@ export const fetchRepos = ({ commit, state }) => {
}); });
}; };
export const fetchList = ({ commit }, list) => { export const fetchList = ({ commit }, { repo, page }) => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, list); commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
return Vue.http.get(list.path) return Vue.http.get(repo.tagsPath, { params: { page } })
.then(res => res.json())
.then((response) => { .then((response) => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, list); const headers = response.headers;
commit(types.SET_REGISTRY_LIST, list, response);
return response.json().then((resp) => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
commit(types.SET_REGISTRY_LIST, { repo, resp, headers });
});
}); });
}; };
......
import * as types from './mutation_types'; import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils';
export default { export default {
...@@ -15,7 +16,7 @@ export default { ...@@ -15,7 +16,7 @@ export default {
isLoading: false, isLoading: false,
list: [], list: [],
location: el.location, location: el.location,
name: el.name, name: el.path,
tagsPath: el.tags_path, tagsPath: el.tags_path,
})), })),
}); });
...@@ -25,10 +26,15 @@ export default { ...@@ -25,10 +26,15 @@ export default {
Object.assign(state, { isLoading: !state.isLoading }); 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); 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, tag: element.name,
revision: element.revision, revision: element.revision,
shortRevision: element.short_revision, shortRevision: element.short_revision,
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
= _('How to use the Container Registry') = _('How to use the Container Registry')
.panel-body .panel-body
%p %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' = link_to _('2FA enabled'), help_page_path('user/profile/account/two_factor_authentication'), target: '_blank'
= _('you need to use a') = _('you need to use a')
= succeed ':' do = succeed ':' do
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
docker login #{Gitlab.config.registry.host_port} docker login #{Gitlab.config.registry.host_port}
%br %br
%p %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 %code
= _('build') = _('build')
= _('and') = _('and')
......
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();
});
});
});
});
...@@ -30,6 +30,14 @@ describe('collapsible registry container', () => { ...@@ -30,6 +30,14 @@ describe('collapsible registry container', () => {
location: 'location', location: 'location',
name: 'foo', name: 'foo',
tagsPath: 'path', tagsPath: 'path',
pagination: {
perPage: 5,
page: 1,
total: 13,
totalPages: 1,
nextPage: null,
previousPage: null,
},
}; };
vm = mountComponent(Component, { repo: mockData }); vm = mountComponent(Component, { repo: mockData });
}); });
...@@ -108,5 +116,16 @@ describe('collapsible registry container', () => { ...@@ -108,5 +116,16 @@ describe('collapsible registry container', () => {
done(); 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();
});
});
});
}); });
}); });
...@@ -59,7 +59,7 @@ describe('Actions Registry Store', () => { ...@@ -59,7 +59,7 @@ describe('Actions Registry Store', () => {
it('should set received list', (done) => { it('should set received list', (done) => {
mockedState.repos = parsedReposServerResponse; 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.TOGGLE_REGISTRY_LIST_LOADING },
{ type: types.SET_REGISTRY_LIST, payload: registryServerResponse }, { type: types.SET_REGISTRY_LIST, payload: registryServerResponse },
], done); ], done);
......
...@@ -9,14 +9,14 @@ export const reposServerResponse = [ ...@@ -9,14 +9,14 @@ export const reposServerResponse = [
destroy_path: 'path', destroy_path: 'path',
id: '123', id: '123',
location: 'location', location: 'location',
name: 'foo', path: 'foo',
tags_path: 'tags_path', tags_path: 'tags_path',
}, },
{ {
destroy_path: 'path_', destroy_path: 'path_',
id: '456', id: '456',
location: 'location_', location: 'location_',
name: 'bar', path: 'bar',
tags_path: 'tags_path_', tags_path: 'tags_path_',
}, },
]; ];
...@@ -50,7 +50,7 @@ export const parsedReposServerResponse = [ ...@@ -50,7 +50,7 @@ export const parsedReposServerResponse = [
isLoading: false, isLoading: false,
list: [], list: [],
location: reposServerResponse[0].location, location: reposServerResponse[0].location,
name: reposServerResponse[0].name, name: reposServerResponse[0].path,
tagsPath: reposServerResponse[0].tags_path, tagsPath: reposServerResponse[0].tags_path,
}, },
{ {
...@@ -60,7 +60,7 @@ export const parsedReposServerResponse = [ ...@@ -60,7 +60,7 @@ export const parsedReposServerResponse = [
isLoading: false, isLoading: false,
list: [], list: [],
location: reposServerResponse[1].location, location: reposServerResponse[1].location,
name: reposServerResponse[1].name, name: reposServerResponse[1].path,
tagsPath: reposServerResponse[1].tags_path, tagsPath: reposServerResponse[1].tags_path,
}, },
]; ];
......
...@@ -39,16 +39,40 @@ describe('Mutations Registry Store', () => { ...@@ -39,16 +39,40 @@ describe('Mutations Registry Store', () => {
describe('SET_REGISTRY_LIST', () => { describe('SET_REGISTRY_LIST', () => {
it('should set a list of registries in a specific repository', () => { it('should set a list of registries in a specific repository', () => {
mutations[types.SET_REPOS_LIST](mockState, reposServerResponse); 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].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', () => { describe('TOGGLE_REGISTRY_LIST_LOADING', () => {
it('should toggle isLoading property for a specific repository', () => { it('should toggle isLoading property for a specific repository', () => {
mutations[types.SET_REPOS_LIST](mockState, reposServerResponse); 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]); mutations[types.TOGGLE_REGISTRY_LIST_LOADING](mockState, mockState.repos[0]);
expect(mockState.repos[0].isLoading).toEqual(true); expect(mockState.repos[0].isLoading).toEqual(true);
......
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