Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-10 09:09:29 +00:00
parent e0df70a614
commit 921173681c
56 changed files with 734 additions and 304 deletions

View File

@ -102,7 +102,7 @@
- name: postgres:11.6 - name: postgres:11.6
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:5.0-alpine - name: redis:5.0-alpine
- name: elasticsearch:7.10.1 - name: elasticsearch:7.11.1
command: ["elasticsearch", "-E", "discovery.type=single-node"] command: ["elasticsearch", "-E", "discovery.type=single-node"]
variables: variables:
POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_HOST_AUTH_METHOD: trust
@ -113,7 +113,7 @@
- name: postgres:12 - name: postgres:12
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:5.0-alpine - name: redis:5.0-alpine
- name: elasticsearch:7.10.1 - name: elasticsearch:7.11.1
command: ["elasticsearch", "-E", "discovery.type=single-node"] command: ["elasticsearch", "-E", "discovery.type=single-node"]
variables: variables:
POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_HOST_AUTH_METHOD: trust

View File

@ -27,7 +27,7 @@ export default {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [boardCardInner], mixins: [boardCardInner],
inject: ['groupId', 'rootPath', 'scopedLabelsAvailable'], inject: ['rootPath', 'scopedLabelsAvailable'],
props: { props: {
item: { item: {
type: Object, type: Object,

View File

@ -107,7 +107,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters(['isEpicBoard']), ...mapGetters(['isEpicBoard', 'isGroupBoard', 'isProjectBoard']),
isNewForm() { isNewForm() {
return this.currentPage === formType.new; return this.currentPage === formType.new;
}, },
@ -178,8 +178,8 @@ export default {
} }
: { : {
...variables, ...variables,
projectPath: this.projectId ? this.fullPath : undefined, projectPath: this.isProjectBoard ? this.fullPath : undefined,
groupPath: this.groupId ? this.fullPath : undefined, groupPath: this.isGroupBoard ? this.fullPath : undefined,
}; };
}, },
boardScopeMutationVariables() { boardScopeMutationVariables() {

View File

@ -1,6 +1,6 @@
<script> <script>
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { getMilestone } from 'ee_else_ce/boards/boards_util'; import { getMilestone } from 'ee_else_ce/boards/boards_util';
import { __ } from '~/locale'; import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@ -32,8 +32,9 @@ export default {
}, },
computed: { computed: {
...mapState(['selectedProject']), ...mapState(['selectedProject']),
...mapGetters(['isGroupBoard']),
disabled() { disabled() {
if (this.groupId) { if (this.isGroupBoard) {
return this.title === '' || !this.selectedProject.name; return this.title === '' || !this.selectedProject.name;
} }
return this.title === ''; return this.title === '';
@ -98,7 +99,7 @@ export default {
name="issue_title" name="issue_title"
autocomplete="off" autocomplete="off"
/> />
<project-select v-if="groupId" :group-id="groupId" :list="list" /> <project-select v-if="isGroupBoard" :group-id="groupId" :list="list" />
<div class="clearfix gl-mt-3"> <div class="clearfix gl-mt-3">
<gl-button <gl-button
ref="submitButton" ref="submitButton"

View File

@ -1,5 +1,6 @@
<script> <script>
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { getMilestone } from 'ee_else_ce/boards/boards_util'; import { getMilestone } from 'ee_else_ce/boards/boards_util';
import ListIssue from 'ee_else_ce/boards/models/issue'; import ListIssue from 'ee_else_ce/boards/models/issue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@ -31,8 +32,9 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters(['isGroupBoard']),
disabled() { disabled() {
if (this.groupId) { if (this.isGroupBoard) {
return this.title === '' || !this.selectedProject.name; return this.title === '' || !this.selectedProject.name;
} }
return this.title === ''; return this.title === '';
@ -110,7 +112,7 @@ export default {
name="issue_title" name="issue_title"
autocomplete="off" autocomplete="off"
/> />
<project-select v-if="groupId" :group-id="groupId" :list="list" /> <project-select v-if="isGroupBoard" :group-id="groupId" :list="list" />
<div class="clearfix gl-mt-3"> <div class="clearfix gl-mt-3">
<gl-button <gl-button
ref="submitButton" ref="submitButton"

View File

@ -9,6 +9,7 @@ import {
GlModalDirective, GlModalDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { mapGetters, mapState } from 'vuex';
import BoardForm from 'ee_else_ce/boards/components/board_form.vue'; import BoardForm from 'ee_else_ce/boards/components/board_form.vue';
@ -109,8 +110,10 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['boardType']),
...mapGetters(['isGroupBoard']),
parentType() { parentType() {
return this.groupId ? 'group' : 'project'; return this.boardType;
}, },
loading() { loading() {
return this.loadingRecentBoards || Boolean(this.loadingBoards); return this.loadingRecentBoards || Boolean(this.loadingBoards);
@ -171,7 +174,7 @@ export default {
})); }));
}, },
boardQuery() { boardQuery() {
return this.groupId ? groupQuery : projectQuery; return this.isGroupBoard ? groupQuery : projectQuery;
}, },
loadBoards(toggleDropdown = true) { loadBoards(toggleDropdown = true) {
if (toggleDropdown && this.boards.length > 0) { if (toggleDropdown && this.boards.length > 0) {

View File

@ -9,6 +9,7 @@ import {
GlModalDirective, GlModalDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { mapGetters, mapState } from 'vuex';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
@ -108,8 +109,10 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['boardType']),
...mapGetters(['isGroupBoard']),
parentType() { parentType() {
return this.groupId ? 'group' : 'project'; return this.boardType;
}, },
loading() { loading() {
return this.loadingRecentBoards || Boolean(this.loadingBoards); return this.loadingRecentBoards || Boolean(this.loadingBoards);
@ -167,7 +170,7 @@ export default {
return { fullPath: this.state.endpoints.fullPath }; return { fullPath: this.state.endpoints.fullPath };
}, },
query() { query() {
return this.groupId ? groupQuery : projectQuery; return this.isGroupBoard ? groupQuery : projectQuery;
}, },
loadingKey: 'loadingBoards', loadingKey: 'loadingBoards',
update(data) { update(data) {

View File

@ -0,0 +1,54 @@
<script>
import { mapActions } from 'vuex';
import { historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
export default {
i18n: {
search: __('Search'),
},
components: { FilteredSearch },
props: {
search: {
type: String,
required: false,
default: '',
},
},
computed: {
initialSearch() {
return [{ type: 'filtered-search-term', value: { data: this.search } }];
},
},
methods: {
...mapActions(['performSearch']),
handleSearch(filters) {
let itemValue = '';
const [item] = filters;
if (filters.length === 0) {
itemValue = '';
} else {
itemValue = item?.value?.data;
}
historyPushState(setUrlParams({ search: itemValue }, window.location.href));
this.performSearch();
},
},
};
</script>
<template>
<filtered-search
class="gl-w-full"
namespace=""
:tokens="[]"
:search-input-placeholder="$options.i18n.search"
:initial-filter-value="initialSearch"
@onFilter="handleSearch"
/>
</template>

View File

@ -0,0 +1,25 @@
import Vue from 'vue';
import store from '~/boards/stores';
import { queryToObject } from '~/lib/utils/url_utility';
import FilteredSearch from './components/filtered_search.vue';
export default () => {
const queryParams = queryToObject(window.location.search);
const el = document.getElementById('js-board-filtered-search');
/*
When https://github.com/vuejs/vue-apollo/pull/1153 is merged and deployed
we can remove apolloProvider option from here. Currently without it its causing
an error
*/
return new Vue({
el,
store,
apolloProvider: {},
render: (createElement) =>
createElement(FilteredSearch, {
props: { search: queryParams.search },
}),
});
};

View File

@ -53,7 +53,6 @@ let issueBoardsApp;
export default () => { export default () => {
const $boardApp = document.getElementById('board-app'); const $boardApp = document.getElementById('board-app');
// check for browser back and trigger a hard reload to circumvent browser caching. // check for browser back and trigger a hard reload to circumvent browser caching.
window.addEventListener('pageshow', (event) => { window.addEventListener('pageshow', (event) => {
const isNavTypeBackForward = const isNavTypeBackForward =
@ -73,6 +72,14 @@ export default () => {
boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours); boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours);
} }
if (gon?.features?.boardsFilteredSearch) {
import('~/boards/filtered_search')
.then(({ default: initFilteredSearch }) => {
initFilteredSearch(apolloProvider);
})
.catch(() => {});
}
// eslint-disable-next-line @gitlab/no-runtime-template-compiler // eslint-disable-next-line @gitlab/no-runtime-template-compiler
issueBoardsApp = new Vue({ issueBoardsApp = new Vue({
el: $boardApp, el: $boardApp,
@ -164,8 +171,15 @@ export default () => {
eventHub.$off('initialBoardLoad', this.initialBoardLoad); eventHub.$off('initialBoardLoad', this.initialBoardLoad);
}, },
mounted() { mounted() {
this.filterManager = new FilteredSearchBoards(boardsStore.filter, true, boardsStore.cantEdit); if (!gon.features?.boardsFilteredSearch) {
this.filterManager.setup(); this.filterManager = new FilteredSearchBoards(
boardsStore.filter,
true,
boardsStore.cantEdit,
);
this.filterManager.setup();
}
this.performSearch(); this.performSearch();

View File

@ -1,7 +1,9 @@
import { find } from 'lodash'; import { find } from 'lodash';
import { inactiveId } from '../constants'; import { BoardType, inactiveId } from '../constants';
export default { export default {
isGroupBoard: (state) => state.boardType === BoardType.group,
isProjectBoard: (state) => state.boardType === BoardType.project,
isSidebarOpen: (state) => state.activeId !== inactiveId, isSidebarOpen: (state) => state.activeId !== inactiveId,
isSwimlanesOn: () => false, isSwimlanesOn: () => false,
getBoardItemById: (state) => (id) => { getBoardItemById: (state) => (id) => {

View File

@ -1,29 +1,36 @@
<script> <script>
/* eslint-disable vue/no-v-html */ import {
import { GlLoadingIcon, GlBadge, GlTooltipDirective } from '@gitlab/ui'; GlLoadingIcon,
import { visitUrl } from '../../lib/utils/url_utility'; GlBadge,
import identicon from '../../vue_shared/components/identicon.vue'; GlIcon,
GlTooltipDirective,
GlSafeHtmlDirective,
} from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import identicon from '~/vue_shared/components/identicon.vue';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '../constants'; import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import itemActions from './item_actions.vue'; import itemActions from './item_actions.vue';
import itemCaret from './item_caret.vue'; import itemCaret from './item_caret.vue';
import itemStats from './item_stats.vue'; import itemStats from './item_stats.vue';
import itemStatsValue from './item_stats_value.vue';
import itemTypeIcon from './item_type_icon.vue'; import itemTypeIcon from './item_type_icon.vue';
export default { export default {
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
SafeHtml: GlSafeHtmlDirective,
}, },
components: { components: {
GlBadge, GlBadge,
GlLoadingIcon, GlLoadingIcon,
GlIcon,
UserAccessRoleBadge,
identicon, identicon,
itemCaret, itemCaret,
itemTypeIcon, itemTypeIcon,
itemStats, itemStats,
itemStatsValue,
itemActions, itemActions,
}, },
props: { props: {
@ -91,6 +98,7 @@ export default {
} }
}, },
}, },
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
}; };
</script> </script>
@ -140,28 +148,31 @@ export default {
data-testid="group-name" data-testid="group-name"
:href="group.relativePath" :href="group.relativePath"
:title="group.fullName" :title="group.fullName"
class="no-expand gl-mt-3 gl-mr-3 gl-text-gray-900!" class="no-expand gl-mr-3 gl-mt-3 gl-text-gray-900!"
:itemprop="microdata.nameItemprop" :itemprop="microdata.nameItemprop"
>{{ >
{{
// ending bracket must be by closing tag to prevent // ending bracket must be by closing tag to prevent
// link hover text-decoration from over-extending // link hover text-decoration from over-extending
group.name group.name
}}</a }}
> </a>
<item-stats-value <gl-icon
:icon-name="visibilityIcon" v-gl-tooltip.hover.bottom
class="gl-display-inline-flex gl-align-items-center gl-mr-3 gl-mt-3 gl-text-gray-500"
:name="visibilityIcon"
:title="visibilityTooltip" :title="visibilityTooltip"
css-class="item-visibility d-inline-flex align-items-center gl-mt-3 gl-mr-2 text-secondary" data-testid="group-visibility-icon"
/> />
<span v-if="group.permission" class="user-access-role gl-mt-3"> <user-access-role-badge v-if="group.permission" class="gl-mt-3">
{{ group.permission }} {{ group.permission }}
</span> </user-access-role-badge>
</div> </div>
<div v-if="group.description" class="description"> <div v-if="group.description" class="description">
<span <span
v-safe-html:[$options.safeHtmlConfig]="group.description"
:itemprop="microdata.descriptionItemprop" :itemprop="microdata.descriptionItemprop"
data-testid="group-description" data-testid="group-description"
v-html="group.description"
> >
</span> </span>
</div> </div>

View File

@ -7,6 +7,7 @@ import { deprecatedCreateFlash as flash } from '~/flash';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import eventHub from '~/sidebar/event_hub'; import eventHub from '~/sidebar/event_hub';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { splitCamelCase } from '../../lib/utils/text_utility'; import { splitCamelCase } from '../../lib/utils/text_utility';
import ReplyButton from './note_actions/reply_button.vue'; import ReplyButton from './note_actions/reply_button.vue';
@ -17,6 +18,7 @@ export default {
ReplyButton, ReplyButton,
GlButton, GlButton,
GlDropdownItem, GlDropdownItem,
UserAccessRoleBadge,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
@ -226,24 +228,30 @@ export default {
<template> <template>
<div class="note-actions"> <div class="note-actions">
<span <user-access-role-badge
v-if="isAuthor" v-if="isAuthor"
class="note-role user-access-role has-tooltip d-none d-md-inline-block" v-gl-tooltip
class="gl-mx-3 d-none d-md-inline-block"
:title="displayAuthorBadgeText" :title="displayAuthorBadgeText"
>{{ __('Author') }}</span
> >
<span {{ __('Author') }}
</user-access-role-badge>
<user-access-role-badge
v-if="accessLevel" v-if="accessLevel"
class="note-role user-access-role has-tooltip" v-gl-tooltip
class="gl-mx-3"
:title="displayMemberBadgeText" :title="displayMemberBadgeText"
>{{ accessLevel }}</span
> >
<span {{ accessLevel }}
</user-access-role-badge>
<user-access-role-badge
v-else-if="isContributor" v-else-if="isContributor"
class="note-role user-access-role has-tooltip" v-gl-tooltip
class="gl-mx-3"
:title="displayContributorBadgeText" :title="displayContributorBadgeText"
>{{ __('Contributor') }}</span
> >
{{ __('Contributor') }}
</user-access-role-badge>
<gl-button <gl-button
v-if="canResolve" v-if="canResolve"
ref="resolveButton" ref="resolveButton"

View File

@ -11,6 +11,7 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/groups/constants'; import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/groups/constants';
import csrf from '~/lib/utils/csrf'; import csrf from '~/lib/utils/csrf';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
export default { export default {
components: { components: {
@ -20,6 +21,7 @@ export default {
GlButton, GlButton,
GlTooltip, GlTooltip,
GlLink, GlLink,
UserAccessRoleBadge,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
@ -72,7 +74,9 @@ export default {
<template> <template>
<li :class="rowClass" class="group-row"> <li :class="rowClass" class="group-row">
<div class="group-row-contents gl-display-flex gl-align-items-center gl-py-3 gl-pr-5"> <div class="group-row-contents gl-display-flex gl-align-items-center gl-py-3 gl-pr-5">
<div class="folder-toggle-wrap gl-mr-2 gl-display-flex gl-align-items-center"> <div
class="folder-toggle-wrap gl-mr-3 gl-display-flex gl-align-items-center gl-text-gray-500"
>
<gl-icon name="folder-o" /> <gl-icon name="folder-o" />
</div> </div>
<gl-link <gl-link
@ -84,12 +88,12 @@ export default {
<div class="gl-min-w-0 gl-display-flex gl-flex-grow-1 gl-flex-shrink-1 gl-align-items-center"> <div class="gl-min-w-0 gl-display-flex gl-flex-grow-1 gl-flex-shrink-1 gl-align-items-center">
<div class="gl-min-w-0 gl-flex-grow-1 flex-shrink-1"> <div class="gl-min-w-0 gl-flex-grow-1 flex-shrink-1">
<div class="title gl-display-flex gl-align-items-center gl-flex-wrap gl-mr-3"> <div class="title gl-display-flex gl-align-items-center gl-flex-wrap gl-mr-3">
<gl-link :href="group.relative_path" class="gl-mt-3 gl-mr-3 gl-text-gray-900!">{{ <gl-link :href="group.relative_path" class="gl-mt-3 gl-mr-3 gl-text-gray-900!">
group.full_name {{ group.full_name }}
}}</gl-link> </gl-link>
<gl-icon <gl-icon
v-gl-tooltip.hover.bottom v-gl-tooltip.hover.bottom
class="gl-mr-0 gl-inline-flex gl-mt-3 text-secondary" class="gl-display-inline-flex gl-mt-3 gl-mr-3 gl-text-gray-500"
:name="visibilityIcon" :name="visibilityIcon"
:title="visibilityTooltip" :title="visibilityTooltip"
/> />
@ -99,11 +103,11 @@ export default {
class="gl-display-none gl-sm-display-flex gl-mt-3 gl-mr-1" class="gl-display-none gl-sm-display-flex gl-mt-3 gl-mr-1"
>{{ __('pending removal') }}</gl-badge >{{ __('pending removal') }}</gl-badge
> >
<span v-if="group.permission" class="user-access-role gl-mt-3"> <user-access-role-badge v-if="group.permission" class="gl-mt-3">
{{ group.permission }} {{ group.permission }}
</span> </user-access-role-badge>
</div> </div>
<div v-if="group.description" class="description"> <div v-if="group.description" class="description gl-line-height-20">
<span v-safe-html="group.markdown_description"> </span> <span v-safe-html="group.markdown_description"> </span>
</div> </div>
</div> </div>

View File

@ -208,6 +208,7 @@ export default {
ref="searchBox" ref="searchBox"
v-model.trim="query" v-model.trim="query"
:placeholder="i18n.searchPlaceholder" :placeholder="i18n.searchPlaceholder"
autocomplete="off"
@input="onSearchBoxInput" @input="onSearchBoxInput"
@keydown.enter.prevent="onSearchBoxEnter" @keydown.enter.prevent="onSearchBoxEnter"
/> />

View File

@ -0,0 +1,22 @@
<script>
/**
* This component applies particular styling to GlBadge that isn't
* available in the current GlBadge variants.
* Where possible, prefer one of the supported GlBadge variants.
* Discussion issue: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1247
*/
import { GlBadge } from '@gitlab/ui';
export default {
name: 'UserAccessRoleBadge',
components: {
GlBadge,
},
};
</script>
<template>
<gl-badge class="gl-bg-transparent! gl-inset-border-1-gray-100!">
<slot></slot>
</gl-badge>
</template>

View File

@ -519,10 +519,6 @@ table.pipeline-project-metrics tr td {
margin-left: 25px; margin-left: 25px;
} }
.item-visibility {
margin-right: 0;
}
.last-updated { .last-updated {
position: relative; position: relative;
min-width: 250px; min-width: 250px;

View File

@ -9,6 +9,7 @@ class Groups::BoardsController < Groups::ApplicationController
before_action :assign_endpoint_vars before_action :assign_endpoint_vars
before_action do before_action do
push_frontend_feature_flag(:graphql_board_lists, group, default_enabled: false) push_frontend_feature_flag(:graphql_board_lists, group, default_enabled: false)
push_frontend_feature_flag(:boards_filtered_search, group)
end end
feature_category :boards feature_category :boards

View File

@ -2026,10 +2026,10 @@ class Project < ApplicationRecord
Gitlab::Ci::Variables::Collection.new.tap do |variables| Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables unless Gitlab.config.dependency_proxy.enabled break variables unless Gitlab.config.dependency_proxy.enabled
variables.append(key: 'CI_DEPENDENCY_PROXY_SERVER', value: "#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}") variables.append(key: 'CI_DEPENDENCY_PROXY_SERVER', value: Gitlab.host_with_port)
variables.append( variables.append(
key: 'CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX', key: 'CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX',
value: "#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/#{namespace.root_ancestor.path}#{DependencyProxy::URL_SUFFIX}" value: "#{Gitlab.host_with_port}/#{namespace.root_ancestor.path}#{DependencyProxy::URL_SUFFIX}"
) )
end end
end end

View File

@ -48,4 +48,4 @@
.text-secondary= s_('DeployTokens|Allows write access to the package registry.') .text-secondary= s_('DeployTokens|Allows write access to the package registry.')
.gl-mt-3 .gl-mt-3
= f.submit s_('DeployTokens|Create deploy token'), class: 'btn gl-button btn-success qa-create-deploy-token' = f.submit s_('DeployTokens|Create deploy token'), class: 'btn gl-button btn-confirm qa-create-deploy-token'

View File

@ -21,178 +21,181 @@
- if @can_bulk_update - if @can_bulk_update
.check-all-holder.d-none.d-sm-block.hidden .check-all-holder.d-none.d-sm-block.hidden
= check_box_tag "check-all-issues", nil, false, class: "check-all-issues left" = check_box_tag "check-all-issues", nil, false, class: "check-all-issues left"
.issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row - if Feature.enabled?(:boards_filtered_search, @group)
.filtered-search-box #js-board-filtered-search
- if type != :boards_modal && type != :boards - else
- text = tag.span(sprite_icon('history'), class: "d-md-none") + tag.span(_('Recent searches'), class: "d-none d-md-inline") .issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row
= dropdown_tag(text, .filtered-search-box
options: { wrapper_class: "filtered-search-history-dropdown-wrapper", - if type != :boards_modal && type != :boards
toggle_class: "btn filtered-search-history-dropdown-toggle-button", - text = tag.span(sprite_icon('history'), class: "d-md-none") + tag.span(_('Recent searches'), class: "d-none d-md-inline")
dropdown_class: "filtered-search-history-dropdown", = dropdown_tag(text,
content_class: "filtered-search-history-dropdown-content" }) do options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
.js-filtered-search-history-dropdown{ data: { full_path: search_history_storage_prefix } } toggle_class: "btn filtered-search-history-dropdown-toggle-button",
.filtered-search-box-input-container.droplab-dropdown dropdown_class: "filtered-search-history-dropdown",
.scroll-container content_class: "filtered-search-history-dropdown-content" }) do
%ul.tokens-container.list-unstyled .js-filtered-search-history-dropdown{ data: { full_path: search_history_storage_prefix } }
%li.input-token .filtered-search-box-input-container.droplab-dropdown
%input.form-control.filtered-search{ search_filter_input_options(type, placeholder) } .scroll-container
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown %ul.tokens-container.list-unstyled
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.input-token
%li.filter-dropdown-item{ data: {hint: "#{'{{hint}}'}", tag: "#{'{{tag}}'}", action: "#{'{{hint === \'search\' ? \'submit\' : \'\' }}'}" } } %input.form-control.filtered-search{ search_filter_input_options(type, placeholder) }
%button.btn.btn-link{ type: 'button' } #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
-# Encapsulate static class name `{{icon}}` inside #{} to bypass %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
-# haml lint's ClassAttributeWithStaticValue %li.filter-dropdown-item{ data: {hint: "#{'{{hint}}'}", tag: "#{'{{tag}}'}", action: "#{'{{hint === \'search\' ? \'submit\' : \'\' }}'}" } }
%svg %button.btn.btn-link{ type: 'button' }
%use{ 'xlink:href': "#{'{{icon}}'}" } -# Encapsulate static class name `{{icon}}` inside #{} to bypass
%span.js-filter-hint -# haml lint's ClassAttributeWithStaticValue
{{formattedKey}} %svg
#js-dropdown-operator.filtered-search-input-dropdown-menu.dropdown-menu %use{ 'xlink:href': "#{'{{icon}}'}" }
%ul.filter-dropdown{ data: { dropdown: true, dynamic: true } } %span.js-filter-hint
%li.filter-dropdown-item{ data: { value: "{{ title }}" } } {{formattedKey}}
%button.btn.btn-link{ type: 'button' } #js-dropdown-operator.filtered-search-input-dropdown-menu.dropdown-menu
{{ title }} %ul.filter-dropdown{ data: { dropdown: true, dynamic: true } }
%span.btn-helptext %li.filter-dropdown-item{ data: { value: "{{ title }}" } }
{{ help }} %button.btn.btn-link{ type: 'button' }
#js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu {{ title }}
- if current_user %span.btn-helptext
{{ help }}
#js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu
- if current_user
%ul{ data: { dropdown: true } }
= render 'shared/issuable/user_dropdown_item',
user: current_user
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
= render 'shared/issuable/user_dropdown_item',
user: User.new(username: '{{username}}', name: '{{name}}'),
avatar: { lazy: true, url: '{{avatar_url}}' }
#js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } } %ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'None' } }
%button.btn.btn-link{ type: 'button' }
= _('None')
%li.filter-dropdown-item{ data: { value: 'Any' } }
%button.btn.btn-link{ type: 'button' }
= _('Any')
%li.divider.droplab-item-ignore
- if current_user
= render 'shared/issuable/user_dropdown_item',
user: current_user
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
= render 'shared/issuable/user_dropdown_item', = render 'shared/issuable/user_dropdown_item',
user: current_user user: User.new(username: '{{username}}', name: '{{name}}'),
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } avatar: { lazy: true, url: '{{avatar_url}}' }
= render 'shared/issuable/user_dropdown_item', #js-dropdown-reviewer.filtered-search-input-dropdown-menu.dropdown-menu
user: User.new(username: '{{username}}', name: '{{name}}'), %ul{ data: { dropdown: true } }
avatar: { lazy: true, url: '{{avatar_url}}' } %li.filter-dropdown-item{ data: { value: 'None' } }
#js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu %button.btn.btn-link{ type: 'button' }
%ul{ data: { dropdown: true } } = _('None')
%li.filter-dropdown-item{ data: { value: 'None' } } %li.filter-dropdown-item{ data: { value: 'Any' } }
%button.btn.btn-link{ type: 'button' } %button.btn.btn-link{ type: 'button' }
= _('None') = _('Any')
%li.filter-dropdown-item{ data: { value: 'Any' } } %li.divider.droplab-item-ignore
%button.btn.btn-link{ type: 'button' } - if current_user
= _('Any') = render 'shared/issuable/user_dropdown_item',
%li.divider.droplab-item-ignore user: current_user
- if current_user %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
= render 'shared/issuable/user_dropdown_item', = render 'shared/issuable/user_dropdown_item',
user: current_user user: User.new(username: '{{username}}', name: '{{name}}'),
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } avatar: { lazy: true, url: '{{avatar_url}}' }
= render 'shared/issuable/user_dropdown_item', = render_if_exists 'shared/issuable/approver_dropdown'
user: User.new(username: '{{username}}', name: '{{name}}'), = render_if_exists 'shared/issuable/approved_by_dropdown'
avatar: { lazy: true, url: '{{avatar_url}}' } #js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu
#js-dropdown-reviewer.filtered-search-input-dropdown-menu.dropdown-menu %ul{ data: { dropdown: true } }
%ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { value: 'None' } }
%li.filter-dropdown-item{ data: { value: 'None' } } %button.btn.btn-link{ type: 'button' }
%button.btn.btn-link{ type: 'button' } = _('None')
= _('None') %li.filter-dropdown-item{ data: { value: 'Any' } }
%li.filter-dropdown-item{ data: { value: 'Any' } } %button.btn.btn-link{ type: 'button' }
%button.btn.btn-link{ type: 'button' } = _('Any')
= _('Any') %li.filter-dropdown-item{ data: { value: 'Upcoming' } }
%li.divider.droplab-item-ignore %button.btn.btn-link{ type: 'button' }
- if current_user = _('Upcoming')
= render 'shared/issuable/user_dropdown_item', %li.filter-dropdown-item{ data: { value: 'Started' } }
user: current_user %button.btn.btn-link{ type: 'button' }
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } = _('Started')
= render 'shared/issuable/user_dropdown_item', %li.divider.droplab-item-ignore
user: User.new(username: '{{username}}', name: '{{name}}'),
avatar: { lazy: true, url: '{{avatar_url}}' }
= render_if_exists 'shared/issuable/approver_dropdown'
= render_if_exists 'shared/issuable/approved_by_dropdown'
#js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'None' } }
%button.btn.btn-link{ type: 'button' }
= _('None')
%li.filter-dropdown-item{ data: { value: 'Any' } }
%button.btn.btn-link{ type: 'button' }
= _('Any')
%li.filter-dropdown-item{ data: { value: 'Upcoming' } }
%button.btn.btn-link{ type: 'button' }
= _('Upcoming')
%li.filter-dropdown-item{ data: { value: 'Started' } }
%button.btn.btn-link{ type: 'button' }
= _('Started')
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value{ type: 'button' }
{{title}}
= render_if_exists 'shared/issuable/filter_iteration', type: type
#js-dropdown-release.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'None' } }
%button.btn.btn-link{ type: 'button' }
= _('None')
%li.filter-dropdown-item{ data: { value: 'Any' } }
%button.btn.btn-link{ type: 'button' }
= _('Any')
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value{ type: 'button' }
{{title}}
#js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'None' } }
%button.btn.btn-link{ type: 'button' }
= _('None')
%li.filter-dropdown-item{ data: { value: 'Any' } }
%button.btn.btn-link{ type: 'button' }
= _('Any')
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link{ type: 'button' }
%span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value
{{title}}
#js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'None' } }
%button.btn.btn-link{ type: 'button' }
= _('None')
%li.filter-dropdown-item{ data: { value: 'Any' } }
%button.btn.btn-link{ type: 'button' }
= _('Any')
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link{ type: 'button' }
%gl-emoji
%span.js-data-value.gl-ml-3
{{name}}
#js-dropdown-wip.filtered-search-input-dropdown-menu.dropdown-menu
%ul.filter-dropdown{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } }
%button.btn.btn-link{ type: 'button' }
= _('Yes')
%li.filter-dropdown-item{ data: { value: 'no', capitalize: true } }
%button.btn.btn-link{ type: 'button' }
= _('No')
#js-dropdown-confidential.filtered-search-input-dropdown-menu.dropdown-menu
%ul.filter-dropdown{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } }
%button.btn.btn-link{ type: 'button' }
= _('Yes')
%li.filter-dropdown-item{ data: { value: 'no', capitalize: true } }
%button.btn.btn-link{ type: 'button' }
= _('No')
- unless disable_target_branch
#js-dropdown-target-branch.filtered-search-input-dropdown-menu.dropdown-menu
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item %li.filter-dropdown-item
%button.btn.btn-link.js-data-value.monospace %button.btn.btn-link.js-data-value{ type: 'button' }
{{title}}
= render_if_exists 'shared/issuable/filter_iteration', type: type
#js-dropdown-release.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'None' } }
%button.btn.btn-link{ type: 'button' }
= _('None')
%li.filter-dropdown-item{ data: { value: 'Any' } }
%button.btn.btn-link{ type: 'button' }
= _('Any')
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value{ type: 'button' }
{{title}}
#js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'None' } }
%button.btn.btn-link{ type: 'button' }
= _('None')
%li.filter-dropdown-item{ data: { value: 'Any' } }
%button.btn.btn-link{ type: 'button' }
= _('Any')
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link{ type: 'button' }
%span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value
{{title}}
#js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'None' } }
%button.btn.btn-link{ type: 'button' }
= _('None')
%li.filter-dropdown-item{ data: { value: 'Any' } }
%button.btn.btn-link{ type: 'button' }
= _('Any')
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link{ type: 'button' }
%gl-emoji
%span.js-data-value.gl-ml-3
{{name}}
#js-dropdown-wip.filtered-search-input-dropdown-menu.dropdown-menu
%ul.filter-dropdown{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } }
%button.btn.btn-link{ type: 'button' }
= _('Yes')
%li.filter-dropdown-item{ data: { value: 'no', capitalize: true } }
%button.btn.btn-link{ type: 'button' }
= _('No')
#js-dropdown-confidential.filtered-search-input-dropdown-menu.dropdown-menu
%ul.filter-dropdown{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } }
%button.btn.btn-link{ type: 'button' }
= _('Yes')
%li.filter-dropdown-item{ data: { value: 'no', capitalize: true } }
%button.btn.btn-link{ type: 'button' }
= _('No')
- unless disable_target_branch
#js-dropdown-target-branch.filtered-search-input-dropdown-menu.dropdown-menu
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value.monospace
{{title}}
#js-dropdown-environment.filtered-search-input-dropdown-menu.dropdown-menu
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value{ type: 'button' }
{{title}} {{title}}
#js-dropdown-environment.filtered-search-input-dropdown-menu.dropdown-menu
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value{ type: 'button' }
{{title}}
= render_if_exists 'shared/issuable/filter_weight', type: type = render_if_exists 'shared/issuable/filter_weight', type: type
= render_if_exists 'shared/issuable/filter_epic', type: type = render_if_exists 'shared/issuable/filter_epic', type: type
%button.clear-search.hidden{ type: 'button' } %button.clear-search.hidden{ type: 'button' }
= sprite_icon('close', size: 16, css_class: 'clear-search-icon') = sprite_icon('close', size: 16, css_class: 'clear-search-icon')
.filter-dropdown-container.d-flex.flex-column.flex-md-row .filter-dropdown-container.d-flex.flex-column.flex-md-row
- if type == :boards - if type == :boards
#js-board-labels-toggle #js-board-labels-toggle

View File

@ -0,0 +1,5 @@
---
title: Move from btn-success to btn-confirm in shared/deploy_tokens directory
merge_request: 55300
author: Yogi (@yo)
type: changed

View File

@ -0,0 +1,5 @@
---
title: Turn off native autocomplete for ref selector components
merge_request: 56128
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Improve styling of user access role badges
merge_request: 56061
author:
type: changed

View File

@ -15,8 +15,10 @@
- audit_reports - audit_reports
- authentication_and_authorization - authentication_and_authorization
- auto_devops - auto_devops
- auto_portfolio_mgmt
- backup_restore - backup_restore
- boards - boards
- browser_performance
- chatops - chatops
- cloud_native_installation - cloud_native_installation
- cluster_cost_management - cluster_cost_management
@ -32,17 +34,18 @@
- continuous_delivery - continuous_delivery
- continuous_integration - continuous_integration
- database - database
- dataops
- dependency_firewall - dependency_firewall
- dependency_proxy - dependency_proxy
- dependency_scanning - dependency_scanning
- design_management - design_management
- devops_reports - devops_reports
- disaster_recovery - disaster_recovery
- dora_metrics
- dynamic_application_security_testing - dynamic_application_security_testing
- editor_extension - editor_extension
- epics - epics
- error_tracking - error_tracking
- experimentation
- feature_flags - feature_flags
- five_minute_production_app - five_minute_production_app
- foundations - foundations
@ -61,6 +64,7 @@
- insider_threat - insider_threat
- insights - insights
- integrations - integrations
- intel_code_security
- interactive_application_security_testing - interactive_application_security_testing
- internationalization - internationalization
- issue_tracking - issue_tracking
@ -79,6 +83,7 @@
- mobile_signing_deployment - mobile_signing_deployment
- navigation - navigation
- omnibus_package - omnibus_package
- onboarding
- package_registry - package_registry
- pages - pages
- pipeline_authoring - pipeline_authoring
@ -120,5 +125,4 @@
- vulnerability_management - vulnerability_management
- web_firewall - web_firewall
- web_ide - web_ide
- web_performance
- wiki - wiki

View File

@ -1,8 +1,8 @@
--- ---
name: saas_add_seats_button name: boards_filtered_search
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49242 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54641
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/291060 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/322778
milestone: '13.7' milestone: '13.10'
type: development type: development
group: group::purchase group: group::project management
default_enabled: false default_enabled: false

View File

@ -6,7 +6,7 @@
# For a list of all options, see https://errata-ai.gitbook.io/vale/getting-started/styles # For a list of all options, see https://errata-ai.gitbook.io/vale/getting-started/styles
extends: substitution extends: substitution
message: 'Verify this use of the word "admin". Can it be updated to "administration", "administrator", "administer", or "Admin Area"?' message: 'Verify this use of the word "admin". Can it be updated to "administration", "administrator", "administer", or "Admin Area"?'
link: https://docs.gitlab.com/ee/development/documentation/styleguide.html link: https://docs.gitlab.com/ee/development/documentation/styleguide/index.html
level: suggestion level: suggestion
ignorecase: true ignorecase: true
swap: swap:

View File

@ -10,7 +10,7 @@
# For a list of all options, see https://errata-ai.gitbook.io/vale/getting-started/styles # For a list of all options, see https://errata-ai.gitbook.io/vale/getting-started/styles
extends: existence extends: existence
message: 'Alert box "%s" must use the formatting in the style guide.' message: 'Alert box "%s" must use the formatting in the style guide.'
link: https://docs.gitlab.com/ee/development/documentation/styleguide.html#alert-boxes link: https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#alert-boxes
level: error level: error
nonword: true nonword: true
scope: raw scope: raw

View File

@ -0,0 +1,13 @@
---
# Error: gitlab.RelativeLinksDoubleSlashes
#
# Checks for the presence of double slashes in relative URLs.
#
# For a list of all options, see https://errata-ai.gitbook.io/vale/getting-started/styles
extends: existence
message: 'Relative links must not include a double slash.'
link: https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#links-to-internal-documentation
level: error
scope: raw
raw:
- '\.//'

View File

@ -239,7 +239,7 @@ control over how the Pages daemon runs and serves content in your environment.
| `gitlab_secret` | The OAuth application secret. Leave blank to automatically fill when Pages authenticates with GitLab. | | `gitlab_secret` | The OAuth application secret. Leave blank to automatically fill when Pages authenticates with GitLab. |
| `auth_scope` | The OAuth application scope to use for authentication. Must match GitLab Pages OAuth application settings. Leave blank to use `api` scope by default. | | `auth_scope` | The OAuth application scope to use for authentication. Must match GitLab Pages OAuth application settings. Leave blank to use `api` scope by default. |
| `gitlab_server` | Server to use for authentication when access control is enabled; defaults to GitLab `external_url`. | | `gitlab_server` | Server to use for authentication when access control is enabled; defaults to GitLab `external_url`. |
| `headers` | Specify any additional http headers that should be sent to the client with each response. | | `headers` | Specify any additional http headers that should be sent to the client with each response. Multiple headers can be given as an array, header and value as one string, for example `['my-header: myvalue', 'my-other-header: my-other-value']` |
| `inplace_chroot` | On [systems that don't support bind-mounts](index.md#additional-configuration-for-docker-container), this instructs GitLab Pages to `chroot` into its `pages_path` directory. Some caveats exist when using in-place `chroot`; refer to the GitLab Pages [README](https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md#caveats) for more information. | | `inplace_chroot` | On [systems that don't support bind-mounts](index.md#additional-configuration-for-docker-container), this instructs GitLab Pages to `chroot` into its `pages_path` directory. Some caveats exist when using in-place `chroot`; refer to the GitLab Pages [README](https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md#caveats) for more information. |
| `insecure_ciphers` | Use default list of cipher suites, may contain insecure ones like 3DES and RC4. | | `insecure_ciphers` | Use default list of cipher suites, may contain insecure ones like 3DES and RC4. |
| `internal_gitlab_server` | Internal GitLab server address used exclusively for API requests. Useful if you want to send that traffic over an internal load balancer. Defaults to GitLab `external_url`. | | `internal_gitlab_server` | Internal GitLab server address used exclusively for API requests. Useful if you want to send that traffic over an internal load balancer. Defaults to GitLab `external_url`. |

View File

@ -36,7 +36,7 @@ The type of problem will determine what steps to take. The possible troubleshoot
- Indexing. - Indexing.
- Integration. - Integration.
- Performance. - Performance.
- Background Migrations. - Advanced Search Migrations.
### Search Results workflow ### Search Results workflow
@ -148,7 +148,7 @@ graph TD;
F7(Escalate to<br>GitLab support.) F7(Escalate to<br>GitLab support.)
``` ```
### Background Migrations workflow ### Advanced Search Migrations workflow
```mermaid ```mermaid
graph TD; graph TD;
@ -180,7 +180,7 @@ Most Elasticsearch troubleshooting can be broken down into 4 categories:
- [Troubleshooting indexing](#troubleshooting-indexing) - [Troubleshooting indexing](#troubleshooting-indexing)
- [Troubleshooting integration](#troubleshooting-integration) - [Troubleshooting integration](#troubleshooting-integration)
- [Troubleshooting performance](#troubleshooting-performance) - [Troubleshooting performance](#troubleshooting-performance)
- [Troubleshooting background migrations](#troubleshooting-background-migrations) - [Troubleshooting Advanced Search migrations](#troubleshooting-advanced-search-migrations)
Generally speaking, if it does not fall into those four categories, it is either: Generally speaking, if it does not fall into those four categories, it is either:
@ -356,15 +356,16 @@ dig further into these.
Feel free to reach out to GitLab support, but this is likely to be something a skilled Feel free to reach out to GitLab support, but this is likely to be something a skilled
Elasticsearch administrator has more experience with. Elasticsearch administrator has more experience with.
### Troubleshooting background migrations ### Troubleshooting Advanced Search migrations
Troubleshooting background migration failures can be difficult and may require contacting Troubleshooting Advanced Search migration failures can be difficult and may
an Elasticsearch administrator or GitLab Support. require contacting an Elasticsearch administrator or GitLab Support.
The best place to start while debugging issues with a background migration is the The best place to start while debugging issues with an Advanced Search
[`elasticsearch.log` file](../logs.md#elasticsearchlog). Migrations will migration is the [`elasticsearch.log` file](../logs.md#elasticsearchlog).
print information while a migration is in progress and any errors encountered. Migrations will log information while a migration is in progress and any
Apply fixes for any errors found in the log and retry the migration. errors encountered. Apply fixes for any errors found in the log and retry
the migration.
If you still encounter issues after retrying the migration, reach out to GitLab support. If you still encounter issues after retrying the migration, reach out to GitLab support.

View File

@ -1,8 +0,0 @@
---
redirect_to: 'styleguide/index.md'
---
This document was moved to [another location](styleguide/index.md).
<!-- This redirect file can be deleted after <2022-02-13>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -249,12 +249,12 @@ end
### Multi-version compatibility ### Multi-version compatibility
These Elasticsearch migrations, like any other GitLab changes, need to support the case where These Advanced Search migrations, like any other GitLab changes, need to support the case where
[multiple versions of the application are running at the same time](multi_version_compatibility.md). [multiple versions of the application are running at the same time](multi_version_compatibility.md).
Depending on the order of deployment, it's possible that the migration Depending on the order of deployment, it's possible that the migration
has started or finished and there's still a server running the application code from before the has started or finished and there's still a server running the application code from before the
migration. We need to take this into consideration until we can [ensure all Elasticsearch migrations migration. We need to take this into consideration until we can [ensure all Advanced Search migrations
start after the deployment has finished](https://gitlab.com/gitlab-org/gitlab/-/issues/321619). start after the deployment has finished](https://gitlab.com/gitlab-org/gitlab/-/issues/321619).
### Reverting a migration ### Reverting a migration
@ -268,7 +268,7 @@ some data is moved) to a later merge request after the migrations have
completed successfully. To be safe, for self-managed customers we should also completed successfully. To be safe, for self-managed customers we should also
defer it to another release if there is risk of important data loss. defer it to another release if there is risk of important data loss.
### Best practices for Elasticsearch migrations ### Best practices for Advanced Search migrations
Follow these best practices for best results: Follow these best practices for best results:
@ -286,6 +286,63 @@ Follow these best practices for best results:
- Consider adding a retry limit if there is potential for the migration to fail. - Consider adding a retry limit if there is potential for the migration to fail.
This ensures that migrations can be halted if an issue occurs. This ensures that migrations can be halted if an issue occurs.
## Deleting Advanced Search migrations in a major version upgrade
Since our Advanced Search migrations usually require us to support multiple
code paths for a long period of time, it's important to clean those up when we
safely can.
We choose to use GitLab major version upgrades as a safe time to remove
backwards compatibility for indices that have not been fully migrated. We
[document this in our upgrade
documentation](../update/index.md#upgrading-to-a-new-major-version). We also
choose to remove the migration code and tests so that:
- We don't need to maintain any code that is called from our Advanced Search
migrations.
- We don't waste CI time running tests for migrations that we don't support
anymore.
To be extra safe, we will not delete migrations that were created in the last
minor version before the major upgrade. So, if the we are upgrading to `%14.0`,
we should not delete migrations that were only added in `%13.11`. This is an
extra safety net as we expect there are migrations that get merged that may
take multiple weeks to finish on GitLab.com. It would be bad if we upgraded
GitLab.com to `%14.0` before the migrations in `%13.11` were finished. Since
our deployments to GitLab.com are automated and we currently don't have
automated checks to prevent this, the extra precaution is warranted.
Additionally, even if we did have automated checks to prevent it, we wouldn't
actually want to hold up GitLab.com deployments on Advanced Search migrations,
as they may still have another week to go, and that's too long to block
deployments.
### Process for removing migrations
For every migration that was created 2 minor versions before the major version
being upgraded to, we do the following:
1. Confirm the migration has actually completed successfully for GitLab.com.
1. Replace the content of `migrate` and `completed?` methods as follows:
```ruby
def migrate
log_raise "Migration has been deleted in the last major version upgrade." \
"Migrations are supposed to be finished before upgrading major version https://docs.gitlab.com/ee/update/#upgrading-to-a-new-major-version ." \
"In order to correct this issue you will need to reacreate your index from scratch https://docs.gitlab.com/ee/integration/elasticsearch.html#last-resort-to-recreate-an-index ."
end
def completed?
false
end
```
1. Delete any spec files to support this migration.
1. Remove any logic handling backwards compatibility for this migration. You
can find this by looking for
`Elastic::DataMigrationService.migration_has_finished?(:migration_name_in_lowercase)`.
1. Create a merge request with these changes. Noting that we should not
accidentally merge this before the major release is started.
## Performance Monitoring ## Performance Monitoring
### Prometheus ### Prometheus

View File

@ -222,7 +222,7 @@ The following Elasticsearch settings are available:
| `Pause Elasticsearch indexing` | Enables or disables temporary indexing pause. This is useful for cluster migration/reindexing. All changes are still tracked, but they are not committed to the Elasticsearch index until resumed. | | `Pause Elasticsearch indexing` | Enables or disables temporary indexing pause. This is useful for cluster migration/reindexing. All changes are still tracked, but they are not committed to the Elasticsearch index until resumed. |
| `Search with Elasticsearch enabled` | Enables or disables using Elasticsearch in search. | | `Search with Elasticsearch enabled` | Enables or disables using Elasticsearch in search. |
| `URL` | The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., `http://host1, https://host2:9200`). If your Elasticsearch instance is password protected, pass the `username:password` in the URL (e.g., `http://<username>:<password>@<elastic_host>:9200/`). Special characters in the username or password should use [percentage encoding](https://en.wikipedia.org/wiki/Percent-encoding). | | `URL` | The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., `http://host1, https://host2:9200`). If your Elasticsearch instance is password protected, pass the `username:password` in the URL (e.g., `http://<username>:<password>@<elastic_host>:9200/`). Special characters in the username or password should use [percentage encoding](https://en.wikipedia.org/wiki/Percent-encoding). |
| `Number of Elasticsearch shards` | Elasticsearch indexes are split into multiple shards for performance reasons. In general, larger indexes need to have more shards. Changes to this value do not take effect until the index is recreated. You can read more about tradeoffs in the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/scalability.html). | | `Number of Elasticsearch shards` | Elasticsearch indexes are split into multiple shards for performance reasons. In general, you should use at least 5 shards, and indexes with tens of millions of documents need to have more shards ([see below](#guidance-on-choosing-optimal-cluster-configuration)). Changes to this value do not take effect until the index is recreated. You can read more about tradeoffs in the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/scalability.html). |
| `Number of Elasticsearch replicas` | Each Elasticsearch shard can have a number of replicas. These are a complete copy of the shard, and can provide increased query performance or resilience against hardware failure. Increasing this value will greatly increase total disk space required by the index. | | `Number of Elasticsearch replicas` | Each Elasticsearch shard can have a number of replicas. These are a complete copy of the shard, and can provide increased query performance or resilience against hardware failure. Increasing this value will greatly increase total disk space required by the index. |
| `Limit namespaces and projects that can be indexed` | Enabling this will allow you to select namespaces and projects to index. All other namespaces and projects will use database search instead. Please note that if you enable this option but do not select any namespaces or projects, none will be indexed. [Read more below](#limiting-namespaces-and-projects). | `Limit namespaces and projects that can be indexed` | Enabling this will allow you to select namespaces and projects to index. All other namespaces and projects will use database search instead. Please note that if you enable this option but do not select any namespaces or projects, none will be indexed. [Read more below](#limiting-namespaces-and-projects).
| `Using AWS hosted Elasticsearch with IAM credentials` | Sign your Elasticsearch requests using [AWS IAM authorization](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html), [AWS EC2 Instance Profile Credentials](https://docs.aws.amazon.com/codedeploy/latest/userguide/getting-started-create-iam-instance-profile.html#getting-started-create-iam-instance-profile-cli), or [AWS ECS Tasks Credentials](https://docs.aws.amazon.com/AmazonECS/latest/userguide/task-iam-roles.html). Please refer to [Identity and Access Management in Amazon Elasticsearch Service](https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-ac.html) for details of AWS hosted Elasticsearch domain access policy configuration. | | `Using AWS hosted Elasticsearch with IAM credentials` | Sign your Elasticsearch requests using [AWS IAM authorization](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html), [AWS EC2 Instance Profile Credentials](https://docs.aws.amazon.com/codedeploy/latest/userguide/getting-started-create-iam-instance-profile.html#getting-started-create-iam-instance-profile-cli), or [AWS ECS Tasks Credentials](https://docs.aws.amazon.com/AmazonECS/latest/userguide/task-iam-roles.html). Please refer to [Identity and Access Management in Amazon Elasticsearch Service](https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-ac.html) for details of AWS hosted Elasticsearch domain access policy configuration. |
@ -344,7 +344,7 @@ Sometimes, you might want to abandon the unfinished reindex job and resume the i
1. Uncheck the "Pause Elasticsearch indexing" checkbox in **Admin Area > Settings > Advanced Search**. 1. Uncheck the "Pause Elasticsearch indexing" checkbox in **Admin Area > Settings > Advanced Search**.
## Background migrations ## Advanced Search migrations
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/234046) in GitLab 13.6. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/234046) in GitLab 13.6.
@ -352,7 +352,7 @@ With reindex migrations running in the background, there's no need for a manual
intervention. This usually happens in situations where new features are added to intervention. This usually happens in situations where new features are added to
Advanced Search, which means adding or changing the way content is indexed. Advanced Search, which means adding or changing the way content is indexed.
To confirm that the background migrations ran, you can check with: To confirm that the Advanced Search migrations ran, you can check with:
```shell ```shell
curl "$CLUSTER_URL/gitlab-production-migrations/_search?q=*" | jq . curl "$CLUSTER_URL/gitlab-production-migrations/_search?q=*" | jq .
@ -402,6 +402,21 @@ debug why the migration was halted and make any changes before retrying the migr
fixed the cause of the failure, click "Retry migration", and the migration will be scheduled to be retried fixed the cause of the failure, click "Retry migration", and the migration will be scheduled to be retried
in the background. in the background.
If you cannot get the migration to succeed, you may
consider the [last resort to recreate the index from
scratch](#last-resort-to-recreate-an-index). This may allow you to skip over
the problem because a newly created index will skip all migrations as the index
will be recreated with the correct up-to-date schema.
### All migrations must be finished before doing a major upgrade
Before doing a major version GitLab upgrade, you should have completed all
migrations that exist up until the latest minor version before that major
version. If you have halted migrations, these will need to be resolved and
[retried](#retry-a-halted-migration) before proceeding with a major version
upgrade. Read more about [upgrading to a new major
version](../update/index.md#upgrading-to-a-new-major-version).
## GitLab Advanced Search Rake tasks ## GitLab Advanced Search Rake tasks
Rake tasks are available to: Rake tasks are available to:
@ -476,7 +491,12 @@ For basic guidance on choosing a cluster configuration you may refer to [Elastic
- `Heap size` should be set to no more than 50% of your physical RAM. Additionally, it shouldn't be set to more than the threshold for zero-based compressed oops. The exact threshold varies, but 26 GB is safe on most systems, but can also be as large as 30 GB on some systems. See [Heap size settings](https://www.elastic.co/guide/en/elasticsearch/reference/current/important-settings.html#heap-size-settings) and [Setting JVM options](https://www.elastic.co/guide/en/elasticsearch/reference/current/jvm-options.html) for more details. - `Heap size` should be set to no more than 50% of your physical RAM. Additionally, it shouldn't be set to more than the threshold for zero-based compressed oops. The exact threshold varies, but 26 GB is safe on most systems, but can also be as large as 30 GB on some systems. See [Heap size settings](https://www.elastic.co/guide/en/elasticsearch/reference/current/important-settings.html#heap-size-settings) and [Setting JVM options](https://www.elastic.co/guide/en/elasticsearch/reference/current/jvm-options.html) for more details.
- Number of CPUs (CPU cores) per node usually corresponds to the `Number of Elasticsearch shards` setting described below. - Number of CPUs (CPU cores) per node usually corresponds to the `Number of Elasticsearch shards` setting described below.
- A good guideline is to ensure you keep the number of shards per node below 20 per GB heap it has configured. A node with a 30GB heap should therefore have a maximum of 600 shards, but the further below this limit you can keep it the better. This will generally help the cluster stay in good health. - A good guideline is to ensure you keep the number of shards per node below 20 per GB heap it has configured. A node with a 30GB heap should therefore have a maximum of 600 shards, but the further below this limit you can keep it the better. This will generally help the cluster stay in good health.
- Small shards result in small segments, which increases overhead. Aim to keep the average shard size between at least a few GB and a few tens of GB. Another consideration is the number of documents, you should aim for this simple formula for the number of shards: `number of expected documents / 5M +1`. - Number of Elasticsearch shards:
- Small shards result in small segments, which increases overhead. Aim to keep the average shard size between at least a few GB and a few tens of GB.
- Another consideration is the number of documents. To determine the number of shards to use, sum the numbers in the **Admin Area > Dashboard > Statistics** pane (the number of documents to be indexed), divide by 5 million, and add 5. For example:
- If you have fewer than about 2,000,000 documents, use the default of 5 shards
- 10,000,000 documents: `10000000/5000000 + 5` = 7 shards
- 100,000,000 documents: `100000000/5000000 + 5` = 25 shards
- `refresh_interval` is a per index setting. You may want to adjust that from default `1s` to a bigger value if you don't need data in real-time. This will change how soon you will see fresh results. If that's important for you, you should leave it as close as possible to the default value. - `refresh_interval` is a per index setting. You may want to adjust that from default `1s` to a bigger value if you don't need data in real-time. This will change how soon you will see fresh results. If that's important for you, you should leave it as close as possible to the default value.
- You might want to raise [`indices.memory.index_buffer_size`](https://www.elastic.co/guide/en/elasticsearch/reference/current/indexing-buffer.html) to 30% or 40% if you have a lot of heavy indexing operations. - You might want to raise [`indices.memory.index_buffer_size`](https://www.elastic.co/guide/en/elasticsearch/reference/current/indexing-buffer.html) to 30% or 40% if you have a lot of heavy indexing operations.
@ -884,3 +904,35 @@ Improvements to the `code_analyzer` pattern and filters are being discussed in [
### Some binary files may not be searchable by name ### Some binary files may not be searchable by name
In GitLab 13.9, a change was made where [binary file names are being indexed](https://gitlab.com/gitlab-org/gitlab/-/issues/301083). However, without indexing all projects' data from scratch, only binary files that are added or updated after the GitLab 13.9 release are searchable. In GitLab 13.9, a change was made where [binary file names are being indexed](https://gitlab.com/gitlab-org/gitlab/-/issues/301083). However, without indexing all projects' data from scratch, only binary files that are added or updated after the GitLab 13.9 release are searchable.
### Last resort to recreate an index
There may be cases where somehow data never got indexed and it's not in the
queue, or the index is somehow in a state where migrations just simply cannot
proceed. It is always best to try to troubleshoot the root cause of the problem
using the above [troubleshooting](#troubleshooting) steps.
If there are no other options, then you always have the option of recreating the
entire index from scratch. If you have a small GitLab installation, this can
sometimes be a quick way to resolve a problem, but if you have a large GitLab
installation, then this will likely take a very long time to complete. Until the
index is fully recreated, your index will not be serving correct search results,
so you may want to disable **Search with Elasticsearch** while it is running.
If you are sure you've read the above caveats and want to proceed, then you
should run the following Rake task to recreate the entire index from scratch:
**For Omnibus installations**
```shell
# WARNING: DO NOT RUN THIS UNTIL YOU READ THE DESCRIPTION ABOVE
sudo gitlab-rake gitlab:elastic:index
```
**For installations from source**
```shell
# WARNING: DO NOT RUN THIS UNTIL YOU READ THE DESCRIPTION ABOVE
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:elastic:index
```

View File

@ -138,14 +138,16 @@ pending_job_classes = scheduled_queue.select { |job| job["class"] == "Background
pending_job_classes.each { |job_class| Gitlab::BackgroundMigration.steal(job_class) } pending_job_classes.each { |job_class| Gitlab::BackgroundMigration.steal(job_class) }
``` ```
## Checking for pending Elasticsearch migrations ## Checking for pending Advanced Search migrations
This section is only applicable if you have enabled the [Elasticsearch This section is only applicable if you have enabled the [Elasticsearch
integration](../integration/elasticsearch.md). integration](../integration/elasticsearch.md).
Certain major releases might require [Elasticsearch Major releases require all [Advanced Search
migrations](../integration/elasticsearch.md#background-migrations) to be migrations](../integration/elasticsearch.md#advanced-search-migrations) to
finished. You can find pending migrations by running the following command: be finished from the most recent minor release in your current version
before the major version upgrade. You can find pending migrations by
running the following command:
**For Omnibus installations** **For Omnibus installations**
@ -160,7 +162,7 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:elastic:list_pending_migrations sudo -u git -H bundle exec rake gitlab:elastic:list_pending_migrations
``` ```
### What do I do if my Elasticsearch migrations are stuck? ### What do I do if my Advanced Search migrations are stuck?
See [how to retry a halted See [how to retry a halted
migration](../integration/elasticsearch.md#retry-a-halted-migration). migration](../integration/elasticsearch.md#retry-a-halted-migration).
@ -208,6 +210,13 @@ It's also important to ensure that any background migrations have been fully com
before upgrading to a new major version. To see the current size of the `background_migration` queue, before upgrading to a new major version. To see the current size of the `background_migration` queue,
[Check for background migrations before upgrading](#checking-for-background-migrations-before-upgrading). [Check for background migrations before upgrading](#checking-for-background-migrations-before-upgrading).
If you have enabled the [Elasticsearch
integration](../integration/elasticsearch.md), then you will also need to ensure
all Advanced Search migrations are completed in the last minor version within
your current version. Be sure to [check for pending Advanced Search
migrations](#checking-for-pending-advanced-search-migrations) before proceeding
with the major version upgrade.
If your GitLab instance has any runners associated with it, it is very If your GitLab instance has any runners associated with it, it is very
important to upgrade GitLab Runner to match the GitLab minor version that was important to upgrade GitLab Runner to match the GitLab minor version that was
upgraded to. This is to ensure [compatibility with GitLab versions](https://docs.gitlab.com/runner/#compatibility-with-gitlab-versions). upgraded to. This is to ensure [compatibility with GitLab versions](https://docs.gitlab.com/runner/#compatibility-with-gitlab-versions).

View File

@ -1,8 +0,0 @@
---
redirect_to: 'usage_trends.md'
---
This document was moved to [another location](usage_trends.md).
<!-- This redirect file can be deleted after <2022-03-20>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -1322,7 +1322,7 @@ Examples:
<!-- <!--
The "2." and "4." in the example above are changed to "1." below, to match the style The "2." and "4." in the example above are changed to "1." below, to match the style
standards on docs.gitlab.com. standards on docs.gitlab.com.
See https://docs.gitlab.com/ee/development/documentation/styleguide.html#lists See https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#lists
--> -->
1. First ordered list item 1. First ordered list item
@ -1356,7 +1356,7 @@ They can even:
<!-- <!--
The "*" and "+" in the example above are changed to "-" below, to match the style The "*" and "+" in the example above are changed to "-" below, to match the style
standards on docs.gitlab.com. standards on docs.gitlab.com.
See https://docs.gitlab.com/ee/development/documentation/styleguide.html#lists See https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#lists
--> -->
Unordered lists can: Unordered lists can:

View File

@ -1,8 +0,0 @@
---
redirect_to: advanced_search.md
---
This document was moved to [another location](advanced_search.md).
<!-- This redirect file can be deleted after <2021-02-12>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -1,8 +0,0 @@
---
redirect_to: advanced_search.md
---
This document was moved to [another location](advanced_search.md).
<!-- This redirect file can be deleted after <2021-02-12>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -19,6 +19,10 @@ module Gitlab
Settings Settings
end end
def self.host_with_port
"#{self.config.gitlab.host}:#{self.config.gitlab.port}"
end
def self.revision def self.revision
@_revision ||= begin @_revision ||= begin
if File.exist?(root.join("REVISION")) if File.exist?(root.join("REVISION"))

View File

@ -7,7 +7,7 @@ module Gitlab
module Registry module Registry
class DependencyProxy < GitlabRegistry class DependencyProxy < GitlabRegistry
def url def url
"#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}" Gitlab.host_with_port
end end
def valid? def valid?

View File

@ -78,7 +78,7 @@ then
echo echo
echo ' ✖ ERROR: The number of README.md file(s) has changed. Use index.md instead of README.md.' >&2 echo ' ✖ ERROR: The number of README.md file(s) has changed. Use index.md instead of README.md.' >&2
echo ' ✖ If removing a README.md file, update NUMBER_READMES in lint-doc.sh.' >&2 echo ' ✖ If removing a README.md file, update NUMBER_READMES in lint-doc.sh.' >&2
echo ' https://docs.gitlab.com/ee/development/documentation/styleguide.html#work-with-directories-and-files' echo ' https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#work-with-directories-and-files'
echo echo
((ERRORCODE++)) ((ERRORCODE++))
fi fi

View File

@ -42,7 +42,6 @@ describe('Board card component', () => {
GlLabel: true, GlLabel: true,
}, },
provide: { provide: {
groupId: null,
rootPath: '/', rootPath: '/',
scopedLabelsAvailable: false, scopedLabelsAvailable: false,
}, },

View File

@ -19,7 +19,11 @@ const createStore = (state = defaultState) => {
return new Vuex.Store({ return new Vuex.Store({
state, state,
actions, actions,
getters: { isEpicBoard: () => false }, getters: {
isGroupBoard: () => false,
isProjectBoard: () => true,
isEpicBoard: () => false,
},
}); });
}; };

View File

@ -3,6 +3,7 @@
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex';
import boardNewIssue from '~/boards/components/board_new_issue_deprecated.vue'; import boardNewIssue from '~/boards/components/board_new_issue_deprecated.vue';
import boardsStore from '~/boards/stores/boards_store'; import boardsStore from '~/boards/stores/boards_store';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
@ -10,6 +11,8 @@ import axios from '~/lib/utils/axios_utils';
import '~/boards/models/list'; import '~/boards/models/list';
import { listObj, boardsMockInterceptor } from './mock_data'; import { listObj, boardsMockInterceptor } from './mock_data';
Vue.use(Vuex);
describe('Issue boards new issue form', () => { describe('Issue boards new issue form', () => {
let wrapper; let wrapper;
let vm; let vm;
@ -43,11 +46,16 @@ describe('Issue boards new issue form', () => {
newIssueMock = Promise.resolve(promiseReturn); newIssueMock = Promise.resolve(promiseReturn);
jest.spyOn(list, 'newIssue').mockImplementation(() => newIssueMock); jest.spyOn(list, 'newIssue').mockImplementation(() => newIssueMock);
const store = new Vuex.Store({
getters: { isGroupBoard: () => false },
});
wrapper = mount(BoardNewIssueComp, { wrapper = mount(BoardNewIssueComp, {
propsData: { propsData: {
disabled: false, disabled: false,
list, list,
}, },
store,
provide: { provide: {
groupId: null, groupId: null,
}, },

View File

@ -8,6 +8,7 @@ import { formType } from '~/boards/constants';
import createBoardMutation from '~/boards/graphql/board_create.mutation.graphql'; import createBoardMutation from '~/boards/graphql/board_create.mutation.graphql';
import destroyBoardMutation from '~/boards/graphql/board_destroy.mutation.graphql'; import destroyBoardMutation from '~/boards/graphql/board_destroy.mutation.graphql';
import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql'; import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql';
import { createStore } from '~/boards/stores';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
@ -48,6 +49,13 @@ describe('BoardForm', () => {
const findDeleteConfirmation = () => wrapper.find('[data-testid="delete-confirmation-message"]'); const findDeleteConfirmation = () => wrapper.find('[data-testid="delete-confirmation-message"]');
const findInput = () => wrapper.find('#board-new-name'); const findInput = () => wrapper.find('#board-new-name');
const store = createStore({
getters: {
isGroupBoard: () => true,
isProjectBoard: () => false,
},
});
const createComponent = (props, data) => { const createComponent = (props, data) => {
wrapper = shallowMount(BoardForm, { wrapper = shallowMount(BoardForm, {
propsData: { ...defaultProps, ...props }, propsData: { ...defaultProps, ...props },
@ -64,6 +72,7 @@ describe('BoardForm', () => {
mutate, mutate,
}, },
}, },
store,
attachTo: document.body, attachTo: document.body,
}); });
}; };

View File

@ -2,7 +2,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import BoardNewIssue from '~/boards/components/board_new_issue.vue'; import BoardNewIssue from '~/boards/components/board_new_issue.vue';
import '~/boards/models/list';
import { mockList, mockGroupProjects } from '../mock_data'; import { mockList, mockGroupProjects } from '../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
@ -31,7 +30,7 @@ describe('Issue boards new issue form', () => {
const store = new Vuex.Store({ const store = new Vuex.Store({
state: { selectedProject: mockGroupProjects[0] }, state: { selectedProject: mockGroupProjects[0] },
actions: { addListNewIssue: addListNewIssuesSpy }, actions: { addListNewIssue: addListNewIssuesSpy },
getters: {}, getters: { isGroupBoard: () => false, isProjectBoard: () => true },
}); });
wrapper = shallowMount(BoardNewIssue, { wrapper = shallowMount(BoardNewIssue, {

View File

@ -0,0 +1,65 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import FilteredSearch from '~/boards/components/filtered_search.vue';
import { createStore } from '~/boards/stores';
import * as commonUtils from '~/lib/utils/common_utils';
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('FilteredSearch', () => {
let wrapper;
let store;
const createComponent = () => {
wrapper = shallowMount(FilteredSearch, {
localVue,
propsData: { search: '' },
store,
attachTo: document.body,
});
};
beforeEach(() => {
// this needed for actions call for performSearch
window.gon = { features: {} };
});
afterEach(() => {
wrapper.destroy();
});
describe('default', () => {
beforeEach(() => {
store = createStore();
jest.spyOn(store, 'dispatch');
createComponent();
});
it('finds FilteredSearch', () => {
expect(wrapper.find(FilteredSearchBarRoot).exists()).toBe(true);
});
describe('when onFilter is emitted', () => {
it('calls performSearch', () => {
wrapper.find(FilteredSearchBarRoot).vm.$emit('onFilter', [{ value: { data: '' } }]);
expect(store.dispatch).toHaveBeenCalledWith('performSearch');
});
it('calls historyPushState', () => {
commonUtils.historyPushState = jest.fn();
wrapper
.find(FilteredSearchBarRoot)
.vm.$emit('onFilter', [{ value: { data: 'searchQuery' } }]);
expect(commonUtils.historyPushState).toHaveBeenCalledWith(
'http://test.host/?search=searchQuery',
);
});
});
});
});

View File

@ -10,6 +10,42 @@ import {
} from '../mock_data'; } from '../mock_data';
describe('Boards - Getters', () => { describe('Boards - Getters', () => {
describe('isGroupBoard', () => {
it('returns true when boardType on state is group', () => {
const state = {
boardType: 'group',
};
expect(getters.isGroupBoard(state)).toBe(true);
});
it('returns false when boardType on state is not group', () => {
const state = {
boardType: 'project',
};
expect(getters.isGroupBoard(state)).toBe(false);
});
});
describe('isProjectBoard', () => {
it('returns true when boardType on state is project', () => {
const state = {
boardType: 'project',
};
expect(getters.isProjectBoard(state)).toBe(true);
});
it('returns false when boardType on state is not project', () => {
const state = {
boardType: 'group',
};
expect(getters.isProjectBoard(state)).toBe(false);
});
});
describe('isSidebarOpen', () => { describe('isSidebarOpen', () => {
it('returns true when activeId is not equal to 0', () => { it('returns true when activeId is not equal to 0', () => {
const state = { const state = {

View File

@ -16,6 +16,8 @@ RSpec.describe Projects::IssuesController, '(JavaScript fixtures)', type: :contr
end end
before do before do
stub_feature_flags(boards_filtered_search: false)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)
end end

View File

@ -188,7 +188,7 @@ describe('GroupItemComponent', () => {
}); });
it('should render component template correctly', () => { it('should render component template correctly', () => {
const visibilityIconEl = vm.$el.querySelector('.item-visibility'); const visibilityIconEl = vm.$el.querySelector('[data-testid="group-visibility-icon"]');
expect(vm.$el.getAttribute('id')).toBe('group-55'); expect(vm.$el.getAttribute('id')).toBe('group-55');
expect(vm.$el.classList.contains('group-row')).toBeTruthy(); expect(vm.$el.classList.contains('group-row')).toBeTruthy();
@ -209,8 +209,7 @@ describe('GroupItemComponent', () => {
expect(vm.$el.querySelector('.title a.no-expand')).toBeDefined(); expect(vm.$el.querySelector('.title a.no-expand')).toBeDefined();
expect(visibilityIconEl).not.toBe(null); expect(visibilityIconEl).not.toBe(null);
expect(visibilityIconEl.title).toBe(vm.visibilityTooltip); expect(visibilityIconEl.getAttribute('title')).toBe(vm.visibilityTooltip);
expect(visibilityIconEl.querySelectorAll('svg').length).toBeGreaterThan(0);
expect(vm.$el.querySelector('.access-type')).toBeDefined(); expect(vm.$el.querySelector('.access-type')).toBeDefined();
expect(vm.$el.querySelector('.description')).toBeDefined(); expect(vm.$el.querySelector('.description')).toBeDefined();

View File

@ -6,6 +6,7 @@ import axios from '~/lib/utils/axios_utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import noteActions from '~/notes/components/note_actions.vue'; import noteActions from '~/notes/components/note_actions.vue';
import createStore from '~/notes/stores'; import createStore from '~/notes/stores';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { userDataMock } from '../mock_data'; import { userDataMock } from '../mock_data';
describe('noteActions', () => { describe('noteActions', () => {
@ -15,6 +16,9 @@ describe('noteActions', () => {
let actions; let actions;
let axiosMock; let axiosMock;
const findUserAccessRoleBadge = (idx) => wrapper.findAll(UserAccessRoleBadge).at(idx);
const findUserAccessRoleBadgeText = (idx) => findUserAccessRoleBadge(idx).text().trim();
const mountNoteActions = (propsData, computed) => { const mountNoteActions = (propsData, computed) => {
const localVue = createLocalVue(); const localVue = createLocalVue();
return mount(localVue.extend(noteActions), { return mount(localVue.extend(noteActions), {
@ -66,11 +70,11 @@ describe('noteActions', () => {
}); });
it('should render noteable author badge', () => { it('should render noteable author badge', () => {
expect(wrapper.findAll('.note-role').at(0).text().trim()).toEqual('Author'); expect(findUserAccessRoleBadgeText(0)).toBe('Author');
}); });
it('should render access level badge', () => { it('should render access level badge', () => {
expect(wrapper.findAll('.note-role').at(1).text().trim()).toEqual(props.accessLevel); expect(findUserAccessRoleBadgeText(1)).toBe(props.accessLevel);
}); });
it('should render contributor badge', () => { it('should render contributor badge', () => {
@ -80,7 +84,7 @@ describe('noteActions', () => {
}); });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.findAll('.note-role').at(1).text().trim()).toBe('Contributor'); expect(findUserAccessRoleBadgeText(1)).toBe('Contributor');
}); });
}); });

View File

@ -0,0 +1,26 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
describe('UserAccessRoleBadge', () => {
let wrapper;
const createComponent = ({ slots } = {}) => {
wrapper = shallowMount(UserAccessRoleBadge, {
slots,
});
};
it('renders slot content inside GlBadge', () => {
createComponent({
slots: {
default: 'test slot content',
},
});
const badge = wrapper.find(GlBadge);
expect(badge.exists()).toBe(true);
expect(badge.html()).toContain('test slot content');
});
});

View File

@ -166,7 +166,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
let(:ports) { Project::VALID_IMPORT_PORTS } let(:ports) { Project::VALID_IMPORT_PORTS }
it 'allows imports from configured web host and port' do it 'allows imports from configured web host and port' do
import_url = "http://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/t.git" import_url = "http://#{Gitlab.host_with_port}/t.git"
expect(described_class.blocked_url?(import_url)).to be false expect(described_class.blocked_url?(import_url)).to be false
end end
@ -190,7 +190,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
end end
it 'returns true for bad protocol on configured web/SSH host and ports' do it 'returns true for bad protocol on configured web/SSH host and ports' do
web_url = "javascript://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/t.git%0aalert(1)" web_url = "javascript://#{Gitlab.host_with_port}/t.git%0aalert(1)"
expect(described_class.blocked_url?(web_url)).to be true expect(described_class.blocked_url?(web_url)).to be true
ssh_url = "javascript://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git%0aalert(1)" ssh_url = "javascript://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git%0aalert(1)"

View File

@ -2465,9 +2465,9 @@ RSpec.describe Ci::Build do
{ key: 'CI_CONFIG_PATH', value: project.ci_config_path_or_default, public: true, masked: false }, { key: 'CI_CONFIG_PATH', value: project.ci_config_path_or_default, public: true, masked: false },
{ key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host, public: true, masked: false }, { key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host, public: true, masked: false },
{ key: 'CI_PAGES_URL', value: project.pages_url, public: true, masked: false }, { key: 'CI_PAGES_URL', value: project.pages_url, public: true, masked: false },
{ key: 'CI_DEPENDENCY_PROXY_SERVER', value: "#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}", public: true, masked: false }, { key: 'CI_DEPENDENCY_PROXY_SERVER', value: Gitlab.host_with_port, public: true, masked: false },
{ key: 'CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX', { key: 'CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX',
value: "#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/#{project.namespace.root_ancestor.path}#{DependencyProxy::URL_SUFFIX}", value: "#{Gitlab.host_with_port}/#{project.namespace.root_ancestor.path}#{DependencyProxy::URL_SUFFIX}",
public: true, public: true,
masked: false }, masked: false },
{ key: 'CI_API_V4_URL', value: 'http://localhost/api/v4', public: true, masked: false }, { key: 'CI_API_V4_URL', value: 'http://localhost/api/v4', public: true, masked: false },

View File

@ -246,6 +246,12 @@ RSpec.configure do |config|
stub_feature_flags(unified_diff_components: false) stub_feature_flags(unified_diff_components: false)
# Disable this feature flag as we iterate and
# refactor filtered search to use gitlab ui
# components to meet feature parody. More details found
# https://gitlab.com/groups/gitlab-org/-/epics/5501
stub_feature_flags(boards_filtered_search: false)
allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(enable_rugged) allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(enable_rugged)
else else
unstub_all_feature_flags unstub_all_feature_flags