diff options
Diffstat (limited to 'app/assets/javascripts/registry')
-rw-r--r-- | app/assets/javascripts/registry/components/app.vue | 62 | ||||
-rw-r--r-- | app/assets/javascripts/registry/components/collapsible_container.vue | 131 | ||||
-rw-r--r-- | app/assets/javascripts/registry/components/table_registry.vue | 137 | ||||
-rw-r--r-- | app/assets/javascripts/registry/constants.js | 15 | ||||
-rw-r--r-- | app/assets/javascripts/registry/index.js | 25 | ||||
-rw-r--r-- | app/assets/javascripts/registry/stores/actions.js | 39 | ||||
-rw-r--r-- | app/assets/javascripts/registry/stores/getters.js | 2 | ||||
-rw-r--r-- | app/assets/javascripts/registry/stores/index.js | 39 | ||||
-rw-r--r-- | app/assets/javascripts/registry/stores/mutation_types.js | 7 | ||||
-rw-r--r-- | app/assets/javascripts/registry/stores/mutations.js | 54 |
10 files changed, 511 insertions, 0 deletions
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue new file mode 100644 index 00000000000..2d8ca443ea7 --- /dev/null +++ b/app/assets/javascripts/registry/components/app.vue @@ -0,0 +1,62 @@ +<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', + ]), + }, + created() { + this.setMainEndpoint(this.endpoint); + }, + mounted() { + this.fetchRepos() + .catch(() => Flash(errorMessages[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" + /> + + <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 new file mode 100644 index 00000000000..41ea9742406 --- /dev/null +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -0,0 +1,131 @@ +<script> + /* globals Flash */ + import { mapActions } from 'vuex'; + import '../../flash'; + 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'; + import tableRegistry from './table_registry.vue'; + import { errorMessages, errorMessagesTypes } from '../constants'; + + export default { + name: 'collapsibeContainerRegisty', + props: { + repo: { + type: Object, + required: true, + }, + }, + components: { + clipboardButton, + loadingIcon, + tableRegistry, + }, + directives: { + tooltip, + }, + data() { + return { + isOpen: false, + }; + }, + computed: { + clipboardText() { + return `docker pull ${this.repo.location}`; + }, + }, + methods: { + ...mapActions([ + 'fetchRepos', + 'fetchList', + 'deleteRepo', + ]), + + toggleRepo() { + this.isOpen = !this.isOpen; + + if (this.isOpen) { + this.fetchList({ repo: this.repo }) + .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY)); + } + }, + + handleDeleteRepository() { + this.deleteRepo(this.repo) + .then(() => this.fetchRepos()) + .catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); + }, + + showError(message) { + Flash((errorMessages[message])); + }, + }, + }; +</script> + +<template> + <div class="container-image"> + <div + class="container-image-head"> + <button + type="button" + @click="toggleRepo" + class="js-toggle-repo btn-link"> + <i + class="fa" + :class="{ + 'fa-chevron-right': !isOpen, + 'fa-chevron-up': isOpen, + }" + aria-hidden="true"> + </i> + {{repo.name}} + </button> + + <clipboard-button + v-if="repo.location" + :text="clipboardText" + :title="repo.location" + /> + + <div class="controls hidden-xs pull-right"> + <button + v-if="repo.canDelete" + type="button" + class="js-remove-repo btn btn-danger" + :title="s__('ContainerRegistry|Remove repository')" + :aria-label="s__('ContainerRegistry|Remove repository')" + v-tooltip + @click="handleDeleteRepository"> + <i + class="fa fa-trash" + aria-hidden="true"> + </i> + </button> + </div> + + </div> + + <loading-icon + v-if="repo.isLoading" + class="append-bottom-20" + size="2" + /> + + <div + v-else-if="!repo.isLoading && isOpen" + class="container-image-tags"> + + <table-registry + v-if="repo.list.length" + :repo="repo" + /> + + <div + v-else + class="nothing-here-block"> + {{s__("ContainerRegistry|No tags in Container Registry for this container image.")}} + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue new file mode 100644 index 00000000000..4ce1571b0aa --- /dev/null +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -0,0 +1,137 @@ +<script> + /* globals Flash */ + import { mapActions } from 'vuex'; + import { n__ } from '../../locale'; + import '../../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 timeagoMixin from '../../vue_shared/mixins/timeago'; + import { errorMessages, errorMessagesTypes } from '../constants'; + + export default { + props: { + repo: { + type: Object, + required: true, + }, + }, + components: { + clipboardButton, + tablePagination, + }, + mixins: [ + timeagoMixin, + ], + directives: { + tooltip, + }, + computed: { + shouldRenderPagination() { + return this.repo.pagination.total > this.repo.pagination.perPage; + }, + }, + methods: { + ...mapActions([ + 'fetchList', + 'deleteRegistry', + ]), + + layers(item) { + return item.layers ? n__('%d layer', '%d layers', item.layers) : ''; + }, + + 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)); + }, + + clipboardText(text) { + return `docker pull ${text}`; + }, + + showError(message) { + Flash((errorMessages[message])); + }, + }, + }; +</script> +<template> +<div> + <table class="table tags"> + <thead> + <tr> + <th>{{s__('ContainerRegistry|Tag')}}</th> + <th>{{s__('ContainerRegistry|Tag ID')}}</th> + <th>{{s__("ContainerRegistry|Size")}}</th> + <th>{{s__("ContainerRegistry|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="clipboardText(item.location)" + /> + </td> + <td> + <span + v-tooltip + :title="item.revision" + data-placement="bottom"> + {{item.shortRevision}} + </span> + </td> + <td> + {{item.size}} + <template v-if="item.size && item.layers"> + · + </template> + {{layers(item)}} + </td> + + <td> + {{timeFormated(item.createdAt)}} + </td> + + <td class="content"> + <button + v-if="item.canDelete" + type="button" + class="js-delete-registry btn btn-danger hidden-xs pull-right" + :title="s__('ContainerRegistry|Remove tag')" + :aria-label="s__('ContainerRegistry|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" + /> +</div> +</template> diff --git a/app/assets/javascripts/registry/constants.js b/app/assets/javascripts/registry/constants.js new file mode 100644 index 00000000000..712b0fade3d --- /dev/null +++ b/app/assets/javascripts/registry/constants.js @@ -0,0 +1,15 @@ +import { __ } from '../locale'; + +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 projects.'), + [errorMessagesTypes.DELETE_REPO]: __('Something went wrong on our end.'), + [errorMessagesTypes.DELETE_REGISTRY]: __('Something went wrong on our end.'), +}; diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js new file mode 100644 index 00000000000..d8edff73f72 --- /dev/null +++ b/app/assets/javascripts/registry/index.js @@ -0,0 +1,25 @@ +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', + 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 new file mode 100644 index 00000000000..34ed40b8b65 --- /dev/null +++ b/app/assets/javascripts/registry/stores/actions.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; +import * as types from './mutation_types'; + +Vue.use(VueResource); + +export const fetchRepos = ({ commit, state }) => { + commit(types.TOGGLE_MAIN_LOADING); + + return Vue.http.get(state.endpoint) + .then(res => res.json()) + .then((response) => { + commit(types.TOGGLE_MAIN_LOADING); + commit(types.SET_REPOS_LIST, response); + }); +}; + +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.headers; + + return response.json().then((resp) => { + commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); + commit(types.SET_REGISTRY_LIST, { repo, resp, headers }); + }); + }); +}; + +export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath) + .then(res => res.json()); + +export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath) + .then(res => res.json()); + +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/getters.js b/app/assets/javascripts/registry/stores/getters.js new file mode 100644 index 00000000000..588f479c492 --- /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; diff --git a/app/assets/javascripts/registry/stores/index.js b/app/assets/javascripts/registry/stores/index.js new file mode 100644 index 00000000000..78b67881210 --- /dev/null +++ b/app/assets/javascripts/registry/stores/index.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; + +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: [], + }, + actions, + getters, + mutations, +}); diff --git a/app/assets/javascripts/registry/stores/mutation_types.js b/app/assets/javascripts/registry/stores/mutation_types.js new file mode 100644 index 00000000000..2c69bf11807 --- /dev/null +++ b/app/assets/javascripts/registry/stores/mutation_types.js @@ -0,0 +1,7 @@ +export const SET_MAIN_ENDPOINT = 'SET_MAIN_ENDPOINT'; + +export const SET_REPOS_LIST = 'SET_REPOS_LIST'; +export const TOGGLE_MAIN_LOADING = 'TOGGLE_MAIN_LOADING'; + +export const SET_REGISTRY_LIST = 'SET_REGISTRY_LIST'; +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 new file mode 100644 index 00000000000..e40382e7afc --- /dev/null +++ b/app/assets/javascripts/registry/stores/mutations.js @@ -0,0 +1,54 @@ +import * as types from './mutation_types'; +import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils'; + +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 => ({ + canDelete: !!el.destroy_path, + destroyPath: el.destroy_path, + id: el.id, + isLoading: false, + list: [], + location: el.location, + name: el.path, + tagsPath: el.tags_path, + })), + }); + }, + + [types.TOGGLE_MAIN_LOADING](state) { + Object.assign(state, { isLoading: !state.isLoading }); + }, + + [types.SET_REGISTRY_LIST](state, { repo, resp, headers }) { + const listToUpdate = state.repos.find(el => el.id === repo.id); + + 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, + size: element.size, + layers: element.layers, + location: element.location, + createdAt: element.created_at, + destroyPath: element.destroy_path, + canDelete: !!element.destroy_path, + })); + }, + + [types.TOGGLE_REGISTRY_LIST_LOADING](state, list) { + const listToUpdate = state.repos.find(el => el.id === list.id); + listToUpdate.isLoading = !listToUpdate.isLoading; + }, +}; |