summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/registry/explorer/components/project_empty_state.vue63
-rw-r--r--app/assets/javascripts/registry/explorer/components/quickstart_dropdown.vue92
-rw-r--r--app/assets/javascripts/registry/explorer/constants.js8
-rw-r--r--app/assets/javascripts/registry/explorer/pages/list.vue10
-rw-r--r--app/assets/javascripts/registry/explorer/stores/getters.js16
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb2
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb2
-rw-r--r--app/views/admin/groups/show.html.haml7
-rw-r--r--app/views/admin/users/_user.html.haml95
-rw-r--r--app/views/ci/variables/_index.html.haml2
-rw-r--r--changelogs/unreleased/195309-remove-special-user-action-buttons.yml5
-rw-r--r--changelogs/unreleased/33905-refactor-package-list-page-from-haml-to-vue.yml5
-rw-r--r--changelogs/unreleased/fix-ci-delete-variable-bug.yml5
-rw-r--r--changelogs/unreleased/forking_access_level.yml5
-rw-r--r--changelogs/unreleased/turn-on-new-variables-ui-ff.yml5
-rw-r--r--doc/api/group_activity_analytics.md8
-rw-r--r--doc/api/projects.md3
-rw-r--r--doc/ci/caching/index.md10
-rw-r--r--doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md2
-rw-r--r--lib/api/entities/project.rb1
-rw-r--r--lib/api/helpers/projects_helpers.rb2
-rw-r--r--lib/gitlab/graphql/connections.rb4
-rw-r--r--lib/gitlab/graphql/pagination/offset_active_record_relation_connection.rb2
-rw-r--r--lib/gitlab/graphql/pagination/relations/offset_active_record_relation.rb12
-rw-r--r--locale/gitlab.pot9
-rw-r--r--spec/features/projects/environments_pod_logs_spec.rb63
-rw-r--r--spec/frontend/registry/explorer/components/project_empty_state_spec.js2
-rw-r--r--spec/frontend/registry/explorer/components/quickstart_dropdown_spec.js95
-rw-r--r--spec/frontend/registry/explorer/pages/list_spec.js50
-rw-r--r--spec/frontend/registry/explorer/stores/getters_spec.js18
-rw-r--r--spec/graphql/gitlab_schema_spec.rb6
-rw-r--r--spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb2
-rw-r--r--spec/views/admin/users/_user.html.haml_spec.rb41
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