Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-08-07 12:08:52 +00:00
parent 9297127929
commit 28f0cd8e07
76 changed files with 1110 additions and 329 deletions

View File

@ -1 +1 @@
0c10f5a60848a35049400dd775126b3ad4481663
e2d9b43be9a9c5fcbc1f1ae1660520ba12e55224

View File

@ -1 +1 @@
14.25.0
14.23.0

View File

@ -94,6 +94,7 @@ export default {
return {
schedules: {
list: [],
currentUser: {},
},
scope,
hasError: false,
@ -135,6 +136,14 @@ export default {
},
];
},
onAllTab() {
// scope is undefined on first load, scope is only defined
// after tab switching
return this.scope === ALL_SCOPE || !this.scope;
},
showEmptyState() {
return !this.isLoading && this.schedulesCount === 0 && this.onAllTab;
},
},
watch: {
// this watcher ensures that the count on the all tab
@ -258,8 +267,10 @@ export default {
</gl-sprintf>
</gl-alert>
<pipeline-schedule-empty-state v-if="showEmptyState" />
<gl-tabs
v-if="isLoading || schedulesCount > 0"
v-else
sync-active-tab-with-query-params
query-param-name="scope"
nav-class="gl-flex-grow-1 gl-align-items-center gl-mt-2"
@ -284,6 +295,7 @@ export default {
</template>
<gl-loading-icon v-if="isLoading" size="lg" />
<pipeline-schedules-table
v-else
:schedules="schedules.list"
@ -306,8 +318,6 @@ export default {
</template>
</gl-tabs>
<pipeline-schedule-empty-state v-else-if="!isLoading && schedulesCount === 0" />
<take-ownership-modal
:visible="showTakeOwnershipModal"
@takeOwnership="takeOwnership"

View File

@ -1,5 +1,5 @@
<script>
import { GlTableLite } from '@gitlab/ui';
import { GlTable } from '@gitlab/ui';
import { s__ } from '~/locale';
import PipelineScheduleActions from './cells/pipeline_schedule_actions.vue';
import PipelineScheduleLastPipeline from './cells/pipeline_schedule_last_pipeline.vue';
@ -8,6 +8,9 @@ import PipelineScheduleOwner from './cells/pipeline_schedule_owner.vue';
import PipelineScheduleTarget from './cells/pipeline_schedule_target.vue';
export default {
i18n: {
emptyText: s__('PipelineSchedules|No pipeline schedules'),
},
fields: [
{
key: 'description',
@ -47,7 +50,7 @@ export default {
},
],
components: {
GlTableLite,
GlTable,
PipelineScheduleActions,
PipelineScheduleLastPipeline,
PipelineScheduleNextRun,
@ -68,10 +71,12 @@ export default {
</script>
<template>
<gl-table-lite
<gl-table
:fields="$options.fields"
:items="schedules"
:tbody-tr-attr="{ 'data-testid': 'pipeline-schedule-table-row' }"
:empty-text="$options.i18n.emptyText"
show-empty
stacked="md"
>
<template #table-colgroup="{ fields }">
@ -109,5 +114,5 @@ export default {
@playPipelineSchedule="$emit('playPipelineSchedule', $event)"
/>
</template>
</gl-table-lite>
</gl-table>
</template>

View File

@ -31,6 +31,23 @@ export default {
type: Number,
required: true,
},
userPermissions: {
type: Object,
required: true,
},
},
computed: {
primaryAction() {
if (!this.userPermissions.createCustomEmoji) return undefined;
return {
text: __('New custom emoji'),
attributes: {
variant: 'info',
to: '/new',
},
};
},
},
methods: {
prevPage() {
@ -47,13 +64,6 @@ export default {
return formatDate(date, 'mmmm d, yyyy');
},
},
primaryAction: {
text: __('New custom emoji'),
attributes: {
variant: 'info',
to: '/new',
},
},
fields: [
{
key: 'emoji',
@ -89,7 +99,7 @@ export default {
<div>
<gl-loading-icon v-if="loading" size="lg" />
<template v-else>
<gl-tabs content-class="gl-pt-0" :action-primary="$options.primaryAction">
<gl-tabs content-class="gl-pt-0" :action-primary="primaryAction">
<gl-tab>
<template #title>
{{ __('Emoji') }}

View File

@ -1,7 +1,7 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import defaultClient from './graphql_client';
import routes from './routes';
import App from './components/app.vue';
@ -14,7 +14,7 @@ export const initCustomEmojis = () => {
if (!el) return;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
defaultClient,
});
const router = new VueRouter({
base: el.dataset.basePath,

View File

@ -0,0 +1,3 @@
import createDefaultClient from '~/lib/graphql';
export default createDefaultClient();

View File

@ -19,6 +19,7 @@ export default {
result({ data }) {
const pageInfo = data.group?.customEmoji?.pageInfo;
this.count = data.group?.customEmoji?.count;
this.userPermissions = data.group?.userPermissions;
if (pageInfo) {
this.pageInfo = pageInfo;
@ -40,6 +41,7 @@ export default {
count: 0,
pageInfo: {},
pagination: {},
userPermissions: {},
};
},
methods: {
@ -59,6 +61,7 @@ export default {
:loading="$apollo.queries.customEmojis.loading"
:page-info="pageInfo"
:custom-emojis="customEmojis"
:user-permissions="userPermissions"
@input="changePage"
/>
</template>

View File

@ -3,6 +3,9 @@
query getCustomEmojis($groupPath: ID!, $after: String = "", $before: String = "") {
group(fullPath: $groupPath) {
id
userPermissions {
createCustomEmoji
}
customEmoji(after: $after, before: $before) {
count
pageInfo {

View File

@ -0,0 +1,8 @@
query customEmojiPermissions($groupPath: ID!) {
group(fullPath: $groupPath) {
id
userPermissions {
createCustomEmoji
}
}
}

View File

@ -1,5 +1,7 @@
import IndexComponent from './pages/index.vue';
import NewComponent from './pages/new.vue';
import userPermissionsQuery from './queries/user_permissions.query.graphql';
import defaultClient from './graphql_client';
export default [
{
@ -9,5 +11,25 @@ export default [
{
path: '/new',
component: NewComponent,
async beforeEnter(to, from, next) {
const {
data: {
group: {
userPermissions: { createCustomEmoji },
},
},
} = await defaultClient.query({
query: userPermissionsQuery,
variables: {
groupPath: document.body.dataset.groupFullPath,
},
});
if (!createCustomEmoji) {
next({ path: '/' });
} else {
next();
}
},
},
];

View File

@ -67,7 +67,16 @@ const transformers = {
const transformOptions = (options = {}) => {
const defaultConfig = {
routes: [],
routes: [
{
path: '/',
component: {
render() {
return '';
},
},
},
],
history: createWebHashHistory(),
};
return Object.keys(options).reduce((acc, key) => {

View File

@ -71,8 +71,8 @@ export default {
type: Object,
required: true,
},
projects: {
type: Array,
targetProjectsPath: {
type: String,
required: true,
},
straight: {
@ -83,7 +83,6 @@ export default {
data() {
return {
from: {
projects: this.projects,
selectedProject: this.targetProject,
revision: this.paramsFrom,
refsProjectPath: this.targetProjectRefsPath,
@ -101,7 +100,7 @@ export default {
this.$refs.form.submit();
},
onSelectProject({ direction, project }) {
const refsPath = joinPaths(gon.relative_url_root || '', `/${project.name}`, '/refs');
const refsPath = joinPaths(gon.relative_url_root || '', `/${project.text}`, '/refs');
// direction is either 'from' or 'to'
this[direction].refsProjectPath = refsPath;
this[direction].selectedProject = project;
@ -149,8 +148,8 @@ export default {
:refs-project-path="to.refsProjectPath"
:revision-text="$options.i18n.source"
params-name="to"
:endpoint="targetProjectsPath"
:params-branch="to.revision"
:projects="to.projects"
:selected-project="to.selectedProject"
@selectProject="onSelectProject"
@selectRevision="onSelectRevision"
@ -179,6 +178,7 @@ export default {
:params-branch="from.revision"
:projects="from.projects"
:selected-project="from.selectedProject"
:endpoint="targetProjectsPath"
@selectProject="onSelectProject"
@selectRevision="onSelectRevision"
/>

View File

@ -1,5 +1,9 @@
<script>
import { GlCollapsibleListbox } from '@gitlab/ui';
import { debounce } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
export default {
components: {
@ -10,10 +14,10 @@ export default {
type: String,
required: true,
},
projects: {
type: Array,
endpoint: {
type: String,
required: false,
default: null,
default: '',
},
selectedProject: {
type: Object,
@ -22,34 +26,58 @@ export default {
},
data() {
return {
searchTerm: '',
isLoading: false,
selectedProjectId: this.selectedProject.id,
projects: [],
searchStr: '',
debouncedProjectsSearch: null,
};
},
computed: {
disableRepoDropdown() {
return this.projects === null;
},
filteredRepos() {
if (this.disableRepoDropdown) return [];
const lowerCaseSearchTerm = this.searchTerm.toLowerCase();
return this.projects
.filter(({ name }) => name.toLowerCase().includes(lowerCaseSearchTerm))
.map((project) => ({ text: project.name, value: project.id }));
isDropdownDisabled() {
return this.paramsName === 'to';
},
inputName() {
return `${this.paramsName}_project_id`;
},
},
created() {
if (!this.isDropdownDisabled) {
this.fetchProjects();
}
this.debouncedProjectsSearch = debounce(this.fetchProjects, 500);
},
methods: {
emitTargetProject(projectId) {
if (this.disableRepoDropdown) return;
const project = this.projects.find(({ id }) => id === projectId);
if (this.isDropdownDisabled) return;
const project = this.projects.find(({ value }) => value === projectId);
this.$emit('selectProject', { direction: this.paramsName, project });
},
onSearch(searchTerm) {
this.searchTerm = searchTerm;
async fetchProjects() {
if (!this.endpoint) return;
this.isLoading = true;
try {
const { data } = await axios.get(this.endpoint, {
params: { search: this.searchStr },
});
this.projects = data.map((p) => ({
value: `${p.id}`,
text: p.full_path.replace(/^\//, ''),
}));
} catch {
createAlert({
message: __('Error fetching data. Please try again.'),
primaryButton: { text: __('Try again'), clickHandler: () => this.fetchProjects() },
});
}
this.isLoading = false;
},
searchProjects(search) {
this.searchStr = search;
this.debouncedProjectsSearch();
},
},
};
@ -60,16 +88,17 @@ export default {
<input type="hidden" :name="inputName" :value="selectedProjectId" />
<gl-collapsible-listbox
v-model="selectedProjectId"
:toggle-text="selectedProject.name"
:toggle-text="selectedProject.text"
:loading="isLoading"
:header-text="s__(`CompareRevisions|Select target project`)"
class="gl-font-monospace"
toggle-class="gl-min-w-0"
:disabled="disableRepoDropdown"
:items="filteredRepos"
:disabled="isDropdownDisabled"
:items="projects"
block
searchable
@select="emitTargetProject"
@search="onSearch"
@search="searchProjects"
/>
</div>
</template>

View File

@ -25,8 +25,8 @@ export default {
required: false,
default: null,
},
projects: {
type: Array,
endpoint: {
type: String,
required: false,
default: null,
},
@ -47,7 +47,7 @@ export default {
<repo-dropdown
class="gl-sm-w-half"
:params-name="paramsName"
:projects="projects"
:endpoint="endpoint"
:selected-project="selectedProject"
v-on="$listeners"
/>

View File

@ -146,7 +146,7 @@ export default {
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="branch in branches"
:key="branch"
:key="`${branch}-branch`"
is-check-item
:is-checked="selectedRevision === branch"
data-testid="branches-dropdown-item"
@ -159,7 +159,7 @@ export default {
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="tag in tags"
:key="tag"
:key="`${tag}-tag`"
is-check-item
:is-checked="selectedRevision === tag"
data-testid="tags-dropdown-item"

View File

@ -16,7 +16,7 @@ export default function init() {
createMrPath,
sourceProject,
targetProject,
projectsFrom,
targetProjectsPath,
} = el.dataset;
return new Vue({
@ -35,9 +35,9 @@ export default function init() {
projectCompareIndexPath,
projectMergeRequestPath,
createMrPath,
targetProjectsPath,
sourceProject: JSON.parse(sourceProject),
targetProject: JSON.parse(targetProject),
projects: JSON.parse(projectsFrom),
},
});
},

View File

@ -6,8 +6,7 @@ import {
GlFormCheckbox,
GlFormInput,
GlFormInputGroup,
GlDropdown,
GlDropdownItem,
GlCollapsibleListbox,
GlFormGroup,
} from '@gitlab/ui';
import $ from 'jquery';
@ -25,8 +24,7 @@ export default {
GlFormCheckbox,
GlFormInput,
GlFormInputGroup,
GlDropdown,
GlDropdownItem,
GlCollapsibleListbox,
GlFormGroup,
EmojiPicker: () => import('~/emoji/components/picker.vue'),
},
@ -79,6 +77,9 @@ export default {
noEmoji() {
return this.emojiTag === '';
},
clearStatusAfterValue() {
return this.clearStatusAfter?.name;
},
clearStatusAfterDropdownText() {
if (this.clearStatusAfter === null && this.currentClearStatusAfter.length) {
return this.formatClearStatusAfterDate(new Date(this.currentClearStatusAfter));
@ -94,11 +95,18 @@ export default {
return NEVER_TIME_RANGE.label;
},
clearStatusAfterDropdownItems() {
return TIME_RANGES_WITH_NEVER.map((item) => ({ text: item.label, value: item.name }));
},
},
mounted() {
this.setupEmojiListAndAutocomplete();
},
methods: {
onClearStatusAfterValueChange(value) {
const selectedValue = TIME_RANGES_WITH_NEVER.find((i) => i.name === value);
this.$emit('clear-status-after-click', selectedValue);
},
async setupEmojiListAndAutocomplete() {
const emojiAutocomplete = new GfmAutoComplete();
emojiAutocomplete.setup($(this.$refs.statusMessageField.$el), { emojis: true });
@ -221,20 +229,13 @@ export default {
</gl-form-checkbox>
<gl-form-group :label="$options.i18n.clearStatusAfterDropdownLabel" class="gl-mb-0">
<gl-dropdown
block
:text="clearStatusAfterDropdownText"
<gl-collapsible-listbox
:selected="clearStatusAfterValue"
:toggle-text="clearStatusAfterDropdownText"
:items="clearStatusAfterDropdownItems"
data-testid="clear-status-at-dropdown"
toggle-class="gl-mb-0 gl-form-input-md"
>
<gl-dropdown-item
v-for="after in $options.TIME_RANGES_WITH_NEVER"
:key="after.name"
:data-testid="after.name"
@click="$emit('clear-status-after-click', after)"
>{{ after.label }}</gl-dropdown-item
>
</gl-dropdown>
@select="onClearStatusAfterValueChange"
/>
</gl-form-group>
</div>
</template>

View File

@ -1,12 +1,10 @@
export const AJAX_USERS_SELECT_PARAMS_MAP = {
project_id: 'projectId',
group_id: 'groupId',
skip_ldap: 'skipLdap',
todo_filter: 'todoFilter',
todo_state_filter: 'todoStateFilter',
current_user: 'showCurrentUser',
author_id: 'authorId',
skip_users: 'skipUsers',
states: 'states',
};

View File

@ -4,7 +4,7 @@
import $ from 'jquery';
import { escape, template, uniqBy } from 'lodash';
import { AJAX_USERS_SELECT_PARAMS_MAP } from 'ee_else_ce/users_select/constants';
import { AJAX_USERS_SELECT_PARAMS_MAP } from '~/users_select/constants';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { TYPE_MERGE_REQUEST } from '~/issues/constants';
import { isUserBusy } from '~/set_status_modal/utils';

View File

@ -1,12 +1,11 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlCollapsibleListbox } from '@gitlab/ui';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
export default {
name: 'PersistedDropdownSelection',
components: {
GlDropdown,
GlDropdownItem,
GlCollapsibleListbox,
LocalStorageSync,
},
props: {
@ -21,16 +20,15 @@ export default {
},
data() {
return {
selected: null,
selected: this.options[0].value,
};
},
computed: {
dropdownText() {
const selected = this.parsedOptions.find((o) => o.selected);
return selected?.label || this.options[0].label;
},
parsedOptions() {
return this.options.map((o) => ({ ...o, selected: o.value === this.selected }));
listboxItems() {
return this.options.map((option) => ({
value: option.value,
text: option.label,
}));
},
},
methods: {
@ -44,16 +42,6 @@ export default {
<template>
<local-storage-sync :storage-key="storageKey" :value="selected" as-string @input="setSelected">
<gl-dropdown :text="dropdownText" lazy>
<gl-dropdown-item
v-for="option in parsedOptions"
:key="option.value"
:is-checked="option.selected"
is-check-item
@click="setSelected(option.value)"
>
{{ option.label }}
</gl-dropdown-item>
</gl-dropdown>
<gl-collapsible-listbox v-model="selected" :items="listboxItems" @select="setSelected" />
</local-storage-sync>
</template>

View File

@ -3,6 +3,7 @@ import { GlAvatarLink, GlSprintf } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import WorkItemStateBadge from '~/work_items/components/work_item_state_badge.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
export default {
@ -11,6 +12,7 @@ export default {
GlSprintf,
TimeAgoTooltip,
WorkItemStateBadge,
WorkItemTypeIcon,
},
inject: ['fullPath'],
props: {
@ -36,6 +38,12 @@ export default {
workItemState() {
return this.workItem?.state;
},
workItemType() {
return this.workItem?.workItemType?.name;
},
workItemIconName() {
return this.workItem?.workItemType?.iconName;
},
},
apollo: {
workItem: {
@ -58,10 +66,16 @@ export default {
</script>
<template>
<div class="gl-mb-3">
<div class="gl-mb-3 gl-text-gray-700">
<work-item-state-badge v-if="workItemState" :work-item-state="workItemState" />
<work-item-type-icon
class="gl-vertical-align-middle gl-mr-0!"
:work-item-icon-name="workItemIconName"
:work-item-type="workItemType"
show-text
/>
<span data-testid="work-item-created" class="gl-vertical-align-middle">
<gl-sprintf v-if="author.name" :message="__('Created %{timeAgo} by %{author}')">
<gl-sprintf v-if="author.name" :message="__('created %{timeAgo} by %{author}')">
<template #timeAgo>
<time-ago-tooltip :time="createdAt" />
</template>
@ -76,7 +90,7 @@ export default {
</gl-avatar-link>
</template>
</gl-sprintf>
<gl-sprintf v-else-if="createdAt" :message="__('Created %{timeAgo}')">
<gl-sprintf v-else-if="createdAt" :message="__('created %{timeAgo}')">
<template #timeAgo>
<time-ago-tooltip :time="createdAt" />
</template>
@ -85,7 +99,7 @@ export default {
<span
v-if="updatedAt"
class="gl-ml-5 gl-display-none gl-sm-display-inline-block"
class="gl-ml-5 gl-display-none gl-sm-display-inline-block gl-vertical-align-middle"
data-testid="work-item-updated"
>
<gl-sprintf :message="__('Updated %{timeAgo}')">

View File

@ -172,7 +172,7 @@ export default {
return this.workItem.workItemType?.id;
},
workItemBreadcrumbReference() {
return this.workItemType ? `${this.workItemType} #${this.workItem.iid}` : '';
return this.workItemType ? `#${this.workItem.iid}` : '';
},
canUpdate() {
return this.workItem?.userPermissions?.updateWorkItem;
@ -421,7 +421,8 @@ export default {
>
<work-item-type-icon
:work-item-icon-name="workItemIconName"
:work-item-type="workItemType && workItemType.toUpperCase()"
:work-item-type="workItemType"
show-text
/>
{{ workItemBreadcrumbReference }}
</li>
@ -433,7 +434,8 @@ export default {
>
<work-item-type-icon
:work-item-icon-name="workItemIconName"
:work-item-type="workItemType && workItemType.toUpperCase()"
:work-item-type="workItemType"
show-text
/>
{{ workItemBreadcrumbReference }}
</div>

View File

@ -32,13 +32,18 @@ export default {
},
},
computed: {
workItemTypeUppercase() {
return this.workItemType.toUpperCase().split(' ').join('_');
},
iconName() {
return (
this.workItemIconName || WORK_ITEMS_TYPE_MAP[this.workItemType]?.icon || 'issue-type-issue'
this.workItemIconName ||
WORK_ITEMS_TYPE_MAP[this.workItemTypeUppercase]?.icon ||
'issue-type-issue'
);
},
workItemTypeName() {
return WORK_ITEMS_TYPE_MAP[this.workItemType]?.name;
return WORK_ITEMS_TYPE_MAP[this.workItemTypeUppercase]?.name;
},
workItemTooltipTitle() {
return this.showTooltipOnHover ? this.workItemTypeName : '';

View File

@ -159,7 +159,7 @@ export const WORK_ITEMS_TYPE_MAP = {
},
[WORK_ITEM_TYPE_ENUM_KEY_RESULT]: {
icon: `issue-type-keyresult`,
name: s__('WorkItem|Key Result'),
name: s__('WorkItem|Key result'),
value: WORK_ITEM_TYPE_VALUE_KEY_RESULT,
},
};

View File

@ -4,7 +4,7 @@ module Organizations
class OrganizationsController < ApplicationController
feature_category :cell
before_action { authorize_action!(:admin_organization) }
before_action { authorize_action!(:read_organization) }
def show; end

View File

@ -111,7 +111,7 @@ class SearchController < ApplicationController
end
def autocomplete
term = params[:term]
term = params.require(:term)
@project = search_service.project
@ref = params[:project_ref] if params[:project_ref].present?

View File

@ -10,21 +10,21 @@ module Autocomplete
# ensure good performance.
LIMIT = 20
attr_reader :current_user, :project, :group, :search, :skip_users,
attr_reader :current_user, :project, :group, :search,
:author_id, :todo_filter, :todo_state_filter,
:filter_by_current_user, :states
:filter_by_current_user, :states, :push_code
def initialize(params:, current_user:, project:, group:)
@current_user = current_user
@project = project
@group = group
@search = params[:search]
@skip_users = params[:skip_users]
@author_id = params[:author_id]
@todo_filter = params[:todo_filter]
@todo_state_filter = params[:todo_state_filter]
@filter_by_current_user = params[:current_user]
@states = params[:states] || ['active']
@push_code = params[:push_code]
end
def execute
@ -39,6 +39,8 @@ module Autocomplete
end
end
items = filter_users_by_push_ability(items)
items.uniq.tap do |unique_items|
preload_associations(unique_items)
end
@ -65,7 +67,6 @@ module Autocomplete
.non_internal
.reorder_by_name
.optionally_search(search, use_minimum_char_limit: use_minimum_char_limit)
.where_not_in(skip_users)
.limit_to_todo_authors(
user: current_user,
with_todos: todo_filter,
@ -96,6 +97,12 @@ module Autocomplete
end
end
def filter_users_by_push_ability(items)
return items unless project && push_code.present?
items.select { |user| user.can?(:push_code, project) }
end
# rubocop: disable CodeReuse/ActiveRecord
def preload_associations(items)
ActiveRecord::Associations::Preloader.new(records: items, associations: :status).call
@ -109,5 +116,3 @@ module Autocomplete
end
end
end
Autocomplete::UsersFinder.prepend_mod_with('Autocomplete::UsersFinder')

View File

@ -36,19 +36,16 @@ module CompareHelper
def project_compare_selector_data(project, merge_request, params)
{
target_projects_path: project_new_merge_request_json_target_projects_path(@target_project),
project_compare_index_path: project_compare_index_path(project),
source_project: { id: project.id, name: project.full_path }.to_json,
target_project: { id: @target_project.id, name: @target_project.full_path }.to_json,
source_project: { id: project.id, text: project.full_path }.to_json,
target_project: { id: @target_project.id, text: @target_project.full_path }.to_json,
source_project_refs_path: refs_project_path(project),
target_project_refs_path: refs_project_path(@target_project),
params_from: params[:from],
params_to: params[:to],
straight: params[:straight]
}.tap do |data|
data[:projects_from] = target_projects(project).map do |target_project|
{ id: target_project.id, name: target_project.full_path }
end.to_json
data[:project_merge_request_path] =
if merge_request.present?
project_merge_request_path(project, merge_request)

View File

@ -16,7 +16,7 @@ module PackagesHelper
end
def package_registry_project_url(project_id, registry_type = :maven)
project_api_path = expose_path(api_v4_projects_path(id: project_id))
project_api_path = api_v4_projects_path(id: project_id)
package_registry_project_path = "#{project_api_path}/packages/#{registry_type}"
expose_url(package_registry_project_path)
end

View File

@ -37,6 +37,10 @@ module Organizations
path
end
def user?(user)
users.exists?(user.id)
end
private
def check_if_default_organization

View File

@ -2,8 +2,15 @@
module Organizations
class OrganizationPolicy < BasePolicy
condition(:organization_user) { @subject.user?(@user) }
rule { admin }.policy do
enable :admin_organization
enable :read_organization
end
rule { organization_user }.policy do
enable :read_organization
end
end
end

View File

@ -1,7 +1,13 @@
- page_title _("Sign in")
- content_for :page_specific_javascripts do
= render "layouts/google_tag_manager_head"
= render "layouts/one_trust"
- content_for :sessions_broadcast do
- unless Gitlab.com?
= render "layouts/broadcast"
= render "layouts/google_tag_manager_body"
#signin-container

View File

@ -7,6 +7,7 @@
= header_message
= render "layouts/init_client_detection_flags"
- if Feature.enabled?(:restyle_login_page, @project)
= yield :sessions_broadcast
.gl-h-full.borderless.gl-display-flex.gl-flex-wrap
.container
.content
@ -36,6 +37,7 @@
= render 'devise/shared/footer'
- else
= render "layouts/header/empty"
= yield :sessions_broadcast
.gl-h-full.gl-display-flex.gl-flex-wrap
.container
.content

View File

@ -0,0 +1,6 @@
---
migration_job_name: FixAllowDescendantsOverrideDisabledSharedRunners
description: Clears invalid combination of shared runners settings (fixes subgroup creation)
feature_category: runner_fleet
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128112
milestone: 16.3

View File

@ -12,6 +12,3 @@ allow_cross_transactions:
- gitlab_main_clusterwide
allow_cross_foreign_keys:
- gitlab_main
# Temporarily allow FKs between clusterwide and cell schemas
# This is to be removed once we remove all FKs between those
- gitlab_main_clusterwide

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
class QueueFixAllowDescendantsOverrideDisabledSharedRunners < Gitlab::Database::Migration[2.1]
MIGRATION = "FixAllowDescendantsOverrideDisabledSharedRunners"
DELAY_INTERVAL = 2.minutes
BATCH_SIZE = 25000
SUB_BATCH_SIZE = 250
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main
def up
queue_batched_background_migration(
MIGRATION,
:namespaces,
:id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
end
def down
delete_batched_background_migration(MIGRATION, :namespaces, :id, [])
end
end

View File

@ -0,0 +1 @@
f96b1ce7addca2cb49faf339e3e92ac69a4ee98e1ff298393d2dafc375309909

View File

@ -205,7 +205,7 @@ To upload the payload manually:
1. Select **Download payload**.
1. Save the JSON file.
1. Visit [Service usage data center](https://version.gitlab.com/usage_data/new).
1. Select **Choose file** and choose the file from p5.
1. Select **Choose file**, then select the JSON file that contains the downloaded payload.
1. Select **Upload**.
The uploaded file is encrypted and sent using secure HTTPS protocol. HTTPS creates a secure

View File

@ -339,7 +339,7 @@ We recommend adopting at least the `MAJOR.MINOR` format.
For example: `2.1`, `1.0.0`, `1.0.0-alpha`, `2.1.3`, `3.0.0-rc.1`.
## CI/CD Catalog
## CI/CD Catalog **(PREMIUM)**
The CI/CD Catalog is a list of [components repositories](#components-repository),
each containing resources that you can add to your CI/CD pipelines.

View File

@ -498,7 +498,7 @@ query {
```
This cache is especially useful for chat functionality. For other services,
caching is disabled. (It can be enabled for a service by using `skip_cache: false`
caching is disabled. (It can be enabled for a service by using `cache_response: true`
option.)
Caching has following limitations:

View File

@ -281,62 +281,9 @@ NOTE:
Specific information that follow related to Ruby and Git versions do not apply to [Omnibus installations](https://docs.gitlab.com/omnibus/)
and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with appropriate Ruby and Git versions and are not using system binaries for Ruby and Git. There is no need to install Ruby or Git when utilizing these two approaches.
### 16.2.0
### GitLab 16
- Legacy LDAP configuration settings may cause
[`NoMethodError: undefined method 'devise' for User:Class` errors](https://gitlab.com/gitlab-org/gitlab/-/issues/419485).
This error occurs if you have TLS options (such as `ca_file`) not specified
in the `tls_options` hash, or use the legacy `gitlab_rails['ldap_host']` option.
See the [configuration workarounds](https://gitlab.com/gitlab-org/gitlab/-/issues/419485#workarounds)
for more details.
- Git 2.41.0 and later is required by Gitaly. For installations from source, you should use the [Git version provided by Gitaly](../install/installation.md#git).
- New job artifacts are not replicated if job artifacts are configured to be stored in object storage and `direct_upload` is enabled. This bug is fixed in GitLab versions 16.1.4,
16.2.3, 16.3.0, and later.
- Impacted versions: GitLab versions 16.1.0 - 16.1.3 and 16.2.0 - 16.2.2.
- If you deployed an affected version, after upgrading to a fixed GitLab version, follow [these instructions](https://gitlab.com/gitlab-org/gitlab/-/issues/419742#to-fix-data)
to resync the affected job artifacts.
### 16.1.0
- A `MigrateHumanUserType` background migration will be finalized with
the `FinalizeUserTypeMigration` migration.
GitLab 16.0 introduced a [batched background migration](background_migrations.md#batched-background-migrations) to
[migrate `user_type` values from `NULL` to `0`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115849). This
migration may take multiple days to complete on larger GitLab instances. Make sure the migration
has completed successfully before upgrading to 16.1.0.
- A `BackfillPreparedAtMergeRequests` background migration will be finalized with
the `FinalizeBackFillPreparedAtMergeRequests` post-deploy migration.
GitLab 15.10.0 introduced a [batched background migration](background_migrations.md#batched-background-migrations) to
[backfill `prepared_at` values on the `merge_requests` table](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111865). This
migration may take multiple days to complete on larger GitLab instances. Make sure the migration
has completed successfully before upgrading to 16.1.0.
- Geo: Some project imports do not initialize wiki repositories on project creation. Since the migration of project wikis to SSF, [missing wiki repositories are being incorrectly flagged as failing verification](https://gitlab.com/gitlab-org/gitlab/-/issues/409704). This is not a result of an actual replication/verification failure but an invalid internal state for these missing repositories inside Geo and results in errors in the logs and the verification progress reporting a failed state for these wiki repositories. If you have not imported projects you are not impacted by this issue.
- Impacted versions: GitLab versions 15.11.x, 16.0.x, and 16.1.0 - 16.1.2.
- Versions containing fix: GitLab 16.1.3 and later.
- Geo: Since the migration of project designs to SSF, [missing design repositories are being incorrectly flagged as failing verification](https://gitlab.com/gitlab-org/gitlab/-/issues/414279). This is not a result of an actual replication/verification failure but an invalid internal state for these missing repositories inside Geo and results in errors in the logs and the verification progress reporting a failed state for these design repositories. You could be impacted by this issue even if you have not imported projects.
- Impacted versions: GitLab versions 16.1.x.
- Versions containing fix: GitLab 16.2.0 and later.
- For self-compiled installations: You must remove any settings related to Puma worker killer from the `puma.rb` configuration file, since those have been [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118645). For more information, see the [`puma.rb.example`](https://gitlab.com/gitlab-org/gitlab/-/blob/16-0-stable-ee/config/puma.rb.example) file.
- New job artifacts are not replicated if job artifacts are configured to be stored in object storage and `direct_upload` is enabled. This bug is fixed in GitLab versions 16.1.4,
16.2.3, 16.3.0, and later.
- Impacted versions: GitLab versions 16.1.0 - 16.1.3 and 16.2.0 - 16.2.2.
- If you deployed an affected version, after upgrading to a fixed GitLab version, follow [these instructions](https://gitlab.com/gitlab-org/gitlab/-/issues/419742#to-fix-data)
to resync the affected job artifacts.
### 16.0.0
- Sidekiq crashes if there are non-ASCII characters in the `/etc/gitlab/gitlab.rb` file. You can fix this
by following the workaround in [issue 412767](https://gitlab.com/gitlab-org/gitlab/-/issues/412767#note_1404507549).
- Sidekiq jobs are only routed to `default` and `mailers` queues by default, and as a result,
every Sidekiq process also listens to those queues to ensure all jobs are processed across
all queues. This behavior does not apply if you have configured the [routing rules](../administration/sidekiq/processing_specific_job_classes.md#routing-rules).
- Docker 20.10.10 or later is required to run the GitLab Docker image. Older versions
[throw errors on startup](../install/docker.md#threaderror-cant-create-thread-operation-not-permitted).
- Geo: Some project imports do not initialize wiki repositories on project creation. Since the migration of project wikis to SSF, [missing wiki repositories are being incorrectly flagged as failing verification](https://gitlab.com/gitlab-org/gitlab/-/issues/409704). This is not a result of an actual replication/verification failure but an invalid internal state for these missing repositories inside Geo and results in errors in the logs and the verification progress reporting a failed state for these wiki repositories. If you have not imported projects you are not impacted by this issue.
- Impacted versions: GitLab versions 15.11.x, 16.0.x, and 16.1.0 - 16.1.2.
- Versions containing fix: GitLab 16.1.3 and later.
- Starting with 16.0, GitLab self-managed installations now have two database connections by default, instead of one. This change doubles the number of PostgreSQL connections. It makes self-managed versions of GitLab behave similarly to GitLab.com, and is a step toward enabling a separate database for CI features for self-managed versions of GitLab. Before upgrading to 16.0, determine if you need to [increase max connections for PostgreSQL](https://docs.gitlab.com/omnibus/settings/database.html#configuring-multiple-database-connections).
- This change applies to installation methods with Linux packages (Omnibus GitLab), GitLab Helm chart, GitLab Operator, GitLab Docker images, and installation from source.
Before upgrading, see [GitLab 16 changes](versions/gitlab_16_changes.md).
### 15.11.1

View File

@ -0,0 +1,131 @@
---
stage: Systems
group: Distribution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# GitLab 16 changes
This page contains upgrade information for minor and patch versions of GitLab 16.
Ensure you review these instructions and any specific instructions for your installation type.
For more information about upgrading GitLab Helm Chart, see [the release notes for 7.0](https://docs.gitlab.com/charts/releases/7_0.html).
## GitLab 16.2.0
- Legacy LDAP configuration settings may cause
[`NoMethodError: undefined method 'devise' for User:Class` errors](https://gitlab.com/gitlab-org/gitlab/-/issues/419485).
This error occurs if you have TLS options (such as `ca_file`) not specified
in the `tls_options` hash, or use the legacy `gitlab_rails['ldap_host']` option.
See the [configuration workarounds](https://gitlab.com/gitlab-org/gitlab/-/issues/419485#workarounds)
for more details.
- Git 2.41.0 and later is required by Gitaly. For installations from source, you should use the [Git version provided by Gitaly](../../install/installation.md#git).
- New job artifacts are not replicated if job artifacts are configured to be stored in object storage and `direct_upload` is enabled. This bug is fixed in GitLab versions 16.1.4,
16.2.3, 16.3.0, and later.
- Impacted versions: GitLab versions 16.1.0 - 16.1.3 and 16.2.0 - 16.2.2.
- If you deployed an affected version, after upgrading to a fixed GitLab version, follow [these instructions](https://gitlab.com/gitlab-org/gitlab/-/issues/419742#to-fix-data)
to resync the affected job artifacts.
### Linux package installations
Specific information applies to Linux package installations:
- In 16.2, we are upgrading Redis from 6.2.11 to 7.0.12. This upgrade is expected to be fully backwards compatible.
Redis will not be automatically restarted as part of `gitlab-ctl reconfigure`.
Hence, users are manually required to run `sudo gitlab-ctl restart redis` after
the reconfigure run so that the new Redis version gets used. A warning
mentioning that the installed Redis version is different than the one running is
displayed at the end of reconfigure run until the restart is performed.
If your instance has Redis HA with Sentinel, follow the upgrade steps mentioned in
[Zero Downtime documentation](../zero_downtime.md#redis-ha-using-sentinel).
## GitLab 16.1.0
- A `MigrateHumanUserType` background migration will be finalized with
the `FinalizeUserTypeMigration` migration.
GitLab 16.0 introduced a [batched background migration](../background_migrations.md#batched-background-migrations) to
[migrate `user_type` values from `NULL` to `0`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115849). This
migration may take multiple days to complete on larger GitLab instances. Make sure the migration
has completed successfully before upgrading to 16.1.0.
- A `BackfillPreparedAtMergeRequests` background migration will be finalized with
the `FinalizeBackFillPreparedAtMergeRequests` post-deploy migration.
GitLab 15.10.0 introduced a [batched background migration](../background_migrations.md#batched-background-migrations) to
[backfill `prepared_at` values on the `merge_requests` table](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111865). This
migration may take multiple days to complete on larger GitLab instances. Make sure the migration
has completed successfully before upgrading to 16.1.0.
- New job artifacts are not replicated if job artifacts are configured to be stored in object storage and `direct_upload` is enabled. This bug is fixed in GitLab versions 16.1.4,
16.2.3, 16.3.0, and later.
- Impacted versions: GitLab versions 16.1.0 - 16.1.3 and 16.2.0 - 16.2.2.
- If you deployed an affected version, after upgrading to a fixed GitLab version, follow [these instructions](https://gitlab.com/gitlab-org/gitlab/-/issues/419742#to-fix-data)
to resync the affected job artifacts.
### Self-compiled installations
- You must remove any settings related to Puma worker killer from the `puma.rb` configuration file, since those have been
[removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118645). For more information, see the
[`puma.rb.example`](https://gitlab.com/gitlab-org/gitlab/-/blob/16-0-stable-ee/config/puma.rb.example) file.
### Geo installations
Specific information applies to installations using Geo:
- Some project imports do not initialize wiki repositories on project creation. Since the migration of project wikis to
SSF, [missing wiki repositories are being incorrectly flagged as failing verification](https://gitlab.com/gitlab-org/gitlab/-/issues/409704).
This is not a result of an actual replication/verification failure but an invalid internal state for these missing
repositories inside Geo and results in errors in the logs and the verification progress reporting a failed state for
these wiki repositories. If you have not imported projects you are not impacted by this issue.
- Impacted versions: GitLab versions 15.11.x, 16.0.x, and 16.1.0 - 16.1.2.
- Versions containing fix: GitLab 16.1.3 and later.
- Since the migration of project designs to SSF, [missing design repositories are being incorrectly flagged as failing verification](https://gitlab.com/gitlab-org/gitlab/-/issues/414279).
This is not a result of an actual replication/verification failure but an invalid internal state for these missing
repositories inside Geo and results in errors in the logs and the verification progress reporting a failed state for
these design repositories. You could be impacted by this issue even if you have not imported projects.
- Impacted versions: GitLab versions 16.1.x.
- Versions containing fix: GitLab 16.2.0 and later.
## GitLab 16.0.0
- Sidekiq crashes if there are non-ASCII characters in the `/etc/gitlab/gitlab.rb` file. You can fix this
by following the workaround in [issue 412767](https://gitlab.com/gitlab-org/gitlab/-/issues/412767#note_1404507549).
- Sidekiq jobs are only routed to `default` and `mailers` queues by default, and as a result,
every Sidekiq process also listens to those queues to ensure all jobs are processed across
all queues. This behavior does not apply if you have configured the [routing rules](../../administration/sidekiq/processing_specific_job_classes.md#routing-rules).
- Docker 20.10.10 or later is required to run the GitLab Docker image. Older versions
[throw errors on startup](../../install/docker.md#threaderror-cant-create-thread-operation-not-permitted).
- Starting with 16.0, GitLab self-managed installations now have two database connections by default, instead of one. This change doubles the number of PostgreSQL connections. It makes self-managed versions of GitLab behave similarly to GitLab.com, and is a step toward enabling a separate database for CI features for self-managed versions of GitLab. Before upgrading to 16.0, determine if you need to [increase max connections for PostgreSQL](https://docs.gitlab.com/omnibus/settings/database.html#configuring-multiple-database-connections).
- This change applies to installation methods with Linux packages (Omnibus GitLab), GitLab Helm chart, GitLab Operator, GitLab Docker images, and installation from source.
### Linux package installations
Specific information applies to Linux package installations:
- The binaries for PostgreSQL 12 have been removed.
Prior to upgrading, administrators of Linux package installations must ensure the installation is using
[PostgreSQL 13](https://docs.gitlab.com/omnibus/settings/database.html#upgrade-packaged-postgresql-server).
- Bundled Grafana is deprecated and is no longer supported. It is removed in GitLab 16.3.
For more information, see [deprecation notes](../../administration/monitoring/performance/grafana_configuration.md#deprecation-of-bundled-grafana).
- This upgrades `openssh-server` to `1:8.9p1-3`.
Using `ssh-keyscan -t rsa` with older OpenSSH clients to obtain public key information will no longer
be viable due to deprecations listed in [OpenSSH 8.7 Release Notes](https://www.openssh.com/txt/release-8.7).
Workaround is to make use of a different key type, or upgrade the client OpenSSH to a version >= 8.7.
### Geo installations
Specific information applies to installations using Geo:
- Some project imports do not initialize wiki repositories on project creation. Since the migration of project wikis to
SSF, [missing wiki repositories are being incorrectly flagged as failing verification](https://gitlab.com/gitlab-org/gitlab/-/issues/409704).
This is not a result of an actual replication/verification failure but an invalid internal state for these missing
repositories inside Geo and results in errors in the logs and the verification progress reporting a failed state for
these wiki repositories. If you have not imported projects you are not impacted by this issue.
- Impacted versions: GitLab versions 15.11.x, 16.0.x, and 16.1.0 - 16.1.2.
- Versions containing fix: GitLab 16.1.3 and later.

View File

@ -4,7 +4,17 @@ group: IDE
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# Tutorial: Connect a remote machine to the Web IDE **(FREE)**
# Tutorial: Connect a remote machine to the Web IDE (Beta) **(FREE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95169) in GitLab 15.4 [with a flag](../../../administration/feature_flags.md) named `vscode_web_ide`. Disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/371084) in GitLab 15.7.
> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115741) in GitLab 15.11.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `vscode_web_ide`. On GitLab.com, this feature is available. The feature is not ready for production use.
WARNING:
This feature is in [Beta](../../../policy/experiment-beta-support.md#beta) and subject to change without notice.
This tutorial shows you how to:

View File

@ -0,0 +1,119 @@
---
stage: Create
group: IDE
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# Tutorial: Create a custom workspace image that supports arbitrary user IDs (Beta) **(PREMIUM)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112397) in GitLab 15.11 [with a flag](../../administration/feature_flags.md) named `remote_development_feature_flag`. Disabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/391543) in GitLab 16.0.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../../administration/feature_flags.md) named `remote_development_feature_flag`. On GitLab.com, this feature is available. The feature is not ready for production use.
WARNING:
This feature is in [Beta](../../policy/experiment-beta-support.md#beta) and subject to change without notice. To leave feedback, see the [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/410031).
In this tutorial, you'll learn how to create a custom workspace image that supports arbitrary user IDs.
You can then use this custom image with any [workspace](index.md) you create in GitLab.
To create a custom workspace image that supports arbitrary user IDs, you'll:
1. [Create a base Dockerfile](#create-a-base-dockerfile).
1. [Add support for arbitrary user IDs](#add-support-for-arbitrary-user-ids).
1. [Build the custom workspace image](#build-the-custom-workspace-image).
1. [Push the custom workspace image to the GitLab container registry](#push-the-custom-workspace-image-to-the-gitlab-container-registry).
1. [Use the custom workspace image in GitLab](#use-the-custom-workspace-image-in-gitlab).
## Prerequisites
- A GitLab account with permission to create and push container images to the GitLab container registry
- Docker installation
## Create a base Dockerfile
To create a base Dockerfile for the container image, let's use the Python `3.11-slim-bullseye` image from Docker Hub:
```Dockerfile
FROM python:3.11-slim-bullseye
```
Next, you'll modify this base image.
## Add support for arbitrary user IDs
To add support for arbitrary user IDs to the base image, let's:
1. Add a new `gitlab-workspaces` user with a `5001` user ID.
1. Set the necessary directory permissions.
```Dockerfile
RUN useradd -l -u 5001 -G sudo -md /home/gitlab-workspaces -s /bin/bash -p gitlab-workspaces gitlab-workspaces
ENV HOME=/home/gitlab-workspaces
WORKDIR $HOME
RUN mkdir -p /home/gitlab-workspaces && chgrp -R 0 /home && chmod -R g=u /etc/passwd /etc/group /home
USER 5001
```
Now that the image supports arbitrary user IDs, it's time to build the custom workspace image.
## Build the custom workspace image
To build the custom workspace image, run this command:
```shell
docker build -t my-gitlab-workspace .
```
When the build is complete, you can test the image locally:
```shell
docker run -ti my-gitlab-workspace sh
```
You should now be able to run commands as the `gitlab-workspaces` user.
## Push the custom workspace image to the GitLab container registry
To push the custom workspace image to the GitLab container registry:
1. Sign in to your GitLab account:
```shell
docker login registry.gitlab.com
```
1. Tag the image with the GitLab container registry URL:
```shell
docker tag my-gitlab-workspace registry.gitlab.com/your-namespace/my-gitlab-workspace:latest
```
1. Push the image to the GitLab container registry:
```shell
docker push registry.gitlab.com/your-namespace/my-gitlab-workspace:latest
```
Now that you've pushed the custom workspace image to the GitLab container registry, you can use the image in GitLab.
## Use the custom workspace image in GitLab
To use the custom workspace image in GitLab, in your project's `.devfile.yaml`, update the container image:
```yaml
schemaVersion: 2.2.0
components:
- name: tooling-container
attributes:
gl/inject-editor: true
container:
image: registry.gitlab.com/your-namespace/my-gitlab-workspace:latest
```
You're all set! You can now use this custom image with any workspace you create in GitLab.

View File

@ -193,7 +193,9 @@ If you already have running workspaces, an administrator must manually delete th
You can provide your own container image, which can run as any Linux user ID. It's not possible for GitLab to predict the Linux user ID for a container image.
GitLab uses the Linux root group ID permission to create, update, or delete files in a container. The container runtime used by the Kubernetes cluster must ensure all containers have a default Linux group ID of `0`.
If you have a container image that does not support arbitrary user IDs, you cannot create, update, or delete files in a workspace. To create a container image that supports arbitrary user IDs, see the [OpenShift documentation](https://docs.openshift.com/container-platform/4.12/openshift_images/create-images.html#use-uid_create-images).
If you have a container image that does not support arbitrary user IDs, you cannot create, update, or delete files in a workspace. To create a container image that supports arbitrary user IDs, see [Create a custom workspace image that supports arbitrary user IDs](../workspace/create_image.md).
For more information, see the [OpenShift documentation](https://docs.openshift.com/container-platform/4.12/openshift_images/create-images.html#use-uid_create-images).
## Related topics

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# Fixes invalid combination of shared runners being enabled and
# allow_descendants_override = true
# This combination fails validation and doesn't make sense:
# we always allow descendants to disable shared runners
class FixAllowDescendantsOverrideDisabledSharedRunners < BatchedMigrationJob
feature_category :runner_fleet
operation_name :fix_allow_descendants_override_disabled_shared_runners
def perform
each_sub_batch do |sub_batch|
sub_batch.where(shared_runners_enabled: true,
allow_descendants_override_disabled_shared_runners: true)
.update_all(allow_descendants_override_disabled_shared_runners: false)
end
end
end
end
end

View File

@ -12176,7 +12176,7 @@ msgstr ""
msgid "ComplianceReport|No projects found that match filters"
msgstr ""
msgid "ComplianceReport|No standards adherences found"
msgid "ComplianceReport|No projects with standards adherence checks found"
msgstr ""
msgid "ComplianceReport|No violations found"
@ -34162,6 +34162,9 @@ msgstr ""
msgid "PipelineSchedules|Next Run"
msgstr ""
msgid "PipelineSchedules|No pipeline schedules"
msgstr ""
msgid "PipelineSchedules|None"
msgstr ""
@ -53054,6 +53057,9 @@ msgstr ""
msgid "WorkItem|Key Result"
msgstr ""
msgid "WorkItem|Key result"
msgstr ""
msgid "WorkItem|Mark as done"
msgstr ""

View File

@ -49,6 +49,9 @@ module QA
end
def choose_namespace(namespace)
# The current group is the default, we use end_with? in case we want to select the top level group
return if find_element(:select_namespace_dropdown).text.end_with?(namespace)
click_element :select_namespace_dropdown
fill_element :select_namespace_dropdown_search_field, namespace
select_item(namespace, css: '.gl-dropdown-item')

View File

@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe AutocompleteController do
let(:project) { create(:project) }
let(:user) { project.first_owner }
let_it_be(:project) { create(:project) }
let_it_be(:user) { project.first_owner }
context 'GET users', feature_category: :user_management do
let!(:user2) { create(:user) }
@ -25,6 +25,22 @@ RSpec.describe AutocompleteController do
expect(json_response.size).to eq(1)
expect(json_response.map { |u| u["username"] }).to include(user.username)
end
context "with push_code param" do
let(:reporter) { create(:user) }
before do
project.add_reporter(reporter)
get(:users, params: { project_id: project.id, push_code: 'true' })
end
it 'returns users that can push code', :aggregate_failures do
expect(json_response).to be_kind_of(Array)
expect(json_response.size).to eq(1)
expect(json_response.map { |user| user["username"] }).to match_array([user.username])
end
end
end
describe 'GET #users with unknown project' do
@ -67,6 +83,7 @@ RSpec.describe AutocompleteController do
context 'non-member login for public project' do
let(:project) { create(:project, :public) }
let(:user) { project.first_owner }
before do
sign_in(non_member)
@ -207,20 +224,6 @@ RSpec.describe AutocompleteController do
end
end
context 'skip_users parameter included' do
before do
sign_in(user)
end
it 'skips the user IDs passed' do
get(:users, params: { skip_users: [user, user2].map(&:id) })
response_user_ids = json_response.map { |user| user['id'] }
expect(response_user_ids).to contain_exactly(non_member.id)
end
end
context 'merge_request_iid parameter included' do
before do
sign_in(user)

View File

@ -439,6 +439,12 @@ RSpec.describe SearchController, feature_category: :global_search do
it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' }
it_behaves_like 'support for active record query timeouts', :autocomplete, { term: 'hello' }, :project, :json
it 'raises an error if search term is missing' do
expect do
get :autocomplete
end.to raise_error(ActionController::ParameterMissing)
end
it 'returns an empty array when given abusive search term' do
get :autocomplete, params: { term: ('hal' * 4000), scope: 'projects' }
expect(response).to have_gitlab_http_status(:ok)

View File

@ -114,12 +114,6 @@ RSpec.describe Autocomplete::UsersFinder do
end
end
context 'when filtered by skip_users' do
let(:params) { { skip_users: [omniauth_user.id, current_user.id, blocked_user] } }
it { is_expected.to match_array([user1, external_user]) }
end
context 'when todos exist' do
let!(:pending_todo1) { create(:todo, user: current_user, author: user1, state: :pending) }
let!(:pending_todo2) { create(:todo, user: external_user, author: omniauth_user, state: :pending) }

View File

@ -26,7 +26,7 @@ describe('DeleteApplication', () => {
};
const findModal = () => wrapper.findComponent(GlModal);
const findForm = () => wrapper.find('form');
const findForm = () => wrapper.findComponent({ ref: 'deleteForm' });
beforeEach(() => {
setHTMLFixture(`
@ -62,7 +62,7 @@ describe('DeleteApplication', () => {
let formSubmitSpy;
beforeEach(() => {
formSubmitSpy = jest.spyOn(wrapper.vm.$refs.deleteForm, 'submit');
formSubmitSpy = jest.spyOn(findForm().element, 'submit');
findModal().vm.$emit('primary');
});

View File

@ -3,6 +3,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { trimText } from 'helpers/text_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import PipelineSchedules from '~/ci/pipeline_schedules/components/pipeline_schedules.vue';
@ -354,5 +355,19 @@ describe('Pipeline schedules app', () => {
expect(findLink().exists()).toBe(true);
expect(findLink().text()).toContain('scheduled pipelines documentation.');
});
describe('inactive tab', () => {
beforeEach(() => {
setWindowLocation('https://gitlab.com/flightjs/Flight/-/pipeline_schedules?scope=INACTIVE');
});
it('should not show empty state', async () => {
createComponent([[getPipelineSchedulesQuery, successEmptyHandler]]);
await waitForPromises();
expect(findEmptyState().exists()).toBe(false);
});
});
});
});

View File

@ -1,4 +1,4 @@
import { GlTableLite } from '@gitlab/ui';
import { GlTable } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import PipelineSchedulesTable from '~/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue';
import { mockPipelineScheduleNodes, mockPipelineScheduleCurrentUser } from '../../mock_data';
@ -19,7 +19,7 @@ describe('Pipeline schedules table', () => {
});
};
const findTable = () => wrapper.findComponent(GlTableLite);
const findTable = () => wrapper.findComponent(GlTable);
const findScheduleDescription = () => wrapper.findByTestId('pipeline-schedule-description');
beforeEach(() => {

View File

@ -17,6 +17,7 @@ function createComponent(propsData = {}) {
customEmojis: CUSTOM_EMOJI,
pageInfo: {},
count: CUSTOM_EMOJI.length,
userPermissions: { createCustomEmoji: true },
...propsData,
},
});
@ -29,6 +30,21 @@ describe('Custom emoji settings list component', () => {
expect(wrapper.element).toMatchSnapshot();
});
describe('user permissions', () => {
it.each`
createCustomEmoji | visible
${true} | ${true}
${false} | ${false}
`(
'renders create new button if createCustomEmoji is $createCustomEmoji',
({ createCustomEmoji, visible }) => {
createComponent({ userPermissions: { createCustomEmoji } });
expect(wrapper.findByTestId('action-primary').exists()).toBe(visible);
},
);
});
describe('pagination', () => {
it.each`
emits | button | pageInfo

View File

@ -13,47 +13,134 @@ exports[`PypiInstallation renders all the messages 1`] = `
<div>
<div
class="dropdown b-dropdown gl-dropdown btn-group"
id="__BVID__27"
lazy=""
class="gl-new-dropdown"
>
<!---->
<button
aria-expanded="false"
aria-haspopup="menu"
class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle"
id="__BVID__27__BV_toggle_"
aria-controls="base-dropdown-10"
aria-haspopup="listbox"
aria-labelledby="dropdown-toggle-btn-8"
class="btn btn-default btn-md gl-button gl-new-dropdown-toggle"
data-testid="base-dropdown-toggle"
id="dropdown-toggle-btn-8"
listeners="[object Object]"
type="button"
>
<!---->
<!---->
<span
class="gl-dropdown-button-text"
class="gl-button-text"
>
Show PyPi commands
<span
class="gl-new-dropdown-button-text"
>
Show PyPi commands
</span>
<svg
aria-hidden="true"
class="gl-button-icon gl-new-dropdown-chevron gl-icon s16"
data-testid="chevron-down-icon"
role="img"
>
<use
href="file-mock#chevron-down"
/>
</svg>
</span>
<svg
aria-hidden="true"
class="gl-button-icon dropdown-chevron gl-icon s16"
data-testid="chevron-down-icon"
role="img"
>
<use
href="file-mock#chevron-down"
/>
</svg>
</button>
<ul
aria-labelledby="__BVID__27__BV_toggle_"
class="dropdown-menu"
role="menu"
tabindex="-1"
<div
class="gl-new-dropdown-panel gl-w-31!"
data-testid="base-dropdown-menu"
id="base-dropdown-10"
>
<!---->
</ul>
<div
class="gl-new-dropdown-inner"
>
<!---->
<!---->
<ul
aria-labelledby="dropdown-toggle-btn-8"
class="gl-new-dropdown-contents gl-new-dropdown-contents-with-scrim-overlay gl-new-dropdown-contents"
id="listbox-9"
role="listbox"
tabindex="-1"
>
<li
aria-hidden="true"
class="top-scrim-wrapper"
data-testid="top-scrim"
>
<div
class="top-scrim top-scrim-light"
/>
</li>
<li
aria-hidden="true"
/>
<li
aria-selected="true"
class="gl-new-dropdown-item"
data-testid="listbox-item-pypi"
role="option"
tabindex="-1"
>
<span
class="gl-new-dropdown-item-content gl-bg-gray-50!"
>
<svg
aria-hidden="true"
class="gl-icon s16 gl-new-dropdown-item-check-icon gl-mt-3 gl-align-self-start"
data-testid="dropdown-item-checkbox"
role="img"
>
<use
href="file-mock#mobile-issue-close"
/>
</svg>
<span
class="gl-new-dropdown-item-text-wrapper"
>
Show PyPi commands
</span>
</span>
</li>
<!---->
<!---->
<li
aria-hidden="true"
/>
<li
aria-hidden="true"
class="bottom-scrim-wrapper"
data-testid="bottom-scrim"
>
<div
class="bottom-scrim"
/>
</li>
</ul>
<!---->
</div>
</div>
</div>
</div>
</div>
@ -80,7 +167,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
id="installation-pip-command"
>
<label
for="instruction-input_5"
for="instruction-input_11"
>
Pip Command
</label>
@ -94,7 +181,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
<input
class="form-control gl-font-monospace"
data-testid="instruction-input"
id="instruction-input_5"
id="instruction-input_11"
readonly="readonly"
type="text"
/>
@ -109,7 +196,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon"
data-clipboard-handle-tooltip="false"
data-clipboard-text="pip install @gitlab-org/package-15 --index-url http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple"
id="clipboard-button-6"
id="clipboard-button-12"
title="Copy Pip command"
type="button"
>

View File

@ -118,6 +118,7 @@ describe('ForkForm component', () => {
const findInternalRadio = () => wrapper.find('[data-testid="radio-internal"]');
const findPublicRadio = () => wrapper.find('[data-testid="radio-public"]');
const findForkNameInput = () => wrapper.find('[data-testid="fork-name-input"]');
const findGlFormRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
const findForkUrlInput = () => wrapper.findComponent(ProjectNamespace);
const findForkSlugInput = () => wrapper.find('[data-testid="fork-slug-input"]');
const findForkDescriptionTextarea = () =>
@ -235,7 +236,7 @@ describe('ForkForm component', () => {
it('resets the visibility to max allowed below current level', async () => {
createFullComponent({ projectVisibility: 'public' }, { namespaces });
expect(wrapper.vm.form.fields.visibility.value).toBe('public');
expect(findGlFormRadioGroup().vm.$attrs.checked).toBe('public');
fillForm({
name: 'one',
@ -250,7 +251,7 @@ describe('ForkForm component', () => {
it('does not reset the visibility when current level is allowed', async () => {
createFullComponent({ projectVisibility: 'public' }, { namespaces });
expect(wrapper.vm.form.fields.visibility.value).toBe('public');
expect(findGlFormRadioGroup().vm.$attrs.checked).toBe('public');
fillForm({
name: 'two',
@ -265,7 +266,7 @@ describe('ForkForm component', () => {
it('does not reset the visibility when visibility cap is increased', async () => {
createFullComponent({ projectVisibility: 'public' }, { namespaces });
expect(wrapper.vm.form.fields.visibility.value).toBe('public');
expect(findGlFormRadioGroup().vm.$attrs.checked).toBe('public');
fillForm({
name: 'three',
@ -290,7 +291,7 @@ describe('ForkForm component', () => {
{ namespaces },
);
wrapper.vm.form.fields.visibility.value = 'internal';
await findGlFormRadioGroup().vm.$emit('input', 'internal');
fillForm({
name: 'five',
id: 5,
@ -468,7 +469,8 @@ describe('ForkForm component', () => {
jest.spyOn(axios, 'post');
setupComponent();
wrapper.vm.form.fields.visibility.value = null;
await findGlFormRadioGroup().vm.$emit('input', null);
await nextTick();
await submitForm();

View File

@ -104,7 +104,7 @@ describe('CompareApp component', () => {
it('sets the selected project when the "selectProject" event is emitted', async () => {
const project = {
name: 'some-to-name',
text: 'some-to-name',
id: '1',
};

View File

@ -3,18 +3,20 @@ const targetProjectRefsPath = 'some/refs/path';
const paramsName = 'to';
const paramsBranch = 'main';
const sourceProject = {
name: 'some-to-name',
text: 'some-to-name',
id: '2',
};
const targetProject = {
name: 'some-to-name',
text: 'some-to-name',
id: '1',
};
const endpoint = '/flightjs/Flight/-/merge_requests/new/target_projects';
export const appDefaultProps = {
projectCompareIndexPath: 'some/path',
projectMergeRequestPath: '',
projects: [sourceProject],
paramsFrom: 'main',
paramsTo: 'target/branch',
straight: false,
@ -31,11 +33,13 @@ export const revisionCardDefaultProps = {
revisionText: 'Source',
refsProjectPath: sourceProjectRefsPath,
paramsName,
endpoint,
};
export const repoDropdownDefaultProps = {
selectedProject: targetProject,
paramsName,
endpoint,
};
export const revisionDropdownDefaultProps = {
@ -43,3 +47,27 @@ export const revisionDropdownDefaultProps = {
paramsBranch,
paramsName,
};
export const targetProjects = [
{
id: 6,
name: 'Flight',
full_path: '/flightjs/Flight',
full_name: 'Flightjs / Flight',
refs_url: '/flightjs/Flight/refs',
},
{
id: 11,
name: 'Flight',
full_path: '/rolando_kub/Flight',
full_name: 'Kiersten Considine / Flight',
refs_url: '/rolando_kub/Flight/refs',
},
{
id: 12,
name: 'Flight',
full_path: '/janice.douglas/Flight',
full_name: 'Jesse Hayes / Flight',
refs_url: '/janice.douglas/Flight/refs',
},
];

View File

@ -1,11 +1,18 @@
import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import RepoDropdown from '~/projects/compare/components/repo_dropdown.vue';
import { revisionCardDefaultProps as defaultProps } from './mock_data';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import { revisionCardDefaultProps as defaultProps, targetProjects } from './mock_data';
jest.mock('~/alert');
describe('RepoDropdown component', () => {
let wrapper;
let axiosMock;
const createComponent = (props = {}) => {
wrapper = shallowMount(RepoDropdown, {
@ -15,12 +22,20 @@ describe('RepoDropdown component', () => {
},
stubs: {
GlCollapsibleListbox,
GlListboxItem,
},
});
};
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
});
afterEach(() => {
axiosMock.restore();
});
const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findGlCollapsibleListboxItems = () => wrapper.findAllComponents(GlListboxItem);
const findHiddenInput = () => wrapper.find('input[type="hidden"]');
describe('Source Revision', () => {
@ -34,7 +49,7 @@ describe('RepoDropdown component', () => {
it('displays the project name in the disabled dropdown', () => {
expect(findGlCollapsibleListbox().props('toggleText')).toBe(
defaultProps.selectedProject.name,
defaultProps.selectedProject.text,
);
expect(findGlCollapsibleListbox().props('disabled')).toBe(true);
});
@ -47,15 +62,15 @@ describe('RepoDropdown component', () => {
});
describe('Target Revision', () => {
beforeEach(() => {
const projects = [
{
name: 'some-to-name',
id: '1',
},
];
beforeEach(async () => {
axiosMock.onGet(defaultProps.endpoint).reply(HTTP_STATUS_OK, targetProjects);
createComponent({ paramsName: 'from', projects });
createComponent({ paramsName: 'from' });
await waitForPromises();
});
it('fetches target projects on created hook', () => {
expect(findGlCollapsibleListboxItems()).toHaveLength(targetProjects.length);
});
it('set hidden input of the selected project', () => {
@ -64,26 +79,52 @@ describe('RepoDropdown component', () => {
it('displays matching project name of the source revision initially in the dropdown', () => {
expect(findGlCollapsibleListbox().props('toggleText')).toBe(
defaultProps.selectedProject.name,
defaultProps.selectedProject.text,
);
});
it('updates the hidden input value when dropdown item is selected', () => {
const repoId = '1';
it('updates the hidden input value when dropdown item is selected', async () => {
const repoId = '6';
findGlCollapsibleListbox().vm.$emit('select', repoId);
await nextTick();
expect(findHiddenInput().attributes('value')).toBe(repoId);
});
it('emits `selectProject` event when another target project is selected', async () => {
const repoId = '1';
const repoId = '6';
findGlCollapsibleListbox().vm.$emit('select', repoId);
await nextTick();
expect(wrapper.emitted('selectProject')).toEqual([
[
{
direction: 'from',
project: {
text: 'flightjs/Flight',
value: '6',
},
},
],
]);
});
expect(wrapper.emitted('selectProject')[0][0]).toEqual({
direction: 'from',
project: { id: '1', name: 'some-to-name' },
});
it('searches projects', async () => {
findGlCollapsibleListbox().vm.$emit('search', 'test');
jest.advanceTimersByTime(500);
await waitForPromises();
expect(axiosMock.history.get[1].params).toEqual({ search: 'test' });
});
});
describe('On request failure', () => {
it('shows alert', async () => {
axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND);
createComponent({ paramsName: 'from' });
await waitForPromises();
expect(createAlert).toHaveBeenCalled();
});
});
});

View File

@ -186,7 +186,7 @@ describe('SetStatusForm', () => {
it('emits `clear-status-after-click`', async () => {
await createComponent();
await wrapper.findByTestId('thirtyMinutes').trigger('click');
await wrapper.findByTestId('listbox-item-thirtyMinutes').trigger('click');
expect(wrapper.emitted('clear-status-after-click')).toEqual([[thirtyMinutes]]);
});

View File

@ -164,7 +164,7 @@ describe('SetStatusModalWrapper', () => {
findAvailabilityCheckbox().vm.$emit('input', true);
// set the currentClearStatusAfter to 30 minutes
await wrapper.find('[data-testid="thirtyMinutes"]').trigger('click');
await wrapper.find('[data-testid="listbox-item-thirtyMinutes"]').trigger('click');
findModal().vm.$emit('primary');
await nextTick();

View File

@ -1,5 +1,5 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import component from '~/vue_shared/components/registry/persisted_dropdown_selection.vue';
@ -16,7 +16,7 @@ describe('Persisted dropdown selection', () => {
};
function createComponent({ props = {}, data = {} } = {}) {
wrapper = shallowMount(component, {
wrapper = mount(component, {
propsData: {
...defaultProps,
...props,
@ -28,8 +28,10 @@ describe('Persisted dropdown selection', () => {
}
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findGlListboxItems = () => wrapper.findAllComponents(GlListboxItem);
const findGlListboxToggleText = () =>
findGlCollapsibleListbox().find('.gl-new-dropdown-button-text');
describe('local storage sync', () => {
it('uses the local storage sync component with the correct props', () => {
@ -63,20 +65,22 @@ describe('Persisted dropdown selection', () => {
it('has a dropdown component', () => {
createComponent();
expect(findDropdown().exists()).toBe(true);
expect(findGlCollapsibleListbox().exists()).toBe(true);
});
describe('dropdown text', () => {
it('when no selection shows the first', () => {
createComponent();
expect(findDropdown().props('text')).toBe('Maven');
expect(findGlListboxToggleText().text()).toBe('Maven');
});
it('when an option is selected, shows that option label', () => {
createComponent({ data: { selected: defaultProps.options[1].value } });
it('when an option is selected, shows that option label', async () => {
createComponent();
findGlCollapsibleListbox().vm.$emit('select', defaultProps.options[1].value);
await nextTick();
expect(findDropdown().props('text')).toBe('Gradle');
expect(findGlListboxToggleText().text()).toBe('Gradle');
});
});
@ -84,34 +88,20 @@ describe('Persisted dropdown selection', () => {
it('has one item for each option', () => {
createComponent();
expect(findDropdownItems()).toHaveLength(defaultProps.options.length);
});
it('binds the correct props', () => {
createComponent({ data: { selected: defaultProps.options[0].value } });
expect(findDropdownItems().at(0).props()).toMatchObject({
isChecked: true,
isCheckItem: true,
});
expect(findDropdownItems().at(1).props()).toMatchObject({
isChecked: false,
isCheckItem: true,
});
expect(findGlListboxItems()).toHaveLength(defaultProps.options.length);
});
it('on click updates the data and emits event', async () => {
createComponent({ data: { selected: defaultProps.options[0].value } });
expect(findDropdownItems().at(0).props('isChecked')).toBe(true);
createComponent();
const selectedItem = 'gradle';
findDropdownItems().at(1).vm.$emit('click');
expect(findGlCollapsibleListbox().props('selected')).toBe('maven');
findGlCollapsibleListbox().vm.$emit('select', selectedItem);
await nextTick();
expect(wrapper.emitted('change')).toStrictEqual([['gradle']]);
expect(findDropdownItems().at(0).props('isChecked')).toBe(false);
expect(findDropdownItems().at(1).props('isChecked')).toBe(true);
expect(wrapper.emitted('change').at(-1)).toStrictEqual([selectedItem]);
expect(findGlCollapsibleListbox().props('selected')).toBe(selectedItem);
});
});
});

View File

@ -5,6 +5,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import { workItemByIidResponseFactory, mockAssignees } from '../mock_data';
@ -18,6 +19,7 @@ describe('WorkItemCreatedUpdated component', () => {
const findUpdatedAt = () => wrapper.find('[data-testid="work-item-updated"]');
const findCreatedAtText = () => findCreatedAt().text().replace(/\s+/g, ' ');
const findWorkItemTypeIcon = () => wrapper.findComponent(WorkItemTypeIcon);
const createComponent = async ({ workItemIid = '1', author = null, updatedAt } = {}) => {
const workItemQueryResponse = workItemByIidResponseFactory({
@ -48,17 +50,31 @@ describe('WorkItemCreatedUpdated component', () => {
expect(successHandler).not.toHaveBeenCalled();
});
it('shows work item type metadata with type and icon', async () => {
await createComponent();
const {
data: { workspace: { workItems } = {} },
} = workItemByIidResponseFactory();
expect(findWorkItemTypeIcon().props()).toMatchObject({
showText: true,
workItemIconName: workItems.nodes[0].workItemType.iconName,
workItemType: workItems.nodes[0].workItemType.name,
});
});
it('shows author name and link', async () => {
const author = mockAssignees[0];
await createComponent({ author });
expect(findCreatedAtText()).toBe(`Created by ${author.name}`);
expect(findCreatedAtText()).toBe(`created by ${author.name}`);
});
it('shows created time when author is null', async () => {
await createComponent({ author: null });
expect(findCreatedAtText()).toBe('Created');
expect(findCreatedAtText()).toBe('created');
});
it('shows updated time', async () => {

View File

@ -24,6 +24,7 @@ import WorkItemTitle from '~/work_items/components/work_item_title.vue';
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import WorkItemTodos from '~/work_items/components/work_item_todos.vue';
@ -88,6 +89,7 @@ describe('WorkItemDetail component', () => {
const findRightSidebar = () => wrapper.findByTestId('work-item-overview-right-sidebar');
const triggerPageScroll = () => findIntersectionObserver().vm.$emit('disappear');
const findWorkItemStateToggleButton = () => wrapper.findComponent(WorkItemStateToggleButton);
const findWorkItemTypeIcon = () => wrapper.findComponent(WorkItemTypeIcon);
const createComponent = ({
isModal = false,
@ -422,8 +424,8 @@ describe('WorkItemDetail component', () => {
createComponent({ handler: jest.fn().mockResolvedValue(workItemQueryResponseWithoutParent) });
await waitForPromises();
expect(findWorkItemType().exists()).toBe(true);
expect(findWorkItemType().text()).toBe('Task #1');
expect(findWorkItemTypeIcon().props('showText')).toBe(true);
expect(findWorkItemType().text()).toBe('#1');
});
describe('with parent', () => {
@ -475,8 +477,8 @@ describe('WorkItemDetail component', () => {
});
it('shows work item type and iid', () => {
const { iid, workItemType } = workItemQueryResponse.data.workspace.workItems.nodes[0];
expect(findParent().text()).toContain(`${workItemType.name} #${iid}`);
const { iid } = workItemQueryResponse.data.workspace.workItems.nodes[0];
expect(findParent().text()).toContain(`#${iid}`);
});
});
});

View File

@ -27,6 +27,13 @@ describe('Work Item type component', () => {
${'INCIDENT'} | ${''} | ${'issue-type-incident'} | ${'Incident'} | ${false}
${'TEST_CASE'} | ${''} | ${'issue-type-test-case'} | ${'Test case'} | ${true}
${'random-issue-type'} | ${''} | ${'issue-type-issue'} | ${''} | ${true}
${'Task'} | ${''} | ${'issue-type-task'} | ${'Task'} | ${false}
${'Issue'} | ${''} | ${'issue-type-issue'} | ${'Issue'} | ${true}
${'Requirements'} | ${''} | ${'issue-type-requirements'} | ${'Requirements'} | ${true}
${'Incident'} | ${''} | ${'issue-type-incident'} | ${'Incident'} | ${false}
${'Test_case'} | ${''} | ${'issue-type-test-case'} | ${'Test case'} | ${true}
${'Objective'} | ${''} | ${'issue-type-objective'} | ${'Objective'} | ${true}
${'Key Result'} | ${''} | ${'issue-type-keyresult'} | ${'Key result'} | ${true}
`(
'with workItemType set to "$workItemType" and workItemIconName set to "$workItemIconName"',
({ workItemType, workItemIconName, iconName, text, showTooltipOnHover }) => {

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::FixAllowDescendantsOverrideDisabledSharedRunners, schema: 20230802085923, feature_category: :runner_fleet do # rubocop:disable Layout/LineLength
let(:namespaces) { table(:namespaces) }
let!(:valid_enabled) do
namespaces.create!(name: 'valid_enabled', path: 'valid_enabled',
shared_runners_enabled: true,
allow_descendants_override_disabled_shared_runners: false)
end
let!(:invalid_enabled) do
namespaces.create!(name: 'invalid_enabled', path: 'invalid_enabled',
shared_runners_enabled: true,
allow_descendants_override_disabled_shared_runners: true)
end
let!(:disabled_and_overridable) do
namespaces.create!(name: 'disabled_and_overridable', path: 'disabled_and_overridable',
shared_runners_enabled: false,
allow_descendants_override_disabled_shared_runners: true)
end
let!(:disabled_and_unoverridable) do
namespaces.create!(name: 'disabled_and_unoverridable', path: 'disabled_and_unoverridable',
shared_runners_enabled: false,
allow_descendants_override_disabled_shared_runners: false)
end
let(:migration_attrs) do
{
start_id: namespaces.minimum(:id),
end_id: namespaces.maximum(:id),
batch_table: :namespaces,
batch_column: :id,
sub_batch_size: 2,
pause_ms: 0,
connection: ApplicationRecord.connection
}
end
it 'fixes invalid allow_descendants_override_disabled_shared_runners and does not affect others' do
expect do
described_class.new(**migration_attrs).perform
end.to change { invalid_enabled.reload.allow_descendants_override_disabled_shared_runners }.from(true).to(false)
.and not_change { valid_enabled.reload.allow_descendants_override_disabled_shared_runners }.from(false)
.and not_change { disabled_and_overridable.reload.allow_descendants_override_disabled_shared_runners }.from(true)
.and not_change { disabled_and_unoverridable.reload.allow_descendants_override_disabled_shared_runners }
.from(false)
end
end

View File

@ -3,12 +3,18 @@
require 'spec_helper'
RSpec.describe 'cross-database foreign keys' do
# Since we don't expect to have any cross-database foreign keys
# this is empty. If we will have an entry like
# `ci_daily_build_group_report_results.project_id`
# should be added.
let(:allowed_cross_database_foreign_keys) do
%w[].freeze
# While we are building out Cells, we will be moving tables from gitlab_main schema
# to either gitlab_main_clusterwide schema or gitlab_main_cell schema.
# During this transition phase, cross database foreign keys need
# to be temporarily allowed to exist, until we can work on converting these columns to loose foreign keys.
# The issue corresponding to the loose foreign key conversion
# should be added as a comment along with the name of the column.
let!(:allowed_cross_database_foreign_keys) do
[
'routes.namespace_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/420869
'user_details.enterprise_group_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/420868
'user_details.provisioned_by_group_id' # https://gitlab.com/gitlab-org/gitlab/-/issues/420868
]
end
def foreign_keys_for(table_name)

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe QueueFixAllowDescendantsOverrideDisabledSharedRunners, feature_category: :runner_fleet do
let!(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
reversible_migration do |migration|
migration.before -> {
expect(batched_migration).not_to have_scheduled_batched_migration
}
migration.after -> {
expect(batched_migration).to have_scheduled_batched_migration(
table_name: :namespaces,
column_name: :id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE
)
}
end
end
end

View File

@ -162,4 +162,22 @@ RSpec.describe Organizations::Organization, type: :model, feature_category: :cel
expect(described_class.where(id: organization)).not_to exist
end
end
describe '#user?' do
let_it_be(:user) { create :user }
subject { organization.user?(user) }
context 'when user is an organization user' do
before do
create :organization_user, organization: organization, user: user
end
it { is_expected.to eq true }
end
context 'when user is not an organization user' do
it { is_expected.to eq false }
end
end
end

View File

@ -12,16 +12,29 @@ RSpec.describe Organizations::OrganizationPolicy, feature_category: :cell do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:admin_organization) }
it { is_expected.to be_allowed(:read_organization) }
end
context 'when admin mode is disabled' do
it { is_expected.to be_disallowed(:admin_organization) }
it { is_expected.to be_disallowed(:read_organization) }
end
end
context 'when the user is not an admin' do
let_it_be(:current_user) { create(:user) }
context 'when the user is an organization user' do
let_it_be(:current_user) { create :user }
before do
create :organization_user, organization: organization, user: current_user
end
it { is_expected.to be_allowed(:read_organization) }
end
context 'when the user is not an organization user' do
let_it_be(:current_user) { create :user }
it { is_expected.to be_disallowed(:admin_organization) }
it { is_expected.to be_disallowed(:read_organization) }
end
end

View File

@ -5,6 +5,14 @@ require 'spec_helper'
RSpec.describe Organizations::OrganizationsController, feature_category: :cell do
let_it_be(:organization) { create(:organization) }
shared_examples 'successful response' do
it 'renders 200 OK' do
gitlab_request
expect(response).to have_gitlab_http_status(:ok)
end
end
shared_examples 'action disabled by `ui_for_organizations` feature flag' do
before do
stub_feature_flags(ui_for_organizations: false)
@ -34,15 +42,21 @@ RSpec.describe Organizations::OrganizationsController, feature_category: :cell d
it_behaves_like 'action disabled by `ui_for_organizations` feature flag'
end
context 'when the user has authorization', :enable_admin_mode do
context 'when the user is an admin', :enable_admin_mode do
let_it_be(:user) { create(:admin) }
it 'renders 200 OK' do
gitlab_request
it_behaves_like 'successful response'
it_behaves_like 'action disabled by `ui_for_organizations` feature flag'
end
expect(response).to have_gitlab_http_status(:ok)
context 'when the user is an organization user' do
let_it_be(:user) { create :user }
before do
create :organization_user, organization: organization, user: user
end
it_behaves_like 'successful response'
it_behaves_like 'action disabled by `ui_for_organizations` feature flag'
end
end

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'devise/registrations/new', feature_category: :user_management do
describe 'broadcast messaging' do
before do
allow(view).to receive(:devise_mapping).and_return(Devise.mappings[:user])
allow(view).to receive(:resource).and_return(build(:user))
allow(view).to receive(:resource_name).and_return(:user)
allow(view).to receive(:registration_path_params).and_return({})
allow(view).to receive(:glm_tracking_params).and_return({})
allow(view).to receive(:arkose_labs_enabled?).and_return(true)
end
it 'does not render the broadcast layout' do
render
expect(rendered).not_to render_template('layouts/_broadcast')
end
context 'when SaaS', :saas do
it 'does not render the broadcast layout' do
render
expect(rendered).not_to render_template('layouts/_broadcast')
end
end
end
end

View File

@ -102,6 +102,27 @@ RSpec.describe 'devise/sessions/new' do
end
end
describe 'broadcast messaging' do
before do
stub_devise
disable_captcha
end
it 'renders the broadcast layout' do
render
expect(rendered).to render_template('layouts/_broadcast')
end
context 'when SaaS', :saas do
it 'does not render the broadcast layout' do
render
expect(rendered).not_to render_template('layouts/_broadcast')
end
end
end
def disable_other_signin_methods
allow(view).to receive(:password_authentication_enabled_for_web?).and_return(false)
allow(view).to receive(:omniauth_enabled?).and_return(false)

View File

@ -23,12 +23,4 @@ RSpec.describe 'layouts/devise', feature_category: :user_management do
end
end
end
context 'without broadcast messaging' do
it 'does not render the broadcast layout' do
render
expect(rendered).not_to render_template('layouts/_broadcast')
end
end
end