Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
9297127929
commit
28f0cd8e07
|
|
@ -1 +1 @@
|
|||
0c10f5a60848a35049400dd775126b3ad4481663
|
||||
e2d9b43be9a9c5fcbc1f1ae1660520ba12e55224
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
14.25.0
|
||||
14.23.0
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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') }}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
import createDefaultClient from '~/lib/graphql';
|
||||
|
||||
export default createDefaultClient();
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
query customEmojiPermissions($groupPath: ID!) {
|
||||
group(fullPath: $groupPath) {
|
||||
id
|
||||
userPermissions {
|
||||
createCustomEmoji
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}')">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 : '';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ module Organizations
|
|||
path
|
||||
end
|
||||
|
||||
def user?(user)
|
||||
users.exists?(user.id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_if_default_organization
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
f96b1ce7addca2cb49faf339e3e92ac69a4ee98e1ff298393d2dafc375309909
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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]]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue