diff options
14 files changed, 322 insertions, 75 deletions
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index e69de29bb2d..17a57ae248d 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -0,0 +1,90 @@ +<script> + /* globals Flash */ + import { mapGetters, mapActions } from 'vuex'; + import '../../flash'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import store from '../stores'; + import collapsibleContainer from './collapsible_container.vue'; + import { errorMessages, errorMessagesTypes } from '../constants'; + + export default { + name: 'registryListApp', + props: { + endpoint: { + type: String, + required: true + }, + }, + store, + components: { + collapsibleContainer, + loadingIcon, + }, + computed: { + ...mapGetters([ + 'isLoading', + 'repos', + ]), + }, + methods: { + ...mapActions([ + 'setMainEndpoint', + 'fetchRepos', + 'fetchList', + 'deleteRepo', + 'deleteRegistry', + 'toggleIsLoading', + ]), + + fetchRegistryList(repo) { + this.fetchList(repo) + .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY)) + }, + + deleteRegistry(repo, registry) { + this.deleteRegistry(registry) + .then(() => this.fetchRegistry(repo)) + .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY)); + }, + + deleteRepository(repo) { + this.deleteRepo(repo) + .then(() => this.fetchRepo()) + .catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); + }, + + showError(message){ + Flash(__(errorMessages[message])); + } + }, + created() { + this.setMainEndpoint(this.endpoint); + }, + mounted() { + this.fetchRepos() + .catch(() => this.showError(errorMessagesTypes.FETCH_REPOS)); + } + }; +</script> +<template> + <div> + <loading-icon + v-if="isLoading" + size="3" + /> + + <collapsible-container + v-else-if="!isLoading && repos.length" + v-for="(item, index) in repos" + :key="index" + :repo="item" + @fetchRegistryList="fetchRegistryList" + @deleteRepository="deleteRepository" + @deleteRegistry="deleteRegistry" + /> + + <p v-else-if="!isLoading && !repos.length"> + {{__("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 20ebedd2b45..6be2aa60ebd 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -1,24 +1,22 @@ <script> import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; export default { name: 'collapsibeContainerRegisty', props: { - title: { - type: String, - required: true, - }, - clipboardContent: { - type: String, - required: true, - }, - repoData: { + repo: { type: Object, required: true, }, }, components: { clipboardButton, + loadingIcon, + }, + directives: { + tooltip, }, data() { return { @@ -26,37 +24,73 @@ }; }, methods: { - itemSize(item) { + layers(item) { const pluralize = gl.text.pluralize('layer', item.layers); - return `${item.size}·${item.layers}${pluralize}`; - } - } - } + return `${item.layers} ${pluralize}`; + }, + toggleRepo() { + if (this.isOpen === false) { + // consider not fetching data the second time it is toggled? :fry: + this.$emit('fetchRegistryList', this.repo); + } + this.isOpen = !this.isOpen; + }, + handleDeleteRepository() { + this.$emit('deleteRepository', this.repo) + }, + handleDeleteRegistry(registry) { + this.$emit('deleteRegistry', this.repo, registry); + }, + }, + }; </script> <template> <div class="container-image"> - <div class="container-image-head"> - <i - class="fa" - :class="{ - 'chevron-left': !isOpen, - 'chevron-up': isOpen, - }" - aria-hidden="true"> - </i> - {{title}} + <div + class="container-image-head"> + <a + role="button" + @click="toggleRepo"> + <i + class="fa" + :class="{ + 'fa-chevron-right': !isOpen, + 'fa-chevron-up': isOpen, + }" + aria-hidden="true"> + </i> + {{repo.name}} + </a> + + <clipboard-button text="foo" title="bar" /> + + <div class="controls hidden-xs pull-right"> + <button + v-if="repo.canDelete" + type="button" + class="btn btn-remove" + :title="__('Remove repository')" + v-tooltip + @click="handleDeleteRepository"> + <i + class="fa fa-trash" + aria-hidden="true"> + </i> + </button> + </div> - <clipboard-button - :text="" - :title="" - /> </div> + + <loading-icon + v-if="repo.isLoading" + /> + <div - class="container-image-tags" - :class="{ hide: !isOpen }"> + v-else-if="!repo.isLoading && isOpen" + class="container-image-tags"> - <table class="table tags" v-if="true"> + <table class="table tags" v-if="repo.list.length"> <thead> <tr> <th>{{__("Tag")}}</th> @@ -71,23 +105,28 @@ v-for="(item, i) in repo.list" :key="i"> <td> - {{item.name}} + + {{item.tag}} + <clipboard-button - :title="item.location" - :text="item.location" + :title="item.tag" + :text="item.tag" /> </td> <td> <span v-tooltip :title="item.revision" + data-placement="bottom" > {{item.shortRevision}} </span> </td> <td> <template v-if="item.size"> - {{itemSize(item)}} + {{item.size}} + · + {{layers(item)}} </template> <div v-else class="light"> \- @@ -103,18 +142,20 @@ </div> </td> - <td> - <button - type="button" - class="btn btn-remove" - title="Remove tag" - v-tooltip - @click="deleteTag(item)"> - <i - class="fa fa-trash cred" - aria-hidden="true"> - </i> - </button> + <td class="content"> + <div class="controls hidden-xs pull-right"> + <button + type="button" + class="btn btn-remove" + title="Remove tag" + v-tooltip + @click="handleDeleteRegistry(item)"> + <i + class="fa fa-trash" + aria-hidden="true"> + </i> + </button> + </div> </td> </tr> </tbody> @@ -123,9 +164,7 @@ v-else class="nothing-here-block"> {{__("No tags in Container Registry for this container image.")}} - </div> </div> - </div> </template> diff --git a/app/assets/javascripts/registry/constants.js b/app/assets/javascripts/registry/constants.js new file mode 100644 index 00000000000..d3de6441dae --- /dev/null +++ b/app/assets/javascripts/registry/constants.js @@ -0,0 +1,13 @@ +export const errorMessagesTypes = { + FETCH_REGISTRY: 'FETCH_REGISTRY', + FETCH_REPOS: 'FETCH_REPOS', + DELETE_REPO: 'DELETE_REPO', + DELETE_REGISTRY: 'DELETE_REGISTRY', +}; + +export const errorMessages = { + [errorMessagesTypes.FETCH_REGISTRY]: 'Something went wrong while fetching the registry list.', + [errorMessagesTypes.FETCH_REPOS]: 'Something went wrong while fetching the repositories.', + [errorMessagesTypes.DELETE_REPO]: 'Something went wrong while deleting the repository.', + [errorMessagesTypes.DELETE_REGISTRY]: 'Something went wrong while deleting registry.', +}; diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js index e69de29bb2d..4f7895897b2 100644 --- a/app/assets/javascripts/registry/index.js +++ b/app/assets/javascripts/registry/index.js @@ -0,0 +1,25 @@ +import Vue from 'vue'; +import Translate from '../vue_shared/translate'; +import registryApp from './components/app.vue'; + +// Vue.use(Translate); + +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '#js-vue-registry-images', + components: { + registryApp, + }, + data() { + const dataset = document.querySelector(this.$options.el).dataset; + return { + endpoint: dataset.endpoint, + }; + }, + render(createElement) { + return createElement('registry-app', { + props: { + endpoint: this.endpoint, + }, + }); + }, +})); diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js index 6c0286e2be6..5dda16b8d9a 100644 --- a/app/assets/javascripts/registry/stores/actions.js +++ b/app/assets/javascripts/registry/stores/actions.js @@ -16,13 +16,13 @@ export const fetchRepos = ({ commit, state }) => { }; export const fetchList = ({ commit }, list) => { - commit(types.TOGGLE_IMAGE_LOADING, list); + commit(types.TOGGLE_REGISTRY_LIST_LOADING, list); return Vue.http.get(list.path) .then(res => res.json()) .then((response) => { - commit(types.TOGGLE_IMAGE_LOADING, list); - commit(types.SET_IMAGES_LIST, list, response); + commit(types.TOGGLE_REGISTRY_LIST_LOADING, list); + commit(types.SET_REGISTRY_LIST, list, response); }); }; @@ -32,8 +32,11 @@ export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.path) commit(types.DELETE_REPO, repo); }); -export const deleteImage = ({ commit }, image) => Vue.http.delete(image.path) +export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.path) .then(res => res.json()) .then(() => { commit(types.DELETE_IMAGE, image); }); + +export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data); +export const toggleIsLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING); diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/stores/getters.js new file mode 100644 index 00000000000..6c6ed0cd738 --- /dev/null +++ b/app/assets/javascripts/registry/stores/getters.js @@ -0,0 +1,2 @@ +export const isLoading = state => state.isLoading; +export const repos = state => state.repos;
\ No newline at end of file diff --git a/app/assets/javascripts/registry/stores/index.js b/app/assets/javascripts/registry/stores/index.js index 6cf9df57f08..78b67881210 100644 --- a/app/assets/javascripts/registry/stores/index.js +++ b/app/assets/javascripts/registry/stores/index.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import actions from './actions'; +import * as actions from './actions'; +import * as getters from './getters'; import mutations from './mutations'; Vue.use(Vuex); @@ -31,7 +32,8 @@ export default new Vuex.Store({ * } */ repos: [], - actions, - mutations, }, + actions, + getters, + mutations, }); diff --git a/app/assets/javascripts/registry/stores/mutation_types.js b/app/assets/javascripts/registry/stores/mutation_types.js index fb4e24e10e3..aece401a24a 100644 --- a/app/assets/javascripts/registry/stores/mutation_types.js +++ b/app/assets/javascripts/registry/stores/mutation_types.js @@ -1,9 +1,10 @@ +export const SET_MAIN_ENDPOINT = 'SET_MAIN_ENDPOINT'; export const FETCH_REPOS_LIST = 'FETCH_REPOS_LIST'; export const DELETE_REPO = 'DELETE_REPO'; export const SET_REPOS_LIST = 'SET_REPOS_LIST'; export const TOGGLE_MAIN_LOADING = 'TOGGLE_MAIN_LOADING'; export const FETCH_IMAGES_LIST = 'FETCH_IMAGES_LIST'; -export const SET_IMAGES_LIST = 'SET_IMAGES_LIST'; +export const SET_REGISTRY_LIST = 'SET_REGISTRY_LIST'; export const DELETE_IMAGE = 'DELETE_IMAGE'; -export const TOGGLE_IMAGE_LOADING = 'TOGGLE_MAIN_LOADING'; +export const TOGGLE_REGISTRY_LIST_LOADING = 'TOGGLE_REGISTRY_LIST_LOADING'; diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js index 5fa41fb5255..796548bffec 100644 --- a/app/assets/javascripts/registry/stores/mutations.js +++ b/app/assets/javascripts/registry/stores/mutations.js @@ -1,14 +1,22 @@ import * as types from './mutation_types'; export default { + + [types.SET_MAIN_ENDPOINT](state, endpoint) { + Object.assign(state, { endpoint }); + }, + [types.SET_REPOS_LIST](state, list) { Object.assign(state, { repos: list.map(el => ({ - name: el.name, - isLoading: false, canDelete: !!el.destroy_path, destroyPath: el.destroy_path, + isLoading: false, list: [], + location: el.location, + name: el.name, + tagsPath: el.tags_path, + id: el.id, })), }); }, @@ -17,8 +25,29 @@ export default { Object.assign(state, { isLoading: !state.isLoading }); }, - [types.SET_IMAGES_LIST](state, image, list) { - const listToUpdate = state.repos.find(el => el.name === image.name); + [types.SET_REGISTRY_LIST](state, repo, list) { + // mock + list = [ + { + name: 'centos6', + short_revision: '0b6091a66', + revision: '0b6091a665af68bbbbb36a3e088ec3cd6f35389deebf6d4617042d56722d76fb', + size: 706, + layers: 19, + created_at: 1505828744434, + }, + { + name: 'centos7', + short_revision: 'b118ab5b0', + revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43', + size: 679, + layers: 19, + created_at: 1505828744434, + }, + ]; + + const listToUpdate = state.repos.find(el => el.id === repo.id); + listToUpdate.list = list.map(element => ({ tag: element.name, revision: element.revision, @@ -31,8 +60,8 @@ export default { })); }, - [types.TOGGLE_IMAGE_LOADING](state, image) { - const listToUpdate = state.repos.find(el => el.name === image.name); + [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/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue index cb9ad4c7dee..fbf7233b13d 100644 --- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -14,11 +14,11 @@ }, }, mounted() { - return new Clipboard(this.$refs.btn, { - text: () => { - return this.text; - }, - }); + // return new Clipboard(this.$refs.btn, { + // text: () => { + // return this.text; + // }, + // }); } }; </script> diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss index 3266714396e..089a693efe4 100644 --- a/app/assets/stylesheets/pages/container_registry.scss +++ b/app/assets/stylesheets/pages/container_registry.scss @@ -9,6 +9,10 @@ .container-image-head { padding: 0 16px; line-height: 4em; + + &:hover { + text-decoration: underline; + } } .table.tags { diff --git a/app/controllers/projects/registry/repositories_controller.rb b/app/controllers/projects/registry/repositories_controller.rb index 71e7dc70a4d..89093e4172a 100644 --- a/app/controllers/projects/registry/repositories_controller.rb +++ b/app/controllers/projects/registry/repositories_controller.rb @@ -6,6 +6,37 @@ module Projects def index @images = project.container_repositories + + respond_to do |format| + format.html + format.json do + # render json: @images + render json: [ + { + name: 'gitlab-org/omnibus-gitlab/foo', + tags_path: 'foo', + destroy_path: 'bar', + location: 'foo', + id: '134', + destroy_path: 'bar' + }, + { + name: 'gitlab-org/omnibus-gitlab', + tags_path: 'foo', + destroy_path: 'bar', + location: 'foo', + id: '123', + }, + { + name: 'gitlab-org/omnibus-gitlab/bar', + tags_path: 'foo', + destroy_path: 'bar', + location: 'foo', + id: '973', + } + ] + end + end end def destroy diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index 5661af01302..ab263091c1f 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -52,9 +52,15 @@ #{escape_once(@project.container_registry_url)}/optional-image-name:tag #{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag - - if @images.blank? - %p.settings-message.text-center.append-bottom-default - No container images stored for this project. Add one by following the - instructions above. - - else - = render partial: 'image', collection: @images + #js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json)}} + + = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('registry_list') + + + -# - if @images.blank? + -# %p.settings-message.text-center.append-bottom-default + -# No container images stored for this project. Add one by following the + -# instructions above. + -# - else + -# = render partial: 'image', collection: @images diff --git a/config/webpack.config.js b/config/webpack.config.js index 6b0cd023291..4a6c876906b 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -67,6 +67,7 @@ var config = { prometheus_metrics: './prometheus_metrics', protected_branches: './protected_branches', protected_tags: './protected_tags', + registry_list: './registry/index.js', repo: './repo/index.js', sidebar: './sidebar/sidebar_bundle.js', schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js', @@ -199,6 +200,7 @@ var config = { 'pdf_viewer', 'pipelines', 'pipelines_details', + 'registry_list', 'repo', 'schedule_form', 'schedules_index', |