Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-04 09:09:43 +00:00
parent 704b3dfa40
commit 57f8f3552c
23 changed files with 432 additions and 281 deletions

View file

@ -142,6 +142,7 @@ export default {
},
onReachingListBottom() {
if (!this.loadingMore && this.hasNextPage) {
this.showCount = true;
this.loadNextPage();
}
},
@ -150,12 +151,19 @@ export default {
},
handleDragOnEnd(params) {
sortableEnd();
const { newIndex, oldIndex, from, to, item } = params;
const { oldIndex, from, to, item } = params;
let { newIndex } = params;
const { itemId, itemIid, itemPath } = item.dataset;
const { children } = to;
let { children } = to;
let moveBeforeId;
let moveAfterId;
children = Array.from(children).filter((card) => card.classList.contains('board-card'));
if (newIndex > children.length) {
newIndex = children.length;
}
const getItemId = (el) => Number(el.dataset.itemId);
// If item is being moved within the same list
@ -218,6 +226,7 @@ export default {
:data-board="list.id"
:data-board-type="list.listType"
:class="{ 'bg-danger-100': boardItemsSizeExceedsMax }"
draggable=".board-card"
class="board-list gl-w-full gl-h-full gl-list-style-none gl-mb-0 gl-p-2 js-board-list"
data-testid="tree-root-wrapper"
@start="handleDragOnStart"
@ -232,17 +241,17 @@ export default {
:item="item"
:disabled="disabled"
/>
<li v-if="showCount" class="board-list-count gl-text-center" data-issue-id="-1">
<gl-loading-icon
v-if="loadingMore"
:label="$options.i18n.loadingMoreboardItems"
data-testid="count-loading-icon"
/>
<span v-if="showingAllItems">{{ showingAllItemsText }}</span>
<gl-intersection-observer v-else @appear="onReachingListBottom">
<span>{{ paginatedIssueText }}</span>
</gl-intersection-observer>
</li>
<gl-intersection-observer @appear="onReachingListBottom">
<li v-if="showCount" class="board-list-count gl-text-center" data-issue-id="-1">
<gl-loading-icon
v-if="loadingMore"
:label="$options.i18n.loadingMoreboardItems"
data-testid="count-loading-icon"
/>
<span v-if="showingAllItems">{{ showingAllItemsText }}</span>
<span v-else>{{ paginatedIssueText }}</span>
</li>
</gl-intersection-observer>
</component>
</div>
</template>

View file

@ -69,7 +69,7 @@ export default {
class="form-control dropdown-input-field"
@input="searchBranches"
/>
<gl-icon :size="18" name="search" class="ml-3 input-icon" use-deprecated-sizes />
<gl-icon name="search" class="gl-ml-5 gl-mt-1 input-icon" />
</label>
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<gl-loading-icon

View file

@ -1,25 +1,18 @@
<script>
import {
GlFilteredSearchToken,
GlAvatar,
GlFilteredSearchSuggestion,
GlDropdownDivider,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { GlAvatar, GlFilteredSearchSuggestion } from '@gitlab/ui';
import createFlash from '~/flash';
import { __ } from '~/locale';
import { DEFAULT_LABEL_ANY, DEBOUNCE_DELAY } from '../constants';
import { DEFAULT_LABEL_ANY } from '../constants';
import BaseToken from './base_token.vue';
export default {
components: {
GlFilteredSearchToken,
BaseToken,
GlAvatar,
GlFilteredSearchSuggestion,
GlDropdownDivider,
GlLoadingIcon,
},
props: {
config: {
@ -30,45 +23,35 @@ export default {
type: Object,
required: true,
},
active: {
type: Boolean,
required: true,
},
},
data() {
return {
authors: this.config.initialAuthors || [],
defaultAuthors: this.config.defaultAuthors || [DEFAULT_LABEL_ANY],
loading: true,
preloadedAuthors: [
{
id: gon.current_user_id,
name: gon.current_user_fullname,
username: gon.current_username,
avatar_url: gon.current_user_avatar_url,
},
],
loading: false,
};
},
computed: {
currentUser() {
return {
id: gon.current_user_id,
name: gon.current_user_fullname,
username: gon.current_username,
avatar_url: gon.current_user_avatar_url,
};
},
currentValue() {
return this.value.data.toLowerCase();
},
activeAuthor() {
return this.authors.find((author) => author.username.toLowerCase() === this.currentValue);
},
activeAuthorAvatar() {
return this.avatarUrl(this.activeAuthor);
},
},
watch: {
active: {
immediate: true,
handler(newValue) {
if (!newValue && !this.authors.length) {
this.fetchAuthorBySearchTerm(this.value.data);
}
},
},
},
methods: {
getActiveAuthor(authors, currentValue) {
return authors.find((author) => author.username.toLowerCase() === currentValue);
},
getAvatarUrl(author) {
return author.avatarUrl || author.avatar_url;
},
fetchAuthorBySearchTerm(searchTerm) {
this.loading = true;
const fetchPromise = this.config.fetchPath
? this.config.fetchAuthors(this.config.fetchPath, searchTerm)
: this.config.fetchAuthors(searchTerm);
@ -89,69 +72,47 @@ export default {
this.loading = false;
});
},
avatarUrl(author) {
return author.avatarUrl || author.avatar_url;
},
searchAuthors: debounce(function debouncedSearch({ data }) {
this.fetchAuthorBySearchTerm(data);
}, DEBOUNCE_DELAY),
},
};
</script>
<template>
<gl-filtered-search-token
:config="config"
v-bind="{ ...$props, ...$attrs }"
v-on="$listeners"
@input="searchAuthors"
<base-token
:token-config="config"
:token-value="value"
:token-active="active"
:tokens-list-loading="loading"
:token-values="authors"
:fn-active-token-value="getActiveAuthor"
:default-token-values="defaultAuthors"
:preloaded-token-values="preloadedAuthors"
:recent-token-values-storage-key="config.recentTokenValuesStorageKey"
@fetch-token-values="fetchAuthorBySearchTerm"
>
<template #view="{ inputValue }">
<template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
<gl-avatar
v-if="activeAuthor"
v-if="activeTokenValue"
:size="16"
:src="activeAuthorAvatar"
:src="getAvatarUrl(activeTokenValue)"
shape="circle"
class="gl-mr-2"
/>
<span>{{ activeAuthor ? activeAuthor.name : inputValue }}</span>
<span>{{ activeTokenValue ? activeTokenValue.name : inputValue }}</span>
</template>
<template #suggestions>
<template #token-values-list="{ tokenValues }">
<gl-filtered-search-suggestion
v-for="author in defaultAuthors"
:key="author.value"
:value="author.value"
v-for="author in tokenValues"
:key="author.username"
:value="author.username"
>
{{ author.text }}
<div class="gl-display-flex">
<gl-avatar :size="32" :src="getAvatarUrl(author)" />
<div>
<div>{{ author.name }}</div>
<div>@{{ author.username }}</div>
</div>
</div>
</gl-filtered-search-suggestion>
<gl-dropdown-divider v-if="defaultAuthors.length" />
<template v-if="loading">
<gl-filtered-search-suggestion v-if="currentUser.id" :value="currentUser.username">
<div class="gl-display-flex">
<gl-avatar :size="32" :src="avatarUrl(currentUser)" />
<div>
<div>{{ currentUser.name }}</div>
<div>@{{ currentUser.username }}</div>
</div>
</div>
</gl-filtered-search-suggestion>
<gl-loading-icon class="gl-mt-3" />
</template>
<template v-else>
<gl-filtered-search-suggestion
v-for="author in authors"
:key="author.username"
:value="author.username"
>
<div class="d-flex">
<gl-avatar :size="32" :src="avatarUrl(author)" />
<div>
<div>{{ author.name }}</div>
<div>@{{ author.username }}</div>
</div>
</div>
</gl-filtered-search-suggestion>
</template>
</template>
</gl-filtered-search-token>
</base-token>
</template>

View file

@ -48,6 +48,11 @@ export default {
required: false,
default: () => [],
},
preloadedTokenValues: {
type: Array,
required: false,
default: () => [],
},
recentTokenValuesStorageKey: {
type: String,
required: false,
@ -158,6 +163,11 @@ export default {
<slot name="token-values-list" :token-values="recentTokenValues"></slot>
<gl-dropdown-divider />
</template>
<slot
v-if="preloadedTokenValues.length"
name="token-values-list"
:token-values="preloadedTokenValues"
></slot>
<gl-loading-icon v-if="tokensListLoading" />
<template v-else>
<slot name="token-values-list" :token-values="availableTokenValues"></slot>

View file

@ -47,6 +47,14 @@ $top-level-item-color: $purple-900;
@include context-header-collapsed;
.context-header {
@include gl-h-auto;
a {
@include gl-p-2;
}
}
.sidebar-top-level-items > li {
.sidebar-sub-level-items {
&:not(.flyout-list) {
@ -60,17 +68,16 @@ $top-level-item-color: $purple-900;
}
.toggle-sidebar-button {
padding: 16px;
width: $contextual-sidebar-collapsed-width - 1px;
width: $contextual-sidebar-collapsed-width;
.collapse-text,
.icon-chevron-double-lg-left {
.collapse-text {
display: none;
}
.icon-chevron-double-lg-right {
display: block;
margin: 0;
.icon-chevron-double-lg-left {
@include gl-rotate-180;
@include gl-display-block; // TODO: shouldn't be needed after the flag roll out
@include gl-m-0;
}
}
}
@ -83,12 +90,13 @@ $top-level-item-color: $purple-900;
}
.badge.badge-pill:not(.fly-out-badge),
.nav-item-name {
.nav-item-name,
.collapse-text {
@include gl-sr-only;
}
.sidebar-top-level-items > li > a {
min-height: 45px;
min-height: unset;
}
.fly-out-top-item {
@ -98,6 +106,10 @@ $top-level-item-color: $purple-900;
.avatar-container {
margin: 0 auto;
}
li.active > a {
background-color: $indigo-900-alpha-008;
}
}
@mixin sub-level-items-flyout {
@ -158,6 +170,7 @@ $top-level-item-color: $purple-900;
@include gl-p-2;
@include gl-mb-2;
@include gl-mt-0;
.avatar-container {
@include gl-font-weight-normal;
@ -187,7 +200,6 @@ $top-level-item-color: $purple-900;
@include gl-align-items-center;
@include gl-rounded-base;
@include gl-w-auto;
transition: padding $sidebar-transition-duration;
margin: $sidebar-top-item-tb-margin $sidebar-top-item-lr-margin;
&:hover {
@ -226,22 +238,16 @@ $top-level-item-color: $purple-900;
//
.nav-sidebar {
@include gl-fixed;
@include gl-bottom-0;
@include gl-left-0;
transition: width $sidebar-transition-duration, left $sidebar-transition-duration;
position: fixed;
z-index: 600;
width: $contextual-sidebar-width;
top: $header-height;
bottom: 0;
left: 0;
background-color: $gray-50;
transform: translate3d(0, 0, 0);
&:not(.sidebar-collapsed-desktop) {
@media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) {
box-shadow: inset -1px 0 0 $border-color, 2px 1px 3px $dropdown-shadow-color;
}
}
&.sidebar-collapsed-desktop {
@include collapse-contextual-sidebar;
}
@ -380,7 +386,11 @@ $top-level-item-color: $purple-900;
.close-nav-button {
@include side-panel-toggle;
background-color: $gray-50;
border-top: 1px solid $border-color;
color: $top-level-item-color;
position: fixed;
bottom: 0;
width: $contextual-sidebar-width;
.collapse-text,
.icon-chevron-double-lg-left,
@ -389,22 +399,6 @@ $top-level-item-color: $purple-900;
}
}
.toggle-sidebar-button,
.close-nav-button {
position: fixed;
bottom: 0;
width: $contextual-sidebar-width - 1px;
border-top: 1px solid $border-color;
svg {
margin-right: 8px;
}
.icon-chevron-double-lg-right {
display: none;
}
}
.collapse-text {
white-space: nowrap;
overflow: hidden;

View file

@ -9,7 +9,7 @@ $sidebar-transition-duration: 0.3s;
$sidebar-breakpoint: 1024px;
$default-transition-duration: 0.15s;
$contextual-sidebar-width: 220px;
$contextual-sidebar-collapsed-width: 50px;
$contextual-sidebar-collapsed-width: 48px;
$toggle-sidebar-height: 48px;
/**

View file

@ -6,10 +6,6 @@
width: 240px;
}
.rule-elapsed-minutes {
width: 56px;
}
.rule-close-icon {
right: 1rem;
}

View file

@ -1,9 +1,12 @@
- avatar_size = sidebar_refactor_disabled? ? 24 : 18
- avatar_size_class = sidebar_refactor_disabled? ? 's40' : 's32'
%aside.nav-sidebar.qa-admin-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), 'aria-label': _('Admin navigation') }
.nav-sidebar-inner-scroll
.context-header
= link_to admin_root_path, title: _('Admin Overview') do
%span.avatar-container.rect-avatar.s32.settings-avatar
= sprite_icon('admin', size: 18)
%span{ class: ['avatar-container', 'settings-avatar', 'rect-avatar', avatar_size_class] }
= sprite_icon('admin', size: avatar_size)
%span.sidebar-context-title
= _('Admin Area')
%ul.sidebar-top-level-items{ data: { qa_selector: 'admin_sidebar_overview_submenu_content' } }

View file

@ -1,5 +1,9 @@
- avatar_size_class = sidebar_refactor_disabled? ? 's40' : 's32'
- avatar_classes = ['avatar-container', 'rect-avatar', 'group-avatar']
- avatar_classes << avatar_size_class
= link_to group_path(@group), title: @group.name do
%span.avatar-container.rect-avatar.s32.group-avatar
= group_icon(@group, class: "avatar s32 avatar-tile")
%span{ class: avatar_classes }
= group_icon(@group, class: ['avatar', 'avatar-tile', avatar_size_class])
%span.sidebar-context-title
= @group.name

View file

@ -1,9 +1,12 @@
- avatar_size = sidebar_refactor_disabled? ? 40 : 32
- avatar_size_class = sidebar_refactor_disabled? ? 's40' : 's32'
%aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(current_user), 'aria-label': _('User settings') }
.nav-sidebar-inner-scroll
.context-header
= link_to profile_path, title: _('Profile Settings') do
%span.avatar-container.s32.settings-avatar
= image_tag avatar_icon_for_user(current_user, 32), class: "avatar s32 avatar-tile js-sidebar-user-avatar", alt: current_user.name, data: { testid: 'sidebar-user-avatar' }
%span{ class: ['avatar-container', 'settings-avatar', avatar_size_class] }
= image_tag avatar_icon_for_user(current_user, avatar_size), class: ['avatar', 'avatar-tile', 'js-sidebar-user-avatar', avatar_size_class], alt: current_user.name, data: { testid: 'sidebar-user-avatar' }
%span.sidebar-context-title= _('User Settings')
%ul.sidebar-top-level-items
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do

View file

@ -22,15 +22,15 @@
.account-well.gl-mb-3
%ul
%li
= _('Your Primary Email will be used for avatar detection.')
- profile_message = _('Your primary email is used for avatar detection. You can change it in your %{openingTag}profile settings%{closingTag}.') % { openingTag: "<a href='#{profile_path}'>".html_safe, closingTag: '</a>'.html_safe}
= profile_message.html_safe
%li
= _('Your Commit Email will be used for web based operations, such as edits and merges.')
= _('Your commit email is used for web based operations, such as edits and merges.')
%li
- address = profile_notifications_path
- notification_message = _('Your Default Notification Email will be used for account notifications if a %{openingTag}group-specific email address%{closingTag} is not set.') % { openingTag: "<a href='#{address}'>".html_safe, closingTag: '</a>'.html_safe}
- notification_message = _('Your default notification email is used for account notifications if a %{openingTag}group-specific email address%{closingTag} is not set.') % { openingTag: "<a href='#{profile_notifications_path}'>".html_safe, closingTag: '</a>'.html_safe}
= notification_message.html_safe
%li
= _('Your Public Email will be displayed on your public profile.')
= _('Your public email will be displayed on your public profile.')
%li
= _('All email addresses will be used to identify your commits.')
%ul.content-list

View file

@ -1,8 +1,9 @@
%a.toggle-sidebar-button.js-toggle-sidebar.qa-toggle-sidebar.rspec-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
= sprite_icon('chevron-double-lg-left', css_class: 'icon-chevron-double-lg-left')
= sprite_icon('chevron-double-lg-right', css_class: 'icon-chevron-double-lg-right')
%span.collapse-text= _("Collapse sidebar")
- if sidebar_refactor_disabled?
= sprite_icon('chevron-double-lg-right', css_class: 'icon-chevron-double-lg-right')
%span.collapse-text.gl-ml-3= _("Collapse sidebar")
= button_tag class: 'close-nav-button', type: 'button' do
= sprite_icon('close')
%span.collapse-text= _("Close sidebar")
%span.collapse-text.gl-ml-3= _("Close sidebar")

View file

@ -1,5 +1,8 @@
- avatar_size = sidebar_refactor_disabled? ? 40 : 32
- avatar_size_class = sidebar_refactor_disabled? ? 's40' : 's32'
= link_to scope_menu.link, **scope_menu.container_html_options do
%span.avatar-container.rect-avatar.s32.project-avatar
= source_icon(scope_menu.container, alt: scope_menu.title, class: 'avatar s32 avatar-tile', width: 32, height: 32)
%span{ class: ['avatar-container', 'rect-avatar', 'project-avatar', avatar_size_class] }
= source_icon(scope_menu.container, alt: scope_menu.title, class: ['avatar', 'avatar-tile', avatar_size_class], width: avatar_size, height: avatar_size)
%span.sidebar-context-title
= scope_menu.title

View file

@ -62,7 +62,7 @@ investigate it for potential threats by
The **Threat Monitoring** page's **Policy** tab displays deployed
network policies for all available environments. You can check a
network policy's `yaml` manifest, toggle the policy's enforcement
network policy's `yaml` manifest, its enforcement
status, and create and edit deployed policies. This section has the
following prerequisites:
@ -71,8 +71,7 @@ following prerequisites:
Network policies are fetched directly from the selected environment's
deployment platform. Changes performed outside of this tab are
reflected upon refresh. Enforcement status changes are deployed
directly to a deployment namespace of the selected environment.
reflected upon refresh.
By default, the network policy list contains predefined policies in a
disabled state. Once enabled, a predefined policy deploys to the
@ -89,8 +88,9 @@ users must make changes by following the
To change a network policy's enforcement status:
- Click the network policy you want to update.
- Click the **Enforcement status** toggle to update the selected policy.
- Click the **Apply changes** button to deploy network policy changes.
- Click the **Edit policy** button.
- Click the **Policy status** toggle to update the selected policy.
- Click the **Save changes** button to deploy network policy changes.
Disabled network policies have the `network-policy.gitlab.com/disabled_by: gitlab` selector inside
the `podSelector` block. This narrows the scope of such a policy and as a result it doesn't affect

View file

@ -74,6 +74,11 @@ module API
save_current_user_in_env(@current_user) if @current_user
if @current_user
::Gitlab::Database::LoadBalancing::RackMiddleware
.stick_or_unstick(env, :user, @current_user.id)
end
@current_user
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables

View file

@ -34,8 +34,15 @@ module API
end
def current_runner
token = params[:token]
if token
::Gitlab::Database::LoadBalancing::RackMiddleware
.stick_or_unstick(env, :runner, token)
end
strong_memoize(:current_runner) do
::Ci::Runner.find_by_token(params[:token].to_s)
::Ci::Runner.find_by_token(token.to_s)
end
end
@ -65,8 +72,15 @@ module API
end
def current_job
id = params[:id]
if id
::Gitlab::Database::LoadBalancing::RackMiddleware
.stick_or_unstick(env, :build, id)
end
strong_memoize(:current_job) do
::Ci::Build.find_by_id(params[:id])
::Ci::Build.find_by_id(id)
end
end

View file

@ -4035,9 +4035,6 @@ msgstr ""
msgid "Apply a template"
msgstr ""
msgid "Apply changes"
msgstr ""
msgid "Apply suggestion"
msgstr ""
@ -13111,6 +13108,9 @@ msgstr ""
msgid "EscalationPolicies|Edit escalation policy"
msgstr ""
msgid "EscalationPolicies|Elapsed time must be greater than or equal to zero."
msgstr ""
msgid "EscalationPolicies|Email on-call user in schedule"
msgstr ""
@ -21851,7 +21851,7 @@ msgstr ""
msgid "NetworkPolicies|Are you sure you want to delete this policy? This action cannot be undone."
msgstr ""
msgid "NetworkPolicies|Choose whether to enforce this policy."
msgid "NetworkPolicies|Container runtime"
msgstr ""
msgid "NetworkPolicies|Create policy"
@ -37821,12 +37821,6 @@ msgstr ""
msgid "Your CSV import for project"
msgstr ""
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
msgid "Your Default Notification Email will be used for account notifications if a %{openingTag}group-specific email address%{closingTag} is not set."
msgstr ""
msgid "Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. View how you compare with other organizations, discover features you are not using, and learn best practices through blog posts and white papers."
msgstr ""
@ -37854,18 +37848,12 @@ msgstr ""
msgid "Your Personal Access Token was revoked"
msgstr ""
msgid "Your Primary Email will be used for avatar detection."
msgstr ""
msgid "Your Projects (default)"
msgstr ""
msgid "Your Projects' Activity"
msgstr ""
msgid "Your Public Email will be displayed on your public profile."
msgstr ""
msgid "Your SSH key has expired"
msgstr ""
@ -37953,12 +37941,18 @@ msgstr ""
msgid "Your comment will be discarded."
msgstr ""
msgid "Your commit email is used for web based operations, such as edits and merges."
msgstr ""
msgid "Your dashboard has been copied. You can %{web_ide_link_start}edit it here%{web_ide_link_end}."
msgstr ""
msgid "Your dashboard has been updated. You can %{web_ide_link_start}edit it here%{web_ide_link_end}."
msgstr ""
msgid "Your default notification email is used for account notifications if a %{openingTag}group-specific email address%{closingTag} is not set."
msgstr ""
msgid "Your deployment services will be broken, you will need to manually fix the services after renaming."
msgstr ""
@ -38043,6 +38037,9 @@ msgstr ""
msgid "Your personal access tokens will expire in %{days_to_expire} days or less"
msgstr ""
msgid "Your primary email is used for avatar detection. You can change it in your %{openingTag}profile settings%{closingTag}."
msgstr ""
msgid "Your profile"
msgstr ""
@ -38052,6 +38049,9 @@ msgstr ""
msgid "Your projects"
msgstr ""
msgid "Your public email will be displayed on your public profile."
msgstr ""
msgid "Your request for access could not be processed: %{error_meesage}"
msgstr ""

View file

@ -157,7 +157,7 @@ RSpec.describe 'Issue Boards', :js do
end
it 'moves to bottom of another list' do
drag(list_from_index: 1, list_to_index: 2, to_index: 2, duration: 1020)
drag(list_from_index: 1, list_to_index: 2, to_index: 3, duration: 1020)
wait_for_requests

View file

@ -3,15 +3,12 @@
require 'spec_helper'
RSpec.describe 'Projects > Wiki > User views wiki in project page' do
let(:user) { create(:user) }
before do
project.add_maintainer(user)
sign_in(user)
sign_in(project.owner)
end
context 'when repository is disabled for project' do
let(:project) do
let_it_be(:project) do
create(:project,
:wiki_repo,
:repository_disabled,

View file

@ -1,9 +1,8 @@
import {
GlFilteredSearchToken,
GlFilteredSearchTokenSegment,
GlFilteredSearchSuggestion,
GlDropdownDivider,
GlLoadingIcon,
GlAvatar,
} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
@ -16,6 +15,7 @@ import {
DEFAULT_NONE_ANY,
} from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { mockAuthorToken, mockAuthors } from '../mock_data';
@ -62,6 +62,8 @@ describe('AuthorToken', () => {
let mock;
let wrapper;
const getBaseToken = () => wrapper.findComponent(BaseToken);
beforeEach(() => {
window.gon = {
...originalGon,
@ -79,104 +81,127 @@ describe('AuthorToken', () => {
wrapper.destroy();
});
describe('computed', () => {
describe('currentValue', () => {
it('returns lowercase string for `value.data`', () => {
wrapper = createComponent({ value: { data: 'FOO' } });
expect(wrapper.vm.currentValue).toBe('foo');
describe('methods', () => {
describe('fetchAuthorBySearchTerm', () => {
beforeEach(() => {
wrapper = createComponent();
});
});
describe('activeAuthor', () => {
it('returns object for currently present `value.data`', async () => {
wrapper = createComponent({ value: { data: mockAuthors[0].username } });
it('calls `config.fetchAuthors` with provided searchTerm param', () => {
jest.spyOn(wrapper.vm.config, 'fetchAuthors');
wrapper.setData({
authors: mockAuthors,
});
getBaseToken().vm.$emit('fetch-token-values', mockAuthors[0].username);
await wrapper.vm.$nextTick();
expect(wrapper.vm.activeAuthor).toEqual(mockAuthors[0]);
expect(wrapper.vm.config.fetchAuthors).toHaveBeenCalledWith(
mockAuthorToken.fetchPath,
mockAuthors[0].username,
);
});
});
});
describe('fetchAuthorBySearchTerm', () => {
it('calls `config.fetchAuthors` with provided searchTerm param', () => {
wrapper = createComponent();
it('sets response to `authors` when request is succesful', () => {
jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockResolvedValue(mockAuthors);
jest.spyOn(wrapper.vm.config, 'fetchAuthors');
getBaseToken().vm.$emit('fetch-token-values', 'root');
wrapper.vm.fetchAuthorBySearchTerm(mockAuthors[0].username);
expect(wrapper.vm.config.fetchAuthors).toHaveBeenCalledWith(
mockAuthorToken.fetchPath,
mockAuthors[0].username,
);
});
it('sets response to `authors` when request is succesful', () => {
wrapper = createComponent();
jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockResolvedValue(mockAuthors);
wrapper.vm.fetchAuthorBySearchTerm('root');
return waitForPromises().then(() => {
expect(wrapper.vm.authors).toEqual(mockAuthors);
});
});
it('calls `createFlash` with flash error message when request fails', () => {
wrapper = createComponent();
jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
wrapper.vm.fetchAuthorBySearchTerm('root');
return waitForPromises().then(() => {
expect(createFlash).toHaveBeenCalledWith({
message: 'There was a problem fetching users.',
return waitForPromises().then(() => {
expect(getBaseToken().props('tokenValues')).toEqual(mockAuthors);
});
});
});
it('sets `loading` to false when request completes', () => {
wrapper = createComponent();
it('calls `createFlash` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
getBaseToken().vm.$emit('fetch-token-values', 'root');
wrapper.vm.fetchAuthorBySearchTerm('root');
return waitForPromises().then(() => {
expect(createFlash).toHaveBeenCalledWith({
message: 'There was a problem fetching users.',
});
});
});
return waitForPromises().then(() => {
expect(wrapper.vm.loading).toBe(false);
it('sets `loading` to false when request completes', async () => {
jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
getBaseToken().vm.$emit('fetch-token-values', 'root');
await waitForPromises();
expect(getBaseToken().props('tokensListLoading')).toBe(false);
});
});
});
describe('template', () => {
it('renders gl-filtered-search-token component', () => {
wrapper = createComponent({ data: { authors: mockAuthors } });
it('renders base-token component', () => {
wrapper = createComponent({
value: { data: mockAuthors[0].username },
data: { authors: mockAuthors },
});
expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
const baseTokenEl = getBaseToken();
expect(baseTokenEl.exists()).toBe(true);
expect(baseTokenEl.props()).toMatchObject({
tokenValues: mockAuthors,
fnActiveTokenValue: wrapper.vm.getActiveAuthor,
});
});
it('renders token item when value is selected', () => {
wrapper = createComponent({
value: { data: mockAuthors[0].username },
data: { authors: mockAuthors },
stubs: { Portal: true },
});
return wrapper.vm.$nextTick(() => {
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
expect(tokenSegments).toHaveLength(3); // Author, =, "Administrator"
expect(tokenSegments.at(2).text()).toBe(mockAuthors[0].name); // "Administrator"
const tokenValue = tokenSegments.at(2);
expect(tokenValue.findComponent(GlAvatar).props('src')).toBe(mockAuthors[0].avatar_url);
expect(tokenValue.text()).toBe(mockAuthors[0].name); // "Administrator"
});
});
it('renders token value with correct avatarUrl from author object', async () => {
const getAvatarEl = () =>
wrapper.findAll(GlFilteredSearchTokenSegment).at(2).findComponent(GlAvatar);
wrapper = createComponent({
value: { data: mockAuthors[0].username },
data: {
authors: [
{
...mockAuthors[0],
},
],
},
stubs: { Portal: true },
});
await wrapper.vm.$nextTick();
expect(getAvatarEl().props('src')).toBe(mockAuthors[0].avatar_url);
wrapper.setData({
authors: [
{
...mockAuthors[0],
avatarUrl: mockAuthors[0].avatar_url,
avatar_url: undefined,
},
],
});
await wrapper.vm.$nextTick();
expect(getAvatarEl().props('src')).toBe(mockAuthors[0].avatar_url);
});
it('renders provided defaultAuthors as suggestions', async () => {
const defaultAuthors = DEFAULT_NONE_ANY;
wrapper = createComponent({
@ -237,10 +262,6 @@ describe('AuthorToken', () => {
});
});
it('shows loading icon', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('shows current user', () => {
const firstSuggestion = wrapper.findComponent(GlFilteredSearchSuggestion).text();
expect(firstSuggestion).toContain('Administrator');

View file

@ -0,0 +1,69 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Helpers::Runner do
let(:helper) { Class.new { include API::Helpers::Runner }.new }
before do
allow(helper).to receive(:env).and_return({})
end
describe '#current_job' do
let(:build) { create(:ci_build, :running) }
it 'handles sticking of a build when a build ID is specified' do
allow(helper).to receive(:params).and_return(id: build.id)
expect(Gitlab::Database::LoadBalancing::RackMiddleware)
.to receive(:stick_or_unstick)
.with({}, :build, build.id)
helper.current_job
end
it 'does not handle sticking if no build ID was specified' do
allow(helper).to receive(:params).and_return({})
expect(Gitlab::Database::LoadBalancing::RackMiddleware)
.not_to receive(:stick_or_unstick)
helper.current_job
end
it 'returns the build if one could be found' do
allow(helper).to receive(:params).and_return(id: build.id)
expect(helper.current_job).to eq(build)
end
end
describe '#current_runner' do
let(:runner) { create(:ci_runner, token: 'foo') }
it 'handles sticking of a runner if a token is specified' do
allow(helper).to receive(:params).and_return(token: runner.token)
expect(Gitlab::Database::LoadBalancing::RackMiddleware)
.to receive(:stick_or_unstick)
.with({}, :runner, runner.token)
helper.current_runner
end
it 'does not handle sticking if no token was specified' do
allow(helper).to receive(:params).and_return({})
expect(Gitlab::Database::LoadBalancing::RackMiddleware)
.not_to receive(:stick_or_unstick)
helper.current_runner
end
it 'returns the runner if one could be found' do
allow(helper).to receive(:params).and_return(token: runner.token)
expect(helper.current_runner).to eq(runner)
end
end
end

View file

@ -7,6 +7,66 @@ RSpec.describe API::Helpers do
subject { Class.new.include(described_class).new }
describe '#current_user' do
include Rack::Test::Methods
let(:user) { build(:user, id: 42) }
let(:helper) do
Class.new(Grape::API::Instance) do
helpers API::APIGuard::HelperMethods
helpers API::Helpers
format :json
get 'user' do
current_user ? { id: current_user.id } : { found: false }
end
get 'protected' do
authenticate_by_gitlab_geo_node_token!
end
end
end
def app
helper
end
before do
allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
end
it 'handles sticking when a user could be found' do
allow_any_instance_of(API::Helpers).to receive(:initial_current_user).and_return(user)
expect(Gitlab::Database::LoadBalancing::RackMiddleware)
.to receive(:stick_or_unstick).with(any_args, :user, 42)
get 'user'
expect(Gitlab::Json.parse(last_response.body)).to eq({ 'id' => user.id })
end
it 'does not handle sticking if no user could be found' do
allow_any_instance_of(API::Helpers).to receive(:initial_current_user).and_return(nil)
expect(Gitlab::Database::LoadBalancing::RackMiddleware)
.not_to receive(:stick_or_unstick)
get 'user'
expect(Gitlab::Json.parse(last_response.body)).to eq({ 'found' => false })
end
it 'returns the user if one could be found' do
allow_any_instance_of(API::Helpers).to receive(:initial_current_user).and_return(user)
get 'user'
expect(Gitlab::Json.parse(last_response.body)).to eq({ 'id' => user.id })
end
end
describe '#find_project' do
let(:project) { create(:project) }

View file

@ -16,8 +16,19 @@ RSpec.describe API::Wikis do
include WorkhorseHelpers
include AfterNextHelpers
let(:user) { create(:user) }
let(:group) { create(:group).tap { |g| g.add_owner(user) } }
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group).tap { |g| g.add_owner(user) } }
let_it_be(:group_project) { create(:project, :wiki_repo, namespace: group) }
let_it_be(:developer) { create(:user) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:project_wiki_disabled) do
create(:project, :wiki_repo, :wiki_disabled).tap do |project|
project.add_developer(developer)
project.add_maintainer(maintainer)
end
end
let(:project_wiki) { create(:project_wiki, project: project, user: user) }
let(:payload) { { content: 'content', format: 'rdoc', title: 'title' } }
let(:expected_keys_with_content) { %w(content format slug title) }
@ -32,7 +43,7 @@ RSpec.describe API::Wikis do
let(:url) { "/projects/#{project.id}/wikis" }
context 'when wiki is disabled' do
let(:project) { create(:project, :wiki_repo, :wiki_disabled) }
let(:project) { project_wiki_disabled }
context 'when user is guest' do
before do
@ -44,9 +55,7 @@ RSpec.describe API::Wikis do
context 'when user is developer' do
before do
project.add_developer(user)
get api(url, user)
get api(url, developer)
end
include_examples 'wiki API 403 Forbidden'
@ -54,9 +63,7 @@ RSpec.describe API::Wikis do
context 'when user is maintainer' do
before do
project.add_maintainer(user)
get api(url, user)
get api(url, maintainer)
end
include_examples 'wiki API 403 Forbidden'
@ -125,7 +132,7 @@ RSpec.describe API::Wikis do
let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" }
context 'when wiki is disabled' do
let(:project) { create(:project, :wiki_repo, :wiki_disabled) }
let(:project) { project_wiki_disabled }
context 'when user is guest' do
before do
@ -137,9 +144,7 @@ RSpec.describe API::Wikis do
context 'when user is developer' do
before do
project.add_developer(user)
get api(url, user)
get api(url, developer)
end
include_examples 'wiki API 403 Forbidden'
@ -147,9 +152,7 @@ RSpec.describe API::Wikis do
context 'when user is maintainer' do
before do
project.add_maintainer(user)
get api(url, user)
get api(url, maintainer)
end
include_examples 'wiki API 403 Forbidden'
@ -249,7 +252,7 @@ RSpec.describe API::Wikis do
let(:url) { "/projects/#{project.id}/wikis" }
context 'when wiki is disabled' do
let(:project) { create(:project, :wiki_disabled, :wiki_repo) }
let(:project) { project_wiki_disabled }
context 'when user is guest' do
before do
@ -261,8 +264,7 @@ RSpec.describe API::Wikis do
context 'when user is developer' do
before do
project.add_developer(user)
post(api(url, user), params: payload)
post(api(url, developer), params: payload)
end
include_examples 'wiki API 403 Forbidden'
@ -270,8 +272,7 @@ RSpec.describe API::Wikis do
context 'when user is maintainer' do
before do
project.add_maintainer(user)
post(api(url, user), params: payload)
post(api(url, maintainer), params: payload)
end
include_examples 'wiki API 403 Forbidden'
@ -469,7 +470,7 @@ RSpec.describe API::Wikis do
end
context 'when wiki belongs to a group project' do
let(:project) { create(:project, :wiki_repo, namespace: group) }
let(:project) { group_project }
include_examples 'wikis API updates wiki page'
end