From f5c38eccbe4c8f554f7f112b0233722dbb2c3628 Mon Sep 17 00:00:00 2001 From: Markus Koller Date: Tue, 19 Sep 2017 16:32:52 +0200 Subject: [PATCH 01/57] Add link to OpenID Connect documentation --- changelogs/unreleased/docs-openid-connect.yml | 5 +++++ doc/topics/authentication/index.md | 1 + 2 files changed, 6 insertions(+) create mode 100644 changelogs/unreleased/docs-openid-connect.yml diff --git a/changelogs/unreleased/docs-openid-connect.yml b/changelogs/unreleased/docs-openid-connect.yml new file mode 100644 index 00000000000..3989ec53cfa --- /dev/null +++ b/changelogs/unreleased/docs-openid-connect.yml @@ -0,0 +1,5 @@ +--- +title: Add link to OpenID Connect documentation +merge_request: 14368 +author: Markus Koller +type: other diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md index fac91935a45..597c98fbf6b 100644 --- a/doc/topics/authentication/index.md +++ b/doc/topics/authentication/index.md @@ -11,6 +11,7 @@ This page gathers all the resources for the topic **Authentication** within GitL - [Security Webcast with Yubico](https://about.gitlab.com/2016/08/31/gitlab-and-yubico-security-webcast/) - **Integrations:** - [GitLab as OAuth2 authentication service provider](../../integration/oauth_provider.md#introduction-to-oauth) + - [GitLab as OpenID Connect identity provider](../../integration/openid_connect_provider.md) ## GitLab administrators From 13c86bb4ad7a858799440002fa3451e3513167a9 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 15 Sep 2017 14:10:08 +0100 Subject: [PATCH 02/57] Init store for registry --- .../javascripts/registry/components/app.vue | 0 .../components/collapsible_container.vue | 42 +++++++++++++++++++ .../registry/components/registry_table.vue | 0 app/assets/javascripts/registry/index.js | 0 .../javascripts/registry/stores/actions.js | 39 +++++++++++++++++ .../javascripts/registry/stores/index.js | 37 ++++++++++++++++ .../registry/stores/mutation_types.js | 9 ++++ .../javascripts/registry/stores/mutations.js | 38 +++++++++++++++++ .../components/clipboard_button.vue | 40 ++++++++++++++++++ 9 files changed, 205 insertions(+) create mode 100644 app/assets/javascripts/registry/components/app.vue create mode 100644 app/assets/javascripts/registry/components/collapsible_container.vue create mode 100644 app/assets/javascripts/registry/components/registry_table.vue create mode 100644 app/assets/javascripts/registry/index.js create mode 100644 app/assets/javascripts/registry/stores/actions.js create mode 100644 app/assets/javascripts/registry/stores/index.js create mode 100644 app/assets/javascripts/registry/stores/mutation_types.js create mode 100644 app/assets/javascripts/registry/stores/mutations.js create mode 100644 app/assets/javascripts/vue_shared/components/clipboard_button.vue diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue new file mode 100644 index 00000000000..e69de29bb2d 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..7014562be08 --- /dev/null +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -0,0 +1,42 @@ + + + diff --git a/app/assets/javascripts/registry/components/registry_table.vue b/app/assets/javascripts/registry/components/registry_table.vue new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js new file mode 100644 index 00000000000..6c0286e2be6 --- /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 }, list) => { + commit(types.TOGGLE_IMAGE_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); + }); +}; + +export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.path) + .then(res => res.json()) + .then(() => { + commit(types.DELETE_REPO, repo); + }); + +export const deleteImage = ({ commit }, image) => Vue.http.delete(image.path) + .then(res => res.json()) + .then(() => { + commit(types.DELETE_IMAGE, image); + }); diff --git a/app/assets/javascripts/registry/stores/index.js b/app/assets/javascripts/registry/stores/index.js new file mode 100644 index 00000000000..6cf9df57f08 --- /dev/null +++ b/app/assets/javascripts/registry/stores/index.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import actions from './actions'; +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, + 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..fb4e24e10e3 --- /dev/null +++ b/app/assets/javascripts/registry/stores/mutation_types.js @@ -0,0 +1,9 @@ +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 DELETE_IMAGE = 'DELETE_IMAGE'; +export const TOGGLE_IMAGE_LOADING = 'TOGGLE_MAIN_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..5fa41fb5255 --- /dev/null +++ b/app/assets/javascripts/registry/stores/mutations.js @@ -0,0 +1,38 @@ +import * as types from './mutation_types'; + +export default { + [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, + list: [], + })), + }); + }, + + [types.TOGGLE_MAIN_LOADING](state) { + Object.assign(state, { isLoading: !state.isLoading }); + }, + + [types.SET_IMAGES_LIST](state, image, list) { + const listToUpdate = state.repos.find(el => el.name === image.name); + listToUpdate.list = list.map(element => ({ + tag: element.name, + revision: element.revision, + shortRevision: element.short_revision, + size: element.size, + layers: element.layers, + createdAt: element.created_at, + destroyPath: element.destroy_path, + canDelete: !!element.destroy_path, + })); + }, + + [types.TOGGLE_IMAGE_LOADING](state, image) { + const listToUpdate = state.repos.find(el => el.name === image.name); + 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 new file mode 100644 index 00000000000..ebb3dbd0112 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -0,0 +1,40 @@ + +import Clipboard from 'vendor/clipboard'; + + + + From 23024a70dbc50dbd0114ed715c906cac1a9b1d59 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 15 Sep 2017 17:05:46 +0100 Subject: [PATCH 03/57] Adds clipboard button component --- .../components/collapsible_container.vue | 119 +++++++++++++++--- .../registry/components/registry_table.vue | 0 .../components/clipboard_button.vue | 5 +- 3 files changed, 106 insertions(+), 18 deletions(-) delete mode 100644 app/assets/javascripts/registry/components/registry_table.vue diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index 7014562be08..20ebedd2b45 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -1,18 +1,24 @@ diff --git a/app/assets/javascripts/registry/components/registry_table.vue b/app/assets/javascripts/registry/components/registry_table.vue deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue index ebb3dbd0112..cb9ad4c7dee 100644 --- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -1,7 +1,6 @@ - -import Clipboard from 'vendor/clipboard'; - + 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 @@ 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; + // }, + // }); } }; 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', From 6c63520ef5735e56749c77b495f8137a20942504 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 19 Sep 2017 17:53:57 +0100 Subject: [PATCH 05/57] Removes 2 column layout. Adds i18n support --- .../registry/repositories/index.html.haml | 104 ++++++++---------- 1 file changed, 48 insertions(+), 56 deletions(-) diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index ab263091c1f..4a76431494c 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -1,66 +1,58 @@ - page_title "Container Registry" -.row.prepend-top-default.append-bottom-default - .col-lg-3 - %h4.prepend-top-0 +%section + .settings-header + %h4 = page_title %p - With the Docker Container Registry integrated into GitLab, every project - can have its own space to store its Docker images. + = _('With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images.') %p.append-bottom-0 = succeed '.' do Learn more about - = link_to 'Container Registry', help_page_path('user/project/container_registry'), target: '_blank' + = link_to _('Container Registry'), help_page_path('user/project/container_registry'), target: '_blank' + .row + .col-lg-12 + .panel.panel-default + .panel-heading + %h4.panel-title + = _('How to use the Container Registry') + .panel-body + %p + = _('First log in to GitLab’s Container Registry using your GitLab username and password. If you have') + = link_to _('2FA enabled'), help_page_path('user/profile/account/two_factor_authentication'), target: '_blank' + you need to use a + = succeed ':' do + = link_to _('personal access token'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank' + %pre + docker login #{Gitlab.config.registry.host_port} + %br + %p + = _("Once you log in, you’re free to create and upload a container image using the common") + %code + = _('build') + = _('and') + %code push + = _('commands:') + %pre + :plain + docker build -t #{escape_once(@project.container_registry_url)} . + docker push #{escape_once(@project.container_registry_url)} - .col-lg-9 - .panel.panel-default - .panel-heading - %h4.panel-title - How to use the Container Registry - .panel-body - %p - First log in to GitLab’s Container Registry using your GitLab username - and password. If you have - = link_to '2FA enabled', help_page_path('user/profile/account/two_factor_authentication'), target: '_blank' - you need to use a - = succeed ':' do - = link_to 'personal access token', help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank' - %pre - docker login #{Gitlab.config.registry.host_port} - %br - %p - Once you log in, you’re free to create and upload a container image - using the common - %code build - and - %code push - commands: - %pre - :plain - docker build -t #{escape_once(@project.container_registry_url)} . - docker push #{escape_once(@project.container_registry_url)} - - %hr - %h5.prepend-top-default - Use different image names - %p.light - GitLab supports up to 3 levels of image names. The following - examples of images are valid for your project: - %pre - :plain - #{escape_once(@project.container_registry_url)}:tag - #{escape_once(@project.container_registry_url)}/optional-image-name:tag - #{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag - - #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') + %hr + %h5.prepend-top-default + = _('Use different image names') + %p.light + = _('GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:') + %pre + :plain + #{escape_once(@project.container_registry_url)}:tag + #{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 + .row + .col-lg-12 + #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') From 06b31461f34bac86d31d898e4f0e5b573d6b0345 Mon Sep 17 00:00:00 2001 From: Simon Knox Date: Mon, 11 Sep 2017 21:47:33 +1000 Subject: [PATCH 06/57] improve merge request widget status icon UX x to indicate failure or cannot merge --- .../states/mr_widget_ready_to_merge.js | 32 ++++++-- .../states/mr_widget_ready_to_merge_spec.js | 79 +++++++++++++++---- 2 files changed, 88 insertions(+), 23 deletions(-) diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js index ad709da51ee..a0aba7718d4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js @@ -38,24 +38,40 @@ export default { return this.useCommitMessageWithDescription ? withoutDesc : withDesc; }, + status() { + const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr; + + if (hasCI && !ciStatus) { + return 'failed'; + } else if (!pipeline) { + return 'success'; + } else if (isPipelineActive) { + return 'pending'; + } else if (isPipelineFailed) { + return 'failed'; + } + + return 'success'; + }, mergeButtonClass() { const defaultClass = 'btn btn-sm btn-success accept-merge-request'; const failedClass = `${defaultClass} btn-danger`; const inActionClass = `${defaultClass} btn-info`; - const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr; - if (hasCI && !ciStatus) { + if (this.status === 'failed') { return failedClass; - } else if (!pipeline) { - return defaultClass; - } else if (isPipelineActive) { + } else if (this.status === 'pending') { return inActionClass; - } else if (isPipelineFailed) { - return failedClass; } return defaultClass; }, + iconClass() { + if (this.status === 'failed' || !this.commitMessage.length || !this.isMergeAllowed() || this.mr.preventMerge) { + return 'failed'; + } + return 'success'; + }, mergeButtonText() { if (this.isMergingImmediately) { return 'Merge in progress'; @@ -208,7 +224,7 @@ export default { }, template: `
- +
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 03a52f1f91c..b0511c25929 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -95,38 +95,87 @@ describe('MRWidgetReadyToMerge', () => { }); }); + describe('status', () => { + it('defaults to success', () => { + vm.mr.pipeline = true; + expect(vm.status).toEqual('success'); + }); + + it('returns failed when MR has CI but also has an unknown status', () => { + vm.mr.hasCI = true; + expect(vm.status).toEqual('failed'); + }); + + it('returns default when MR has no pipeline', () => { + expect(vm.status).toEqual('success'); + }); + + it('returns pending when pipeline is active', () => { + vm.mr.pipeline = {}; + vm.mr.isPipelineActive = true; + expect(vm.status).toEqual('pending'); + }); + + it('returns failed when pipeline is failed', () => { + vm.mr.pipeline = {}; + vm.mr.isPipelineFailed = true; + expect(vm.status).toEqual('failed'); + }); + }); + describe('mergeButtonClass', () => { const defaultClass = 'btn btn-sm btn-success accept-merge-request'; const failedClass = `${defaultClass} btn-danger`; const inActionClass = `${defaultClass} btn-info`; - it('should return default class', () => { + it('defaults to success class', () => { + expect(vm.mergeButtonClass).toEqual(defaultClass); + }); + + it('returns success class for success status', () => { vm.mr.pipeline = true; expect(vm.mergeButtonClass).toEqual(defaultClass); }); - it('should return failed class when MR has CI but also has an unknown status', () => { - vm.mr.hasCI = true; - expect(vm.mergeButtonClass).toEqual(failedClass); - }); - - it('should return default class when MR has no pipeline', () => { - expect(vm.mergeButtonClass).toEqual(defaultClass); - }); - - it('should return in action class when pipeline is active', () => { + it('returns info class for pending status', () => { vm.mr.pipeline = {}; vm.mr.isPipelineActive = true; expect(vm.mergeButtonClass).toEqual(inActionClass); }); - it('should return failed class when pipeline is failed', () => { - vm.mr.pipeline = {}; - vm.mr.isPipelineFailed = true; + it('returns failed class for failed status', () => { + vm.mr.hasCI = true; expect(vm.mergeButtonClass).toEqual(failedClass); }); }); + describe('status icon', () => { + it('defaults to tick icon', () => { + expect(vm.iconClass).toEqual('success'); + }); + + it('shows tick for success status', () => { + vm.mr.pipeline = true; + expect(vm.iconClass).toEqual('success'); + }); + + it('shows tick for pending status', () => { + vm.mr.pipeline = {}; + vm.mr.isPipelineActive = true; + expect(vm.iconClass).toEqual('success'); + }); + + it('shows x for failed status', () => { + vm.mr.hasCI = true; + expect(vm.iconClass).toEqual('failed'); + }); + + it('shows x for merge not allowed', () => { + vm.mr.hasCI = true; + expect(vm.iconClass).toEqual('failed'); + }); + }); + describe('mergeButtonText', () => { it('should return Merge', () => { expect(vm.mergeButtonText).toEqual('Merge'); @@ -177,7 +226,7 @@ describe('MRWidgetReadyToMerge', () => { expect(vm.isMergeButtonDisabled).toBeTruthy(); }); - it('should return true when there vm instance is making request', () => { + it('should return true when the vm instance is making request', () => { vm.isMakingRequest = true; expect(vm.isMergeButtonDisabled).toBeTruthy(); }); From ee3cf5d6f3d5a3631fa7e94a242f2dfe9b38a935 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 20 Sep 2017 19:03:53 +0100 Subject: [PATCH 07/57] [ci skip] Adds tests to vuex and collapsibe component Formats dates Fixes clipboard button Simplifies HTML --- .../javascripts/registry/components/app.vue | 4 +- .../components/collapsible_container.vue | 69 +++++++---- app/assets/javascripts/registry/index.js | 3 - .../javascripts/registry/stores/actions.js | 12 +- .../javascripts/registry/stores/getters.js | 2 +- .../registry/stores/mutation_types.js | 5 +- .../javascripts/registry/stores/mutations.js | 23 +--- .../components/clipboard_button.vue | 15 +-- .../stylesheets/pages/container_registry.scss | 4 - .../registry/repositories_controller.rb | 28 +++-- .../registry/repositories/index.html.haml | 9 +- .../vuex_action_helper.js} | 0 spec/javascripts/notes/stores/actions_spec.js | 2 +- .../registry/components/app_spec.js | 0 .../components/collapsible_container_spec.js | 112 ++++++++++++++++++ .../registry/stores/actions_spec.js | 85 +++++++++++++ .../registry/stores/getters_spec.js | 43 +++++++ spec/javascripts/registry/stores/mock_data.js | 91 ++++++++++++++ .../registry/stores/mutations_spec.js | 57 +++++++++ 19 files changed, 469 insertions(+), 95 deletions(-) rename spec/javascripts/{notes/stores/helpers.js => helpers/vuex_action_helper.js} (100%) create mode 100644 spec/javascripts/registry/components/app_spec.js create mode 100644 spec/javascripts/registry/components/collapsible_container_spec.js create mode 100644 spec/javascripts/registry/stores/actions_spec.js create mode 100644 spec/javascripts/registry/stores/getters_spec.js create mode 100644 spec/javascripts/registry/stores/mock_data.js create mode 100644 spec/javascripts/registry/stores/mutations_spec.js diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index 17a57ae248d..c4d66382850 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -33,7 +33,7 @@ 'fetchList', 'deleteRepo', 'deleteRegistry', - 'toggleIsLoading', + 'toggleLoading', ]), fetchRegistryList(repo) { @@ -49,7 +49,7 @@ deleteRepository(repo) { this.deleteRepo(repo) - .then(() => this.fetchRepo()) + .then(() => this.fetchRepos()) .catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); }, diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index 6be2aa60ebd..739e48b93f2 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -2,6 +2,7 @@ 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 timeagoMixin from '../../vue_shared/mixins/timeago'; export default { name: 'collapsibeContainerRegisty', @@ -15,6 +16,9 @@ clipboardButton, loadingIcon, }, + mixins: [ + timeagoMixin, + ], directives: { tooltip, }, @@ -28,16 +32,18 @@ const pluralize = gl.text.pluralize('layer', item.layers); 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); }, @@ -51,7 +57,8 @@ class="container-image-head"> + @click="toggleRepo" + class="js-toggle-repo"> +