Commit 0eb4fd2f authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 78d8830c
<script> <script>
import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui'; 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 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 { export default {
name: 'ProjectEmptyState', name: 'ProjectEmptyState',
...@@ -11,20 +13,24 @@ export default { ...@@ -11,20 +13,24 @@ export default {
GlSprintf, GlSprintf,
GlLink, 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: { computed: {
...mapState(['config']), ...mapState(['config']),
dockerBuildCommand() { ...mapGetters(['dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand']),
// 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}`;
},
}, },
}; };
</script> </script>
...@@ -36,28 +42,15 @@ export default { ...@@ -36,28 +42,15 @@ export default {
> >
<template #description> <template #description>
<p class="js-no-container-images-text"> <p class="js-no-container-images-text">
<gl-sprintf <gl-sprintf :message="$options.i18n.introText">
:message="
s__(`ContainerRegistry|With the Container Registry, every project can have its own space to
store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`)
"
>
<template #docLink="{content}"> <template #docLink="{content}">
<gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link> <gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link>
</template> </template>
</gl-sprintf> </gl-sprintf>
</p> </p>
<h5>{{ s__('ContainerRegistry|Quick Start') }}</h5> <h5>{{ $options.i18n.quickStart }}</h5>
<p class="js-not-logged-in-to-registry-text"> <p class="js-not-logged-in-to-registry-text">
<gl-sprintf <gl-sprintf :message="$options.i18n.notLoggedInMessage">
: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.`)
"
>
<template #twofaDocLink="{content}"> <template #twofaDocLink="{content}">
<gl-link :href="config.twoFactorAuthHelpLink" target="_blank">{{ content }}</gl-link> <gl-link :href="config.twoFactorAuthHelpLink" target="_blank">{{ content }}</gl-link>
</template> </template>
...@@ -73,18 +66,14 @@ export default { ...@@ -73,18 +66,14 @@ export default {
<span class="input-group-append"> <span class="input-group-append">
<clipboard-button <clipboard-button
:text="dockerLoginCommand" :text="dockerLoginCommand"
:title="s__('ContainerRegistry|Copy login command')" :title="$options.i18n.copyLoginTitle"
class="input-group-text" class="input-group-text"
/> />
</span> </span>
</div> </div>
<p></p> <p></p>
<p> <p>
{{ {{ $options.i18n.addImageText }}
s__(
'ContainerRegistry|You can add an image to this registry with the following commands:',
)
}}
</p> </p>
<div class="input-group append-bottom-10"> <div class="input-group append-bottom-10">
...@@ -92,7 +81,7 @@ export default { ...@@ -92,7 +81,7 @@ export default {
<span class="input-group-append"> <span class="input-group-append">
<clipboard-button <clipboard-button
:text="dockerBuildCommand" :text="dockerBuildCommand"
:title="s__('ContainerRegistry|Copy build command')" :title="$options.i18n.copyBuildTitle"
class="input-group-text" class="input-group-text"
/> />
</span> </span>
...@@ -103,7 +92,7 @@ export default { ...@@ -103,7 +92,7 @@ export default {
<span class="input-group-append"> <span class="input-group-append">
<clipboard-button <clipboard-button
:text="dockerPushCommand" :text="dockerPushCommand"
:title="s__('ContainerRegistry|Copy push command')" :title="$options.i18n.copyPushTitle"
class="input-group-text" class="input-group-text"
/> />
</span> </span>
......
<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>
...@@ -47,3 +47,11 @@ export const EXPIRATION_POLICY_ALERT_FULL_MESSAGE = s__( ...@@ -47,3 +47,11 @@ export const EXPIRATION_POLICY_ALERT_FULL_MESSAGE = s__(
export const EXPIRATION_POLICY_ALERT_SHORT_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}', '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');
...@@ -16,6 +16,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; ...@@ -16,6 +16,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ProjectEmptyState from '../components/project_empty_state.vue'; import ProjectEmptyState from '../components/project_empty_state.vue';
import GroupEmptyState from '../components/group_empty_state.vue'; import GroupEmptyState from '../components/group_empty_state.vue';
import ProjectPolicyAlert from '../components/project_policy_alert.vue'; import ProjectPolicyAlert from '../components/project_policy_alert.vue';
import QuickstartDropdown from '../components/quickstart_dropdown.vue';
export default { export default {
name: 'RegistryListApp', name: 'RegistryListApp',
...@@ -26,6 +27,7 @@ export default { ...@@ -26,6 +27,7 @@ export default {
GroupEmptyState, GroupEmptyState,
ProjectPolicyAlert, ProjectPolicyAlert,
ClipboardButton, ClipboardButton,
QuickstartDropdown,
GlButton, GlButton,
GlIcon, GlIcon,
GlModal, GlModal,
...@@ -62,6 +64,9 @@ export default { ...@@ -62,6 +64,9 @@ export default {
this.requestImagesList({ page }); this.requestImagesList({ page });
}, },
}, },
showQuickStartDropdown() {
return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length);
},
}, },
methods: { methods: {
...mapActions(['requestImagesList', 'requestDeleteImage']), ...mapActions(['requestImagesList', 'requestDeleteImage']),
...@@ -114,7 +119,10 @@ export default { ...@@ -114,7 +119,10 @@ export default {
<template v-else> <template v-else>
<div> <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> <p>
<gl-sprintf <gl-sprintf
:message=" :message="
......
// eslint-disable-next-line import/prefer-default-export
export const tags = state => { 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 // 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 // this is to take in account isLoading = true and state.tags =[1,2,3] during pagination and delete
return state.isLoading ? [] : state.tags; 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}`;
};
...@@ -7,7 +7,7 @@ module Groups ...@@ -7,7 +7,7 @@ module Groups
before_action :authorize_admin_group! before_action :authorize_admin_group!
before_action :authorize_update_max_artifacts_size!, only: [:update] before_action :authorize_update_max_artifacts_size!, only: [:update]
before_action do before_action do
push_frontend_feature_flag(:new_variables_ui, @group, default_enabled: true) push_frontend_feature_flag(:new_variables_ui, @group)
end end
before_action :define_variables, only: [:show, :create_deploy_token] before_action :define_variables, only: [:show, :create_deploy_token]
......
...@@ -6,7 +6,7 @@ module Projects ...@@ -6,7 +6,7 @@ module Projects
before_action :authorize_admin_pipeline! before_action :authorize_admin_pipeline!
before_action :define_variables before_action :define_variables
before_action do before_action do
push_frontend_feature_flag(:new_variables_ui, @project, default_enabled: true) push_frontend_feature_flag(:new_variables_ui, @project)
end end
def show def show
......
...@@ -78,14 +78,15 @@ ...@@ -78,14 +78,15 @@
.card-footer .card-footer
= paginate @projects, param_name: 'projects_page', theme: 'gitlab' = 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
.card-header .card-header
= _('Projects shared with %{group_name}') % { group_name: @group.name } = _('Projects shared with %{group_name}') % { group_name: @group.name }
%span.badge.badge-pill %span.badge.badge-pill
#{@group.shared_projects.count} #{shared_projects.size}
%ul.content-list %ul.content-list
- @group.shared_projects.sort_by(&:name).each do |project| - shared_projects.each do |project|
%li %li
%strong %strong
= link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project] = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project]
......
...@@ -14,51 +14,52 @@ ...@@ -14,51 +14,52 @@
= _('Last activity') = _('Last activity')
.table-mobile-content .table-mobile-content
= user.last_activity_on.nil? ? _('Never') : l(user.last_activity_on, format: :admin) = user.last_activity_on.nil? ? _('Never') : l(user.last_activity_on, format: :admin)
.table-section.section-20.table-button-footer - unless user.internal?
.table-action-buttons .table-section.section-20.table-button-footer
= link_to _('Edit'), edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn btn-default' .table-action-buttons
- unless user == current_user = link_to _('Edit'), edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn btn-default'
%button.dropdown-new.btn.btn-default{ type: 'button', data: { toggle: 'dropdown' } } - unless user == current_user
= sprite_icon('settings') %button.dropdown-new.btn.btn-default{ type: 'button', data: { toggle: 'dropdown' } }
= sprite_icon('chevron-down') = sprite_icon('settings')
%ul.dropdown-menu.dropdown-menu-right = sprite_icon('chevron-down')
%li.dropdown-header %ul.dropdown-menu.dropdown-menu-right
= _('Settings') %li.dropdown-header
%li = _('Settings')
- 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 %li
%button.btn{ data: { 'gl-modal-action': 'deactivate', - if user.ldap_blocked?
url: deactivate_admin_user_path(user), %span.small
username: sanitize_name(user.name) } } = s_('AdminUsers|Cannot unblock LDAP blocked users')
= s_('AdminUsers|Deactivate') - elsif user.blocked?
- elsif user.deactivated? = link_to _('Unblock'), unblock_admin_user_path(user), method: :put
%li - else
= link_to _('Activate'), activate_admin_user_path(user), method: :put %button.btn{ data: { 'gl-modal-action': 'block',
- if user.access_locked? url: block_admin_user_path(user),
%li username: sanitize_name(user.name) } }
= link_to _('Unlock'), unlock_admin_user_path(user), method: :put, data: { confirm: _('Are you sure?') } = s_('AdminUsers|Block')
- if can?(current_user, :destroy_user, user) - if user.can_be_deactivated?
%li.divider %li
- if user.can_be_removed? %button.btn{ data: { 'gl-modal-action': 'deactivate',
%li url: deactivate_admin_user_path(user),
%button.delete-user-button.btn.text-danger{ data: { 'gl-modal-action': 'delete', username: sanitize_name(user.name) } }
delete_user_url: admin_user_path(user), = s_('AdminUsers|Deactivate')
block_user_url: block_admin_user_path(user), - elsif user.deactivated?
username: sanitize_name(user.name) } } %li
= s_('AdminUsers|Delete user') = link_to _('Activate'), activate_admin_user_path(user), method: :put
%li - if user.access_locked?
%button.delete-user-button.btn.text-danger{ data: { 'gl-modal-action': 'delete-with-contributions', %li
delete_user_url: admin_user_path(user, hard_delete: true), = link_to _('Unlock'), unlock_admin_user_path(user), method: :put, data: { confirm: _('Are you sure?') }
block_user_url: block_admin_user_path(user), - if can?(current_user, :destroy_user, user)
username: sanitize_name(user.name) } } %li.divider
= s_('AdminUsers|Delete user and contributions') - 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')
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protected-variables') } - 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 } = 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? - 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} } #js-ci-project-variables{ data: { endpoint: save_endpoint, project_id: @project&.id || '', group: is_group.to_s, maskable_regex: ci_variable_maskable_regex} }
......
---
title: Hide admin user actions for ghost and bot users
merge_request: 27162
author:
type: fixed
---
title: New package list is enabled which includes filtering by type
merge_request: 18860
author:
type: added
---
title: Update UI for project and group settings CI variables
merge_request: 27411
author:
type: added
---
title: Add forking_access_level to projects API
merge_request: 27514
author: Mathieu Parent
type: added
---
title: Update UI for project and group settings CI variables
merge_request: 26901
author:
type: added
...@@ -23,9 +23,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/ap ...@@ -23,9 +23,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/ap
Example response: Example response:
```json ```json
[ { "issues_count": 10 }
{ issues_count : 10 }
]
``` ```
## Get count of recently created merge requests for group ## 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 ...@@ -49,7 +47,5 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/ap
Example response: Example response:
```json ```json
[ { "merge_requests_count": 10 }
{ merge_requests_count : 10 }
]
``` ```
...@@ -1012,6 +1012,7 @@ POST /projects ...@@ -1012,6 +1012,7 @@ POST /projects
| `issues_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `issues_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `repository_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` | | `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` | | `builds_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `wiki_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` | | `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` |
...@@ -1080,6 +1081,7 @@ POST /projects/user/:user_id ...@@ -1080,6 +1081,7 @@ POST /projects/user/:user_id
| `issues_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `issues_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `repository_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` | | `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` | | `builds_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `wiki_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` | | `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` |
...@@ -1147,6 +1149,7 @@ PUT /projects/:id ...@@ -1147,6 +1149,7 @@ PUT /projects/:id
| `issues_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `issues_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `repository_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` | | `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` | | `builds_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `wiki_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` | | `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` |
......
...@@ -202,7 +202,7 @@ so they don't have to be re-fetched from the public internet. ...@@ -202,7 +202,7 @@ so they don't have to be re-fetched from the public internet.
NOTE: **Note:** NOTE: **Note:**
For more examples, check out our [GitLab CI/CD 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 ### Caching Node.js dependencies
...@@ -214,7 +214,7 @@ we tell npm to use `./.npm` instead, and it is cached per-branch: ...@@ -214,7 +214,7 @@ we tell npm to use `./.npm` instead, and it is cached per-branch:
```yaml ```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 image: node:latest
...@@ -241,7 +241,7 @@ are cached per-branch: ...@@ -241,7 +241,7 @@ are cached per-branch:
```yaml ```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 image: php:7.2
...@@ -270,7 +270,7 @@ pip's cache is defined under `.cache/pip/` and both are cached per-branch: ...@@ -270,7 +270,7 @@ pip's cache is defined under `.cache/pip/` and both are cached per-branch:
```yaml ```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 image: python:latest
...@@ -310,7 +310,7 @@ jobs inherit it. Gems are installed in `vendor/ruby/` and are cached per-branch: ...@@ -310,7 +310,7 @@ jobs inherit it. Gems are installed in `vendor/ruby/` and are cached per-branch:
```yaml ```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 image: ruby:2.6
......
...@@ -45,7 +45,7 @@ In addition, pipelines for merged results have the following limitations: ...@@ -45,7 +45,7 @@ In addition, pipelines for merged results have the following limitations:
see [#11934](https://gitlab.com/gitlab-org/gitlab/issues/11934). see [#11934](https://gitlab.com/gitlab-org/gitlab/issues/11934).
- This feature is not available for - This feature is not available for
[fast forward merges](../../../user/project/merge_requests/fast_forward_merge.md) yet. [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 ## Enabling Pipelines for Merged Results
......
...@@ -59,6 +59,7 @@ module API ...@@ -59,6 +59,7 @@ module API
expose(:issues_access_level) { |project, options| project.project_feature.string_access_level(:issues) } 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(: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(: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(: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(: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) } expose(:snippets_access_level) { |project, options| project.project_feature.string_access_level(:snippets) }
......
...@@ -24,6 +24,7 @@ module API ...@@ -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 :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 :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 :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 :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 :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`' 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 ...@@ -104,6 +105,7 @@ module API
:default_branch, :default_branch,
:description, :description,
:emails_disabled, :emails_disabled,
:forking_access_level,
:issues_access_level, :issues_access_level,
:lfs_enabled, :lfs_enabled,
:merge_requests_access_level, :merge_requests_access_level,
......
...@@ -16,10 +16,6 @@ module Gitlab ...@@ -16,10 +16,6 @@ module Gitlab
Gitlab::Graphql::ExternallyPaginatedArray, Gitlab::Graphql::ExternallyPaginatedArray,
Gitlab::Graphql::Connections::ExternallyPaginatedArrayConnection Gitlab::Graphql::Connections::ExternallyPaginatedArrayConnection
) )
GraphQL::Relay::BaseConnection.register_connection_implementation(
Gitlab::Graphql::Pagination::Relations::OffsetActiveRecordRelation,
Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection
)
end end
end end
end end
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
module Gitlab module Gitlab
module Graphql module Graphql
module Pagination module Pagination
class OffsetActiveRecordRelationConnection < GraphQL::Relay::RelationConnection class OffsetActiveRecordRelationConnection < GraphQL::Pagination::ActiveRecordRelationConnection
end end
end end
end end
......
# frozen_string_literal: true
module Gitlab
module Graphql
module Pagination
module Relations
class OffsetActiveRecordRelation < ::ActiveRecord::Relation
end
end
end
end
end
...@@ -5274,6 +5274,9 @@ msgstr "" ...@@ -5274,6 +5274,9 @@ msgstr ""
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept." msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr "" msgstr ""
msgid "ContainerRegistry|Build an image"
msgstr ""
msgid "ContainerRegistry|Container Registry" msgid "ContainerRegistry|Container Registry"
msgstr "" msgstr ""
...@@ -5328,12 +5331,18 @@ msgstr "" ...@@ -5328,12 +5331,18 @@ msgstr ""
msgid "ContainerRegistry|Last Updated" msgid "ContainerRegistry|Last Updated"
msgstr "" msgstr ""
msgid "ContainerRegistry|Login"
msgstr ""
msgid "ContainerRegistry|Missing or insufficient permission, delete button disabled" msgid "ContainerRegistry|Missing or insufficient permission, delete button disabled"
msgstr "" msgstr ""
msgid "ContainerRegistry|Number of tags to retain:" msgid "ContainerRegistry|Number of tags to retain:"
msgstr "" msgstr ""
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start" msgid "ContainerRegistry|Quick Start"
msgstr "" msgstr ""
......
# 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
...@@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; ...@@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui'; import { GlSprintf } from '@gitlab/ui';
import { GlEmptyState } from '../stubs'; import { GlEmptyState } from '../stubs';
import projectEmptyState from '~/registry/explorer/components/project_empty_state.vue'; import projectEmptyState from '~/registry/explorer/components/project_empty_state.vue';
import * as getters from '~/registry/explorer/stores/getters';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -23,6 +24,7 @@ describe('Registry Project Empty state', () => { ...@@ -23,6 +24,7 @@ describe('Registry Project Empty state', () => {
noContainersImage: 'bazFoo', noContainersImage: 'bazFoo',
}, },
}, },
getters,
}); });
wrapper = shallowMount(projectEmptyState, { wrapper = shallowMount(projectEmptyState, {
localVue, localVue,
......
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]);
});
});
});
...@@ -3,6 +3,9 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; ...@@ -3,6 +3,9 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlPagination, GlSkeletonLoader, GlSprintf } from '@gitlab/ui'; import { GlPagination, GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import component from '~/registry/explorer/pages/list.vue'; 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 store from '~/registry/explorer/stores/';
import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/'; import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/';
import { imagesListResponse } from '../mock_data'; import { imagesListResponse } from '../mock_data';
...@@ -24,6 +27,9 @@ describe('List Page', () => { ...@@ -24,6 +27,9 @@ describe('List Page', () => {
const findDetailsLink = () => wrapper.find({ ref: 'detailsLink' }); const findDetailsLink = () => wrapper.find({ ref: 'detailsLink' });
const findClipboardButton = () => wrapper.find({ ref: 'clipboardButton' }); const findClipboardButton = () => wrapper.find({ ref: 'clipboardButton' });
const findPagination = () => wrapper.find(GlPagination); const findPagination = () => wrapper.find(GlPagination);
const findQuickStartDropdown = () => wrapper.find(QuickstartDropdown);
const findProjectEmptyState = () => wrapper.find(ProjectEmptyState);
const findGroupEmptyState = () => wrapper.find(GroupEmptyState);
beforeEach(() => { beforeEach(() => {
wrapper = shallowMount(component, { wrapper = shallowMount(component, {
...@@ -76,7 +82,7 @@ describe('List Page', () => { ...@@ -76,7 +82,7 @@ describe('List Page', () => {
}); });
}); });
describe('when isLoading is true', () => { describe('isLoading is true', () => {
beforeAll(() => store.commit(SET_MAIN_LOADING, true)); beforeAll(() => store.commit(SET_MAIN_LOADING, true));
afterAll(() => store.commit(SET_MAIN_LOADING, false)); afterAll(() => store.commit(SET_MAIN_LOADING, false));
...@@ -88,9 +94,49 @@ describe('List Page', () => { ...@@ -88,9 +94,49 @@ describe('List Page', () => {
it('imagesList is not visible', () => { it('imagesList is not visible', () => {
expect(findImagesList().exists()).toBe(false); 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', () => { describe('listElement', () => {
let listElements; let listElements;
let firstElement; let firstElement;
......
...@@ -31,4 +31,22 @@ describe('Getters RegistryExplorer store', () => { ...@@ -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(' '));
});
});
}); });
...@@ -52,12 +52,6 @@ describe GitlabSchema do ...@@ -52,12 +52,6 @@ describe GitlabSchema do
expect(connection).to eq(Gitlab::Graphql::Connections::FilterableArrayConnection) expect(connection).to eq(Gitlab::Graphql::Connections::FilterableArrayConnection)
end 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 describe '.execute' do
context 'for different types of users' do context 'for different types of users' do
context 'when no context' do context 'when no context' do
......
...@@ -4,6 +4,6 @@ require 'spec_helper' ...@@ -4,6 +4,6 @@ require 'spec_helper'
describe Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection do describe Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection do
it 'subclasses from GraphQL::Relay::RelationConnection' 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
end end
...@@ -726,6 +726,7 @@ describe API::Projects do ...@@ -726,6 +726,7 @@ describe API::Projects do
issues_enabled: false, issues_enabled: false,
jobs_enabled: false, jobs_enabled: false,
merge_requests_enabled: false, merge_requests_enabled: false,
forking_access_level: 'disabled',
wiki_enabled: false, wiki_enabled: false,
resolve_outdated_diff_discussions: false, resolve_outdated_diff_discussions: false,
remove_source_branch_after_merge: true, remove_source_branch_after_merge: true,
...@@ -1400,6 +1401,7 @@ describe API::Projects do ...@@ -1400,6 +1401,7 @@ describe API::Projects do
expect(json_response['repository_access_level']).to be_present expect(json_response['repository_access_level']).to be_present
expect(json_response['issues_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['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['wiki_access_level']).to be_present
expect(json_response['builds_access_level']).to be_present expect(json_response['builds_access_level']).to be_present
expect(json_response).to have_key('emails_disabled') expect(json_response).to have_key('emails_disabled')
......
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment