[ci skip] First iteration: port haml into vue
This commit is contained in:
parent
23024a70db
commit
51c9f8b6d6
14 changed files with 324 additions and 77 deletions
|
@ -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>
|
|
@ -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}}
|
||||
|
||||
<clipboard-button
|
||||
:text=""
|
||||
:title=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="container-image-tags"
|
||||
:class="{ hide: !isOpen }">
|
||||
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>
|
||||
|
||||
<table class="table tags" v-if="true">
|
||||
<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>
|
||||
|
||||
</div>
|
||||
|
||||
<loading-icon
|
||||
v-if="repo.isLoading"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else-if="!repo.isLoading && isOpen"
|
||||
class="container-image-tags">
|
||||
|
||||
<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>
|
||||
|
|
13
app/assets/javascripts/registry/constants.js
Normal file
13
app/assets/javascripts/registry/constants.js
Normal file
|
@ -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.',
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
}));
|
|
@ -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);
|
||||
|
|
2
app/assets/javascripts/registry/stores/getters.js
Normal file
2
app/assets/javascripts/registry/stores/getters.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const isLoading = state => state.isLoading;
|
||||
export const repos = state => state.repos;
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
.container-image-head {
|
||||
padding: 0 16px;
|
||||
line-height: 4em;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.table.tags {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue