Commit d0e5bafa authored by Filipa Lacerda's avatar Filipa Lacerda

Adds pagination

Adds specs
parent 07b0d933
......@@ -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>
<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">
......
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',
......
......@@ -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 });
});
});
};
......
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,
......
......@@ -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')
......
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', () => {
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();
});
});
});
});
});
......@@ -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);
......
......@@ -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,
},
];
......
......@@ -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);
......
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