Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
cb09086128
commit
886ecba0bd
|
@ -1 +1 @@
|
|||
2.7.4
|
||||
2.7.5
|
||||
|
|
|
@ -1 +1 @@
|
|||
3ef55853e9e161204464868390d97d1a1577042d
|
||||
fe6bcc9ca347b59714c46adf65d100dd93abde52
|
||||
|
|
|
@ -62,7 +62,7 @@ const createFlashEl = (message, type) => `
|
|||
</div>
|
||||
`;
|
||||
|
||||
const removeFlashClickListener = (flashEl, fadeTransition) => {
|
||||
const addDismissFlashClickListener = (flashEl, fadeTransition) => {
|
||||
// There are some flash elements which do not have a closeEl.
|
||||
// https://gitlab.com/gitlab-org/gitlab/blob/763426ef344488972eb63ea5be8744e0f8459e6b/ee/app/views/layouts/header/_read_only_banner.html.haml
|
||||
getCloseEl(flashEl)?.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
|
||||
|
@ -113,7 +113,7 @@ const createFlash = function createFlash({
|
|||
}
|
||||
}
|
||||
|
||||
removeFlashClickListener(flashEl, fadeTransition);
|
||||
addDismissFlashClickListener(flashEl, fadeTransition);
|
||||
|
||||
flashContainer.classList.add('gl-display-block');
|
||||
|
||||
|
@ -130,9 +130,8 @@ const createFlash = function createFlash({
|
|||
|
||||
export {
|
||||
createFlash as default,
|
||||
createAction,
|
||||
hideFlash,
|
||||
removeFlashClickListener,
|
||||
addDismissFlashClickListener,
|
||||
FLASH_TYPES,
|
||||
FLASH_CLOSED_EVENT,
|
||||
};
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
/* eslint-disable no-useless-return */
|
||||
|
||||
import $ from 'jquery';
|
||||
import TemplateSelector from '~/blob/template_selector';
|
||||
import { __ } from '~/locale';
|
||||
import Api from '../api';
|
||||
import TemplateSelector from '../blob/template_selector';
|
||||
|
||||
export default class IssuableTemplateSelector extends TemplateSelector {
|
||||
constructor(...args) {
|
||||
|
@ -109,7 +107,5 @@ export default class IssuableTemplateSelector extends TemplateSelector {
|
|||
} else {
|
||||
this.setEditorContent(this.currentTemplate, { skipFocus: false });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
/* eslint-disable no-new, class-methods-use-this */
|
||||
|
||||
import $ from 'jquery';
|
||||
import IssuableTemplateSelector from './issuable_template_selector';
|
||||
|
||||
|
@ -10,6 +8,8 @@ export default class IssuableTemplateSelectors {
|
|||
|
||||
this.$dropdowns.each((i, dropdown) => {
|
||||
const $dropdown = $(dropdown);
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new IssuableTemplateSelector({
|
||||
pattern: /(\.md)/,
|
||||
data: $dropdown.data('data'),
|
||||
|
@ -21,6 +21,7 @@ export default class IssuableTemplateSelectors {
|
|||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
initEditor() {
|
||||
const editor = $('.markdown-area');
|
||||
// Proxy ace-editor's .setValue to jQuery's .val
|
|
@ -8,7 +8,7 @@ import initSuggestions from '~/issues/suggestions';
|
|||
import initIssuableTypeSelector from '~/issues/type_selector';
|
||||
import LabelsSelect from '~/labels/labels_select';
|
||||
import MilestoneSelect from '~/milestones/milestone_select';
|
||||
import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
|
||||
import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors';
|
||||
|
||||
export default () => {
|
||||
new ShortcutsNavigation();
|
|
@ -0,0 +1,11 @@
|
|||
import FilteredSearchServiceDesk from './filtered_search_service_desk';
|
||||
|
||||
export function initFilteredSearchServiceDesk() {
|
||||
if (document.querySelector('.filtered-search')) {
|
||||
const supportBotData = JSON.parse(
|
||||
document.querySelector('.js-service-desk-issues').dataset.supportBot,
|
||||
);
|
||||
const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
|
||||
filteredSearchManager.setup();
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import $ from 'jquery';
|
||||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
|
||||
import createFlash from './flash';
|
||||
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from './issuable/constants';
|
||||
import axios from './lib/utils/axios_utils';
|
||||
import { addDelimiter } from './lib/utils/text_utility';
|
||||
import { __ } from './locale';
|
||||
import CreateMergeRequestDropdown from '~/create_merge_request_dropdown';
|
||||
import createFlash from '~/flash';
|
||||
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { addDelimiter } from '~/lib/utils/text_utility';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default class Issue {
|
||||
constructor() {
|
|
@ -2,7 +2,7 @@ import loadAwardsHandler from '~/awards_handler';
|
|||
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
|
||||
import initIssuableSidebar from '~/issuable/init_issuable_sidebar';
|
||||
import { IssuableType } from '~/vue_shared/issuable/show/constants';
|
||||
import Issue from '~/issue';
|
||||
import Issue from '~/issues/issue';
|
||||
import { initIncidentApp, initIncidentHeaderActions } from '~/issues/show/incident';
|
||||
import { initIssuableApp, initIssueHeaderActions } from '~/issues/show/issue';
|
||||
import { parseIssuableData } from '~/issues/show/utils/parse_data';
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import $ from 'jquery';
|
||||
import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
|
||||
import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
@ -11,7 +11,7 @@ import axios from '~/lib/utils/axios_utils';
|
|||
import { scrollToElement, historyPushState } from '~/lib/utils/common_utils';
|
||||
import { setUrlParams, queryToObject, getParameterByName } from '~/lib/utils/url_utility';
|
||||
import { __ } from '~/locale';
|
||||
import initManualOrdering from '~/manual_ordering';
|
||||
import initManualOrdering from '~/issues/manual_ordering';
|
||||
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
|
||||
import {
|
||||
sortOrderMap,
|
||||
|
|
|
@ -16,7 +16,7 @@ import * as popovers from '~/popovers';
|
|||
import * as tooltips from '~/tooltips';
|
||||
import { initHeaderSearchApp } from '~/header_search';
|
||||
import initAlertHandler from './alert_handler';
|
||||
import { removeFlashClickListener } from './flash';
|
||||
import { addDismissFlashClickListener } from './flash';
|
||||
import initTodoToggle from './header';
|
||||
import initLayoutNav from './layout_nav';
|
||||
import { logHelloDeferred } from './lib/logger/hello_deferred';
|
||||
|
@ -259,7 +259,7 @@ if (flashContainer && flashContainer.children.length) {
|
|||
flashContainer
|
||||
.querySelectorAll('.flash-alert, .flash-notice, .flash-success')
|
||||
.forEach((flashEl) => {
|
||||
removeFlashClickListener(flashEl);
|
||||
addDismissFlashClickListener(flashEl);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
|
||||
import initManualOrdering from '~/manual_ordering';
|
||||
import initManualOrdering from '~/issues/manual_ordering';
|
||||
import { FILTERED_SEARCH } from '~/filtered_search/constants';
|
||||
import initFilteredSearch from '~/pages/search/init_filtered_search';
|
||||
import projectSelect from '~/project_select';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
|
||||
import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
|
||||
import { mountIssuablesListApp, mountIssuesListApp } from '~/issues_list';
|
||||
import initManualOrdering from '~/manual_ordering';
|
||||
import initManualOrdering from '~/issues/manual_ordering';
|
||||
import { FILTERED_SEARCH } from '~/filtered_search/constants';
|
||||
import initFilteredSearch from '~/pages/search/init_filtered_search';
|
||||
import projectSelect from '~/project_select';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import initRelatedIssues from '~/related_issues';
|
||||
import initSidebarBundle from '~/sidebar/sidebar_bundle';
|
||||
import initShow from '../../issues/show';
|
||||
import initShow from '~/issues/show';
|
||||
|
||||
initShow();
|
||||
initSidebarBundle();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import initForm from 'ee_else_ce/pages/projects/issues/form';
|
||||
import initForm from 'ee_else_ce/issues/form';
|
||||
|
||||
initForm();
|
||||
|
|
|
@ -4,7 +4,7 @@ import initCsvImportExportButtons from '~/issuable/init_csv_import_export_button
|
|||
import initIssuableByEmail from '~/issuable/init_issuable_by_email';
|
||||
import IssuableIndex from '~/issuable/issuable_index';
|
||||
import { mountIssuablesListApp, mountIssuesListApp, mountJiraIssuesListApp } from '~/issues_list';
|
||||
import initManualOrdering from '~/manual_ordering';
|
||||
import initManualOrdering from '~/issues/manual_ordering';
|
||||
import { FILTERED_SEARCH } from '~/filtered_search/constants';
|
||||
import { ISSUABLE_INDEX } from '~/issuable/constants';
|
||||
import initFilteredSearch from '~/pages/search/init_filtered_search';
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import initForm from 'ee_else_ce/pages/projects/issues/form';
|
||||
import initForm from 'ee_else_ce/issues/form';
|
||||
|
||||
initForm();
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
import { mountIssuablesListApp } from '~/issues_list';
|
||||
import FilteredSearchServiceDesk from './filtered_search';
|
||||
import { initFilteredSearchServiceDesk } from '~/issues/init_filtered_search_service_desk';
|
||||
|
||||
const supportBotData = JSON.parse(
|
||||
document.querySelector('.js-service-desk-issues').dataset.supportBot,
|
||||
);
|
||||
|
||||
if (document.querySelector('.filtered-search')) {
|
||||
const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
|
||||
filteredSearchManager.setup();
|
||||
}
|
||||
initFilteredSearchServiceDesk();
|
||||
|
||||
if (gon.features?.vueIssuablesList) {
|
||||
mountIssuablesListApp();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { store } from '~/notes/stores';
|
||||
import initRelatedIssues from '~/related_issues';
|
||||
import initSidebarBundle from '~/sidebar/sidebar_bundle';
|
||||
import initShow from '../show';
|
||||
import initShow from '~/issues/show';
|
||||
|
||||
initShow();
|
||||
initSidebarBundle(store);
|
||||
|
|
|
@ -7,7 +7,7 @@ import Diff from '~/diff';
|
|||
import GLForm from '~/gl_form';
|
||||
import LabelsSelect from '~/labels/labels_select';
|
||||
import MilestoneSelect from '~/milestones/milestone_select';
|
||||
import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
|
||||
import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors';
|
||||
|
||||
export default () => {
|
||||
new Diff();
|
||||
|
|
|
@ -157,11 +157,18 @@ export default {
|
|||
},
|
||||
canLock() {
|
||||
const { pushCode, downloadCode } = this.project.userPermissions;
|
||||
const currentUsername = window.gon?.current_username;
|
||||
|
||||
if (this.pathLockedByUser && this.pathLockedByUser.username !== currentUsername) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pushCode && downloadCode;
|
||||
},
|
||||
isLocked() {
|
||||
return this.project.pathLocks.nodes.some((node) => node.path === this.path);
|
||||
pathLockedByUser() {
|
||||
const pathLock = this.project.pathLocks.nodes.find((node) => node.path === this.path);
|
||||
|
||||
return pathLock ? pathLock.user : null;
|
||||
},
|
||||
showForkSuggestion() {
|
||||
const { createMergeRequestIn, forkProject } = this.project.userPermissions;
|
||||
|
@ -270,7 +277,7 @@ export default {
|
|||
:can-push-to-branch="blobInfo.canCurrentUserPushToBranch"
|
||||
:empty-repo="project.repository.empty"
|
||||
:project-path="projectPath"
|
||||
:is-locked="isLocked"
|
||||
:is-locked="Boolean(pathLockedByUser)"
|
||||
:can-lock="canLock"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -11,6 +11,10 @@ query getBlobInfo($projectPath: ID!, $filePath: String!, $ref: String!) {
|
|||
nodes {
|
||||
id
|
||||
path
|
||||
user {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
repository {
|
||||
|
|
|
@ -156,27 +156,23 @@ export const securityFeatures = [
|
|||
// https://gitlab.com/gitlab-org/gitlab/-/issues/331621
|
||||
canEnableByMergeRequest: true,
|
||||
},
|
||||
...(gon?.features?.configureIacScanningViaMr
|
||||
? [
|
||||
{
|
||||
name: SAST_IAC_NAME,
|
||||
shortName: SAST_IAC_SHORT_NAME,
|
||||
description: SAST_IAC_DESCRIPTION,
|
||||
helpPath: SAST_IAC_HELP_PATH,
|
||||
configurationHelpPath: SAST_IAC_CONFIG_HELP_PATH,
|
||||
type: REPORT_TYPE_SAST_IAC,
|
||||
{
|
||||
name: SAST_IAC_NAME,
|
||||
shortName: SAST_IAC_SHORT_NAME,
|
||||
description: SAST_IAC_DESCRIPTION,
|
||||
helpPath: SAST_IAC_HELP_PATH,
|
||||
configurationHelpPath: SAST_IAC_CONFIG_HELP_PATH,
|
||||
type: REPORT_TYPE_SAST_IAC,
|
||||
|
||||
// This field is currently hardcoded because SAST IaC is always available.
|
||||
// It will eventually come from the Backend, the progress is tracked in
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/331622
|
||||
available: true,
|
||||
// This field is currently hardcoded because SAST IaC is always available.
|
||||
// It will eventually come from the Backend, the progress is tracked in
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/331622
|
||||
available: true,
|
||||
|
||||
// This field will eventually come from the backend, the progress is
|
||||
// tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/331621
|
||||
canEnableByMergeRequest: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// This field will eventually come from the backend, the progress is
|
||||
// tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/331621
|
||||
canEnableByMergeRequest: true,
|
||||
},
|
||||
{
|
||||
name: DAST_NAME,
|
||||
shortName: DAST_SHORT_NAME,
|
||||
|
@ -278,21 +274,17 @@ export const featureToMutationMap = {
|
|||
},
|
||||
}),
|
||||
},
|
||||
...(gon?.features?.configureIacScanningViaMr
|
||||
? {
|
||||
[REPORT_TYPE_SAST_IAC]: {
|
||||
mutationId: 'configureSastIac',
|
||||
getMutationPayload: (projectPath) => ({
|
||||
mutation: configureSastIacMutation,
|
||||
variables: {
|
||||
input: {
|
||||
projectPath,
|
||||
},
|
||||
},
|
||||
}),
|
||||
[REPORT_TYPE_SAST_IAC]: {
|
||||
mutationId: 'configureSastIac',
|
||||
getMutationPayload: (projectPath) => ({
|
||||
mutation: configureSastIacMutation,
|
||||
variables: {
|
||||
input: {
|
||||
projectPath,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
[REPORT_TYPE_SECRET_DETECTION]: {
|
||||
mutationId: 'configureSecretDetection',
|
||||
getMutationPayload: (projectPath) => ({
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
GlLoadingIcon,
|
||||
GlTooltip,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
GlButton,
|
||||
} from '@gitlab/ui';
|
||||
import createFlash from '~/flash';
|
||||
import { INCIDENT_SEVERITY, ISSUABLE_TYPES, I18N } from './constants';
|
||||
|
@ -20,7 +20,7 @@ export default {
|
|||
GlSprintf,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlLink,
|
||||
GlButton,
|
||||
SeverityToken,
|
||||
},
|
||||
inject: ['canUpdate'],
|
||||
|
@ -150,23 +150,25 @@ export default {
|
|||
|
||||
<div class="hide-collapsed">
|
||||
<p
|
||||
class="gl-line-height-20 gl-mb-0 gl-text-gray-900 gl-display-flex gl-justify-content-space-between"
|
||||
class="gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-display-flex gl-justify-content-space-between"
|
||||
>
|
||||
{{ $options.i18n.SEVERITY }}
|
||||
<gl-link
|
||||
<gl-button
|
||||
v-if="canUpdate"
|
||||
category="tertiary"
|
||||
size="small"
|
||||
data-testid="editButton"
|
||||
href="#"
|
||||
@click="toggleFormDropdown"
|
||||
@keydown.esc="hideDropdown"
|
||||
>
|
||||
{{ $options.i18n.EDIT }}
|
||||
</gl-link>
|
||||
</gl-button>
|
||||
</p>
|
||||
|
||||
<gl-dropdown
|
||||
:class="dropdownClass"
|
||||
block
|
||||
:header-text="__('Assign severity')"
|
||||
:text="selectedItem.label"
|
||||
toggle-class="dropdown-menu-toggle gl-mb-2"
|
||||
@keydown.esc.native="hideDropdown"
|
||||
|
|
|
@ -79,6 +79,20 @@ export default class SidebarMediator {
|
|||
}),
|
||||
);
|
||||
} else {
|
||||
const currentUserId = gon.current_user_id;
|
||||
|
||||
if (currentUserId !== user.id) {
|
||||
const currentUserReviewerOrAssignee = isReviewer
|
||||
? this.store.findReviewer({ id: currentUserId })
|
||||
: this.store.findAssignee({ id: currentUserId });
|
||||
|
||||
if (currentUserReviewerOrAssignee?.attention_requested) {
|
||||
// Update current users attention_requested state
|
||||
this.store.updateReviewer(currentUserId, 'attention_requested');
|
||||
this.store.updateAssignee(currentUserId, 'attention_requested');
|
||||
}
|
||||
}
|
||||
|
||||
toast(sprintf(__('Requested attention from @%{username}'), { username: user.username }));
|
||||
}
|
||||
|
||||
|
|
|
@ -220,16 +220,17 @@ export default {
|
|||
class="gl-text-gray-900 gl-mb-2 gl-line-height-20 gl-display-flex gl-justify-content-space-between"
|
||||
>
|
||||
{{ __('Assignee') }}
|
||||
<a
|
||||
<gl-button
|
||||
v-if="isEditable"
|
||||
ref="editButton"
|
||||
class="btn-link"
|
||||
href="#"
|
||||
category="tertiary"
|
||||
size="small"
|
||||
class="gl-text-black-normal!"
|
||||
@click="toggleFormDropdown"
|
||||
@keydown.esc="hideDropdown"
|
||||
>
|
||||
{{ __('Edit') }}
|
||||
</a>
|
||||
</gl-button>
|
||||
</p>
|
||||
|
||||
<gl-dropdown
|
||||
|
|
|
@ -100,7 +100,8 @@ export default {
|
|||
<gl-button
|
||||
v-if="isEditable"
|
||||
class="gl-text-black-normal!"
|
||||
variant="link"
|
||||
category="tertiary"
|
||||
size="small"
|
||||
@click="toggleFormDropdown"
|
||||
@keydown.esc="hideDropdown"
|
||||
>
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
<script>
|
||||
import { escape } from 'lodash';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
initialTitle: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: __('Add a title...'),
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: this.initialTitle,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getSanitizedTitle(inputEl) {
|
||||
const { innerText } = inputEl;
|
||||
return escape(innerText);
|
||||
},
|
||||
handleBlur({ target }) {
|
||||
this.$emit('title-changed', this.getSanitizedTitle(target));
|
||||
},
|
||||
handleInput({ target }) {
|
||||
this.$emit('title-input', this.getSanitizedTitle(target));
|
||||
},
|
||||
handleSubmit() {
|
||||
this.$refs.titleEl.blur();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2
|
||||
class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5 gl-display-inline-block"
|
||||
:class="{ 'gl-cursor-not-allowed': disabled }"
|
||||
data-testid="title"
|
||||
aria-labelledby="item-title"
|
||||
>
|
||||
<span
|
||||
id="item-title"
|
||||
ref="titleEl"
|
||||
role="textbox"
|
||||
:aria-label="__('Title')"
|
||||
:data-placeholder="placeholder"
|
||||
:contenteditable="!disabled"
|
||||
class="gl-pseudo-placeholder"
|
||||
@blur="handleBlur"
|
||||
@keyup="handleInput"
|
||||
@keydown.enter.exact="handleSubmit"
|
||||
@keydown.ctrl.u.prevent
|
||||
@keydown.meta.u.prevent
|
||||
@keydown.ctrl.b.prevent
|
||||
@keydown.meta.b.prevent
|
||||
>{{ title }}</span
|
||||
>
|
||||
</h2>
|
||||
</template>
|
|
@ -29,5 +29,30 @@ export const resolvers = {
|
|||
workItem,
|
||||
};
|
||||
},
|
||||
|
||||
updateWorkItem(_, { input }, { cache }) {
|
||||
const workItemTitle = {
|
||||
__typename: 'TitleWidget',
|
||||
type: 'TITLE',
|
||||
enabled: true,
|
||||
contentText: input.title,
|
||||
};
|
||||
const workItem = {
|
||||
__typename: 'WorkItem',
|
||||
type: 'FEATURE',
|
||||
id: input.id,
|
||||
widgets: {
|
||||
__typename: 'WorkItemWidgetConnection',
|
||||
nodes: [workItemTitle],
|
||||
},
|
||||
};
|
||||
|
||||
cache.writeQuery({ query: workItemQuery, variables: { id: input.id }, data: { workItem } });
|
||||
|
||||
return {
|
||||
__typename: 'UpdateWorkItemPayload',
|
||||
workItem,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -37,14 +37,24 @@ type CreateWorkItemInput {
|
|||
title: String!
|
||||
}
|
||||
|
||||
type UpdateWorkItemInput {
|
||||
id: ID!
|
||||
title: String
|
||||
}
|
||||
|
||||
type CreateWorkItemPayload {
|
||||
workItem: WorkItem!
|
||||
}
|
||||
|
||||
type UpdateWorkItemPayload {
|
||||
workItem: WorkItem!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
workItem(id: ID!): WorkItem!
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
createWorkItem(input: CreateWorkItemInput!): CreateWorkItemPayload!
|
||||
updateWorkItem(input: UpdateWorkItemInput!): UpdateWorkItemPayload!
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
#import './widget.fragment.graphql'
|
||||
|
||||
mutation updateWorkItem($input: UpdateWorkItemInput) {
|
||||
updateWorkItem(input: $input) @client {
|
||||
workItem {
|
||||
id
|
||||
type
|
||||
widgets {
|
||||
nodes {
|
||||
...WidgetBase
|
||||
... on TitleWidget {
|
||||
contentText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,10 +2,13 @@
|
|||
import { GlButton, GlAlert } from '@gitlab/ui';
|
||||
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
|
||||
|
||||
import ItemTitle from '../components/item_title.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlAlert,
|
||||
ItemTitle,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -37,6 +40,9 @@ export default {
|
|||
this.error = true;
|
||||
}
|
||||
},
|
||||
handleTitleInput(title) {
|
||||
this.title = title;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -46,15 +52,7 @@ export default {
|
|||
<gl-alert v-if="error" variant="danger" @dismiss="error = false">{{
|
||||
__('Something went wrong when creating a work item. Please try again')
|
||||
}}</gl-alert>
|
||||
<label for="title" class="gl-sr-only">{{ __('Title') }}</label>
|
||||
<input
|
||||
id="title"
|
||||
v-model.trim="title"
|
||||
type="text"
|
||||
class="gl-font-size-h-display gl-font-weight-bold gl-my-5 gl-border-none gl-w-full gl-pl-2"
|
||||
data-testid="title-input"
|
||||
:placeholder="__('Add a title…')"
|
||||
/>
|
||||
<item-title data-testid="title-input" @title-input="handleTitleInput" />
|
||||
<div class="gl-bg-gray-10 gl-py-5 gl-px-6">
|
||||
<gl-button
|
||||
variant="confirm"
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
<script>
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import workItemQuery from '../graphql/work_item.query.graphql';
|
||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||
import { widgetTypes } from '../constants';
|
||||
|
||||
import ItemTitle from '../components/item_title.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ItemTitle,
|
||||
GlAlert,
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
|
@ -12,6 +20,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
workItem: null,
|
||||
error: false,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
@ -29,20 +38,39 @@ export default {
|
|||
return this.workItem?.widgets?.nodes?.find((widget) => widget.type === widgetTypes.title);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async updateWorkItem(title) {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: updateWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: this.id,
|
||||
title,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
this.error = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<gl-alert v-if="error" variant="danger" @dismiss="error = false">{{
|
||||
__('Something went wrong while updating work item. Please try again')
|
||||
}}</gl-alert>
|
||||
<!-- Title widget placeholder -->
|
||||
<div>
|
||||
<h2
|
||||
<item-title
|
||||
v-if="titleWidgetData"
|
||||
class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5"
|
||||
:initial-title="titleWidgetData.contentText"
|
||||
data-testid="title"
|
||||
>
|
||||
{{ titleWidgetData.contentText }}
|
||||
</h2>
|
||||
@title-changed="updateWorkItem"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
|
|
@ -479,6 +479,13 @@ img.emoji {
|
|||
border-top: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.gl-pseudo-placeholder:empty::before {
|
||||
content: attr(data-placeholder);
|
||||
font-weight: $gl-font-weight-normal;
|
||||
color: $gl-text-color-secondary;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
/**
|
||||
🚨 Do not use these classes — they clash with the Gitlab UI design system and will be removed. 🚨
|
||||
See https://gitlab.com/gitlab-org/gitlab/issues/36857 for more details.
|
||||
|
|
|
@ -1831,6 +1831,9 @@ body.gl-dark .navbar-gitlab .search form:active {
|
|||
background-color: var(--gray-100);
|
||||
box-shadow: inset 0 0 0 1px var(--blue-200);
|
||||
}
|
||||
body.gl-dark .navbar-gitlab .search form .search-input {
|
||||
color: var(--gl-text-color);
|
||||
}
|
||||
|
||||
body.gl-dark {
|
||||
--gray-10: #1f1f1f;
|
||||
|
|
|
@ -122,6 +122,10 @@ body.gl-dark {
|
|||
background-color: var(--gray-100);
|
||||
box-shadow: inset 0 0 0 1px var(--blue-200);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
color: var(--gl-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
|
||||
after_action :verify_known_sign_in
|
||||
|
||||
protect_from_forgery except: [:kerberos, :saml, :cas3, :failure], with: :exception, prepend: true
|
||||
protect_from_forgery except: [:kerberos, :saml, :cas3, :failure] + AuthHelper.saml_providers, with: :exception, prepend: true
|
||||
|
||||
feature_category :authentication_and_authorization
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController
|
|||
def setup_walkthrough_experiment
|
||||
experiment(:pipeline_editor_walkthrough, namespace: @project.namespace, sticky_to: current_user) do |e|
|
||||
e.candidate {}
|
||||
e.record!
|
||||
e.publish_to_database
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,7 +21,7 @@ class Projects::LearnGitlabController < Projects::ApplicationController
|
|||
|
||||
experiment(:invite_for_help_continuous_onboarding, namespace: project.namespace) do |e|
|
||||
e.candidate {}
|
||||
e.record!
|
||||
e.publish_to_database
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -312,7 +312,7 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
|
||||
e.control {}
|
||||
e.candidate {}
|
||||
e.record!
|
||||
e.publish_to_database
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -325,7 +325,7 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
|
||||
e.control {}
|
||||
e.candidate {}
|
||||
e.record!
|
||||
e.publish_to_database
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:highlight_js, @project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml)
|
||||
push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
|
||||
end
|
||||
|
||||
layout :determine_layout
|
||||
|
|
|
@ -13,7 +13,6 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp
|
|||
super
|
||||
|
||||
publish_to_client
|
||||
publish_to_database if @record
|
||||
end
|
||||
|
||||
def publish_to_client
|
||||
|
@ -25,6 +24,8 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp
|
|||
end
|
||||
|
||||
def publish_to_database
|
||||
ActiveSupport::Deprecation.warn('publish_to_database is deprecated and should not be used for reporting anymore')
|
||||
|
||||
return unless should_track?
|
||||
|
||||
# if the context contains a namespace, group, project, user, or actor
|
||||
|
@ -36,10 +37,6 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp
|
|||
Experiment.add_subject(name, variant: variant_name || :control, subject: subject)
|
||||
end
|
||||
|
||||
def record!
|
||||
@record = true
|
||||
end
|
||||
|
||||
def control_behavior
|
||||
# define a default nil control behavior so we can omit it when not needed
|
||||
end
|
||||
|
|
|
@ -6,7 +6,7 @@ class NewProjectReadmeContentExperiment < ApplicationExperiment # rubocop:disabl
|
|||
|
||||
def run_with(project, variant: nil)
|
||||
@project = project
|
||||
record!
|
||||
publish_to_database
|
||||
run(variant)
|
||||
end
|
||||
|
||||
|
|
|
@ -96,7 +96,7 @@ module Types
|
|||
description: 'Rebase commit SHA of the merge request.'
|
||||
field :rebase_in_progress, GraphQL::Types::Boolean, method: :rebase_in_progress?, null: false, calls_gitaly: true,
|
||||
description: 'Indicates if there is a rebase currently in progress for the merge request.'
|
||||
field :default_merge_commit_message, GraphQL::Types::String, null: true,
|
||||
field :default_merge_commit_message, GraphQL::Types::String, null: true, calls_gitaly: true,
|
||||
description: 'Default merge commit message of the merge request.'
|
||||
field :default_merge_commit_message_with_description, GraphQL::Types::String, null: true,
|
||||
description: 'Default merge commit message of the merge request with description. Will have the same value as `defaultMergeCommitMessage` when project has `mergeCommitTemplate` set.',
|
||||
|
|
|
@ -86,6 +86,17 @@ module AuthHelper
|
|||
auth_providers.select { |provider| form_based_provider?(provider) }
|
||||
end
|
||||
|
||||
def saml_providers
|
||||
auth_providers.select { |provider| auth_strategy_class(provider) == 'OmniAuth::Strategies::SAML' }
|
||||
end
|
||||
|
||||
def auth_strategy_class(provider)
|
||||
config = Gitlab::Auth::OAuth::Provider.config_for(provider)
|
||||
return if config.nil? || config['args'].blank?
|
||||
|
||||
config.args['strategy_class']
|
||||
end
|
||||
|
||||
def any_form_based_providers_enabled?
|
||||
form_based_providers.any? { |provider| form_enabled_for_sign_in?(provider) }
|
||||
end
|
||||
|
|
|
@ -4,6 +4,8 @@ module Clusters
|
|||
class Agent < ApplicationRecord
|
||||
self.table_name = 'cluster_agents'
|
||||
|
||||
INACTIVE_AFTER = 1.hour.freeze
|
||||
|
||||
belongs_to :created_by_user, class_name: 'User', optional: true
|
||||
belongs_to :project, class_name: '::Project' # Otherwise, it will load ::Clusters::Project
|
||||
|
||||
|
@ -33,5 +35,9 @@ module Clusters
|
|||
def has_access_to?(requested_project)
|
||||
requested_project == project
|
||||
end
|
||||
|
||||
def active?
|
||||
agent_tokens.where("last_used_at > ?", INACTIVE_AFTER.ago).exists?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -28,8 +28,12 @@ module Clusters
|
|||
|
||||
cache_attributes(track_values)
|
||||
|
||||
# Use update_column so updated_at is skipped
|
||||
update_columns(track_values) if can_update_track_values?
|
||||
if can_update_track_values?
|
||||
log_activity_event!(track_values[:last_used_at]) unless agent.active?
|
||||
|
||||
# Use update_column so updated_at is skipped
|
||||
update_columns(track_values)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -44,5 +48,14 @@ module Clusters
|
|||
real_last_used_at.nil? ||
|
||||
(Time.current - real_last_used_at) >= last_used_at_max_age
|
||||
end
|
||||
|
||||
def log_activity_event!(recorded_at)
|
||||
agent.activity_events.create!(
|
||||
kind: :agent_connected,
|
||||
level: :info,
|
||||
recorded_at: recorded_at,
|
||||
agent_token: self
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,10 @@ module Clusters
|
|||
nullify_if_blank :detail
|
||||
|
||||
enum kind: {
|
||||
token_created: 0
|
||||
token_created: 0,
|
||||
token_revoked: 1,
|
||||
agent_connected: 2,
|
||||
agent_disconnected: 3
|
||||
}, _prefix: true
|
||||
|
||||
enum level: {
|
||||
|
|
|
@ -52,6 +52,7 @@ class Member < ApplicationRecord
|
|||
message: _('project bots cannot be added to other groups / projects')
|
||||
},
|
||||
if: :project_bot?
|
||||
validate :access_level_inclusion
|
||||
|
||||
scope :with_invited_user_state, -> do
|
||||
joins('LEFT JOIN users as invited_user ON invited_user.email = members.invite_email')
|
||||
|
@ -382,6 +383,12 @@ class Member < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def access_level_inclusion
|
||||
return if access_level.in?(Gitlab::Access.all_values)
|
||||
|
||||
errors.add(:access_level, "is not included in the list")
|
||||
end
|
||||
|
||||
def send_invite
|
||||
# override in subclass
|
||||
end
|
||||
|
|
|
@ -6,6 +6,7 @@ class GroupMember < Member
|
|||
include CreatedAtFilterable
|
||||
|
||||
SOURCE_TYPE = 'Namespace'
|
||||
SOURCE_TYPE_FORMAT = /\ANamespace\z/.freeze
|
||||
|
||||
belongs_to :group, foreign_key: 'source_id'
|
||||
alias_attribute :namespace_id, :source_id
|
||||
|
@ -13,9 +14,7 @@ class GroupMember < Member
|
|||
|
||||
# Make sure group member points only to group as it source
|
||||
default_value_for :source_type, SOURCE_TYPE
|
||||
validates :source_type, format: { with: /\ANamespace\z/ }
|
||||
validates :access_level, presence: true
|
||||
validate :access_level_inclusion
|
||||
validates :source_type, format: { with: SOURCE_TYPE_FORMAT }
|
||||
|
||||
default_scope { where(source_type: SOURCE_TYPE) } # rubocop:disable Cop/DefaultScope
|
||||
|
||||
|
@ -65,12 +64,6 @@ class GroupMember < Member
|
|||
super
|
||||
end
|
||||
|
||||
def access_level_inclusion
|
||||
return if access_level.in?(Gitlab::Access.all_values)
|
||||
|
||||
errors.add(:access_level, "is not included in the list")
|
||||
end
|
||||
|
||||
def send_invite
|
||||
run_after_commit_or_now { notification_service.invite_group_member(self, @raw_invite_token) }
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class ProjectMember < Member
|
||||
extend ::Gitlab::Utils::Override
|
||||
SOURCE_TYPE = 'Project'
|
||||
SOURCE_TYPE_FORMAT = /\AProject\z/.freeze
|
||||
|
||||
belongs_to :project, foreign_key: 'source_id'
|
||||
|
||||
|
@ -10,8 +11,7 @@ class ProjectMember < Member
|
|||
|
||||
# Make sure project member points only to project as it source
|
||||
default_value_for :source_type, SOURCE_TYPE
|
||||
validates :source_type, format: { with: /\AProject\z/ }
|
||||
validates :access_level, inclusion: { in: Gitlab::Access.values }
|
||||
validates :source_type, format: { with: SOURCE_TYPE_FORMAT }
|
||||
default_scope { where(source_type: SOURCE_TYPE) } # rubocop:disable Cop/DefaultScope
|
||||
|
||||
scope :in_project, ->(project) { where(source_id: project.id) }
|
||||
|
@ -92,6 +92,13 @@ class ProjectMember < Member
|
|||
|
||||
private
|
||||
|
||||
override :access_level_inclusion
|
||||
def access_level_inclusion
|
||||
return if access_level.in?(Gitlab::Access.values)
|
||||
|
||||
errors.add(:access_level, "is not included in the list")
|
||||
end
|
||||
|
||||
override :refresh_member_authorized_projects
|
||||
def refresh_member_authorized_projects(blocking:)
|
||||
return unless user
|
||||
|
|
|
@ -14,6 +14,7 @@ module MergeRequests
|
|||
create_approval_note(merge_request)
|
||||
mark_pending_todos_as_done(merge_request)
|
||||
execute_approval_hooks(merge_request, current_user)
|
||||
remove_attention_requested(merge_request, current_user)
|
||||
merge_request_activity_counter.track_approve_mr_action(user: current_user)
|
||||
|
||||
success
|
||||
|
|
|
@ -58,6 +58,8 @@ module MergeRequests
|
|||
new_reviewers = merge_request.reviewers - old_reviewers
|
||||
merge_request_activity_counter.track_users_review_requested(users: new_reviewers)
|
||||
merge_request_activity_counter.track_reviewers_changed_action(user: current_user)
|
||||
|
||||
remove_attention_requested(merge_request, current_user)
|
||||
end
|
||||
|
||||
def cleanup_environments(merge_request)
|
||||
|
@ -238,6 +240,18 @@ module MergeRequests
|
|||
|
||||
Milestones::MergeRequestsCountService.new(milestone).delete_cache
|
||||
end
|
||||
|
||||
def remove_all_attention_requests(merge_request)
|
||||
return unless merge_request.attention_requested_enabled?
|
||||
|
||||
::MergeRequests::BulkRemoveAttentionRequestedService.new(project: merge_request.project, current_user: current_user, merge_request: merge_request).execute
|
||||
end
|
||||
|
||||
def remove_attention_requested(merge_request, user)
|
||||
return unless merge_request.attention_requested_enabled?
|
||||
|
||||
::MergeRequests::RemoveAttentionRequestedService.new(project: merge_request.project, current_user: current_user, merge_request: merge_request, user: user).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module MergeRequests
|
||||
class BulkRemoveAttentionRequestedService < MergeRequests::BaseService
|
||||
attr_accessor :merge_request
|
||||
|
||||
def initialize(project:, current_user:, merge_request:)
|
||||
super(project: project, current_user: current_user)
|
||||
|
||||
@merge_request = merge_request
|
||||
end
|
||||
|
||||
def execute
|
||||
return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request)
|
||||
|
||||
merge_request.merge_request_assignees.update_all(state: :reviewed)
|
||||
merge_request.merge_request_reviewers.update_all(state: :reviewed)
|
||||
|
||||
success
|
||||
end
|
||||
end
|
||||
end
|
|
@ -17,6 +17,7 @@ module MergeRequests
|
|||
create_note(merge_request)
|
||||
notification_service.async.close_mr(merge_request, current_user)
|
||||
todo_service.close_merge_request(merge_request, current_user)
|
||||
remove_all_attention_requests(merge_request)
|
||||
execute_hooks(merge_request, 'close')
|
||||
invalidate_cache_counts(merge_request, users: merge_request.assignees | merge_request.reviewers)
|
||||
merge_request.update_project_counter_caches
|
||||
|
|
|
@ -22,6 +22,8 @@ module MergeRequests
|
|||
merge_request_activity_counter.track_assignees_changed_action(user: current_user)
|
||||
|
||||
execute_assignees_hooks(merge_request, old_assignees) if options[:execute_hooks]
|
||||
|
||||
remove_attention_requested(merge_request, current_user)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -28,6 +28,7 @@ module MergeRequests
|
|||
notification_service.merge_mr(merge_request, current_user)
|
||||
invalidate_cache_counts(merge_request, users: merge_request.assignees | merge_request.reviewers)
|
||||
merge_request.update_project_counter_caches
|
||||
remove_all_attention_requests(merge_request)
|
||||
delete_non_latest_diffs(merge_request)
|
||||
cancel_review_app_jobs!(merge_request)
|
||||
cleanup_environments(merge_request)
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module MergeRequests
|
||||
class RemoveAttentionRequestedService < MergeRequests::BaseService
|
||||
attr_accessor :merge_request, :user
|
||||
|
||||
def initialize(project:, current_user:, merge_request:, user:)
|
||||
super(project: project, current_user: current_user)
|
||||
|
||||
@merge_request = merge_request
|
||||
@user = user
|
||||
end
|
||||
|
||||
def execute
|
||||
return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request)
|
||||
|
||||
if reviewer || assignee
|
||||
update_state(reviewer)
|
||||
update_state(assignee)
|
||||
|
||||
success
|
||||
else
|
||||
error("User is not a reviewer or assignee of the merge request")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assignee
|
||||
merge_request.find_assignee(user)
|
||||
end
|
||||
|
||||
def reviewer
|
||||
merge_request.find_reviewer(user)
|
||||
end
|
||||
|
||||
def update_state(reviewer_or_assignee)
|
||||
reviewer_or_assignee&.update(state: :reviewed)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -21,6 +21,10 @@ module MergeRequests
|
|||
if reviewer&.attention_requested? || assignee&.attention_requested?
|
||||
create_attention_request_note
|
||||
notity_user
|
||||
|
||||
if current_user.id != user.id
|
||||
remove_attention_requested(merge_request, current_user)
|
||||
end
|
||||
else
|
||||
create_remove_attention_request_note
|
||||
end
|
||||
|
|
|
@ -29,13 +29,12 @@ module Namespaces
|
|||
return if email_for_track_sent_to_user?
|
||||
|
||||
experiment(:invite_team_email, group: group) do |e|
|
||||
e.publish_to_database
|
||||
e.candidate do
|
||||
send_email(user, group)
|
||||
sent_email_records.add(user, track, series)
|
||||
sent_email_records.save!
|
||||
end
|
||||
|
||||
e.record!
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -6,11 +6,13 @@
|
|||
- providers.each do |provider|
|
||||
- unlink_allowed = unlink_provider_allowed?(provider)
|
||||
- link_allowed = link_provider_allowed?(provider)
|
||||
- has_icon = provider_has_icon?(provider)
|
||||
- if unlink_allowed || link_allowed
|
||||
- if auth_active?(provider)
|
||||
- if unlink_allowed
|
||||
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: button_class do
|
||||
.social-provider-btn-image.gl-button-icon= provider_image_tag(provider)
|
||||
- if has_icon
|
||||
.social-provider-btn-image.gl-button-icon= provider_image_tag(provider)
|
||||
.gl-button-text
|
||||
= s_('Profiles|Disconnect %{provider}') % { provider: label_for_provider(provider) }
|
||||
- else
|
||||
|
@ -19,7 +21,8 @@
|
|||
= s_('Profiles|%{provider} Active') % { provider: label_for_provider(provider) }
|
||||
- elsif link_allowed
|
||||
= link_to omniauth_authorize_path(:user, provider), method: :post, class: button_class do
|
||||
.social-provider-btn-image.gl-button-icon= provider_image_tag(provider)
|
||||
- if has_icon
|
||||
.social-provider-btn-image.gl-button-icon= provider_image_tag(provider)
|
||||
.gl-button-text
|
||||
= s_('Profiles|Connect %{provider}') % { provider: label_for_provider(provider) }
|
||||
= render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: group_saml_identities
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: configure_iac_scanning_via_mr
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73155
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/343966
|
||||
milestone: '14.5'
|
||||
type: development
|
||||
group: group::static analysis
|
||||
default_enabled: true
|
|
@ -2128,9 +2128,7 @@ from the latest pipeline that completed successfully.
|
|||
|
||||
**Possible inputs**:
|
||||
|
||||
- `needs:project`: A full project path, including namespace and group. If the
|
||||
project is in the same group or namespace, you can omit them from the `project`
|
||||
keyword. For example: `project: group/project-name` or `project: project-name`.
|
||||
- `needs:project`: A full project path, including namespace and group.
|
||||
- `job`: The job to download artifacts from.
|
||||
- `ref`: The ref to download artifacts from.
|
||||
- `artifacts`: Must be `true` to download artifacts.
|
||||
|
|
|
@ -394,26 +394,6 @@ You may be asked from time to time to track a specific record ID in experiments.
|
|||
The approach is largely up to the PM and engineer creating the implementation.
|
||||
No recommendations are provided here at this time.
|
||||
|
||||
### Record experiment subjects
|
||||
|
||||
Snowplow tracking of identifiable users or groups is prohibited, but you can still
|
||||
determine if an experiment is successful or not. We're allowed to record the ID of
|
||||
a namespace, project or user in our database. Therefore, we can tell the experiment
|
||||
to record their ID together with the assigned experiment variant in the
|
||||
`experiment_subjects` database table for later analysis.
|
||||
|
||||
For the recording to work, the experiment's context must include a `namespace`,
|
||||
`group`, `project`, `user`, or `actor`.
|
||||
|
||||
To record the experiment subject when you first assign a variant, call `record!` in
|
||||
the experiment's block:
|
||||
|
||||
```ruby
|
||||
experiment(:pill_color, actor: current_user) do |e|
|
||||
e.record!
|
||||
end
|
||||
```
|
||||
|
||||
## Test with RSpec
|
||||
|
||||
This gem provides some RSpec helpers and custom matchers. These are in flux as of GitLab 13.10.
|
||||
|
|
|
@ -163,6 +163,74 @@ On the sign in page there should now be a SAML button below the regular sign in
|
|||
Click the icon to begin the authentication process. If everything goes well the user
|
||||
is returned to GitLab and signed in.
|
||||
|
||||
### Use multiple SAML identity providers
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14361) in GitLab 14.6.
|
||||
|
||||
You can configure GitLab to use multiple SAML identity providers if:
|
||||
|
||||
- Each provider has a unique name set that matches a name set in `args`.
|
||||
- The providers' names are:
|
||||
- Used in OmniAuth configuration for properties based on the provider name. For example, `allowBypassTwoFactor`, `allowSingleSignOn`, and
|
||||
`syncProfileFromProvider`.
|
||||
- Used for association to each existing user as an additional identity.
|
||||
- The `assertion_consumer_service_url` matches the provider name.
|
||||
- The `strategy_class` is explicitly set because it cannot be inferred from provider name.
|
||||
|
||||
Example multiple providers configuration for Omnibus GitLab:
|
||||
|
||||
```ruby
|
||||
gitlab_rails['omniauth_providers'] = [
|
||||
{
|
||||
name: 'saml_1',
|
||||
args: {
|
||||
name: 'saml_1', # This is mandatory and must match the provider name
|
||||
strategy_class: 'OmniAuth::Strategies::SAML'
|
||||
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml_1/callback', # URL must match the name of the provider
|
||||
... # Put here all the required arguments similar to a single provider
|
||||
},
|
||||
label: 'Provider 1' # Differentiate the two buttons and providers in the UI
|
||||
},
|
||||
{
|
||||
name: 'saml_2',
|
||||
args: {
|
||||
name: 'saml_2', # This is mandatory and must match the provider name
|
||||
strategy_class: 'OmniAuth::Strategies::SAML'
|
||||
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml_2/callback', # URL must match the name of the provider
|
||||
... # Put here all the required arguments similar to a single provider
|
||||
},
|
||||
label: 'Provider 2' # Differentiate the two buttons and providers in the UI
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Example providers configuration for installations from source:
|
||||
|
||||
```yaml
|
||||
omniauth:
|
||||
providers:
|
||||
- {
|
||||
name: 'saml_1',
|
||||
args: {
|
||||
name: 'saml_1', # This is mandatory and must match the provider name
|
||||
strategy_class: 'OmniAuth::Strategies::SAML',
|
||||
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml_1/callback', # URL must match the name of the provider
|
||||
... # Put here all the required arguments similar to a single provider
|
||||
},
|
||||
label: 'Provider 1' # Differentiate the two buttons and providers in the UI
|
||||
}
|
||||
- {
|
||||
name: 'saml_2',
|
||||
args: {
|
||||
name: 'saml_2', # This is mandatory and must match the provider name
|
||||
strategy_class: 'OmniAuth::Strategies::SAML',
|
||||
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml_2/callback', # URL must match the name of the provider
|
||||
... # Put here all the required arguments similar to a single provider
|
||||
},
|
||||
label: 'Provider 2' # Differentiate the two buttons and providers in the UI
|
||||
}
|
||||
```
|
||||
|
||||
### Notes on configuring your identity provider
|
||||
|
||||
When configuring a SAML app on the IdP, you need at least:
|
||||
|
|
|
@ -194,12 +194,13 @@ To set a limit on how long these sessions are valid:
|
|||
|
||||
## Limit the lifetime of SSH keys **(ULTIMATE SELF)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1007) in GitLab 14.6 [with a flag](../../../administration/feature_flags.md) named `ff_limit_ssh_key_lifetime`. Disabled by default.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1007) in GitLab 14.6 [with a flag](../../../administration/feature_flags.md) named `ff_limit_ssh_key_lifetime`. Disabled by default.
|
||||
> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/346753) in GitLab 14.6.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available,
|
||||
ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `ff_limit_ssh_key_lifetime`.
|
||||
On GitLab.com, this feature is not available. The feature is not ready for production use.
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature,
|
||||
ask an administrator to [disable the feature flag](../../../administration/feature_flags.md) named `ff_limit_ssh_key_lifetime`.
|
||||
On GitLab.com, this feature is not available.
|
||||
|
||||
Users can optionally specify a lifetime for
|
||||
[SSH keys](../../../ssh/index.md).
|
||||
|
|
|
@ -65,6 +65,9 @@ GitLab creates a squash commit message with this template:
|
|||
|
||||
## Supported variables in commit templates
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/20263) in GitLab 14.5.
|
||||
> - [Added](https://gitlab.com/gitlab-org/gitlab/-/issues/346805) `first_commit` and `first_multiline_commit` variables in GitLab 14.6.
|
||||
|
||||
Commit message templates support these variables:
|
||||
|
||||
| Variable | Description | Output example |
|
||||
|
@ -73,8 +76,10 @@ Commit message templates support these variables:
|
|||
| `%{target_branch}` | The name of the branch that the changes are applied to. | `main` |
|
||||
| `%{title}` | Title of the merge request. | `Fix tests and translations` |
|
||||
| `%{issues}` | String with phrase `Closes <issue numbers>`. Contains all issues mentioned in the merge request description that match [issue closing patterns](../issues/managing_issues.md#closing-issues-automatically). Empty if no issues are mentioned. | `Closes #465, #190 and #400` |
|
||||
| `%{description}` | Description of the merge request. | `Merge request description.<br>Can be multiline.` |
|
||||
| `%{description}` | Description of the merge request. | `Merge request description.`<br>`Can be multiline.` |
|
||||
| `%{reference}` | Reference to the merge request. | `group-name/project-name!72359` |
|
||||
| `%{first_commit}` | Full message of the first commit in merge request diff. | `Update README.md` |
|
||||
| `%{first_multiline_commit}` | Full message of the first commit that's not a merge commit and has more than one line in message body. Merge Request title if all commits aren't multiline. | `Update README.md`<br><br>`Improved project description in readme file.` |
|
||||
|
||||
Empty variables that are the only word in a line are removed, along with all newline characters preceding it.
|
||||
|
||||
|
|
|
@ -8,6 +8,13 @@ module API
|
|||
|
||||
before { authenticate! }
|
||||
|
||||
urgency :low, [
|
||||
'/projects/:id/merge_requests/:noteable_id/discussions',
|
||||
'/projects/:id/merge_requests/:noteable_id/discussions/:discussion_id',
|
||||
'/projects/:id/merge_requests/:noteable_id/discussions/:discussion_id/notes',
|
||||
'/projects/:id/merge_requests/:noteable_id/discussions/:discussion_id/notes/:note_id'
|
||||
]
|
||||
|
||||
Helpers::DiscussionsHelpers.feature_category_per_noteable_type.each do |noteable_type, feature_category|
|
||||
parent_type = noteable_type.parent_class.to_s.underscore
|
||||
noteables_str = noteable_type.to_s.underscore.pluralize
|
||||
|
|
|
@ -38,7 +38,7 @@ module API
|
|||
requires :version_id, type: Integer, desc: 'The ID of a merge request diff version'
|
||||
end
|
||||
|
||||
get ":id/merge_requests/:merge_request_iid/versions/:version_id" do
|
||||
get ":id/merge_requests/:merge_request_iid/versions/:version_id", urgency: :low do
|
||||
merge_request = find_merge_request_with_access(params[:merge_request_iid])
|
||||
|
||||
present_cached merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull, cache_context: nil
|
||||
|
|
|
@ -134,7 +134,7 @@ module API
|
|||
use :merge_requests_params
|
||||
use :optional_scope_param
|
||||
end
|
||||
get feature_category: :code_review do
|
||||
get feature_category: :code_review, urgency: :low do
|
||||
authenticate! unless params[:scope] == 'all'
|
||||
validate_anonymous_search_access! if params[:search].present?
|
||||
merge_requests = find_merge_requests
|
||||
|
@ -155,7 +155,7 @@ module API
|
|||
optional :non_archived, type: Boolean, desc: 'Return merge requests from non archived projects',
|
||||
default: true
|
||||
end
|
||||
get ":id/merge_requests", feature_category: :code_review do
|
||||
get ":id/merge_requests", feature_category: :code_review, urgency: :low do
|
||||
validate_anonymous_search_access! if declared_params[:search].present?
|
||||
merge_requests = find_merge_requests(group_id: user_group.id, include_subgroups: true)
|
||||
|
||||
|
@ -195,7 +195,7 @@ module API
|
|||
use :merge_requests_params
|
||||
optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IID array of merge requests'
|
||||
end
|
||||
get ":id/merge_requests", feature_category: :code_review do
|
||||
get ":id/merge_requests", feature_category: :code_review, urgency: :low do
|
||||
authorize! :read_merge_request, user_project
|
||||
validate_anonymous_search_access! if declared_params[:search].present?
|
||||
|
||||
|
@ -222,7 +222,7 @@ module API
|
|||
desc: 'The target project of the merge request defaults to the :id of the project'
|
||||
use :optional_params
|
||||
end
|
||||
post ":id/merge_requests", feature_category: :code_review do
|
||||
post ":id/merge_requests", feature_category: :code_review, urgency: :low do
|
||||
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20770')
|
||||
|
||||
authorize! :create_merge_request_from, user_project
|
||||
|
@ -244,7 +244,7 @@ module API
|
|||
params do
|
||||
requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
|
||||
end
|
||||
delete ":id/merge_requests/:merge_request_iid", feature_category: :code_review do
|
||||
delete ":id/merge_requests/:merge_request_iid", feature_category: :code_review, urgency: :low do
|
||||
merge_request = find_project_merge_request(params[:merge_request_iid])
|
||||
|
||||
authorize!(:destroy_merge_request, merge_request)
|
||||
|
@ -263,7 +263,7 @@ module API
|
|||
desc 'Get a single merge request' do
|
||||
success Entities::MergeRequest
|
||||
end
|
||||
get ':id/merge_requests/:merge_request_iid', feature_category: :code_review do
|
||||
get ':id/merge_requests/:merge_request_iid', feature_category: :code_review, urgency: :low do
|
||||
merge_request = find_merge_request_with_access(params[:merge_request_iid])
|
||||
|
||||
present merge_request,
|
||||
|
@ -279,7 +279,7 @@ module API
|
|||
desc 'Get the participants of a merge request' do
|
||||
success Entities::UserBasic
|
||||
end
|
||||
get ':id/merge_requests/:merge_request_iid/participants', feature_category: :code_review do
|
||||
get ':id/merge_requests/:merge_request_iid/participants', feature_category: :code_review, urgency: :low do
|
||||
merge_request = find_merge_request_with_access(params[:merge_request_iid])
|
||||
|
||||
participants = ::Kaminari.paginate_array(merge_request.participants)
|
||||
|
@ -290,7 +290,7 @@ module API
|
|||
desc 'Get the commits of a merge request' do
|
||||
success Entities::Commit
|
||||
end
|
||||
get ':id/merge_requests/:merge_request_iid/commits', feature_category: :code_review do
|
||||
get ':id/merge_requests/:merge_request_iid/commits', feature_category: :code_review, urgency: :low do
|
||||
merge_request = find_merge_request_with_access(params[:merge_request_iid])
|
||||
|
||||
commits =
|
||||
|
@ -371,7 +371,7 @@ module API
|
|||
desc 'Show the merge request changes' do
|
||||
success Entities::MergeRequestChanges
|
||||
end
|
||||
get ':id/merge_requests/:merge_request_iid/changes', feature_category: :code_review do
|
||||
get ':id/merge_requests/:merge_request_iid/changes', feature_category: :code_review, urgency: :low do
|
||||
merge_request = find_merge_request_with_access(params[:merge_request_iid])
|
||||
|
||||
present merge_request,
|
||||
|
@ -422,7 +422,7 @@ module API
|
|||
use :optional_params
|
||||
at_least_one_of(*::API::MergeRequests.update_params_at_least_one_of)
|
||||
end
|
||||
put ':id/merge_requests/:merge_request_iid', feature_category: :code_review do
|
||||
put ':id/merge_requests/:merge_request_iid', feature_category: :code_review, urgency: :low do
|
||||
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20772')
|
||||
|
||||
merge_request = find_merge_request_with_access(params.delete(:merge_request_iid), :update_merge_request)
|
||||
|
@ -454,7 +454,7 @@ module API
|
|||
optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
|
||||
optional :squash, type: Grape::API::Boolean, desc: 'When true, the commits will be squashed into a single commit on merge'
|
||||
end
|
||||
put ':id/merge_requests/:merge_request_iid/merge', feature_category: :code_review do
|
||||
put ':id/merge_requests/:merge_request_iid/merge', feature_category: :code_review, urgency: :low do
|
||||
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/4796')
|
||||
|
||||
merge_request = find_project_merge_request(params[:merge_request_iid])
|
||||
|
@ -524,7 +524,7 @@ module API
|
|||
params do
|
||||
optional :skip_ci, type: Boolean, desc: 'Do not create CI pipeline'
|
||||
end
|
||||
put ':id/merge_requests/:merge_request_iid/rebase', feature_category: :code_review do
|
||||
put ':id/merge_requests/:merge_request_iid/rebase', feature_category: :code_review, urgency: :low do
|
||||
merge_request = find_project_merge_request(params[:merge_request_iid])
|
||||
|
||||
authorize_push_to_merge_request!(merge_request)
|
||||
|
@ -543,7 +543,7 @@ module API
|
|||
params do
|
||||
use :pagination
|
||||
end
|
||||
get ':id/merge_requests/:merge_request_iid/closes_issues', feature_category: :code_review do
|
||||
get ':id/merge_requests/:merge_request_iid/closes_issues', feature_category: :code_review, urgency: :low do
|
||||
merge_request = find_merge_request_with_access(params[:merge_request_iid])
|
||||
issues = ::Kaminari.paginate_array(merge_request.visible_closing_issues_for(current_user))
|
||||
issues = paginate(issues)
|
||||
|
|
|
@ -7,6 +7,11 @@ module API
|
|||
|
||||
before { authenticate! }
|
||||
|
||||
urgency :low, [
|
||||
'/projects/:id/merge_requests/:noteable_id/notes',
|
||||
'/projects/:id/merge_requests/:noteable_id/notes/:note_id'
|
||||
]
|
||||
|
||||
Helpers::NotesHelpers.feature_category_per_noteable_type.each do |noteable_type, feature_category|
|
||||
parent_type = noteable_type.parent_class.to_s.underscore
|
||||
noteables_str = noteable_type.to_s.underscore.pluralize
|
||||
|
|
|
@ -24,7 +24,7 @@ module API
|
|||
use :pagination
|
||||
end
|
||||
|
||||
get ":id/#{eventables_str}/:eventable_id/resource_label_events", feature_category: feature_category do
|
||||
get ":id/#{eventables_str}/:eventable_id/resource_label_events", feature_category: feature_category, urgency: :low do
|
||||
eventable = find_noteable(eventable_type, params[:eventable_id])
|
||||
|
||||
events = eventable.resource_label_events.inc_relations
|
||||
|
|
|
@ -26,7 +26,7 @@ module API
|
|||
use :pagination
|
||||
end
|
||||
|
||||
get ":id/#{eventables_str}/:eventable_id/resource_milestone_events", feature_category: feature_category do
|
||||
get ":id/#{eventables_str}/:eventable_id/resource_milestone_events", feature_category: feature_category, urgency: :low do
|
||||
eventable = find_noteable(eventable_type, params[:eventable_id])
|
||||
|
||||
events = ResourceMilestoneEventFinder.new(current_user, eventable).execute
|
||||
|
|
|
@ -25,7 +25,7 @@ module API
|
|||
use :pagination
|
||||
end
|
||||
|
||||
get ":id/#{eventable_name.pluralize}/:eventable_iid/resource_state_events", feature_category: feature_category do
|
||||
get ":id/#{eventable_name.pluralize}/:eventable_iid/resource_state_events", feature_category: feature_category, urgency: :low do
|
||||
eventable = find_noteable(eventable_class, params[:eventable_iid])
|
||||
|
||||
events = ResourceStateEventFinder.new(current_user, eventable).execute
|
||||
|
|
|
@ -14,7 +14,7 @@ module API
|
|||
requires :id, type: String, desc: 'The suggestion ID'
|
||||
optional :commit_message, type: String, desc: "A custom commit message to use instead of the default generated message or the project's default message"
|
||||
end
|
||||
put ':id/apply' do
|
||||
put ':id/apply', urgency: :low do
|
||||
suggestion = Suggestion.find_by_id(params[:id])
|
||||
|
||||
if suggestion
|
||||
|
@ -31,7 +31,7 @@ module API
|
|||
requires :ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: "An array of suggestion ID's"
|
||||
optional :commit_message, type: String, desc: "A custom commit message to use instead of the default generated message or the project's default message"
|
||||
end
|
||||
put 'batch_apply' do
|
||||
put 'batch_apply', urgency: :low do
|
||||
ids = params[:ids]
|
||||
|
||||
suggestions = Suggestion.id_in(ids)
|
||||
|
|
|
@ -54,8 +54,7 @@ variables:
|
|||
# KUBE_INGRESS_BASE_DOMAIN: domain.example.com
|
||||
|
||||
# Allows Container-Scanning to correctly correlate image names when using Jobs/Build.gitlab-ci.yml
|
||||
CI_APPLICATION_TAG: $CI_COMMIT_SHA
|
||||
CS_DEFAULT_BRANCH_IMAGE: $CI_REGISTRY_IMAGE/$CI_DEFAULT_BRANCH:$CI_APPLICATION_TAG
|
||||
CS_DEFAULT_BRANCH_IMAGE: $CI_REGISTRY_IMAGE/$CI_DEFAULT_BRANCH:$CI_COMMIT_SHA
|
||||
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: testing-password
|
||||
|
|
|
@ -67,16 +67,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def get_wal_locations(job)
|
||||
job['dedup_wal_locations'] || job['wal_locations'] || legacy_wal_location(job)
|
||||
end
|
||||
|
||||
# Already scheduled jobs could still contain legacy database write location.
|
||||
# TODO: remove this in the next iteration
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/338213
|
||||
def legacy_wal_location(job)
|
||||
wal_location = job['database_write_location'] || job['database_replica_location']
|
||||
|
||||
{ ::Gitlab::Database::MAIN_DATABASE_NAME.to_sym => wal_location } if wal_location
|
||||
job['dedup_wal_locations'] || job['wal_locations']
|
||||
end
|
||||
|
||||
def load_balancing_available?(worker_class)
|
||||
|
|
|
@ -56,7 +56,6 @@ module Gitlab
|
|||
push_frontend_feature_flag(:security_auto_fix, default_enabled: false)
|
||||
push_frontend_feature_flag(:improved_emoji_picker, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:new_header_search, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:configure_iac_scanning_via_mr, current_user, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:bootstrap_confirmation_modals, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
|
|
|
@ -35,7 +35,9 @@ module Gitlab
|
|||
"Closes #{closes_issues_references.to_sentence}"
|
||||
end,
|
||||
'description' => ->(merge_request) { merge_request.description.presence || '' },
|
||||
'reference' => ->(merge_request) { merge_request.to_reference(full: true) }
|
||||
'reference' => ->(merge_request) { merge_request.to_reference(full: true) },
|
||||
'first_commit' => -> (merge_request) { merge_request.first_commit&.safe_message&.strip.presence || '' },
|
||||
'first_multiline_commit' => -> (merge_request) { merge_request.first_multiline_commit&.safe_message&.strip.presence || merge_request.title }
|
||||
}.freeze
|
||||
|
||||
PLACEHOLDERS_REGEX = Regexp.union(PLACEHOLDERS.keys.map do |key|
|
||||
|
|
|
@ -243,7 +243,9 @@ namespace :gitlab do
|
|||
# Only for development environments,
|
||||
# we execute pending data migrations inline for convenience.
|
||||
Rake::Task['db:migrate'].enhance do
|
||||
Rake::Task['gitlab:db:execute_batched_migrations'].invoke if Rails.env.development?
|
||||
if Rails.env.development? && Gitlab::Database::BackgroundMigration::BatchedMigration.table_exists?
|
||||
Rake::Task['gitlab:db:execute_batched_migrations'].invoke
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2036,7 +2036,7 @@ msgstr ""
|
|||
msgid "Add a task list"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add a title…"
|
||||
msgid "Add a title..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Add a to do"
|
||||
|
@ -4769,6 +4769,9 @@ msgstr ""
|
|||
msgid "Assign reviewer(s)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Assign severity"
|
||||
msgstr ""
|
||||
|
||||
msgid "Assign some issues to this milestone."
|
||||
msgstr ""
|
||||
|
||||
|
@ -32840,6 +32843,9 @@ msgstr ""
|
|||
msgid "Something went wrong while updating assignees"
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while updating work item. Please try again"
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while updating your list settings"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -79,14 +79,6 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
application_experiment.publish
|
||||
end
|
||||
|
||||
it "publishes to the database if we've opted for that" do
|
||||
application_experiment.record!
|
||||
|
||||
expect(application_experiment).to receive(:publish_to_database)
|
||||
|
||||
application_experiment.publish
|
||||
end
|
||||
|
||||
context 'when we should not track' do
|
||||
let(:should_track) { false }
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
FactoryBot.define do
|
||||
sequence(:username) { |n| "user#{n}" }
|
||||
sequence(:name) { |n| "John Doe#{n}" }
|
||||
sequence(:name) { |n| "Sidney Jones#{n}" }
|
||||
sequence(:email) { |n| "user#{n}@example.org" }
|
||||
sequence(:email_alias) { |n| "user.alias#{n}@example.org" }
|
||||
sequence(:title) { |n| "My title #{n}" }
|
||||
|
|
|
@ -60,7 +60,7 @@ RSpec.describe 'Alert details', :js do
|
|||
|
||||
expect(alert_status).to have_content('Triggered')
|
||||
|
||||
find('.btn-link').click
|
||||
find('.gl-button').click
|
||||
find('.gl-new-dropdown-item', text: 'Acknowledged').click
|
||||
|
||||
wait_for_requests
|
||||
|
@ -79,7 +79,7 @@ RSpec.describe 'Alert details', :js do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
expect(alert_assignee).to have_content('Assignee Edit John Doe')
|
||||
expect(alert_assignee).to have_content('Assignee Edit Sidney Jones')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,9 +31,10 @@ RSpec.describe "Gitlab::Experiment", :js do
|
|||
|
||||
expect(page).to have_content('Abuse Reports')
|
||||
|
||||
published_experiments = page.evaluate_script('window.gon.experiment')
|
||||
published_experiments = page.evaluate_script('window.gl.experiments')
|
||||
expect(published_experiments).to include({
|
||||
'null_hypothesis' => {
|
||||
'excluded' => false,
|
||||
'experiment' => 'null_hypothesis',
|
||||
'key' => anything,
|
||||
'variant' => 'candidate'
|
||||
|
|
|
@ -180,7 +180,7 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
|
||||
describe 'assignees' do
|
||||
it 'does not wrap with quotes for assignee values' do
|
||||
fill_in 'Comment', with: "@#{user.username[0]}"
|
||||
fill_in 'Comment', with: "@#{user.username}"
|
||||
|
||||
find_highlighted_autocomplete_item.click
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ RSpec.describe 'Pipeline Schedules', :js do
|
|||
click_link 'Take ownership'
|
||||
page.within('.pipeline-schedule-table-row') do
|
||||
expect(page).not_to have_content('No owner')
|
||||
expect(page).to have_link('John Doe')
|
||||
expect(page).to have_link('Sidney Jones')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import * as Sentry from '@sentry/browser';
|
||||
import createFlash, {
|
||||
createAction,
|
||||
hideFlash,
|
||||
removeFlashClickListener,
|
||||
addDismissFlashClickListener,
|
||||
FLASH_TYPES,
|
||||
FLASH_CLOSED_EVENT,
|
||||
} from '~/flash';
|
||||
|
||||
jest.mock('@sentry/browser');
|
||||
|
||||
describe('Flash', () => {
|
||||
describe('hideFlash', () => {
|
||||
let el;
|
||||
|
@ -66,49 +68,6 @@ describe('Flash', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('createAction', () => {
|
||||
let el;
|
||||
|
||||
beforeEach(() => {
|
||||
el = document.createElement('div');
|
||||
});
|
||||
|
||||
it('creates link with href', () => {
|
||||
el.innerHTML = createAction({
|
||||
href: 'testing',
|
||||
title: 'test',
|
||||
});
|
||||
|
||||
expect(el.querySelector('.flash-action').href).toContain('testing');
|
||||
});
|
||||
|
||||
it('uses hash as href when no href is present', () => {
|
||||
el.innerHTML = createAction({
|
||||
title: 'test',
|
||||
});
|
||||
|
||||
expect(el.querySelector('.flash-action').href).toContain('#');
|
||||
});
|
||||
|
||||
it('adds role when no href is present', () => {
|
||||
el.innerHTML = createAction({
|
||||
title: 'test',
|
||||
});
|
||||
|
||||
expect(el.querySelector('.flash-action').getAttribute('role')).toBe('button');
|
||||
});
|
||||
|
||||
it('escapes the title text', () => {
|
||||
el.innerHTML = createAction({
|
||||
title: '<script>alert("a")</script>',
|
||||
});
|
||||
|
||||
expect(el.querySelector('.flash-action').textContent.trim()).toBe(
|
||||
'<script>alert("a")</script>',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFlash', () => {
|
||||
const message = 'test';
|
||||
const fadeTransition = false;
|
||||
|
@ -194,7 +153,26 @@ describe('Flash', () => {
|
|||
expect(document.body.className).not.toContain('flash-shown');
|
||||
});
|
||||
|
||||
it('does not capture error using Sentry', () => {
|
||||
createFlash({ ...defaultParams, captureError: false, error: new Error('Error!') });
|
||||
|
||||
expect(Sentry.captureException).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('captures error using Sentry', () => {
|
||||
createFlash({ ...defaultParams, captureError: true, error: new Error('Error!') });
|
||||
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(expect.any(Error));
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Error!',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('with actionConfig', () => {
|
||||
const findFlashAction = () => document.querySelector('.flash-container .flash-action');
|
||||
|
||||
it('adds action link', () => {
|
||||
createFlash({
|
||||
...defaultParams,
|
||||
|
@ -203,20 +181,69 @@ describe('Flash', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(document.querySelector('.flash-action')).not.toBeNull();
|
||||
expect(findFlashAction()).not.toBeNull();
|
||||
});
|
||||
|
||||
it('creates link with href', () => {
|
||||
createFlash({
|
||||
...defaultParams,
|
||||
actionConfig: {
|
||||
href: 'testing',
|
||||
title: 'test',
|
||||
},
|
||||
});
|
||||
|
||||
expect(findFlashAction().href).toBe(`${window.location}testing`);
|
||||
expect(findFlashAction().textContent.trim()).toBe('test');
|
||||
});
|
||||
|
||||
it('uses hash as href when no href is present', () => {
|
||||
createFlash({
|
||||
...defaultParams,
|
||||
actionConfig: {
|
||||
title: 'test',
|
||||
},
|
||||
});
|
||||
|
||||
expect(findFlashAction().href).toBe(`${window.location}#`);
|
||||
});
|
||||
|
||||
it('adds role when no href is present', () => {
|
||||
createFlash({
|
||||
...defaultParams,
|
||||
actionConfig: {
|
||||
title: 'test',
|
||||
},
|
||||
});
|
||||
|
||||
expect(findFlashAction().getAttribute('role')).toBe('button');
|
||||
});
|
||||
|
||||
it('escapes the title text', () => {
|
||||
createFlash({
|
||||
...defaultParams,
|
||||
actionConfig: {
|
||||
title: '<script>alert("a")</script>',
|
||||
},
|
||||
});
|
||||
|
||||
expect(findFlashAction().textContent.trim()).toBe('<script>alert("a")</script>');
|
||||
});
|
||||
|
||||
it('calls actionConfig clickHandler on click', () => {
|
||||
const actionConfig = {
|
||||
title: 'test',
|
||||
clickHandler: jest.fn(),
|
||||
};
|
||||
const clickHandler = jest.fn();
|
||||
|
||||
createFlash({ ...defaultParams, actionConfig });
|
||||
createFlash({
|
||||
...defaultParams,
|
||||
actionConfig: {
|
||||
title: 'test',
|
||||
clickHandler,
|
||||
},
|
||||
});
|
||||
|
||||
document.querySelector('.flash-action').click();
|
||||
findFlashAction().click();
|
||||
|
||||
expect(actionConfig.clickHandler).toHaveBeenCalled();
|
||||
expect(clickHandler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -236,7 +263,7 @@ describe('Flash', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('removeFlashClickListener', () => {
|
||||
describe('addDismissFlashClickListener', () => {
|
||||
let el;
|
||||
|
||||
describe('with close icon', () => {
|
||||
|
@ -252,7 +279,7 @@ describe('Flash', () => {
|
|||
});
|
||||
|
||||
it('removes global flash on click', (done) => {
|
||||
removeFlashClickListener(el, false);
|
||||
addDismissFlashClickListener(el, false);
|
||||
|
||||
el.querySelector('.js-close-icon').click();
|
||||
|
||||
|
@ -276,7 +303,7 @@ describe('Flash', () => {
|
|||
});
|
||||
|
||||
it('does not throw', () => {
|
||||
expect(() => removeFlashClickListener(el, false)).not.toThrow();
|
||||
expect(() => addDismissFlashClickListener(el, false)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { getByText } from '@testing-library/dom';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
|
||||
import Issue from '~/issue';
|
||||
import Issue from '~/issues/issue';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
describe('Issue', () => {
|
|
@ -318,8 +318,14 @@ describe('Blob content viewer component', () => {
|
|||
repository: { empty },
|
||||
} = projectMock;
|
||||
|
||||
afterEach(() => {
|
||||
delete gon.current_user_id;
|
||||
delete gon.current_username;
|
||||
});
|
||||
|
||||
it('renders component', async () => {
|
||||
window.gon.current_user_id = 1;
|
||||
window.gon.current_username = 'root';
|
||||
|
||||
await createComponent({ pushCode, downloadCode, empty }, mount);
|
||||
|
||||
|
@ -330,28 +336,34 @@ describe('Blob content viewer component', () => {
|
|||
deletePath: webPath,
|
||||
canPushCode: pushCode,
|
||||
canLock: true,
|
||||
isLocked: false,
|
||||
isLocked: true,
|
||||
emptyRepo: empty,
|
||||
});
|
||||
});
|
||||
|
||||
it.each`
|
||||
canPushCode | canDownloadCode | canLock
|
||||
${true} | ${true} | ${true}
|
||||
${false} | ${true} | ${false}
|
||||
${true} | ${false} | ${false}
|
||||
`('passes the correct lock states', async ({ canPushCode, canDownloadCode, canLock }) => {
|
||||
await createComponent(
|
||||
{
|
||||
pushCode: canPushCode,
|
||||
downloadCode: canDownloadCode,
|
||||
empty,
|
||||
},
|
||||
mount,
|
||||
);
|
||||
canPushCode | canDownloadCode | username | canLock
|
||||
${true} | ${true} | ${'root'} | ${true}
|
||||
${false} | ${true} | ${'root'} | ${false}
|
||||
${true} | ${false} | ${'root'} | ${false}
|
||||
${true} | ${true} | ${'peter'} | ${false}
|
||||
`(
|
||||
'passes the correct lock states',
|
||||
async ({ canPushCode, canDownloadCode, username, canLock }) => {
|
||||
gon.current_username = username;
|
||||
|
||||
expect(findBlobButtonGroup().props('canLock')).toBe(canLock);
|
||||
});
|
||||
await createComponent(
|
||||
{
|
||||
pushCode: canPushCode,
|
||||
downloadCode: canDownloadCode,
|
||||
empty,
|
||||
},
|
||||
mount,
|
||||
);
|
||||
|
||||
expect(findBlobButtonGroup().props('canLock')).toBe(canLock);
|
||||
},
|
||||
);
|
||||
|
||||
it('does not render if not logged in', async () => {
|
||||
isLoggedIn.mockReturnValueOnce(false);
|
||||
|
|
|
@ -47,7 +47,13 @@ export const projectMock = {
|
|||
id: '1234',
|
||||
userPermissions: userPermissionsMock,
|
||||
pathLocks: {
|
||||
nodes: [],
|
||||
nodes: [
|
||||
{
|
||||
id: 'test',
|
||||
path: simpleViewerMock.path,
|
||||
user: { id: '123', username: 'root' },
|
||||
},
|
||||
],
|
||||
},
|
||||
repository: {
|
||||
empty: false,
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { escape } from 'lodash';
|
||||
import ItemTitle from '~/work_items/components/item_title.vue';
|
||||
|
||||
jest.mock('lodash/escape', () => jest.fn((fn) => fn));
|
||||
|
||||
const createComponent = ({ initialTitle = 'Sample title', disabled = false } = {}) =>
|
||||
shallowMount(ItemTitle, {
|
||||
propsData: {
|
||||
initialTitle,
|
||||
disabled,
|
||||
},
|
||||
});
|
||||
|
||||
describe('ItemTitle', () => {
|
||||
let wrapper;
|
||||
const mockUpdatedTitle = 'Updated title';
|
||||
const findInputEl = () => wrapper.find('span#item-title');
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders title contents', () => {
|
||||
expect(findInputEl().attributes()).toMatchObject({
|
||||
'data-placeholder': 'Add a title...',
|
||||
contenteditable: 'true',
|
||||
});
|
||||
expect(findInputEl().text()).toBe('Sample title');
|
||||
});
|
||||
|
||||
it('renders title contents with editing disabled', () => {
|
||||
wrapper = createComponent({
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
expect(wrapper.classes()).toContain('gl-cursor-not-allowed');
|
||||
expect(findInputEl().attributes('contenteditable')).toBe('false');
|
||||
});
|
||||
|
||||
it.each`
|
||||
eventName | sourceEvent
|
||||
${'title-changed'} | ${'blur'}
|
||||
${'title-input'} | ${'keyup'}
|
||||
`('emits "$eventName" event on input $sourceEvent', async ({ eventName, sourceEvent }) => {
|
||||
findInputEl().element.innerText = mockUpdatedTitle;
|
||||
await findInputEl().trigger(sourceEvent);
|
||||
|
||||
expect(wrapper.emitted(eventName)).toBeTruthy();
|
||||
expect(escape).toHaveBeenCalledWith(mockUpdatedTitle);
|
||||
});
|
||||
});
|
|
@ -15,3 +15,22 @@ export const workItemQueryResponse = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const updateWorkItemMutationResponse = {
|
||||
__typename: 'UpdateWorkItemPayload',
|
||||
workItem: {
|
||||
__typename: 'WorkItem',
|
||||
id: '1',
|
||||
widgets: {
|
||||
__typename: 'WorkItemWidgetConnection',
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'TitleWidget',
|
||||
type: 'TITLE',
|
||||
enabled: true,
|
||||
contentText: 'Updated title',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@ import { shallowMount } from '@vue/test-utils';
|
|||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
|
||||
import ItemTitle from '~/work_items/components/item_title.vue';
|
||||
import { resolvers } from '~/work_items/graphql/resolvers';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
@ -14,9 +15,9 @@ describe('Create work item component', () => {
|
|||
let fakeApollo;
|
||||
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findTitleInput = () => wrapper.findComponent(ItemTitle);
|
||||
const findCreateButton = () => wrapper.find('[data-testid="create-button"]');
|
||||
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
|
||||
const findTitleInput = () => wrapper.find('[data-testid="title-input"]');
|
||||
|
||||
const createComponent = ({ data = {} } = {}) => {
|
||||
fakeApollo = createMockApollo([], resolvers);
|
||||
|
@ -70,9 +71,10 @@ describe('Create work item component', () => {
|
|||
});
|
||||
|
||||
describe('when title input field has a text', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
const mockTitle = 'Test title';
|
||||
createComponent();
|
||||
findTitleInput().setValue('Test title');
|
||||
await findTitleInput().vm.$emit('title-input', mockTitle);
|
||||
});
|
||||
|
||||
it('renders a non-disabled Create button', () => {
|
||||
|
|
|
@ -2,8 +2,12 @@ import Vue from 'vue';
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
|
||||
import ItemTitle from '~/work_items/components/item_title.vue';
|
||||
import { resolvers } from '~/work_items/graphql/resolvers';
|
||||
import { workItemQueryResponse } from '../mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
@ -14,10 +18,10 @@ describe('Work items root component', () => {
|
|||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
const findTitle = () => wrapper.find('[data-testid="title"]');
|
||||
const findTitle = () => wrapper.findComponent(ItemTitle);
|
||||
|
||||
const createComponent = ({ queryResponse = workItemQueryResponse } = {}) => {
|
||||
fakeApollo = createMockApollo();
|
||||
fakeApollo = createMockApollo([], resolvers);
|
||||
fakeApollo.clients.defaultClient.cache.writeQuery({
|
||||
query: workItemQuery,
|
||||
variables: {
|
||||
|
@ -43,7 +47,28 @@ describe('Work items root component', () => {
|
|||
createComponent();
|
||||
|
||||
expect(findTitle().exists()).toBe(true);
|
||||
expect(findTitle().text()).toBe('Test');
|
||||
expect(findTitle().props('initialTitle')).toBe('Test');
|
||||
});
|
||||
|
||||
it('updates the title when it is edited', async () => {
|
||||
createComponent();
|
||||
jest.spyOn(wrapper.vm.$apollo, 'mutate');
|
||||
const mockUpdatedTitle = 'Updated title';
|
||||
|
||||
await findTitle().vm.$emit('title-changed', mockUpdatedTitle);
|
||||
|
||||
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
|
||||
mutation: updateWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: WORK_ITEM_ID,
|
||||
title: mockUpdatedTitle,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
expect(findTitle().props('initialTitle')).toBe(mockUpdatedTitle);
|
||||
});
|
||||
|
||||
it('does not render the title if title is not in the widgets list', () => {
|
||||
|
|
|
@ -395,4 +395,170 @@ RSpec.describe AuthHelper do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#auth_strategy_class' do
|
||||
subject(:auth_strategy_class) { helper.auth_strategy_class(name) }
|
||||
|
||||
context 'when configuration specifies no provider' do
|
||||
let(:name) { 'does_not_exist' }
|
||||
|
||||
before do
|
||||
allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(auth_strategy_class).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when configuration specifies a provider with args but without strategy_class' do
|
||||
let(:name) { 'google_oauth2' }
|
||||
let(:provider) do
|
||||
Struct.new(:name, :args).new(
|
||||
name,
|
||||
'app_id' => 'YOUR_APP_ID'
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(auth_strategy_class).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when configuration specifies a provider with args and strategy_class' do
|
||||
let(:name) { 'provider1' }
|
||||
let(:strategy) { 'OmniAuth::Strategies::LDAP' }
|
||||
let(:provider) do
|
||||
Struct.new(:name, :args).new(
|
||||
name,
|
||||
'strategy_class' => strategy
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
|
||||
end
|
||||
|
||||
it 'returns the class' do
|
||||
expect(auth_strategy_class).to eq(strategy)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when configuration specifies another provider with args and another strategy_class' do
|
||||
let(:name) { 'provider1' }
|
||||
let(:strategy) { 'OmniAuth::Strategies::LDAP' }
|
||||
let(:provider) do
|
||||
Struct.new(:name, :args).new(
|
||||
'another_name',
|
||||
'strategy_class' => strategy
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(auth_strategy_class).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#saml_providers' do
|
||||
subject(:saml_providers) { helper.saml_providers }
|
||||
|
||||
let(:saml_strategy) { 'OmniAuth::Strategies::SAML' }
|
||||
|
||||
let(:saml_provider_1_name) { 'saml_provider_1' }
|
||||
let(:saml_provider_1) do
|
||||
Struct.new(:name, :args).new(
|
||||
saml_provider_1_name,
|
||||
'strategy_class' => saml_strategy
|
||||
)
|
||||
end
|
||||
|
||||
let(:saml_provider_2_name) { 'saml_provider_2' }
|
||||
let(:saml_provider_2) do
|
||||
Struct.new(:name, :args).new(
|
||||
saml_provider_2_name,
|
||||
'strategy_class' => saml_strategy
|
||||
)
|
||||
end
|
||||
|
||||
let(:ldap_provider_name) { 'ldap_provider' }
|
||||
let(:ldap_strategy) { 'OmniAuth::Strategies::LDAP' }
|
||||
let(:ldap_provider) do
|
||||
Struct.new(:name, :args).new(
|
||||
ldap_provider_name,
|
||||
'strategy_class' => ldap_strategy
|
||||
)
|
||||
end
|
||||
|
||||
let(:google_oauth2_provider_name) { 'google_oauth2' }
|
||||
let(:google_oauth2_provider) do
|
||||
Struct.new(:name, :args).new(
|
||||
google_oauth2_provider_name,
|
||||
'app_id' => 'YOUR_APP_ID'
|
||||
)
|
||||
end
|
||||
|
||||
context 'when configuration specifies no provider' do
|
||||
before do
|
||||
allow(Devise).to receive(:omniauth_providers).and_return([])
|
||||
allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
|
||||
end
|
||||
|
||||
it 'returns an empty list' do
|
||||
expect(saml_providers).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when configuration specifies a provider with a SAML strategy_class' do
|
||||
before do
|
||||
allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name])
|
||||
allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1])
|
||||
end
|
||||
|
||||
it 'returns the provider' do
|
||||
expect(saml_providers).to match_array([saml_provider_1_name])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when configuration specifies two providers with a SAML strategy_class' do
|
||||
before do
|
||||
allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name, saml_provider_2_name])
|
||||
allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1, saml_provider_2])
|
||||
end
|
||||
|
||||
it 'returns the provider' do
|
||||
expect(saml_providers).to match_array([saml_provider_1_name, saml_provider_2_name])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when configuration specifies a provider with a non-SAML strategy_class' do
|
||||
before do
|
||||
allow(Devise).to receive(:omniauth_providers).and_return([ldap_provider_name])
|
||||
allow(Gitlab.config.omniauth).to receive(:providers).and_return([ldap_provider])
|
||||
end
|
||||
|
||||
it 'returns an empty list' do
|
||||
expect(saml_providers).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when configuration specifies four providers but only two with SAML strategy_class' do
|
||||
before do
|
||||
allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name, ldap_provider_name, saml_provider_2_name, google_oauth2_provider_name])
|
||||
allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1, ldap_provider, saml_provider_2, google_oauth2_provider])
|
||||
end
|
||||
|
||||
it 'returns the provider' do
|
||||
expect(saml_providers).to match_array([saml_provider_1_name, saml_provider_2_name])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,9 @@ require 'spec_helper'
|
|||
RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_gitlab_redis_queues do
|
||||
let(:middleware) { described_class.new }
|
||||
let(:worker) { worker_class.new }
|
||||
let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'database_replica_location' => '0/D525E3A8' } }
|
||||
let(:location) {'0/D525E3A8' }
|
||||
let(:wal_locations) { { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => location } }
|
||||
let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'wal_locations' => wal_locations } }
|
||||
|
||||
before do
|
||||
skip_feature_flags_yaml_validation
|
||||
|
@ -60,9 +62,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_
|
|||
end
|
||||
|
||||
shared_examples_for 'replica is up to date' do |expected_strategy|
|
||||
let(:location) {'0/D525E3A8' }
|
||||
let(:wal_locations) { { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => location } }
|
||||
|
||||
it 'does not stick to the primary', :aggregate_failures do
|
||||
expect(ActiveRecord::Base.load_balancer)
|
||||
.to receive(:select_up_to_date_host)
|
||||
|
@ -114,19 +113,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_
|
|||
it_behaves_like 'replica is up to date', 'replica'
|
||||
end
|
||||
|
||||
context 'when legacy wal location is set' do
|
||||
let(:job) { { 'job_id' => 'a180b47c-3fd6-41b8-81e9-34da61c3400e', 'database_write_location' => '0/D525E3A8' } }
|
||||
|
||||
before do
|
||||
allow(ActiveRecord::Base.load_balancer)
|
||||
.to receive(:select_up_to_date_host)
|
||||
.with('0/D525E3A8')
|
||||
.and_return(true)
|
||||
end
|
||||
|
||||
it_behaves_like 'replica is up to date', 'replica'
|
||||
end
|
||||
|
||||
context 'when database location is not set' do
|
||||
let(:job) { { 'job_id' => 'a180b47c-3fd6-41b8-81e9-34da61c3400e' } }
|
||||
|
||||
|
@ -146,7 +132,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_
|
|||
end
|
||||
|
||||
context 'when WAL locations are present', :freeze_time do
|
||||
let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", "database_replica_location" => "0/D525E3A8", "created_at" => Time.current.to_f - elapsed_time } }
|
||||
let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'wal_locations' => wal_locations, "created_at" => Time.current.to_f - elapsed_time } }
|
||||
|
||||
context 'when delay interval has not elapsed' do
|
||||
let(:elapsed_time) { described_class::MINIMUM_DELAY_INTERVAL - 0.3 }
|
||||
|
@ -192,7 +178,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_
|
|||
include_examples 'stick to the primary', 'primary'
|
||||
|
||||
context 'when delay interval has not elapsed', :freeze_time do
|
||||
let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'database_replica_location' => '0/D525E3A8', "created_at" => Time.current.to_f - elapsed_time } }
|
||||
let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'wal_locations' => wal_locations, "created_at" => Time.current.to_f - elapsed_time } }
|
||||
let(:elapsed_time) { described_class::MINIMUM_DELAY_INTERVAL - 0.3 }
|
||||
|
||||
it 'does not sleep' do
|
||||
|
@ -235,7 +221,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_
|
|||
end
|
||||
|
||||
context 'when job is retried' do
|
||||
let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'database_replica_location' => '0/D525E3A8', 'retry_count' => 0 } }
|
||||
let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'wal_locations' => wal_locations, 'retry_count' => 0 } }
|
||||
|
||||
context 'and replica still lagging behind' do
|
||||
include_examples 'stick to the primary', 'primary'
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue