diff options
34 files changed, 514 insertions, 140 deletions
diff --git a/app/assets/javascripts/registry/explorer/components/project_empty_state.vue b/app/assets/javascripts/registry/explorer/components/project_empty_state.vue index 556df10ea5b..0ce38c4a9ec 100644 --- a/app/assets/javascripts/registry/explorer/components/project_empty_state.vue +++ b/app/assets/javascripts/registry/explorer/components/project_empty_state.vue @@ -1,7 +1,9 @@ <script> import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui'; +import { mapState, mapGetters } from 'vuex'; +import { s__ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import { mapState } from 'vuex'; +import { COPY_LOGIN_TITLE, COPY_BUILD_TITLE, COPY_PUSH_TITLE, QUICK_START } from '../constants'; export default { name: 'ProjectEmptyState', @@ -11,20 +13,24 @@ export default { GlSprintf, GlLink, }, + i18n: { + quickStart: QUICK_START, + copyLoginTitle: COPY_LOGIN_TITLE, + copyBuildTitle: COPY_BUILD_TITLE, + copyPushTitle: COPY_PUSH_TITLE, + introText: s__( + `ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`, + ), + notLoggedInMessage: s__( + `ContainerRegistry|If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a %{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd} instead of a password.`, + ), + addImageText: s__( + 'ContainerRegistry|You can add an image to this registry with the following commands:', + ), + }, computed: { ...mapState(['config']), - dockerBuildCommand() { - // eslint-disable-next-line @gitlab/require-i18n-strings - return `docker build -t ${this.config.repositoryUrl} .`; - }, - dockerPushCommand() { - // eslint-disable-next-line @gitlab/require-i18n-strings - return `docker push ${this.config.repositoryUrl}`; - }, - dockerLoginCommand() { - // eslint-disable-next-line @gitlab/require-i18n-strings - return `docker login ${this.config.registryHostUrlWithPort}`; - }, + ...mapGetters(['dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand']), }, }; </script> @@ -36,28 +42,15 @@ export default { > <template #description> <p class="js-no-container-images-text"> - <gl-sprintf - :message=" - s__(`ContainerRegistry|With the Container Registry, every project can have its own space to - store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`) - " - > + <gl-sprintf :message="$options.i18n.introText"> <template #docLink="{content}"> <gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link> </template> </gl-sprintf> </p> - <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5> + <h5>{{ $options.i18n.quickStart }}</h5> <p class="js-not-logged-in-to-registry-text"> - <gl-sprintf - :message=" - s__(`ContainerRegistry|If you are not already logged in, you need to authenticate to - the Container Registry by using your GitLab username and password. If you have - %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a - %{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd} - instead of a password.`) - " - > + <gl-sprintf :message="$options.i18n.notLoggedInMessage"> <template #twofaDocLink="{content}"> <gl-link :href="config.twoFactorAuthHelpLink" target="_blank">{{ content }}</gl-link> </template> @@ -73,18 +66,14 @@ export default { <span class="input-group-append"> <clipboard-button :text="dockerLoginCommand" - :title="s__('ContainerRegistry|Copy login command')" + :title="$options.i18n.copyLoginTitle" class="input-group-text" /> </span> </div> <p></p> <p> - {{ - s__( - 'ContainerRegistry|You can add an image to this registry with the following commands:', - ) - }} + {{ $options.i18n.addImageText }} </p> <div class="input-group append-bottom-10"> @@ -92,7 +81,7 @@ export default { <span class="input-group-append"> <clipboard-button :text="dockerBuildCommand" - :title="s__('ContainerRegistry|Copy build command')" + :title="$options.i18n.copyBuildTitle" class="input-group-text" /> </span> @@ -103,7 +92,7 @@ export default { <span class="input-group-append"> <clipboard-button :text="dockerPushCommand" - :title="s__('ContainerRegistry|Copy push command')" + :title="$options.i18n.copyPushTitle" class="input-group-text" /> </span> diff --git a/app/assets/javascripts/registry/explorer/components/quickstart_dropdown.vue b/app/assets/javascripts/registry/explorer/components/quickstart_dropdown.vue new file mode 100644 index 00000000000..99361b6e08d --- /dev/null +++ b/app/assets/javascripts/registry/explorer/components/quickstart_dropdown.vue @@ -0,0 +1,92 @@ +<script> +import { GlDropdown, GlFormGroup, GlFormInputGroup } from '@gitlab/ui'; +import { mapGetters } from 'vuex'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import { + QUICK_START, + LOGIN_COMMAND_LABEL, + COPY_LOGIN_TITLE, + BUILD_COMMAND_LABEL, + COPY_BUILD_TITLE, + PUSH_COMMAND_LABEL, + COPY_PUSH_TITLE, +} from '../constants'; + +export default { + components: { + GlDropdown, + GlFormGroup, + GlFormInputGroup, + ClipboardButton, + }, + i18n: { + dropdownTitle: QUICK_START, + loginCommandLabel: LOGIN_COMMAND_LABEL, + copyLoginTitle: COPY_LOGIN_TITLE, + buildCommandLabel: BUILD_COMMAND_LABEL, + copyBuildTitle: COPY_BUILD_TITLE, + pushCommandLabel: PUSH_COMMAND_LABEL, + copyPushTitle: COPY_PUSH_TITLE, + }, + computed: { + ...mapGetters(['dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand']), + }, +}; +</script> +<template> + <gl-dropdown :text="$options.i18n.dropdownTitle" variant="primary" size="sm" right> + <!-- This li is used as a container since gl-dropdown produces a root ul, this mimics the functionality exposed by b-dropdown-form --> + <li role="presentation" class="px-2 py-1 dropdown-menu-large"> + <form> + <gl-form-group + label-size="sm" + label-for="docker-login-btn" + :label="$options.i18n.loginCommandLabel" + > + <gl-form-input-group id="docker-login-btn" :value="dockerLoginCommand" readonly> + <template #append> + <clipboard-button + class="border" + :text="dockerLoginCommand" + :title="$options.i18n.copyLoginTitle" + /> + </template> + </gl-form-input-group> + </gl-form-group> + + <gl-form-group + label-size="sm" + label-for="docker-build-btn" + :label="$options.i18n.buildCommandLabel" + > + <gl-form-input-group id="docker-build-btn" :value="dockerBuildCommand" readonly> + <template #append> + <clipboard-button + class="border" + :text="dockerBuildCommand" + :title="$options.i18n.copyBuildTitle" + /> + </template> + </gl-form-input-group> + </gl-form-group> + + <gl-form-group + class="mb-0" + label-size="sm" + label-for="docker-push-btn" + :label="$options.i18n.pushCommandLabel" + > + <gl-form-input-group id="docker-push-btn" :value="dockerPushCommand" readonly> + <template #append> + <clipboard-button + class="border" + :text="dockerPushCommand" + :title="$options.i18n.copyPushTitle" + /> + </template> + </gl-form-input-group> + </gl-form-group> + </form> + </li> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/registry/explorer/constants.js b/app/assets/javascripts/registry/explorer/constants.js index ef72c085972..f5d935a2d70 100644 --- a/app/assets/javascripts/registry/explorer/constants.js +++ b/app/assets/javascripts/registry/explorer/constants.js @@ -47,3 +47,11 @@ export const EXPIRATION_POLICY_ALERT_FULL_MESSAGE = s__( export const EXPIRATION_POLICY_ALERT_SHORT_MESSAGE = s__( 'ContainerRegistry|The retention and expiration policy for this Container Registry has been enabled. For more information visit the %{linkStart}documentation%{linkEnd}', ); + +export const QUICK_START = s__('ContainerRegistry|Quick Start'); +export const LOGIN_COMMAND_LABEL = s__('ContainerRegistry|Login'); +export const COPY_LOGIN_TITLE = s__('ContainerRegistry|Copy login command'); +export const BUILD_COMMAND_LABEL = s__('ContainerRegistry|Build an image'); +export const COPY_BUILD_TITLE = s__('ContainerRegistry|Copy build command'); +export const PUSH_COMMAND_LABEL = s__('ContainerRegistry|Push an image'); +export const COPY_PUSH_TITLE = s__('ContainerRegistry|Copy push command'); diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue index c6ba06cd68c..7e321e927d3 100644 --- a/app/assets/javascripts/registry/explorer/pages/list.vue +++ b/app/assets/javascripts/registry/explorer/pages/list.vue @@ -16,6 +16,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ProjectEmptyState from '../components/project_empty_state.vue'; import GroupEmptyState from '../components/group_empty_state.vue'; import ProjectPolicyAlert from '../components/project_policy_alert.vue'; +import QuickstartDropdown from '../components/quickstart_dropdown.vue'; export default { name: 'RegistryListApp', @@ -26,6 +27,7 @@ export default { GroupEmptyState, ProjectPolicyAlert, ClipboardButton, + QuickstartDropdown, GlButton, GlIcon, GlModal, @@ -62,6 +64,9 @@ export default { this.requestImagesList({ page }); }, }, + showQuickStartDropdown() { + return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length); + }, }, methods: { ...mapActions(['requestImagesList', 'requestDeleteImage']), @@ -114,7 +119,10 @@ export default { <template v-else> <div> - <h4>{{ s__('ContainerRegistry|Container Registry') }}</h4> + <div class="d-flex justify-content-between align-items-center"> + <h4>{{ s__('ContainerRegistry|Container Registry') }}</h4> + <quickstart-dropdown v-if="showQuickStartDropdown" class="d-none d-sm-block" /> + </div> <p> <gl-sprintf :message=" diff --git a/app/assets/javascripts/registry/explorer/stores/getters.js b/app/assets/javascripts/registry/explorer/stores/getters.js index 5619b73d495..1136257a024 100644 --- a/app/assets/javascripts/registry/explorer/stores/getters.js +++ b/app/assets/javascripts/registry/explorer/stores/getters.js @@ -1,6 +1,20 @@ -// eslint-disable-next-line import/prefer-default-export export const tags = state => { // to show the loader inside the table we need to pass an empty array to gl-table whenever the table is loading // this is to take in account isLoading = true and state.tags =[1,2,3] during pagination and delete return state.isLoading ? [] : state.tags; }; + +export const dockerBuildCommand = state => { + /* eslint-disable @gitlab/require-i18n-strings */ + return `docker build -t ${state.config.repositoryUrl} .`; +}; + +export const dockerPushCommand = state => { + /* eslint-disable @gitlab/require-i18n-strings */ + return `docker push ${state.config.repositoryUrl}`; +}; + +export const dockerLoginCommand = state => { + /* eslint-disable @gitlab/require-i18n-strings */ + return `docker login ${state.config.registryHostUrlWithPort}`; +}; diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb index 3d347429398..ffa3f2c3364 100644 --- a/app/controllers/groups/settings/ci_cd_controller.rb +++ b/app/controllers/groups/settings/ci_cd_controller.rb @@ -7,7 +7,7 @@ module Groups before_action :authorize_admin_group! before_action :authorize_update_max_artifacts_size!, only: [:update] before_action do - push_frontend_feature_flag(:new_variables_ui, @group, default_enabled: true) + push_frontend_feature_flag(:new_variables_ui, @group) end before_action :define_variables, only: [:show, :create_deploy_token] diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 43c798bfc6e..aac6ecb07e4 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -6,7 +6,7 @@ module Projects before_action :authorize_admin_pipeline! before_action :define_variables before_action do - push_frontend_feature_flag(:new_variables_ui, @project, default_enabled: true) + push_frontend_feature_flag(:new_variables_ui, @project) end def show diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 160c3b4d06d..91ccfb07f49 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -78,14 +78,15 @@ .card-footer = paginate @projects, param_name: 'projects_page', theme: 'gitlab' - - if @group.shared_projects.any? + - shared_projects = @group.shared_projects.sort_by(&:name) + - unless shared_projects.empty? .card .card-header = _('Projects shared with %{group_name}') % { group_name: @group.name } %span.badge.badge-pill - #{@group.shared_projects.count} + #{shared_projects.size} %ul.content-list - - @group.shared_projects.sort_by(&:name).each do |project| + - shared_projects.each do |project| %li %strong = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project] diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml index 978e830d0e4..440eaac1917 100644 --- a/app/views/admin/users/_user.html.haml +++ b/app/views/admin/users/_user.html.haml @@ -14,51 +14,52 @@ = _('Last activity') .table-mobile-content = user.last_activity_on.nil? ? _('Never') : l(user.last_activity_on, format: :admin) - .table-section.section-20.table-button-footer - .table-action-buttons - = link_to _('Edit'), edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn btn-default' - - unless user == current_user - %button.dropdown-new.btn.btn-default{ type: 'button', data: { toggle: 'dropdown' } } - = sprite_icon('settings') - = sprite_icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-right - %li.dropdown-header - = _('Settings') - %li - - if user.ldap_blocked? - %span.small - = s_('AdminUsers|Cannot unblock LDAP blocked users') - - elsif user.blocked? - = link_to _('Unblock'), unblock_admin_user_path(user), method: :put - - else - %button.btn{ data: { 'gl-modal-action': 'block', - url: block_admin_user_path(user), - username: sanitize_name(user.name) } } - = s_('AdminUsers|Block') - - if user.can_be_deactivated? + - unless user.internal? + .table-section.section-20.table-button-footer + .table-action-buttons + = link_to _('Edit'), edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn btn-default' + - unless user == current_user + %button.dropdown-new.btn.btn-default{ type: 'button', data: { toggle: 'dropdown' } } + = sprite_icon('settings') + = sprite_icon('chevron-down') + %ul.dropdown-menu.dropdown-menu-right + %li.dropdown-header + = _('Settings') %li - %button.btn{ data: { 'gl-modal-action': 'deactivate', - url: deactivate_admin_user_path(user), - username: sanitize_name(user.name) } } - = s_('AdminUsers|Deactivate') - - elsif user.deactivated? - %li - = link_to _('Activate'), activate_admin_user_path(user), method: :put - - if user.access_locked? - %li - = link_to _('Unlock'), unlock_admin_user_path(user), method: :put, data: { confirm: _('Are you sure?') } - - if can?(current_user, :destroy_user, user) - %li.divider - - if user.can_be_removed? - %li - %button.delete-user-button.btn.text-danger{ data: { 'gl-modal-action': 'delete', - delete_user_url: admin_user_path(user), - block_user_url: block_admin_user_path(user), - username: sanitize_name(user.name) } } - = s_('AdminUsers|Delete user') - %li - %button.delete-user-button.btn.text-danger{ data: { 'gl-modal-action': 'delete-with-contributions', - delete_user_url: admin_user_path(user, hard_delete: true), - block_user_url: block_admin_user_path(user), - username: sanitize_name(user.name) } } - = s_('AdminUsers|Delete user and contributions') + - if user.ldap_blocked? + %span.small + = s_('AdminUsers|Cannot unblock LDAP blocked users') + - elsif user.blocked? + = link_to _('Unblock'), unblock_admin_user_path(user), method: :put + - else + %button.btn{ data: { 'gl-modal-action': 'block', + url: block_admin_user_path(user), + username: sanitize_name(user.name) } } + = s_('AdminUsers|Block') + - if user.can_be_deactivated? + %li + %button.btn{ data: { 'gl-modal-action': 'deactivate', + url: deactivate_admin_user_path(user), + username: sanitize_name(user.name) } } + = s_('AdminUsers|Deactivate') + - elsif user.deactivated? + %li + = link_to _('Activate'), activate_admin_user_path(user), method: :put + - if user.access_locked? + %li + = link_to _('Unlock'), unlock_admin_user_path(user), method: :put, data: { confirm: _('Are you sure?') } + - if can?(current_user, :destroy_user, user) + %li.divider + - if user.can_be_removed? + %li + %button.delete-user-button.btn.text-danger{ data: { 'gl-modal-action': 'delete', + delete_user_url: admin_user_path(user), + block_user_url: block_admin_user_path(user), + username: sanitize_name(user.name) } } + = s_('AdminUsers|Delete user') + %li + %button.delete-user-button.btn.text-danger{ data: { 'gl-modal-action': 'delete-with-contributions', + delete_user_url: admin_user_path(user, hard_delete: true), + block_user_url: block_admin_user_path(user), + username: sanitize_name(user.name) } } + = s_('AdminUsers|Delete user and contributions') diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml index aadb2c62d83..f11c730eba6 100644 --- a/app/views/ci/variables/_index.html.haml +++ b/app/views/ci/variables/_index.html.haml @@ -5,7 +5,7 @@ - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protected-variables') } = s_('Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } -- if Feature.enabled?(:new_variables_ui, @project || @group, default_enabled: true) +- if Feature.enabled?(:new_variables_ui, @project || @group) - is_group = !@group.nil? #js-ci-project-variables{ data: { endpoint: save_endpoint, project_id: @project&.id || '', group: is_group.to_s, maskable_regex: ci_variable_maskable_regex} } diff --git a/changelogs/unreleased/195309-remove-special-user-action-buttons.yml b/changelogs/unreleased/195309-remove-special-user-action-buttons.yml new file mode 100644 index 00000000000..e737840701b --- /dev/null +++ b/changelogs/unreleased/195309-remove-special-user-action-buttons.yml @@ -0,0 +1,5 @@ +--- +title: Hide admin user actions for ghost and bot users +merge_request: 27162 +author: +type: fixed diff --git a/changelogs/unreleased/33905-refactor-package-list-page-from-haml-to-vue.yml b/changelogs/unreleased/33905-refactor-package-list-page-from-haml-to-vue.yml new file mode 100644 index 00000000000..541cc42169b --- /dev/null +++ b/changelogs/unreleased/33905-refactor-package-list-page-from-haml-to-vue.yml @@ -0,0 +1,5 @@ +--- +title: New package list is enabled which includes filtering by type +merge_request: 18860 +author: +type: added diff --git a/changelogs/unreleased/fix-ci-delete-variable-bug.yml b/changelogs/unreleased/fix-ci-delete-variable-bug.yml deleted file mode 100644 index 9a47f2d74d4..00000000000 --- a/changelogs/unreleased/fix-ci-delete-variable-bug.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update UI for project and group settings CI variables -merge_request: 27411 -author: -type: added diff --git a/changelogs/unreleased/forking_access_level.yml b/changelogs/unreleased/forking_access_level.yml new file mode 100644 index 00000000000..5ba71014cba --- /dev/null +++ b/changelogs/unreleased/forking_access_level.yml @@ -0,0 +1,5 @@ +--- +title: Add forking_access_level to projects API +merge_request: 27514 +author: Mathieu Parent +type: added diff --git a/changelogs/unreleased/turn-on-new-variables-ui-ff.yml b/changelogs/unreleased/turn-on-new-variables-ui-ff.yml deleted file mode 100644 index 9cb100481db..00000000000 --- a/changelogs/unreleased/turn-on-new-variables-ui-ff.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update UI for project and group settings CI variables -merge_request: 26901 -author: -type: added diff --git a/doc/api/group_activity_analytics.md b/doc/api/group_activity_analytics.md index 2e93967fe64..23dcb46dbc5 100644 --- a/doc/api/group_activity_analytics.md +++ b/doc/api/group_activity_analytics.md @@ -23,9 +23,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/ap Example response: ```json -[ - { issues_count : 10 } -] +{ "issues_count": 10 } ``` ## Get count of recently created merge requests for group @@ -49,7 +47,5 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/ap Example response: ```json -[ - { merge_requests_count : 10 } -] +{ "merge_requests_count": 10 } ``` diff --git a/doc/api/projects.md b/doc/api/projects.md index a00bd442872..ae9f7de427d 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1012,6 +1012,7 @@ POST /projects | `issues_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `repository_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `merge_requests_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `forking_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `builds_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` | @@ -1080,6 +1081,7 @@ POST /projects/user/:user_id | `issues_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `repository_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `merge_requests_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `forking_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `builds_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` | @@ -1147,6 +1149,7 @@ PUT /projects/:id | `issues_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `repository_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `merge_requests_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `forking_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `builds_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` | diff --git a/doc/ci/caching/index.md b/doc/ci/caching/index.md index a60310076a8..4e2e61cfadf 100644 --- a/doc/ci/caching/index.md +++ b/doc/ci/caching/index.md @@ -202,7 +202,7 @@ so they don't have to be re-fetched from the public internet. NOTE: **Note:** For more examples, check out our [GitLab CI/CD -templates](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/gitlab/ci/templates). +templates](https://gitlab.com/gitlab-org/gitlab/tree/master/lib/gitlab/ci/templates). ### Caching Node.js dependencies @@ -214,7 +214,7 @@ we tell npm to use `./.npm` instead, and it is cached per-branch: ```yaml # -# https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml +# https://gitlab.com/gitlab-org/gitlab/tree/master/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml # image: node:latest @@ -241,7 +241,7 @@ are cached per-branch: ```yaml # -# https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/gitlab/ci/templates/PHP.gitlab-ci.yml +# https://gitlab.com/gitlab-org/gitlab/tree/master/lib/gitlab/ci/templates/PHP.gitlab-ci.yml # image: php:7.2 @@ -270,7 +270,7 @@ pip's cache is defined under `.cache/pip/` and both are cached per-branch: ```yaml # -# https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml +# https://gitlab.com/gitlab-org/gitlab/tree/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml # image: python:latest @@ -310,7 +310,7 @@ jobs inherit it. Gems are installed in `vendor/ruby/` and are cached per-branch: ```yaml # -# https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml +# https://gitlab.com/gitlab-org/gitlab/tree/master/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml # image: ruby:2.6 diff --git a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md index 6dbc8f98247..ca980bb58c3 100644 --- a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md +++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md @@ -45,7 +45,7 @@ In addition, pipelines for merged results have the following limitations: see [#11934](https://gitlab.com/gitlab-org/gitlab/issues/11934). - This feature is not available for [fast forward merges](../../../user/project/merge_requests/fast_forward_merge.md) yet. - To follow progress, see [#58226](https://gitlab.com/gitlab-org/gitlab-foss/issues/58226). + To follow progress, see [#58226](https://gitlab.com/gitlab-org/gitlab/-/issues/26996). ## Enabling Pipelines for Merged Results diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb index 85a00273192..39cd2d610e4 100644 --- a/lib/api/entities/project.rb +++ b/lib/api/entities/project.rb @@ -59,6 +59,7 @@ module API expose(:issues_access_level) { |project, options| project.project_feature.string_access_level(:issues) } expose(:repository_access_level) { |project, options| project.project_feature.string_access_level(:repository) } expose(:merge_requests_access_level) { |project, options| project.project_feature.string_access_level(:merge_requests) } + expose(:forking_access_level) { |project, options| project.project_feature.string_access_level(:forking) } expose(:wiki_access_level) { |project, options| project.project_feature.string_access_level(:wiki) } expose(:builds_access_level) { |project, options| project.project_feature.string_access_level(:builds) } expose(:snippets_access_level) { |project, options| project.project_feature.string_access_level(:snippets) } diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 85ed8a4d636..cedf50aaff6 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -24,6 +24,7 @@ module API optional :issues_access_level, type: String, values: %w(disabled private enabled), desc: 'Issues access level. One of `disabled`, `private` or `enabled`' optional :repository_access_level, type: String, values: %w(disabled private enabled), desc: 'Repository access level. One of `disabled`, `private` or `enabled`' optional :merge_requests_access_level, type: String, values: %w(disabled private enabled), desc: 'Merge requests access level. One of `disabled`, `private` or `enabled`' + optional :forking_access_level, type: String, values: %w(disabled private enabled), desc: 'Forks access level. One of `disabled`, `private` or `enabled`' optional :wiki_access_level, type: String, values: %w(disabled private enabled), desc: 'Wiki access level. One of `disabled`, `private` or `enabled`' optional :builds_access_level, type: String, values: %w(disabled private enabled), desc: 'Builds access level. One of `disabled`, `private` or `enabled`' optional :snippets_access_level, type: String, values: %w(disabled private enabled), desc: 'Snippets access level. One of `disabled`, `private` or `enabled`' @@ -104,6 +105,7 @@ module API :default_branch, :description, :emails_disabled, + :forking_access_level, :issues_access_level, :lfs_enabled, :merge_requests_access_level, diff --git a/lib/gitlab/graphql/connections.rb b/lib/gitlab/graphql/connections.rb index 0c0bfe5a458..08d5cd0b72e 100644 --- a/lib/gitlab/graphql/connections.rb +++ b/lib/gitlab/graphql/connections.rb @@ -16,10 +16,6 @@ module Gitlab Gitlab::Graphql::ExternallyPaginatedArray, Gitlab::Graphql::Connections::ExternallyPaginatedArrayConnection ) - GraphQL::Relay::BaseConnection.register_connection_implementation( - Gitlab::Graphql::Pagination::Relations::OffsetActiveRecordRelation, - Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection - ) end end end diff --git a/lib/gitlab/graphql/pagination/offset_active_record_relation_connection.rb b/lib/gitlab/graphql/pagination/offset_active_record_relation_connection.rb index c852fbf0ab8..33f84701562 100644 --- a/lib/gitlab/graphql/pagination/offset_active_record_relation_connection.rb +++ b/lib/gitlab/graphql/pagination/offset_active_record_relation_connection.rb @@ -6,7 +6,7 @@ module Gitlab module Graphql module Pagination - class OffsetActiveRecordRelationConnection < GraphQL::Relay::RelationConnection + class OffsetActiveRecordRelationConnection < GraphQL::Pagination::ActiveRecordRelationConnection end end end diff --git a/lib/gitlab/graphql/pagination/relations/offset_active_record_relation.rb b/lib/gitlab/graphql/pagination/relations/offset_active_record_relation.rb deleted file mode 100644 index 2e5a0d66d4e..00000000000 --- a/lib/gitlab/graphql/pagination/relations/offset_active_record_relation.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Graphql - module Pagination - module Relations - class OffsetActiveRecordRelation < ::ActiveRecord::Relation - end - end - end - end -end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8c0d638c617..e37b665b2b4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5274,6 +5274,9 @@ msgstr "" msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept." msgstr "" +msgid "ContainerRegistry|Build an image" +msgstr "" + msgid "ContainerRegistry|Container Registry" msgstr "" @@ -5328,12 +5331,18 @@ msgstr "" msgid "ContainerRegistry|Last Updated" msgstr "" +msgid "ContainerRegistry|Login" +msgstr "" + msgid "ContainerRegistry|Missing or insufficient permission, delete button disabled" msgstr "" msgid "ContainerRegistry|Number of tags to retain:" msgstr "" +msgid "ContainerRegistry|Push an image" +msgstr "" + msgid "ContainerRegistry|Quick Start" msgstr "" diff --git a/spec/features/projects/environments_pod_logs_spec.rb b/spec/features/projects/environments_pod_logs_spec.rb new file mode 100644 index 00000000000..121a8e1705b --- /dev/null +++ b/spec/features/projects/environments_pod_logs_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Environment > Pod Logs', :js do + include KubernetesHelpers + + let(:pod_names) { %w(kube-pod) } + let(:pod_name) { pod_names.first } + let(:project) { create(:project, :repository) } + let(:environment) { create(:environment, project: project) } + let(:service) { create(:cluster_platform_kubernetes, :configured) } + + before do + create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [project]) + create(:deployment, :success, environment: environment) + + stub_kubeclient_pods(environment.deployment_namespace) + stub_kubeclient_logs(pod_name, environment.deployment_namespace, container: 'container-0') + + sign_in(project.owner) + end + + it "shows environments in dropdown" do + create(:environment, project: project) + + visit project_logs_path(environment.project, environment_name: environment.name, pod_name: pod_name) + + wait_for_requests + + page.within('.js-environments-dropdown') do + toggle = find(".dropdown-menu-toggle:not([disabled])") + + expect(toggle).to have_content(environment.name) + + toggle.click + + dropdown_items = find(".dropdown-menu").all(".dropdown-item") + expect(dropdown_items.first).to have_content(environment.name) + expect(dropdown_items.size).to eq(2) + end + end + + context 'with logs', :use_clean_rails_memory_store_caching do + it "shows pod logs", :sidekiq_might_not_need_inline do + visit project_logs_path(environment.project, environment_name: environment.name, pod_name: pod_name) + + wait_for_requests + + page.within('.js-pods-dropdown') do + find(".dropdown-menu-toggle:not([disabled])").click + + dropdown_items = find(".dropdown-menu").all(".dropdown-item:not([disabled])") + expect(dropdown_items.size).to eq(1) + + dropdown_items.each_with_index do |item, i| + expect(item.text).to eq(pod_names[i]) + end + end + expect(page).to have_content("Dec 13 14:04:22.123Z | kube-pod | Log 1 Dec 13 14:04:23.123Z | kube-pod | Log 2 Dec 13 14:04:24.123Z | kube-pod | Log 3") + end + end +end diff --git a/spec/frontend/registry/explorer/components/project_empty_state_spec.js b/spec/frontend/registry/explorer/components/project_empty_state_spec.js index 8d4b6ca60a2..4b209646da9 100644 --- a/spec/frontend/registry/explorer/components/project_empty_state_spec.js +++ b/spec/frontend/registry/explorer/components/project_empty_state_spec.js @@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import { GlSprintf } from '@gitlab/ui'; import { GlEmptyState } from '../stubs'; import projectEmptyState from '~/registry/explorer/components/project_empty_state.vue'; +import * as getters from '~/registry/explorer/stores/getters'; const localVue = createLocalVue(); localVue.use(Vuex); @@ -23,6 +24,7 @@ describe('Registry Project Empty state', () => { noContainersImage: 'bazFoo', }, }, + getters, }); wrapper = shallowMount(projectEmptyState, { localVue, diff --git a/spec/frontend/registry/explorer/components/quickstart_dropdown_spec.js b/spec/frontend/registry/explorer/components/quickstart_dropdown_spec.js new file mode 100644 index 00000000000..3dfe50ebe13 --- /dev/null +++ b/spec/frontend/registry/explorer/components/quickstart_dropdown_spec.js @@ -0,0 +1,95 @@ +import Vuex from 'vuex'; +import { mount, createLocalVue } from '@vue/test-utils'; +import { GlDropdown, GlFormGroup, GlFormInputGroup } from '@gitlab/ui'; +import * as getters from '~/registry/explorer/stores/getters'; +import QuickstartDropdown from '~/registry/explorer/components/quickstart_dropdown.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + +import { + QUICK_START, + LOGIN_COMMAND_LABEL, + COPY_LOGIN_TITLE, + BUILD_COMMAND_LABEL, + COPY_BUILD_TITLE, + PUSH_COMMAND_LABEL, + COPY_PUSH_TITLE, +} from '~/registry/explorer//constants'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('quickstart_dropdown', () => { + let wrapper; + let store; + + const findDropdownButton = () => wrapper.find(GlDropdown); + const findFormGroups = () => wrapper.findAll(GlFormGroup); + + const mountComponent = () => { + store = new Vuex.Store({ + state: { + config: { + repositoryUrl: 'foo', + registryHostUrlWithPort: 'bar', + }, + }, + getters, + }); + wrapper = mount(QuickstartDropdown, { + localVue, + store, + }); + }; + + beforeEach(() => { + mountComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + store = null; + }); + + it('shows the correct text on the button', () => { + expect(findDropdownButton().text()).toContain(QUICK_START); + }); + + describe.each` + index | id | labelText | titleText | getter + ${0} | ${'docker-login-btn'} | ${LOGIN_COMMAND_LABEL} | ${COPY_LOGIN_TITLE} | ${'dockerLoginCommand'} + ${1} | ${'docker-build-btn'} | ${BUILD_COMMAND_LABEL} | ${COPY_BUILD_TITLE} | ${'dockerBuildCommand'} + ${2} | ${'docker-push-btn'} | ${PUSH_COMMAND_LABEL} | ${COPY_PUSH_TITLE} | ${'dockerPushCommand'} + `('form group at $index', ({ index, id, labelText, titleText, getter }) => { + let formGroup; + + const findFormInputGroup = parent => parent.find(GlFormInputGroup); + const findClipboardButton = parent => parent.find(ClipboardButton); + + beforeEach(() => { + formGroup = findFormGroups().at(index); + }); + + it('exists', () => { + expect(formGroup.exists()).toBe(true); + }); + + it(`has a label ${labelText}`, () => { + expect(formGroup.text()).toBe(labelText); + }); + + it(`contains a form input group with ${id} id and with value equal to ${getter} getter`, () => { + const formInputGroup = findFormInputGroup(formGroup); + expect(formInputGroup.exists()).toBe(true); + expect(formInputGroup.attributes('id')).toBe(id); + expect(formInputGroup.props('value')).toBe(store.getters[getter]); + }); + + it(`contains a clipboard button with title of ${titleText} and text equal to ${getter} getter`, () => { + const clipBoardButton = findClipboardButton(formGroup); + expect(clipBoardButton.exists()).toBe(true); + expect(clipBoardButton.props('title')).toBe(titleText); + expect(clipBoardButton.props('text')).toBe(store.getters[getter]); + }); + }); +}); diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/registry/explorer/pages/list_spec.js index 91c3c242ed4..5b713778495 100644 --- a/spec/frontend/registry/explorer/pages/list_spec.js +++ b/spec/frontend/registry/explorer/pages/list_spec.js @@ -3,6 +3,9 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import { GlPagination, GlSkeletonLoader, GlSprintf } from '@gitlab/ui'; import Tracking from '~/tracking'; import component from '~/registry/explorer/pages/list.vue'; +import QuickstartDropdown from '~/registry/explorer/components/quickstart_dropdown.vue'; +import GroupEmptyState from '~/registry/explorer/components/group_empty_state.vue'; +import ProjectEmptyState from '~/registry/explorer/components/project_empty_state.vue'; import store from '~/registry/explorer/stores/'; import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/'; import { imagesListResponse } from '../mock_data'; @@ -24,6 +27,9 @@ describe('List Page', () => { const findDetailsLink = () => wrapper.find({ ref: 'detailsLink' }); const findClipboardButton = () => wrapper.find({ ref: 'clipboardButton' }); const findPagination = () => wrapper.find(GlPagination); + const findQuickStartDropdown = () => wrapper.find(QuickstartDropdown); + const findProjectEmptyState = () => wrapper.find(ProjectEmptyState); + const findGroupEmptyState = () => wrapper.find(GroupEmptyState); beforeEach(() => { wrapper = shallowMount(component, { @@ -76,7 +82,7 @@ describe('List Page', () => { }); }); - describe('when isLoading is true', () => { + describe('isLoading is true', () => { beforeAll(() => store.commit(SET_MAIN_LOADING, true)); afterAll(() => store.commit(SET_MAIN_LOADING, false)); @@ -88,9 +94,49 @@ describe('List Page', () => { it('imagesList is not visible', () => { expect(findImagesList().exists()).toBe(false); }); + + it('quick start is not visible', () => { + expect(findQuickStartDropdown().exists()).toBe(false); + }); + }); + + describe('list is empty', () => { + beforeEach(() => { + store.dispatch('receiveImagesListSuccess', { data: [] }); + }); + + it('quick start is not visible', () => { + expect(findQuickStartDropdown().exists()).toBe(false); + }); + + it('project empty state is visible', () => { + expect(findProjectEmptyState().exists()).toBe(true); + }); + + describe('is group page is true', () => { + beforeAll(() => { + store.dispatch('setInitialState', { isGroupPage: true }); + }); + + afterAll(() => { + store.dispatch('setInitialState', { isGroupPage: undefined }); + }); + + it('group empty state is visible', () => { + expect(findGroupEmptyState().exists()).toBe(true); + }); + + it('quick start is not visible', () => { + expect(findQuickStartDropdown().exists()).toBe(false); + }); + }); }); - describe('list', () => { + describe('list is not empty', () => { + it('quick start is visible', () => { + expect(findQuickStartDropdown().exists()).toBe(true); + }); + describe('listElement', () => { let listElements; let firstElement; diff --git a/spec/frontend/registry/explorer/stores/getters_spec.js b/spec/frontend/registry/explorer/stores/getters_spec.js index c224f076d30..211b8169d82 100644 --- a/spec/frontend/registry/explorer/stores/getters_spec.js +++ b/spec/frontend/registry/explorer/stores/getters_spec.js @@ -31,4 +31,22 @@ describe('Getters RegistryExplorer store', () => { }); }); }); + + describe.each` + getter | prefix | configParameter | suffix + ${'dockerBuildCommand'} | ${'docker build -t'} | ${'repositoryUrl'} | ${'.'} + ${'dockerPushCommand'} | ${'docker push'} | ${'repositoryUrl'} | ${null} + ${'dockerLoginCommand'} | ${'docker login'} | ${'registryHostUrlWithPort'} | ${null} + `('$getter', ({ getter, prefix, configParameter, suffix }) => { + beforeEach(() => { + state = { + config: { repositoryUrl: 'foo', registryHostUrlWithPort: 'bar' }, + }; + }); + + it(`returns ${prefix} concatenated with ${configParameter} and optionally suffixed with ${suffix}`, () => { + const expectedPieces = [prefix, state.config[configParameter], suffix].filter(p => p); + expect(getters[getter](state)).toBe(expectedPieces.join(' ')); + }); + }); }); diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb index bd22458355c..f8b56b6002a 100644 --- a/spec/graphql/gitlab_schema_spec.rb +++ b/spec/graphql/gitlab_schema_spec.rb @@ -52,12 +52,6 @@ describe GitlabSchema do expect(connection).to eq(Gitlab::Graphql::Connections::FilterableArrayConnection) end - it 'paginates OffsetActiveRecordRelation using `Pagination::OffsetActiveRecordRelationConnection`' do - connection = implementations[Gitlab::Graphql::Pagination::Relations::OffsetActiveRecordRelation.name] - - expect(connection).to eq(Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection) - end - describe '.execute' do context 'for different types of users' do context 'when no context' do diff --git a/spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb b/spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb index 2269b4def82..931b1e708de 100644 --- a/spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb @@ -4,6 +4,6 @@ require 'spec_helper' describe Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection do it 'subclasses from GraphQL::Relay::RelationConnection' do - expect(described_class.superclass).to eq GraphQL::Relay::RelationConnection + expect(described_class.superclass).to eq GraphQL::Pagination::ActiveRecordRelationConnection end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 83f678ad2cb..8706b941e4f 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -726,6 +726,7 @@ describe API::Projects do issues_enabled: false, jobs_enabled: false, merge_requests_enabled: false, + forking_access_level: 'disabled', wiki_enabled: false, resolve_outdated_diff_discussions: false, remove_source_branch_after_merge: true, @@ -1400,6 +1401,7 @@ describe API::Projects do expect(json_response['repository_access_level']).to be_present expect(json_response['issues_access_level']).to be_present expect(json_response['merge_requests_access_level']).to be_present + expect(json_response['forking_access_level']).to be_present expect(json_response['wiki_access_level']).to be_present expect(json_response['builds_access_level']).to be_present expect(json_response).to have_key('emails_disabled') diff --git a/spec/views/admin/users/_user.html.haml_spec.rb b/spec/views/admin/users/_user.html.haml_spec.rb new file mode 100644 index 00000000000..96d84229d94 --- /dev/null +++ b/spec/views/admin/users/_user.html.haml_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'admin/users/_user.html.haml' do + before do + allow(view).to receive(:user).and_return(user) + end + + context 'internal users' do + context 'when showing a `Ghost User`' do + let(:user) { create(:user, ghost: true) } + + it 'does not render action buttons' do + render + + expect(rendered).not_to have_selector('.table-action-buttons') + end + end + + context 'when showing a `Bot User`' do + let(:user) { create(:user, user_type: :alert_bot) } + + it 'does not render action buttons' do + render + + expect(rendered).not_to have_selector('.table-action-buttons') + end + end + end + + context 'when showing an external user' do + let(:user) { create(:user) } + + it 'renders action buttons' do + render + + expect(rendered).to have_selector('.table-action-buttons') + end + end +end |