diff options
author | Phil Hughes <me@iamphill.com> | 2018-12-10 12:38:44 +0000 |
---|---|---|
committer | Yorick Peterse <yorickpeterse@gmail.com> | 2018-12-17 16:11:38 +0100 |
commit | 8ee96a9a2fa29d0aa748997fbaeb166a1ecc849c (patch) | |
tree | 27b75734bd2f8b1a0f9a03842511cc079432d86b | |
parent | a8947a1891d1934decdcdb5e00e157f06e1d739a (diff) | |
download | gitlab-ce-8ee96a9a2fa29d0aa748997fbaeb166a1ecc849c.tar.gz |
Merge branch '39727-registry' into 'master'
Replaces vue-resource with axios for registry code
See merge request gitlab-org/gitlab-ce!23545
10 files changed, 166 insertions, 208 deletions
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index 6233fb169e9..9af5660f764 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -1,15 +1,13 @@ <script> import { mapGetters, mapActions } from 'vuex'; import { GlLoadingIcon } from '@gitlab/ui'; -import Flash from '../../flash'; import store from '../stores'; -import collapsibleContainer from './collapsible_container.vue'; -import { errorMessages, errorMessagesTypes } from '../constants'; +import CollapsibleContainer from './collapsible_container.vue'; export default { name: 'RegistryListApp', components: { - collapsibleContainer, + CollapsibleContainer, GlLoadingIcon, }, props: { @@ -26,7 +24,7 @@ export default { this.setMainEndpoint(this.endpoint); }, mounted() { - this.fetchRepos().catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS])); + this.fetchRepos(); }, methods: { ...mapActions(['setMainEndpoint', 'fetchRepos']), @@ -38,9 +36,9 @@ export default { <gl-loading-icon v-if="isLoading" :size="3" /> <collapsible-container - v-for="(item, index) in repos" + v-for="item in repos" v-else-if="!isLoading && repos.length" - :key="index" + :key="item.id" :repo="item" /> diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index 6514c05a9c7..5451c61026c 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -1,22 +1,24 @@ <script> import { mapActions } from 'vuex'; -import { GlLoadingIcon } from '@gitlab/ui'; -import Flash from '../../flash'; -import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; -import tableRegistry from './table_registry.vue'; +import { GlLoadingIcon, GlButton, GlTooltipDirective } from '@gitlab/ui'; +import createFlash from '../../flash'; +import ClipboardButton from '../../vue_shared/components/clipboard_button.vue'; +import Icon from '../../vue_shared/components/icon.vue'; +import TableRegistry from './table_registry.vue'; import { errorMessages, errorMessagesTypes } from '../constants'; import { __ } from '../../locale'; export default { name: 'CollapsibeContainerRegisty', components: { - clipboardButton, - tableRegistry, + ClipboardButton, + TableRegistry, GlLoadingIcon, + GlButton, + Icon, }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, props: { repo: { @@ -29,30 +31,30 @@ export default { isOpen: false, }; }, + computed: { + iconName() { + return this.isOpen ? 'angle-up' : 'angle-right'; + }, + }, methods: { ...mapActions(['fetchRepos', 'fetchList', 'deleteRepo']), - toggleRepo() { this.isOpen = !this.isOpen; if (this.isOpen) { - this.fetchList({ repo: this.repo }).catch(() => - this.showError(errorMessagesTypes.FETCH_REGISTRY), - ); + this.fetchList({ repo: this.repo }); } }, - handleDeleteRepository() { this.deleteRepo(this.repo) .then(() => { - Flash(__('This container registry has been scheduled for deletion.'), 'notice'); + createFlash(__('This container registry has been scheduled for deletion.'), 'notice'); this.fetchRepos(); }) .catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); }, - showError(message) { - Flash(errorMessages[message]); + createFlash(errorMessages[message]); }, }, }; @@ -61,18 +63,9 @@ export default { <template> <div class="container-image"> <div class="container-image-head"> - <button type="button" class="js-toggle-repo btn-link" @click="toggleRepo"> - <i - :class="{ - 'fa-chevron-right': !isOpen, - 'fa-chevron-up': isOpen, - }" - class="fa" - aria-hidden="true" - > - </i> - {{ repo.name }} - </button> + <gl-button class="js-toggle-repo btn-link align-baseline" @click="toggleRepo"> + <icon :name="iconName" /> {{ repo.name }} + </gl-button> <clipboard-button v-if="repo.location" @@ -82,17 +75,17 @@ export default { /> <div class="controls d-none d-sm-block float-right"> - <button + <gl-button v-if="repo.canDelete" - v-tooltip + v-gl-tooltip :title="s__('ContainerRegistry|Remove repository')" :aria-label="s__('ContainerRegistry|Remove repository')" - type="button" - class="js-remove-repo btn btn-danger" + class="js-remove-repo" + variant="danger" @click="handleDeleteRepository" > - <i class="fa fa-trash" aria-hidden="true"> </i> - </button> + <icon name="remove" /> + </gl-button> </div> </div> diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index 6735c3ff7cf..78c7671856a 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -1,21 +1,24 @@ <script> import { mapActions } from 'vuex'; +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { n__ } from '../../locale'; -import Flash from '../../flash'; -import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; -import tablePagination from '../../vue_shared/components/table_pagination.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; +import createFlash from '../../flash'; +import ClipboardButton from '../../vue_shared/components/clipboard_button.vue'; +import TablePagination from '../../vue_shared/components/table_pagination.vue'; +import Icon from '../../vue_shared/components/icon.vue'; import timeagoMixin from '../../vue_shared/mixins/timeago'; import { errorMessages, errorMessagesTypes } from '../constants'; import { numberToHumanSize } from '../../lib/utils/number_utils'; export default { components: { - clipboardButton, - tablePagination, + ClipboardButton, + TablePagination, + GlButton, + Icon, }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, mixins: [timeagoMixin], props: { @@ -31,29 +34,24 @@ export default { }, methods: { ...mapActions(['fetchList', 'deleteRegistry']), - layers(item) { return item.layers ? n__('%d layer', '%d layers', item.layers) : ''; }, - formatSize(size) { return numberToHumanSize(size); }, - handleDeleteRegistry(registry) { this.deleteRegistry(registry) .then(() => this.fetchList({ repo: this.repo })) .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY)); }, - onPageChange(pageNumber) { this.fetchList({ repo: this.repo, page: pageNumber }).catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY), ); }, - showError(message) { - Flash(errorMessages[message]); + createFlash(errorMessages[message]); }, }, }; @@ -71,10 +69,9 @@ export default { </tr> </thead> <tbody> - <tr v-for="(item, i) in repo.list" :key="i"> + <tr v-for="item in repo.list" :key="item.tag"> <td> {{ item.tag }} - <clipboard-button v-if="item.location" :title="item.location" @@ -83,37 +80,34 @@ export default { /> </td> <td> - <span v-tooltip :title="item.revision" data-placement="bottom"> - {{ item.shortRevision }} - </span> + <span v-gl-tooltip.bottom :title="item.revision">{{ item.shortRevision }}</span> </td> <td> {{ formatSize(item.size) }} - <template v-if="item.size && item.layers"> - · - </template> + <template v-if="item.size && item.layers" + >·</template + > {{ layers(item) }} </td> <td> - <span v-tooltip :title="tooltipTitle(item.createdAt)" data-placement="bottom"> - {{ timeFormated(item.createdAt) }} - </span> + <span v-gl-tooltip.bottom :title="tooltipTitle(item.createdAt)">{{ + timeFormated(item.createdAt) + }}</span> </td> <td class="content"> - <button + <gl-button v-if="item.canDelete" - v-tooltip + v-gl-tooltip :title="s__('ContainerRegistry|Remove tag')" :aria-label="s__('ContainerRegistry|Remove tag')" - type="button" - class="js-delete-registry btn btn-danger d-none d-sm-block float-right" - data-container="body" + variant="danger" + class="js-delete-registry d-none d-sm-block float-right" @click="handleDeleteRegistry(item);" > - <i class="fa fa-trash" aria-hidden="true"> </i> - </button> + <icon name="remove" /> + </gl-button> </td> </tr> </tbody> diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js index a78aa90b7b5..51d057c62c1 100644 --- a/app/assets/javascripts/registry/stores/actions.js +++ b/app/assets/javascripts/registry/stores/actions.js @@ -1,39 +1,45 @@ -import Vue from 'vue'; -import VueResource from 'vue-resource'; +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; import * as types from './mutation_types'; - -Vue.use(VueResource); +import { errorMessages, errorMessagesTypes } from '../constants'; export const fetchRepos = ({ commit, state }) => { commit(types.TOGGLE_MAIN_LOADING); - return Vue.http + return axios .get(state.endpoint) - .then(res => res.json()) - .then(response => { + .then(({ data }) => { + commit(types.TOGGLE_MAIN_LOADING); + commit(types.SET_REPOS_LIST, data); + }) + .catch(() => { commit(types.TOGGLE_MAIN_LOADING); - commit(types.SET_REPOS_LIST, response); + createFlash(errorMessages[errorMessagesTypes.FETCH_REPOS]); }); }; export const fetchList = ({ commit }, { repo, page }) => { commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); - return Vue.http.get(repo.tagsPath, { params: { page } }).then(response => { - const { headers } = response; + return axios + .get(repo.tagsPath, { params: { page } }) + .then(response => { + const { headers, data } = response; - return response.json().then(resp => { commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); - commit(types.SET_REGISTRY_LIST, { repo, resp, headers }); + commit(types.SET_REGISTRY_LIST, { repo, resp: data, headers }); + }) + .catch(() => { + commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); + createFlash(errorMessages[errorMessagesTypes.FETCH_REGISTRY]); }); - }); }; // eslint-disable-next-line no-unused-vars -export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath); +export const deleteRepo = ({ commit }, repo) => axios.delete(repo.destroyPath); // eslint-disable-next-line no-unused-vars -export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath); +export const deleteRegistry = ({ commit }, image) => axios.delete(image.destroyPath); export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data); export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING); diff --git a/app/assets/javascripts/registry/stores/index.js b/app/assets/javascripts/registry/stores/index.js index 78b67881210..1bb06bd6e81 100644 --- a/app/assets/javascripts/registry/stores/index.js +++ b/app/assets/javascripts/registry/stores/index.js @@ -3,36 +3,12 @@ import Vuex from 'vuex'; import * as actions from './actions'; import * as getters from './getters'; import mutations from './mutations'; +import createState from './state'; Vue.use(Vuex); export default new Vuex.Store({ - state: { - isLoading: false, - endpoint: '', // initial endpoint to fetch the repos list - /** - * Each object in `repos` has the following strucure: - * { - * name: String, - * isLoading: Boolean, - * tagsPath: String // endpoint to request the list - * destroyPath: String // endpoit to delete the repo - * list: Array // List of the registry images - * } - * - * Each registry image inside `list` has the following structure: - * { - * tag: String, - * revision: String - * shortRevision: String - * size: Number - * layers: Number - * createdAt: String - * destroyPath: String // endpoit to delete each image - * } - */ - repos: [], - }, + state: createState(), actions, getters, mutations, diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js index 69c051cd2d6..1ac699c538f 100644 --- a/app/assets/javascripts/registry/stores/mutations.js +++ b/app/assets/javascripts/registry/stores/mutations.js @@ -48,6 +48,7 @@ export default { [types.TOGGLE_REGISTRY_LIST_LOADING](state, list) { const listToUpdate = state.repos.find(el => el.id === list.id); + listToUpdate.isLoading = !listToUpdate.isLoading; }, }; diff --git a/app/assets/javascripts/registry/stores/state.js b/app/assets/javascripts/registry/stores/state.js new file mode 100644 index 00000000000..feeac10cbe1 --- /dev/null +++ b/app/assets/javascripts/registry/stores/state.js @@ -0,0 +1,26 @@ +export default () => ({ + isLoading: false, + endpoint: '', // initial endpoint to fetch the repos list + /** + * Each object in `repos` has the following strucure: + * { + * name: String, + * isLoading: Boolean, + * tagsPath: String // endpoint to request the list + * destroyPath: String // endpoit to delete the repo + * list: Array // List of the registry images + * } + * + * Each registry image inside `list` has the following structure: + * { + * tag: String, + * revision: String + * shortRevision: String + * size: Number + * layers: Number + * createdAt: String + * destroyPath: String // endpoit to delete each image + * } + */ + repos: [], +}); diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js index 92ff960277a..67118ac03a5 100644 --- a/spec/javascripts/registry/components/app_spec.js +++ b/spec/javascripts/registry/components/app_spec.js @@ -1,37 +1,30 @@ -import _ from 'underscore'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import Vue from 'vue'; import registry from '~/registry/components/app.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { TEST_HOST } from 'spec/test_constants'; import { reposServerResponse } from '../mock_data'; describe('Registry List', () => { + const Component = Vue.extend(registry); let vm; - let Component; + let mock; beforeEach(() => { - Component = Vue.extend(registry); + mock = new MockAdapter(axios); }); afterEach(() => { + mock.restore(); 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' }); - }); + mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, reposServerResponse); - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` }); }); it('should render a list of repos', done => { @@ -64,9 +57,9 @@ describe('Registry List', () => { Vue.nextTick(() => { vm.$el.querySelector('.js-toggle-repo').click(); Vue.nextTick(() => { - expect(vm.$el.querySelector('.js-toggle-repo i').className).toEqual( - 'fa fa-chevron-up', - ); + expect( + vm.$el.querySelector('.js-toggle-repo use').getAttribute('xlink:href'), + ).toContain('angle-up'); done(); }); }); @@ -76,21 +69,10 @@ describe('Registry List', () => { }); 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' }); - }); + mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []); - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` }); }); it('should render empty message', done => { @@ -109,21 +91,10 @@ describe('Registry List', () => { }); 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' }); - }); + mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []); - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` }); }); it('should render a loading spinner', done => { diff --git a/spec/javascripts/registry/components/collapsible_container_spec.js b/spec/javascripts/registry/components/collapsible_container_spec.js index 256a242f784..a3f7ff76dc7 100644 --- a/spec/javascripts/registry/components/collapsible_container_spec.js +++ b/spec/javascripts/registry/components/collapsible_container_spec.js @@ -1,14 +1,24 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import Vue from 'vue'; import collapsibleComponent from '~/registry/components/collapsible_container.vue'; import store from '~/registry/stores'; -import { repoPropsData } from '../mock_data'; +import * as types from '~/registry/stores/mutation_types'; + +import { repoPropsData, registryServerResponse, reposServerResponse } from '../mock_data'; describe('collapsible registry container', () => { let vm; - let Component; + let mock; + const Component = Vue.extend(collapsibleComponent); beforeEach(() => { - Component = Vue.extend(collapsibleComponent); + mock = new MockAdapter(axios); + + mock.onGet(repoPropsData.tagsPath).replyOnce(200, registryServerResponse, {}); + + store.commit(types.SET_REPOS_LIST, reposServerResponse); + vm = new Component({ store, propsData: { @@ -18,24 +28,23 @@ describe('collapsible registry container', () => { }); afterEach(() => { + mock.restore(); vm.$destroy(); }); describe('toggle', () => { it('should be closed by default', () => { expect(vm.$el.querySelector('.container-image-tags')).toBe(null); - expect(vm.$el.querySelector('.container-image-head i').className).toEqual( - 'fa fa-chevron-right', - ); + expect(vm.iconName).toEqual('angle-right'); }); it('should be open when user clicks on closed repo', done => { vm.$el.querySelector('.js-toggle-repo').click(); + Vue.nextTick(() => { - expect(vm.$el.querySelector('.container-image-tags')).toBeDefined(); - expect(vm.$el.querySelector('.container-image-head i').className).toEqual( - 'fa fa-chevron-up', - ); + expect(vm.$el.querySelector('.container-image-tags')).not.toBeNull(); + expect(vm.iconName).toEqual('angle-up'); + done(); }); }); @@ -45,12 +54,12 @@ describe('collapsible registry container', () => { Vue.nextTick(() => { vm.$el.querySelector('.js-toggle-repo').click(); - Vue.nextTick(() => { - expect(vm.$el.querySelector('.container-image-tags')).toBe(null); - expect(vm.$el.querySelector('.container-image-head i').className).toEqual( - 'fa fa-chevron-right', - ); - done(); + setTimeout(() => { + Vue.nextTick(() => { + expect(vm.$el.querySelector('.container-image-tags')).toBe(null); + expect(vm.iconName).toEqual('angle-right'); + done(); + }); }); }); }); @@ -58,7 +67,7 @@ describe('collapsible registry container', () => { describe('delete repo', () => { it('should be possible to delete a repo', () => { - expect(vm.$el.querySelector('.js-remove-repo')).toBeDefined(); + expect(vm.$el.querySelector('.js-remove-repo')).not.toBeNull(); }); }); }); diff --git a/spec/javascripts/registry/stores/actions_spec.js b/spec/javascripts/registry/stores/actions_spec.js index bc4c444655a..c9aa82dba90 100644 --- a/spec/javascripts/registry/stores/actions_spec.js +++ b/spec/javascripts/registry/stores/actions_spec.js @@ -1,42 +1,34 @@ -import Vue from 'vue'; -import VueResource from 'vue-resource'; -import _ from 'underscore'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import * as actions from '~/registry/stores/actions'; import * as types from '~/registry/stores/mutation_types'; +import state from '~/registry/stores/state'; +import { TEST_HOST } from 'spec/test_constants'; import testAction from '../../helpers/vuex_action_helper'; import { - defaultState, reposServerResponse, registryServerResponse, parsedReposServerResponse, } from '../mock_data'; -Vue.use(VueResource); - describe('Actions Registry Store', () => { - let interceptor; let mockedState; + let mock; beforeEach(() => { - mockedState = defaultState; + mockedState = state(); + mockedState.endpoint = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); }); - describe('server requests', () => { - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); - }); + afterEach(() => { + mock.restore(); + }); + describe('server requests', () => { describe('fetchRepos', () => { beforeEach(() => { - interceptor = (request, next) => { - next( - request.respondWith(JSON.stringify(reposServerResponse), { - status: 200, - }), - ); - }; - - Vue.http.interceptors.push(interceptor); + mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, reposServerResponse, {}); }); it('should set receveived repos', done => { @@ -56,23 +48,15 @@ describe('Actions Registry Store', () => { }); describe('fetchList', () => { + let repo; beforeEach(() => { - interceptor = (request, next) => { - next( - request.respondWith(JSON.stringify(registryServerResponse), { - status: 200, - }), - ); - }; + mockedState.repos = parsedReposServerResponse; + [, repo] = mockedState.repos; - Vue.http.interceptors.push(interceptor); + mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {}); }); it('should set received list', done => { - mockedState.repos = parsedReposServerResponse; - - const repo = mockedState.repos[1]; - testAction( actions.fetchList, { repo }, |