Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5ffb2b7bcd
commit
02f6aecd47
|
|
@ -68,7 +68,7 @@ review-gcp-quotas-checks:
|
|||
script:
|
||||
- ruby scripts/review_apps/gcp-quotas-checks.rb || (scripts/slack review-apps-monitoring "☠️ \`${CI_JOB_NAME}\` failed! ☠️ See ${CI_JOB_URL} - <https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/blob/main/runbooks/review-apps.md#review-gcp-quotas-checks-job-failed|📗 RUNBOOK 📕>" warning "GitLab Bot" && exit 1);
|
||||
|
||||
.start-review-app-pipeline:
|
||||
start-review-app-pipeline:
|
||||
extends:
|
||||
- .review:rules:start-review-app-pipeline
|
||||
resource_group: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # CI_ENVIRONMENT_SLUG is not available here and we want this to be the same as the environment
|
||||
|
|
|
|||
|
|
@ -2562,6 +2562,8 @@
|
|||
when: never
|
||||
- <<: *if-merge-request-labels-pipeline-expedite
|
||||
when: never
|
||||
- if: '$CI_REVIEW_APPS_ENABLED != "true"'
|
||||
when: never
|
||||
- <<: *if-merge-request-labels-run-review-app
|
||||
- <<: *if-dot-com-gitlab-org-merge-request
|
||||
changes: *ci-review-patterns
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
4.3.8
|
||||
4.3.9
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import {
|
||||
GlDisclosureDropdown,
|
||||
GlTooltip,
|
||||
GlTooltipDirective,
|
||||
GlDisclosureDropdownGroup,
|
||||
GlDisclosureDropdownItem,
|
||||
} from '@gitlab/ui';
|
||||
|
|
@ -22,9 +22,11 @@ export default {
|
|||
GlDisclosureDropdown,
|
||||
GlDisclosureDropdownGroup,
|
||||
GlDisclosureDropdownItem,
|
||||
GlTooltip,
|
||||
InviteMembersTrigger,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
i18n: {
|
||||
createNew: __('Create new...'),
|
||||
},
|
||||
|
|
@ -59,45 +61,37 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<gl-disclosure-dropdown
|
||||
category="tertiary"
|
||||
icon="plus"
|
||||
no-caret
|
||||
text-sr-only
|
||||
:toggle-text="$options.i18n.createNew"
|
||||
:toggle-id="$options.toggleId"
|
||||
:dropdown-offset="dropdownOffset"
|
||||
data-qa-selector="new_menu_toggle"
|
||||
data-testid="new-menu-toggle"
|
||||
@shown="dropdownOpen = true"
|
||||
@hidden="dropdownOpen = false"
|
||||
<gl-disclosure-dropdown
|
||||
v-gl-tooltip:super-sidebar.hover.noninteractive.ds500.bottom="
|
||||
dropdownOpen ? '' : $options.i18n.createNew
|
||||
"
|
||||
category="tertiary"
|
||||
icon="plus"
|
||||
no-caret
|
||||
text-sr-only
|
||||
:toggle-text="$options.i18n.createNew"
|
||||
:toggle-id="$options.toggleId"
|
||||
:dropdown-offset="dropdownOffset"
|
||||
data-qa-selector="new_menu_toggle"
|
||||
data-testid="new-menu-toggle"
|
||||
@shown="dropdownOpen = true"
|
||||
@hidden="dropdownOpen = false"
|
||||
>
|
||||
<gl-disclosure-dropdown-group
|
||||
v-for="(group, index) in groups"
|
||||
:key="group.name"
|
||||
:bordered="index !== 0"
|
||||
:group="group"
|
||||
>
|
||||
<gl-disclosure-dropdown-group
|
||||
v-for="(group, index) in groups"
|
||||
:key="group.name"
|
||||
:bordered="index !== 0"
|
||||
:group="group"
|
||||
>
|
||||
<template v-for="groupItem in group.items">
|
||||
<invite-members-trigger
|
||||
v-if="isInvitedMembers(groupItem)"
|
||||
:key="`${groupItem.text}-trigger`"
|
||||
trigger-source="top_nav"
|
||||
:trigger-element="$options.TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN"
|
||||
/>
|
||||
<gl-disclosure-dropdown-item v-else :key="groupItem.text" :item="groupItem" />
|
||||
</template>
|
||||
</gl-disclosure-dropdown-group>
|
||||
</gl-disclosure-dropdown>
|
||||
<gl-tooltip
|
||||
v-if="!dropdownOpen"
|
||||
:target="`#${$options.toggleId}`"
|
||||
placement="bottom"
|
||||
container="#super-sidebar"
|
||||
noninteractive
|
||||
>
|
||||
{{ $options.i18n.createNew }}
|
||||
</gl-tooltip>
|
||||
</div>
|
||||
<template v-for="groupItem in group.items">
|
||||
<invite-members-trigger
|
||||
v-if="isInvitedMembers(groupItem)"
|
||||
:key="`${groupItem.text}-trigger`"
|
||||
trigger-source="top_nav"
|
||||
:trigger-element="$options.TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN"
|
||||
/>
|
||||
<gl-disclosure-dropdown-item v-else :key="groupItem.text" :item="groupItem" />
|
||||
</template>
|
||||
</gl-disclosure-dropdown-group>
|
||||
</gl-disclosure-dropdown>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,23 @@
|
|||
import { computePosition, autoUpdate, offset, flip, shift } from '@floating-ui/dom';
|
||||
import NavItem from './nav_item.vue';
|
||||
|
||||
// Flyout menus are shown when the MenuSection's title is hovered with the mouse.
|
||||
// Their position is dynamically calculated with floating-ui.
|
||||
//
|
||||
// Since flyout menus show all NavItems of a section, they can be very long and
|
||||
// a user might want to move their mouse diagonally from the section title down
|
||||
// to last nav item in the flyout. But this mouse movement over other sections
|
||||
// would loose hover and close the flyout, opening another section's flyout.
|
||||
// To avoid this annoyance, our flyouts come with a "diagonal tolerance". This
|
||||
// is an area between the current mouse position and the top- and bottom-left
|
||||
// corner of the flyout itself. While the mouse stays within this area and
|
||||
// reaches the flyout before a timer expires, the native browser hover stays
|
||||
// within the component.
|
||||
// This is done with an transparent SVG positioned left of the flyout menu,
|
||||
// overlapping the sidebar. The SVG itself ignores pointer events but its two
|
||||
// triangles, one above the section title, one below, do listen to events,
|
||||
// keeping hover.
|
||||
|
||||
export default {
|
||||
name: 'FlyoutMenu',
|
||||
components: { NavItem },
|
||||
|
|
@ -15,13 +32,45 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentMouseX: 0,
|
||||
flyoutX: 0,
|
||||
flyoutY: 0,
|
||||
flyoutHeight: 0,
|
||||
hoverTimeoutId: null,
|
||||
showSVG: true,
|
||||
targetRect: null,
|
||||
};
|
||||
},
|
||||
cleanupFunction: undefined,
|
||||
computed: {
|
||||
topSVGPoints() {
|
||||
const x = (this.currentMouseX / this.targetRect.width) * 100;
|
||||
let y = ((this.targetRect.top - this.flyoutY) / this.flyoutHeight) * 100;
|
||||
y += 1; // overlap title to not loose hover
|
||||
|
||||
return `${x}, ${y} 100, 0 100, ${y}`;
|
||||
},
|
||||
bottomSVGPoints() {
|
||||
const x = (this.currentMouseX / this.targetRect.width) * 100;
|
||||
let y = ((this.targetRect.bottom - this.flyoutY) / this.flyoutHeight) * 100;
|
||||
y -= 1; // overlap title to not loose hover
|
||||
|
||||
return `${x}, ${y} 100, ${y} 100, 100`;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const target = document.querySelector(`#${this.targetId}`);
|
||||
target.addEventListener('mousemove', this.onMouseMove);
|
||||
},
|
||||
mounted() {
|
||||
const target = document.querySelector(`#${this.targetId}`);
|
||||
const flyout = document.querySelector(`#${this.targetId}-flyout`);
|
||||
const sidebar = document.querySelector('#super-sidebar');
|
||||
|
||||
function updatePosition() {
|
||||
return computePosition(target, flyout, {
|
||||
const updatePosition = () =>
|
||||
computePosition(target, flyout, {
|
||||
middleware: [offset({ alignmentAxis: -12 }), flip(), shift()],
|
||||
placement: 'right-start',
|
||||
strategy: 'fixed',
|
||||
|
|
@ -30,13 +79,46 @@ export default {
|
|||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
});
|
||||
this.flyoutX = x;
|
||||
this.flyoutY = y;
|
||||
this.flyoutHeight = flyout.clientHeight;
|
||||
|
||||
// Flyout coordinates are relative to the sidebar which can be
|
||||
// shifted down by the performance-bar etc.
|
||||
// Adjust viewport coordinates from getBoundingClientRect:
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
const sidebarRect = sidebar.getBoundingClientRect();
|
||||
this.targetRect = {
|
||||
top: targetRect.top - sidebarRect.top,
|
||||
bottom: targetRect.bottom - sidebarRect.top,
|
||||
width: targetRect.width,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
this.$options.cleanupFunction = autoUpdate(target, flyout, updatePosition);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.$options.cleanupFunction();
|
||||
clearTimeout(this.hoverTimeoutId);
|
||||
},
|
||||
beforeDestroy() {
|
||||
const target = document.querySelector(`#${this.targetId}`);
|
||||
target.removeEventListener('mousemove', this.onMouseMove);
|
||||
},
|
||||
methods: {
|
||||
startHoverTimeout() {
|
||||
this.hoverTimeoutId = setTimeout(() => {
|
||||
this.showSVG = false;
|
||||
this.$emit('mouseleave');
|
||||
}, 1000);
|
||||
},
|
||||
stopHoverTimeout() {
|
||||
clearTimeout(this.hoverTimeoutId);
|
||||
},
|
||||
onMouseMove(e) {
|
||||
// add some wiggle room to the left of mouse cursor
|
||||
this.currentMouseX = Math.max(0, e.clientX - 20);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -49,8 +131,8 @@ export default {
|
|||
@mouseleave="$emit('mouseleave')"
|
||||
>
|
||||
<ul
|
||||
v-if="items.length > 0"
|
||||
class="gl-min-w-20 gl-max-w-34 gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-100 gl-shadow-md gl-bg-white gl-p-2 gl-pb-1 gl-list-style-none"
|
||||
@mouseenter="showSVG = false"
|
||||
>
|
||||
<nav-item
|
||||
v-for="item of items"
|
||||
|
|
@ -61,5 +143,44 @@ export default {
|
|||
@pin-remove="(itemId) => $emit('pin-remove', itemId)"
|
||||
/>
|
||||
</ul>
|
||||
<svg
|
||||
v-if="targetRect && showSVG"
|
||||
:width="flyoutX"
|
||||
:height="flyoutHeight"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
:style="{
|
||||
top: flyoutY + 'px',
|
||||
}"
|
||||
>
|
||||
<polygon
|
||||
ref="topSVG"
|
||||
:points="topSVGPoints"
|
||||
fill="transparent"
|
||||
@mouseenter="startHoverTimeout"
|
||||
@mouseleave="stopHoverTimeout"
|
||||
/>
|
||||
<polygon
|
||||
ref="bottomSVG"
|
||||
:points="bottomSVGPoints"
|
||||
fill="transparent"
|
||||
@mouseenter="startHoverTimeout"
|
||||
@mouseleave="stopHoverTimeout"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
svg {
|
||||
pointer-events: none;
|
||||
|
||||
position: fixed;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
svg polygon,
|
||||
svg rect {
|
||||
pointer-events: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -79,15 +79,26 @@ export default {
|
|||
isExpanded(newIsExpanded) {
|
||||
this.$emit('collapse-toggle', newIsExpanded);
|
||||
this.keepFlyoutClosed = !this.newIsExpanded;
|
||||
if (!newIsExpanded) {
|
||||
this.isMouseOverFlyout = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handlePointerover(e) {
|
||||
if (!this.hasFlyout) return;
|
||||
|
||||
this.isMouseOverSection = e.pointerType === 'mouse';
|
||||
},
|
||||
handlePointerleave() {
|
||||
this.isMouseOverSection = false;
|
||||
if (!this.hasFlyout) return;
|
||||
|
||||
this.keepFlyoutClosed = false;
|
||||
// delay state change. otherwise the flyout menu gets removed before it
|
||||
// has a chance to emit its mouseover event.
|
||||
setTimeout(() => {
|
||||
this.isMouseOverSection = false;
|
||||
}, 5);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -129,8 +140,7 @@ export default {
|
|||
</button>
|
||||
|
||||
<flyout-menu
|
||||
v-if="hasFlyout"
|
||||
v-show="isMouseOver && !isExpanded && !keepFlyoutClosed"
|
||||
v-if="hasFlyout && isMouseOver && !isExpanded && !keepFlyoutClosed && item.items.length > 0"
|
||||
:target-id="`menu-section-button-${itemId}`"
|
||||
:items="item.items"
|
||||
@mouseover="isMouseOverFlyout = true"
|
||||
|
|
|
|||
|
|
@ -127,7 +127,10 @@ export default {
|
|||
tooltip-container="super-sidebar"
|
||||
data-testid="super-sidebar-collapse-button"
|
||||
/>
|
||||
<create-menu v-if="sidebarData.is_logged_in" :groups="sidebarData.create_new_menu_groups" />
|
||||
<create-menu
|
||||
v-if="sidebarData.is_logged_in && sidebarData.create_new_menu_groups.length > 0"
|
||||
:groups="sidebarData.create_new_menu_groups"
|
||||
/>
|
||||
|
||||
<user-menu v-if="sidebarData.is_logged_in" :data="sidebarData" />
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ module SearchRateLimitable
|
|||
# scopes to get counts, we apply rate limits on the search scope if it is present.
|
||||
#
|
||||
# If abusive search is detected, we have stricter limits and ignore the search scope.
|
||||
check_rate_limit!(:search_rate_limit, scope: [current_user, safe_search_scope].compact)
|
||||
check_rate_limit!(:search_rate_limit, scope: [current_user, safe_search_scope].compact,
|
||||
users_allowlist: Gitlab::CurrentSettings.current_application_settings.search_rate_limit_allowlist)
|
||||
else
|
||||
check_rate_limit!(:search_rate_limit_unauthenticated, scope: [request.ip])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -486,6 +486,7 @@ module ApplicationSettingsHelper
|
|||
:suggest_pipeline_enabled,
|
||||
:search_rate_limit,
|
||||
:search_rate_limit_unauthenticated,
|
||||
:search_rate_limit_allowlist_raw,
|
||||
:users_get_by_id_limit,
|
||||
:users_get_by_id_limit_allowlist_raw,
|
||||
:runner_token_expiration_interval,
|
||||
|
|
|
|||
|
|
@ -646,6 +646,10 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
|
|||
validates :gitlab_shell_operation_limit
|
||||
end
|
||||
|
||||
validates :search_rate_limit_allowlist,
|
||||
length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
|
||||
allow_nil: false
|
||||
|
||||
validates :notes_create_limit_allowlist,
|
||||
length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
|
||||
allow_nil: false
|
||||
|
|
|
|||
|
|
@ -255,6 +255,7 @@ module ApplicationSettingImplementation
|
|||
user_deactivation_emails_enabled: true,
|
||||
search_rate_limit: 30,
|
||||
search_rate_limit_unauthenticated: 10,
|
||||
search_rate_limit_allowlist: [],
|
||||
users_get_by_id_limit: 300,
|
||||
users_get_by_id_limit_allowlist: [],
|
||||
can_create_group: true,
|
||||
|
|
@ -397,6 +398,14 @@ module ApplicationSettingImplementation
|
|||
self.users_get_by_id_limit_allowlist = strings_to_array(values).map(&:downcase)
|
||||
end
|
||||
|
||||
def search_rate_limit_allowlist_raw
|
||||
array_to_string(search_rate_limit_allowlist)
|
||||
end
|
||||
|
||||
def search_rate_limit_allowlist_raw=(values)
|
||||
self.search_rate_limit_allowlist = strings_to_array(values).map(&:downcase)
|
||||
end
|
||||
|
||||
def asset_proxy_whitelist=(values)
|
||||
values = strings_to_array(values) if values.is_a?(String)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ module Releases
|
|||
return error(_('Access Denied'), 403) unless allowed?
|
||||
|
||||
if release.destroy
|
||||
update_catalog_resource!
|
||||
|
||||
success(tag: existing_tag, release: release)
|
||||
else
|
||||
error(release.errors.messages || '400 Bad request', 400)
|
||||
|
|
@ -15,6 +17,14 @@ module Releases
|
|||
|
||||
private
|
||||
|
||||
def update_catalog_resource!
|
||||
return unless project.catalog_resource
|
||||
|
||||
return unless project.catalog_resource.versions.none?
|
||||
|
||||
project.catalog_resource.update!(state: 'draft')
|
||||
end
|
||||
|
||||
def allowed?
|
||||
Ability.allowed?(current_user, :destroy_release, release)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,5 +12,11 @@
|
|||
= f.label :search_rate_limit_unauthenticated, _('Maximum number of requests per minute for an unauthenticated IP address'), class: 'label-bold'
|
||||
= f.number_field :search_rate_limit_unauthenticated, class: 'form-control gl-form-input'
|
||||
|
||||
.form-group
|
||||
= f.label :search_rate_limit_allowlist, _('Users to exclude from the rate limit'), class: 'label-bold'
|
||||
= f.text_area :search_rate_limit_allowlist_raw, class: 'form-control gl-form-input', rows: 5, aria: { describedBy: 'search-rate-limit-allowlist-field-description' }
|
||||
.form-text.text-muted{ id: 'search-rate-limit-allowlist-field-description' }
|
||||
= _('List of users who are allowed to exceed the rate limit. Example: username1, username2')
|
||||
|
||||
|
||||
= f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
|
||||
|
|
|
|||
|
|
@ -2550,6 +2550,15 @@
|
|||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: ci_initialize_pipelines_iid_sequence
|
||||
:worker_name: Ci::InitializePipelinesIidSequenceWorker
|
||||
:feature_category: :continuous_integration
|
||||
:has_external_dependencies: false
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: ci_job_artifacts_expire_project_build_artifacts
|
||||
:worker_name: Ci::JobArtifacts::ExpireProjectBuildArtifactsWorker
|
||||
:feature_category: :build_artifacts
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
class InitializePipelinesIidSequenceWorker
|
||||
include Gitlab::EventStore::Subscriber
|
||||
|
||||
data_consistency :always
|
||||
feature_category :continuous_integration
|
||||
idempotent!
|
||||
|
||||
def handle_event(event)
|
||||
Project.find_by_id(event.data[:project_id]).try do |project|
|
||||
next if project.internal_ids.ci_pipelines.any?
|
||||
|
||||
::Ci::Pipeline.track_project_iid!(project, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -127,6 +127,8 @@
|
|||
- 1
|
||||
- - ci_delete_objects
|
||||
- 1
|
||||
- - ci_initialize_pipelines_iid_sequence
|
||||
- 1
|
||||
- - ci_job_artifacts_expire_project_build_artifacts
|
||||
- 1
|
||||
- - ci_llm_generate_config
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
- title: "GraphQL networkPolicies resource deprecated" # (required) Clearly explain the change, or planned change. For example, "The `confidential` field for a `Note` is deprecated" or "CI/CD job names will be limited to 250 characters."
|
||||
removal_milestone: "17.0" # (required) The milestone when this feature is planned to be removed
|
||||
announcement_milestone: "14.8" # (required) The milestone when this feature was first announced as deprecated.
|
||||
breaking_change: true # (required) Change to false if this is not a breaking change.
|
||||
reporter: g.hickman # (required) GitLab username of the person reporting the change
|
||||
stage: Secure # (required) String value of the stage that the feature was created in. e.g., Growth
|
||||
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/421440 # (required) Link to the deprecation issue in GitLab
|
||||
body: | # (required) Do not modify this line, instead modify the lines below.
|
||||
The `networkPolicies` [GraphQL resource](https://docs.gitlab.com/ee/api/graphql/reference/#projectnetworkpolicies) has been deprecated and will be removed in GitLab 17.0. Since GitLab 15.0 this field has returned no data.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddSearchRateLimitAllowlistToApplicationSettings < Gitlab::Database::Migration[2.1]
|
||||
def change
|
||||
add_column :application_settings, :search_rate_limit_allowlist, :text, array: true, default: [], null: false
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddLinkedItemsWidgetToTicketWorkItemType < Gitlab::Database::Migration[2.1]
|
||||
disable_ddl_transaction!
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
TICKET_ENUM_VALUE = 8
|
||||
WIDGET_NAME = 'Linked items'
|
||||
WIDGET_ENUM_VALUE = 17
|
||||
|
||||
class MigrationWorkItemType < MigrationRecord
|
||||
self.table_name = 'work_item_types'
|
||||
end
|
||||
|
||||
class MigrationWidgetDefinition < MigrationRecord
|
||||
self.table_name = 'work_item_widget_definitions'
|
||||
end
|
||||
|
||||
def up
|
||||
# New instances will not run this migration and add this type via fixtures
|
||||
# checking if record exists mostly because migration specs will run all migrations
|
||||
# and that will conflict with the preloaded base work item types
|
||||
ticket_work_item_type = MigrationWorkItemType.find_by(base_type: TICKET_ENUM_VALUE, namespace_id: nil)
|
||||
|
||||
return say('Ticket work item type does not exist, skipping widget creation') unless ticket_work_item_type
|
||||
|
||||
widgets = [
|
||||
{
|
||||
work_item_type_id: ticket_work_item_type.id,
|
||||
name: WIDGET_NAME,
|
||||
widget_type: WIDGET_ENUM_VALUE
|
||||
}
|
||||
]
|
||||
|
||||
MigrationWidgetDefinition.upsert_all(
|
||||
widgets,
|
||||
unique_by: :index_work_item_widget_definitions_on_default_witype_and_name
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
ticket_work_item_type = MigrationWorkItemType.find_by(base_type: TICKET_ENUM_VALUE, namespace_id: nil)
|
||||
|
||||
return say('Ticket work item type does not exist, skipping widget removal') unless ticket_work_item_type
|
||||
|
||||
MigrationWidgetDefinition.where(work_item_type_id: ticket_work_item_type.id, widget_type: WIDGET_ENUM_VALUE)
|
||||
.delete_all
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
9dd2bca54c0f9560719887dae87b54edd10f01ea3a831141b80a3d2364a4eddf
|
||||
|
|
@ -0,0 +1 @@
|
|||
800d27ba92b45c193bbc8d487bec380fd06ef8660c41b2bef04b52f574ded406
|
||||
|
|
@ -11930,6 +11930,7 @@ CREATE TABLE application_settings (
|
|||
ci_max_total_yaml_size_bytes integer DEFAULT 157286400 NOT NULL,
|
||||
prometheus_alert_db_indicators_settings jsonb,
|
||||
decompress_archive_file_timeout integer DEFAULT 210 NOT NULL,
|
||||
search_rate_limit_allowlist text[] DEFAULT '{}'::text[] NOT NULL,
|
||||
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
|
||||
CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)),
|
||||
CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ For information on how to control the application settings cache for an instance
|
|||
|
||||
## Get current application settings
|
||||
|
||||
> - `always_perform_delayed_deletion` feature flag [enabled](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113332) in GitLab 15.11.
|
||||
> - `delayed_project_deletion` and `delayed_group_deletion` attributes removed in GitLab 16.0.
|
||||
|
||||
List the current [application settings](#list-of-settings-that-can-be-accessed-via-api-calls)
|
||||
of the GitLab instance.
|
||||
|
||||
|
|
@ -127,8 +130,6 @@ these parameters:
|
|||
- `file_template_project_id`
|
||||
- `geo_node_allowed_ips`
|
||||
- `geo_status_timeout`
|
||||
- `delayed_project_deletion`
|
||||
- `delayed_group_deletion`
|
||||
- `default_project_deletion_protection`
|
||||
- `deletion_adjourned_period`
|
||||
- `disable_personal_access_tokens`
|
||||
|
|
@ -136,9 +137,6 @@ these parameters:
|
|||
- `delete_unconfirmed_users`
|
||||
- `unconfirmed_users_delete_after_days`
|
||||
|
||||
From [GitLab 15.11](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113332), with the `always_perform_delayed_deletion` feature flag enabled,
|
||||
the `delayed_project_deletion` and `delayed_group_deletion` attributes will not be exposed. These attributes will be removed in GitLab 16.0.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
|
|
@ -146,8 +144,6 @@ the `delayed_project_deletion` and `delayed_group_deletion` attributes will not
|
|||
"group_owners_can_manage_default_branch_protection": true,
|
||||
"file_template_project_id": 1,
|
||||
"geo_node_allowed_ips": "0.0.0.0/0, ::/0",
|
||||
"delayed_project_deletion": false,
|
||||
"delayed_group_deletion": false,
|
||||
"default_project_deletion_protection": false,
|
||||
"deletion_adjourned_period": 7,
|
||||
"disable_personal_access_tokens": false,
|
||||
|
|
@ -157,6 +153,9 @@ the `delayed_project_deletion` and `delayed_group_deletion` attributes will not
|
|||
|
||||
## Change application settings
|
||||
|
||||
> - `always_perform_delayed_deletion` feature flag [enabled](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113332) in GitLab 15.11.
|
||||
> - `delayed_project_deletion` and `delayed_group_deletion` attributes removed in GitLab 16.0.
|
||||
|
||||
Use an API call to modify GitLab instance
|
||||
[application settings](#list-of-settings-that-can-be-accessed-via-api-calls).
|
||||
|
||||
|
|
@ -273,8 +272,6 @@ these parameters:
|
|||
- `file_template_project_id`
|
||||
- `geo_node_allowed_ips`
|
||||
- `geo_status_timeout`
|
||||
- `delayed_project_deletion`
|
||||
- `delayed_group_deletion`
|
||||
- `default_project_deletion_protection`
|
||||
- `deletion_adjourned_period`
|
||||
- `disable_personal_access_tokens`
|
||||
|
|
@ -282,9 +279,6 @@ these parameters:
|
|||
- `delete_unconfirmed_users`
|
||||
- `unconfirmed_users_delete_after_days`
|
||||
|
||||
From [GitLab 15.11](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113332), with the `always_perform_delayed_deletion` feature flag enabled,
|
||||
the `delayed_project_deletion` and `delayed_group_deletion` attributes will not be exposed. These attributes will be removed in GitLab 16.0.
|
||||
|
||||
Example responses: **(PREMIUM SELF)**
|
||||
|
||||
```json
|
||||
|
|
@ -359,12 +353,10 @@ listed in the descriptions of the relevant settings.
|
|||
| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
|
||||
| `default_projects_limit` | integer | no | Project limit per user. Default is `100000`. |
|
||||
| `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
|
||||
| `default_syntax_highlighting_theme` | integer | no | Default syntax highlighting theme for new users and users who are not signed in. See [IDs of available themes](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/themes.rb#L16).
|
||||
| `delayed_project_deletion` **(PREMIUM SELF)** | boolean | no | Enable delayed project deletion by default in new groups. Default is `false`. [From GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/352960), can only be enabled when `delayed_group_deletion` is true. From [GitLab 15.11](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113332), with the `always_perform_delayed_deletion` feature flag enabled, this attribute has been removed. This attribute will be completely removed in GitLab 16.0. |
|
||||
| `delayed_group_deletion` **(PREMIUM SELF)** | boolean | no | Enable delayed group deletion. Default is `true`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/352959) in GitLab 15.0. [From GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/352960), disables and locks the group-level setting for delayed protect deletion when set to `false`. From [GitLab 15.11](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113332), with the `always_perform_delayed_deletion` feature flag enabled, this attribute has been removed. This attribute will be completely removed in GitLab 16.0. |
|
||||
| `default_syntax_highlighting_theme` | integer | no | Default syntax highlighting theme for users who are new or not signed in. See [IDs of available themes](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/themes.rb#L16). |
|
||||
| `default_project_deletion_protection` **(PREMIUM SELF)** | boolean | no | Enable default project deletion protection so only administrators can delete projects. Default is `false`. |
|
||||
| `delete_unconfirmed_users` **(PREMIUM SELF)** | boolean | no | Specifies whether users who have not confirmed their email should be deleted. Default is `false`. When set to `true`, unconfirmed users are deleted after `unconfirmed_users_delete_after_days` days. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/352514) in GitLab 16.1. |
|
||||
| `deletion_adjourned_period` **(PREMIUM SELF)** | integer | no | The number of days to wait before deleting a project or group that is marked for deletion. Value must be between `1` and `90`. Defaults to `7`. [From GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/352960), a hook on `deletion_adjourned_period` sets the period to `1` on every update, and sets both `delayed_project_deletion` and `delayed_group_deletion` to `false` if the period is `0`. |
|
||||
| `deletion_adjourned_period` **(PREMIUM SELF)** | integer | no | Number of days to wait before deleting a project or group that is marked for deletion. Value must be between `1` and `90`. Defaults to `7`. |
|
||||
| `diagramsnet_enabled` | boolean | no | (If enabled, requires `diagramsnet_url`) Enable [Diagrams.net integration](../administration/integration/diagrams_net.md). Default is `true`. |
|
||||
| `diagramsnet_url` | string | required by: `diagramsnet_enabled` | The Diagrams.net instance URL for integration. |
|
||||
| `diff_max_patch_bytes` | integer | no | Maximum [diff patch size](../administration/diff_limits.md), in bytes. |
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@ requiring downtime.
|
|||
|
||||
## Dropping columns
|
||||
|
||||
Removing columns is tricky because running GitLab processes may still be using
|
||||
the columns. To work around this safely, you need three steps in three releases:
|
||||
Removing columns is tricky because running GitLab processes expects these columns to exist, as ActiveRecord caches the tables schema, even if the columns are not referenced. This happens if the columns are not explicitly marked as ignored. To work around this safely, you need three steps in three releases:
|
||||
|
||||
1. [Ignoring the column](#ignoring-the-column-release-m) (release M)
|
||||
1. [Dropping the column](#dropping-the-column-release-m1) (release M+1)
|
||||
|
|
|
|||
|
|
@ -381,6 +381,20 @@ Use `totalIssueWeight` instead, introduced in GitLab 16.2.
|
|||
|
||||
<div class="deprecation breaking-change" data-milestone="17.0">
|
||||
|
||||
### GraphQL networkPolicies resource deprecated
|
||||
|
||||
<div class="deprecation-notes">
|
||||
- Announced in GitLab <span class="milestone">14.8</span>
|
||||
- Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
|
||||
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/421440).
|
||||
</div>
|
||||
|
||||
The `networkPolicies` [GraphQL resource](https://docs.gitlab.com/ee/api/graphql/reference/#projectnetworkpolicies) has been deprecated and will be removed in GitLab 17.0. Since GitLab 15.0 this field has returned no data.
|
||||
|
||||
</div>
|
||||
|
||||
<div class="deprecation breaking-change" data-milestone="17.0">
|
||||
|
||||
### GraphQL type, `RunnerMembershipFilter` renamed to `CiRunnerMembershipFilter`
|
||||
|
||||
<div class="deprecation-notes">
|
||||
|
|
|
|||
|
|
@ -46,16 +46,6 @@ You can use GitLab to review analytics at the project level. Some of these featu
|
|||
- [Repository](repository_analytics.md)
|
||||
- [Value Stream Management Analytics](../group/value_stream_analytics/index.md), and [Value Stream Management Dashboard](value_streams_dashboard.md)
|
||||
|
||||
### Remove project analytics from the left sidebar
|
||||
|
||||
By default, analytics for a project are displayed under the **Analyze** item in the left sidebar. To remove this item:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. Select **Settings > General**.
|
||||
1. Expand **Visibility, project features, permissions**.
|
||||
1. Turn off the **Analytics** toggle.
|
||||
1. Select **Save changes**.
|
||||
|
||||
## User-configurable analytics
|
||||
|
||||
The following analytics features are available for users to create personalized views:
|
||||
|
|
@ -73,17 +63,10 @@ You can use the following analytics features to analyze and visualize the perfor
|
|||
|
||||
## Glossary
|
||||
|
||||
We use the following terms to describe GitLab analytics:
|
||||
|
||||
- **Mean Time to Change (MTTC):** The average duration between idea and delivery. GitLab measures
|
||||
MTTC from issue creation to the issue's latest related merge request's deployment to production.
|
||||
- **Mean Time to Detect (MTTD):** The average duration that a bug goes undetected in production.
|
||||
GitLab measures MTTD from deployment of bug to issue creation.
|
||||
- **Mean Time To Merge (MTTM):** The average lifespan of a merge request. GitLab measures MTTM from
|
||||
merge request creation to merge request merge (and closed/un-merged merge requests are excluded).
|
||||
For more information, see [Merge Request Analytics](merge_request_analytics.md).
|
||||
- **Mean Time to Recover/Repair/Resolution/Resolve/Restore (MTTR):** The average duration that a bug
|
||||
is not fixed in production. GitLab measures MTTR from deployment of bug to deployment of fix.
|
||||
- **Velocity:** The total issue burden completed in some period of time. The burden is usually measured
|
||||
in points or weight, often per sprint. For example, your velocity may be "30 points per sprint". GitLab
|
||||
measures velocity as the total points or weight of issues closed in a given period of time.
|
||||
| Metric | Definition | Measurement in GitLab |
|
||||
| ------ | ---------- | --------------------- |
|
||||
| Mean Time to Change (MTTC) | The average duration between idea and delivery. | From issue creation to the issue's latest related merge request's deployment to production. |
|
||||
| Mean Time to Detect (MTTD) | The average duration that a bug goes undetected in production. | From deployment of bug to issue creation. |
|
||||
| Mean Time To Merge (MTTM) | The average lifespan of a merge request. | From merge request creation to merge request merge (excluding closed and unmerged merge requests). For more information, see [Merge Request Analytics](merge_request_analytics.md). |
|
||||
| Mean Time to Recover/Repair/Resolution/Resolve/Restore (MTTR) | The average duration that a bug is not fixed in production. | From deployment of bug to deployment of fix. |
|
||||
| Velocity | The total issue burden completed in some period of time. The burden is usually measured in points or weight, often per sprint. | Total points or weight of issues closed in a given period of time. Expressed as, for example, "30 points per sprint". |
|
||||
|
|
|
|||
|
|
@ -283,7 +283,18 @@ To restore a project marked for deletion:
|
|||
1. Expand **Advanced**.
|
||||
1. In the Restore project section, select **Restore project**.
|
||||
|
||||
## Add a compliance framework to a project **(PREMIUM ALL)**
|
||||
## Remove project analytics from the left sidebar
|
||||
|
||||
By default, [analytics for a project](../../analytics/index.md#project-level-analytics) are displayed under the **Analyze** item in the left sidebar.
|
||||
To remove this item:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. Select **Settings > General**.
|
||||
1. Expand **Visibility, project features, permissions**.
|
||||
1. Turn off the **Analytics** toggle.
|
||||
1. Select **Save changes**.
|
||||
|
||||
## Add a compliance framework to a project **(PREMIUM)**
|
||||
|
||||
You can
|
||||
[add compliance frameworks to projects](../../group/compliance_frameworks.md#add-a-compliance-framework-to-a-project)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ All global search scopes are enabled by default on self-managed instances.
|
|||
|
||||
## Global search validation
|
||||
|
||||
> - Support for partial matches in issue search [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71913) in GitLab 14.9 [with a flag](../../administration/feature_flags.md) named `issues_full_text_search`. Disabled by default.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124703) in GitLab 16.2. Feature flag `issues_full_text_search` removed.
|
||||
|
||||
Global search ignores and logs as abusive any search that includes:
|
||||
|
||||
- Fewer than two characters
|
||||
|
|
@ -47,6 +50,10 @@ Global search only flags with an error any search that includes more than:
|
|||
- 4096 characters
|
||||
- 64 terms
|
||||
|
||||
Partial matches are not supported in issue search.
|
||||
For example, when you search issues for `play`, the query does not return issues that contain `display`.
|
||||
However, the query matches all possible variations of the string (for example, `plays`).
|
||||
|
||||
## Autocomplete suggestions
|
||||
|
||||
As you type in the search box, autocomplete suggestions are displayed for:
|
||||
|
|
@ -159,22 +166,6 @@ To search for a commit SHA:
|
|||
If a single result is returned, GitLab redirects to the commit result
|
||||
and gives you the option to return to the search results page.
|
||||
|
||||
## Search for specific terms
|
||||
|
||||
> - [Support for partial matches in issue search](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71913) removed in GitLab 14.9 [with a flag](../../administration/feature_flags.md) named `issues_full_text_search`. Disabled by default.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124703) in GitLab 16.2. Feature flag `issues_full_text_search` removed.
|
||||
|
||||
You can search issues and merge requests for specific terms.
|
||||
For example, when you search issues for `display bug`, the query returns
|
||||
all issues that contain both `display` and `bug` in any order.
|
||||
To search for the exact string, use `"display bug"` instead.
|
||||
|
||||
Partial matches are not supported in issue search.
|
||||
For example, when you search issues for `play`, the query does not return issues that contain `display`.
|
||||
However, the query matches all possible variations of the string (for example, `plays`).
|
||||
|
||||
For more information about query validation, see [Global search validation](#global-search-validation).
|
||||
|
||||
## Run a search from history
|
||||
|
||||
You can run a search from history for issues and merge requests. Search history is stored locally
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ module API
|
|||
before do
|
||||
authenticate!
|
||||
|
||||
check_rate_limit!(:search_rate_limit, scope: [current_user])
|
||||
check_rate_limit!(:search_rate_limit, scope: [current_user],
|
||||
users_allowlist: Gitlab::CurrentSettings.current_application_settings.search_rate_limit_allowlist)
|
||||
end
|
||||
|
||||
feature_category :global_search
|
||||
|
|
|
|||
|
|
@ -141,7 +141,8 @@ module Gitlab
|
|||
:health_status,
|
||||
:notifications,
|
||||
:current_user_todos,
|
||||
:award_emoji
|
||||
:award_emoji,
|
||||
:linked_items
|
||||
]
|
||||
}.freeze
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ module Gitlab
|
|||
store.subscribe ::Ml::ExperimentTracking::AssociateMlCandidateToPackageWorker,
|
||||
to: ::Packages::PackageCreatedEvent,
|
||||
if: -> (event) { ::Ml::ExperimentTracking::AssociateMlCandidateToPackageWorker.handles_event?(event) }
|
||||
store.subscribe ::Ci::InitializePipelinesIidSequenceWorker, to: ::Projects::ProjectCreatedEvent
|
||||
end
|
||||
private_class_method :configure!
|
||||
end
|
||||
|
|
|
|||
|
|
@ -41,21 +41,25 @@ RSpec.describe SearchController, feature_category: :global_search do
|
|||
describe 'rate limit scope' do
|
||||
it 'uses current_user and search scope' do
|
||||
%w[projects blobs users issues merge_requests].each do |scope|
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user, scope])
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit,
|
||||
scope: [user, scope], users_allowlist: [])
|
||||
get :show, params: { search: 'hello', scope: scope }
|
||||
end
|
||||
end
|
||||
|
||||
it 'uses just current_user when no search scope is used' do
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user])
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit,
|
||||
scope: [user], users_allowlist: [])
|
||||
get :show, params: { search: 'hello' }
|
||||
end
|
||||
|
||||
it 'uses just current_user when search scope is abusive' do
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user])
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit,
|
||||
scope: [user], users_allowlist: [])
|
||||
get(:show, params: { search: 'hello', scope: 'hack-the-mainframe' })
|
||||
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user])
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit,
|
||||
scope: [user], users_allowlist: [])
|
||||
get :show, params: { search: 'hello', scope: 'blobs' * 1000 }
|
||||
end
|
||||
end
|
||||
|
|
@ -298,6 +302,14 @@ RSpec.describe SearchController, feature_category: :global_search do
|
|||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'search request exceeding rate limit', :clean_gitlab_redis_cache do
|
||||
let(:current_user) { user }
|
||||
|
||||
def request
|
||||
get(:show, params: { search: 'foo@bar.com', scope: 'users' })
|
||||
end
|
||||
end
|
||||
|
||||
it 'increments the custom search sli apdex' do
|
||||
expect(Gitlab::Metrics::GlobalSearchSlis).to receive(:record_apdex).with(
|
||||
elapsed: a_kind_of(Numeric),
|
||||
|
|
@ -370,16 +382,19 @@ RSpec.describe SearchController, feature_category: :global_search do
|
|||
describe 'rate limit scope' do
|
||||
it 'uses current_user and search scope' do
|
||||
%w[projects blobs users issues merge_requests].each do |scope|
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user, scope])
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit,
|
||||
scope: [user, scope], users_allowlist: [])
|
||||
get :count, params: { search: 'hello', scope: scope }
|
||||
end
|
||||
end
|
||||
|
||||
it 'uses just current_user when search scope is abusive' do
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user])
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit,
|
||||
scope: [user], users_allowlist: [])
|
||||
get :count, params: { search: 'hello', scope: 'hack-the-mainframe' }
|
||||
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user])
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit,
|
||||
scope: [user], users_allowlist: [])
|
||||
get :count, params: { search: 'hello', scope: 'blobs' * 1000 }
|
||||
end
|
||||
end
|
||||
|
|
@ -432,6 +447,14 @@ RSpec.describe SearchController, feature_category: :global_search do
|
|||
get(:count, params: { search: 'foo@bar.com', scope: 'users' })
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'search request exceeding rate limit', :clean_gitlab_redis_cache do
|
||||
let(:current_user) { user }
|
||||
|
||||
def request
|
||||
get(:count, params: { search: 'foo@bar.com', scope: 'users' })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #autocomplete' do
|
||||
|
|
@ -454,16 +477,19 @@ RSpec.describe SearchController, feature_category: :global_search do
|
|||
describe 'rate limit scope' do
|
||||
it 'uses current_user and search scope' do
|
||||
%w[projects blobs users issues merge_requests].each do |scope|
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user, scope])
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit,
|
||||
scope: [user, scope], users_allowlist: [])
|
||||
get :autocomplete, params: { term: 'hello', scope: scope }
|
||||
end
|
||||
end
|
||||
|
||||
it 'uses just current_user when search scope is abusive' do
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user])
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit,
|
||||
scope: [user], users_allowlist: [])
|
||||
get :autocomplete, params: { term: 'hello', scope: 'hack-the-mainframe' }
|
||||
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user])
|
||||
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit,
|
||||
scope: [user], users_allowlist: [])
|
||||
get :autocomplete, params: { term: 'hello', scope: 'blobs' * 1000 }
|
||||
end
|
||||
end
|
||||
|
|
@ -476,6 +502,14 @@ RSpec.describe SearchController, feature_category: :global_search do
|
|||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'search request exceeding rate limit', :clean_gitlab_redis_cache do
|
||||
let(:current_user) { user }
|
||||
|
||||
def request
|
||||
get(:autocomplete, params: { term: 'foo@bar.com', scope: 'users' })
|
||||
end
|
||||
end
|
||||
|
||||
it 'can be filtered with params[:filter]' do
|
||||
get :autocomplete, params: { term: 'setting', filter: 'generic' }
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { nextTick } from 'vue';
|
||||
import {
|
||||
GlDisclosureDropdown,
|
||||
GlTooltip,
|
||||
GlDisclosureDropdownGroup,
|
||||
GlDisclosureDropdownItem,
|
||||
} from '@gitlab/ui';
|
||||
|
|
@ -9,6 +8,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
|||
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
|
||||
import { __ } from '~/locale';
|
||||
import CreateMenu from '~/super_sidebar/components/create_menu.vue';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import { createNewMenuGroups } from '../mock_data';
|
||||
|
||||
describe('CreateMenu component', () => {
|
||||
|
|
@ -18,7 +18,6 @@ describe('CreateMenu component', () => {
|
|||
const findGlDisclosureDropdownGroups = () => wrapper.findAllComponents(GlDisclosureDropdownGroup);
|
||||
const findGlDisclosureDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
|
||||
const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger);
|
||||
const findGlTooltip = () => wrapper.findComponent(GlTooltip);
|
||||
|
||||
const createWrapper = ({ provide = {} } = {}) => {
|
||||
wrapper = shallowMountExtended(CreateMenu, {
|
||||
|
|
@ -33,6 +32,9 @@ describe('CreateMenu component', () => {
|
|||
InviteMembersTrigger,
|
||||
GlDisclosureDropdown,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: createMockDirective('gl-tooltip'),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -74,16 +76,12 @@ describe('CreateMenu component', () => {
|
|||
expect(findInviteMembersTrigger().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("sets the toggle ID and tooltip's target", () => {
|
||||
expect(findGlDisclosureDropdown().props('toggleId')).toBe(wrapper.vm.$options.toggleId);
|
||||
expect(findGlTooltip().props('target')).toBe(`#${wrapper.vm.$options.toggleId}`);
|
||||
});
|
||||
|
||||
it('hides the tooltip when the dropdown is opened', async () => {
|
||||
findGlDisclosureDropdown().vm.$emit('shown');
|
||||
await nextTick();
|
||||
|
||||
expect(findGlTooltip().exists()).toBe(false);
|
||||
const tooltip = getBinding(findGlDisclosureDropdown().element, 'gl-tooltip');
|
||||
expect(tooltip.value).toBe('');
|
||||
});
|
||||
|
||||
it('shows the tooltip when the dropdown is closed', async () => {
|
||||
|
|
@ -91,7 +89,8 @@ describe('CreateMenu component', () => {
|
|||
findGlDisclosureDropdown().vm.$emit('hidden');
|
||||
await nextTick();
|
||||
|
||||
expect(findGlTooltip().exists()).toBe(true);
|
||||
const tooltip = getBinding(findGlDisclosureDropdown().element, 'gl-tooltip');
|
||||
expect(tooltip.value).toBe('Create new...');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,26 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import FlyoutMenu from '~/super_sidebar/components/flyout_menu.vue';
|
||||
|
||||
jest.mock('@floating-ui/dom');
|
||||
|
||||
describe('FlyoutMenu', () => {
|
||||
let wrapper;
|
||||
let dummySection;
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMount(FlyoutMenu, {
|
||||
dummySection = document.createElement('section');
|
||||
dummySection.addEventListener = jest.fn();
|
||||
|
||||
dummySection.getBoundingClientRect = jest.fn();
|
||||
dummySection.getBoundingClientRect.mockReturnValue({ top: 0, bottom: 5, width: 10 });
|
||||
|
||||
document.querySelector = jest.fn();
|
||||
document.querySelector.mockReturnValue(dummySection);
|
||||
|
||||
wrapper = mountExtended(FlyoutMenu, {
|
||||
propsData: {
|
||||
targetId: 'section-1',
|
||||
items: [],
|
||||
items: [{ id: 1, title: 'item 1', link: 'https://example.com' }],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -79,39 +79,55 @@ describe('MenuSection component', () => {
|
|||
});
|
||||
|
||||
describe('when hasFlyout is true', () => {
|
||||
it('is rendered', () => {
|
||||
it('is not yet rendered', () => {
|
||||
createWrapper({ title: 'Asdf' }, { 'has-flyout': true });
|
||||
expect(findFlyout().exists()).toBe(true);
|
||||
expect(findFlyout().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('on mouse hover', () => {
|
||||
describe('when section is expanded', () => {
|
||||
it('is not shown', async () => {
|
||||
it('is not rendered', async () => {
|
||||
createWrapper({ title: 'Asdf' }, { 'has-flyout': true, expanded: true });
|
||||
await findButton().trigger('pointerover', { pointerType: 'mouse' });
|
||||
expect(findFlyout().isVisible()).toBe(false);
|
||||
expect(findFlyout().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when section is not expanded', () => {
|
||||
it('is shown', async () => {
|
||||
createWrapper({ title: 'Asdf' }, { 'has-flyout': true, expanded: false });
|
||||
await findButton().trigger('pointerover', { pointerType: 'mouse' });
|
||||
expect(findFlyout().isVisible()).toBe(true);
|
||||
describe('when section has no items', () => {
|
||||
it('is not rendered', async () => {
|
||||
createWrapper({ title: 'Asdf' }, { 'has-flyout': true, expanded: false });
|
||||
await findButton().trigger('pointerover', { pointerType: 'mouse' });
|
||||
expect(findFlyout().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when section has items', () => {
|
||||
it('is rendered and shown', async () => {
|
||||
createWrapper(
|
||||
{ title: 'Asdf', items: [{ title: 'Item1', href: '/item1' }] },
|
||||
{ 'has-flyout': true, expanded: false },
|
||||
);
|
||||
await findButton().trigger('pointerover', { pointerType: 'mouse' });
|
||||
expect(findFlyout().isVisible()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when section gets closed', () => {
|
||||
beforeEach(async () => {
|
||||
createWrapper({ title: 'Asdf' }, { expanded: true, 'has-flyout': true });
|
||||
createWrapper(
|
||||
{ title: 'Asdf', items: [{ title: 'Item1', href: '/item1' }] },
|
||||
{ expanded: true, 'has-flyout': true },
|
||||
);
|
||||
await findButton().trigger('click');
|
||||
await findButton().trigger('pointerover', { pointerType: 'mouse' });
|
||||
});
|
||||
|
||||
it('shows the flyout only after section title gets hovered out and in again', async () => {
|
||||
expect(findCollapse().props('visible')).toBe(false);
|
||||
expect(findFlyout().isVisible()).toBe(false);
|
||||
expect(findFlyout().exists()).toBe(false);
|
||||
|
||||
await findButton().trigger('pointerleave');
|
||||
await findButton().trigger('pointerover', { pointerType: 'mouse' });
|
||||
|
|
|
|||
|
|
@ -65,8 +65,20 @@ describe('UserBar component', () => {
|
|||
createWrapper();
|
||||
});
|
||||
|
||||
it('passes the "Create new..." menu groups to the create-menu component', () => {
|
||||
expect(findCreateMenu().props('groups')).toBe(mockSidebarData.create_new_menu_groups);
|
||||
describe('"Create new..." menu', () => {
|
||||
describe('when there are no menu items for it', () => {
|
||||
// This scenario usually happens for an "External" user.
|
||||
it('does not render it', () => {
|
||||
createWrapper({ sidebarData: { ...mockSidebarData, create_new_menu_groups: [] } });
|
||||
expect(findCreateMenu().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are menu items for it', () => {
|
||||
it('passes the "Create new..." menu groups to the create-menu component', () => {
|
||||
expect(findCreateMenu().props('groups')).toBe(mockSidebarData.create_new_menu_groups);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('passes the "Merge request" menu groups to the merge_request_menu component', () => {
|
||||
|
|
|
|||
|
|
@ -4,118 +4,22 @@ require 'spec_helper'
|
|||
require_migration!
|
||||
|
||||
RSpec.describe AddCurrentUserTodosWidgetToEpicWorkItemType, :migration, feature_category: :team_planning do
|
||||
include MigrationHelpers::WorkItemTypesHelper
|
||||
|
||||
let(:work_item_types) { table(:work_item_types) }
|
||||
let(:work_item_widget_definitions) { table(:work_item_widget_definitions) }
|
||||
let(:base_types) do
|
||||
{
|
||||
issue: 0,
|
||||
incident: 1,
|
||||
test_case: 2,
|
||||
requirement: 3,
|
||||
task: 4,
|
||||
objective: 5,
|
||||
key_result: 6,
|
||||
epic: 7
|
||||
}
|
||||
end
|
||||
|
||||
let(:epic_widgets) do
|
||||
{
|
||||
'Assignees' => 0,
|
||||
'Description' => 1,
|
||||
'Hierarchy' => 2,
|
||||
'Labels' => 3,
|
||||
'Notes' => 5,
|
||||
'Start and due date' => 6,
|
||||
'Health status' => 7,
|
||||
'Status' => 11,
|
||||
'Notifications' => 14,
|
||||
'Award emoji' => 16
|
||||
}.freeze
|
||||
end
|
||||
|
||||
after(:all) do
|
||||
# Make sure base types are recreated after running the migration
|
||||
# because migration specs are not run in a transaction
|
||||
reset_work_item_types
|
||||
end
|
||||
|
||||
before do
|
||||
reset_db_state_prior_to_migration
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
it 'adds current user todos widget to epic work item type', :aggregate_failures do
|
||||
expect do
|
||||
migrate!
|
||||
end.to change { work_item_widget_definitions.count }.by(1)
|
||||
|
||||
epic_type = work_item_types.find_by(namespace_id: nil, base_type: described_class::EPIC_ENUM_VALUE)
|
||||
created_widget = work_item_widget_definitions.last
|
||||
|
||||
expect(created_widget).to have_attributes(
|
||||
widget_type: described_class::WIDGET_ENUM_VALUE,
|
||||
name: described_class::WIDGET_NAME,
|
||||
work_item_type_id: epic_type.id
|
||||
)
|
||||
end
|
||||
|
||||
context 'when epic type does not exist' do
|
||||
it 'skips creating the new widget definition' do
|
||||
work_item_types.where(namespace_id: nil, base_type: base_types[:epic]).delete_all
|
||||
|
||||
expect do
|
||||
migrate!
|
||||
end.to not_change(work_item_widget_definitions, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'removes current user todos widget from epic work item type' do
|
||||
migrate!
|
||||
|
||||
expect { schema_migrate_down! }.to change { work_item_widget_definitions.count }.by(-1)
|
||||
end
|
||||
end
|
||||
|
||||
def reset_db_state_prior_to_migration
|
||||
# Database needs to be in a similar state as when this migration was created
|
||||
work_item_types.delete_all
|
||||
|
||||
create_work_item!('Issue', :issue, 'issue-type-issue')
|
||||
create_work_item!('Incident', :incident, 'issue-type-incident')
|
||||
create_work_item!('Test Case', :test_case, 'issue-type-test-case')
|
||||
create_work_item!('Requirement', :requirement, 'issue-type-requirements')
|
||||
create_work_item!('Task', :task, 'issue-type-task')
|
||||
create_work_item!('Objective', :objective, 'issue-type-objective')
|
||||
create_work_item!('Key Result', :key_result, 'issue-type-keyresult')
|
||||
|
||||
epic_type = create_work_item!('Epic', :epic, 'issue-type-epic')
|
||||
|
||||
widgets = epic_widgets.map do |widget_name, widget_enum_value|
|
||||
it_behaves_like 'migration that adds a widget to a work item type' do
|
||||
let(:target_type_enum_value) { described_class::EPIC_ENUM_VALUE }
|
||||
let(:target_type) { :epic }
|
||||
let(:widgets_for_type) do
|
||||
{
|
||||
work_item_type_id: epic_type.id,
|
||||
name: widget_name,
|
||||
widget_type: widget_enum_value
|
||||
}
|
||||
'Assignees' => 0,
|
||||
'Description' => 1,
|
||||
'Hierarchy' => 2,
|
||||
'Labels' => 3,
|
||||
'Notes' => 5,
|
||||
'Start and due date' => 6,
|
||||
'Health status' => 7,
|
||||
'Status' => 11,
|
||||
'Notifications' => 14,
|
||||
'Award emoji' => 16
|
||||
}.freeze
|
||||
end
|
||||
|
||||
# Creating all widgets for the type so the state in the DB is as close as possible to the actual state
|
||||
work_item_widget_definitions.upsert_all(
|
||||
widgets,
|
||||
unique_by: :index_work_item_widget_definitions_on_default_witype_and_name
|
||||
)
|
||||
end
|
||||
|
||||
def create_work_item!(type_name, base_type, icon_name)
|
||||
work_item_types.create!(
|
||||
name: type_name,
|
||||
namespace_id: nil,
|
||||
base_type: base_types[base_type],
|
||||
icon_name: icon_name
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe AddLinkedItemsWidgetToTicketWorkItemType, :migration, feature_category: :portfolio_management do
|
||||
it_behaves_like 'migration that adds a widget to a work item type' do
|
||||
let(:target_type_enum_value) { described_class::TICKET_ENUM_VALUE }
|
||||
let(:target_type) { :ticket }
|
||||
let(:additional_types) { { ticket: 8 } }
|
||||
let(:widgets_for_type) do
|
||||
{
|
||||
'Assignees' => 0,
|
||||
'Description' => 1,
|
||||
'Hierarchy' => 2,
|
||||
'Labels' => 3,
|
||||
'Notes' => 5,
|
||||
'Iteration' => 9,
|
||||
'Milestone' => 4,
|
||||
'Weight' => 8,
|
||||
'Current user todos' => 15,
|
||||
'Start and due date' => 6,
|
||||
'Health status' => 7,
|
||||
'Notifications' => 14,
|
||||
'Award emoji' => 16
|
||||
}.freeze
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -241,6 +241,11 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
|
|||
it { is_expected.not_to allow_value(nil).for(:users_get_by_id_limit_allowlist) }
|
||||
it { is_expected.to allow_value([]).for(:users_get_by_id_limit_allowlist) }
|
||||
|
||||
it { is_expected.to allow_value(many_usernames(100)).for(:search_rate_limit_allowlist) }
|
||||
it { is_expected.not_to allow_value(many_usernames(101)).for(:search_rate_limit_allowlist) }
|
||||
it { is_expected.not_to allow_value(nil).for(:search_rate_limit_allowlist) }
|
||||
it { is_expected.to allow_value([]).for(:search_rate_limit_allowlist) }
|
||||
|
||||
it { is_expected.to allow_value('all_tiers').for(:whats_new_variant) }
|
||||
it { is_expected.to allow_value('current_tier').for(:whats_new_variant) }
|
||||
it { is_expected.to allow_value('disabled').for(:whats_new_variant) }
|
||||
|
|
|
|||
|
|
@ -473,6 +473,21 @@ RSpec.describe API::Search, :clean_gitlab_redis_rate_limiting, feature_category:
|
|||
get api(endpoint, current_user), params: { scope: 'users', search: 'foo@bar.com' }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when request exceeds the rate limit', :freeze_time, :clean_gitlab_redis_rate_limiting do
|
||||
before do
|
||||
stub_application_setting(search_rate_limit: 1)
|
||||
end
|
||||
|
||||
it 'allows user whose username is in the allowlist' do
|
||||
stub_application_setting(search_rate_limit_allowlist: [user.username])
|
||||
|
||||
get api(endpoint, user), params: { scope: 'users', search: 'foo@bar.com' }
|
||||
get api(endpoint, user), params: { scope: 'users', search: 'foo@bar.com' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /groups/:id/search" do
|
||||
|
|
@ -658,6 +673,21 @@ RSpec.describe API::Search, :clean_gitlab_redis_rate_limiting, feature_category:
|
|||
get api(endpoint, current_user), params: { scope: 'users', search: 'foo@bar.com' }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when request exceeds the rate limit', :freeze_time, :clean_gitlab_redis_rate_limiting do
|
||||
before do
|
||||
stub_application_setting(search_rate_limit: 1)
|
||||
end
|
||||
|
||||
it 'allows user whose username is in the allowlist' do
|
||||
stub_application_setting(search_rate_limit_allowlist: [user.username])
|
||||
|
||||
get api(endpoint, user), params: { scope: 'users', search: 'foo@bar.com' }
|
||||
get api(endpoint, user), params: { scope: 'users', search: 'foo@bar.com' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -1057,6 +1087,21 @@ RSpec.describe API::Search, :clean_gitlab_redis_rate_limiting, feature_category:
|
|||
get api(endpoint, current_user), params: { scope: 'users', search: 'foo@bar.com' }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when request exceeds the rate limit', :freeze_time, :clean_gitlab_redis_rate_limiting do
|
||||
before do
|
||||
stub_application_setting(search_rate_limit: 1)
|
||||
end
|
||||
|
||||
it 'allows user whose username is in the allowlist' do
|
||||
stub_application_setting(search_rate_limit_allowlist: [user.username])
|
||||
|
||||
get api(endpoint, user), params: { scope: 'users', search: 'foo@bar.com' }
|
||||
get api(endpoint, user), params: { scope: 'users', search: 'foo@bar.com' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -28,6 +28,26 @@ RSpec.describe Releases::DestroyService, feature_category: :release_orchestratio
|
|||
it 'returns the destroyed object' do
|
||||
is_expected.to include(status: :success, release: release)
|
||||
end
|
||||
|
||||
context 'when the release is for a catalog resource' do
|
||||
let!(:catalog_resource) { create(:catalog_resource, project: project, state: 'published') }
|
||||
let!(:version) { create(:catalog_resource_version, catalog_resource: catalog_resource, release: release) }
|
||||
|
||||
it 'does not update the catalog resources if there are still releases' do
|
||||
second_release = create(:release, project: project, tag: 'v1.2.0')
|
||||
create(:catalog_resource_version, catalog_resource: catalog_resource, release: second_release)
|
||||
|
||||
subject
|
||||
|
||||
expect(catalog_resource.reload.state).to eq('published')
|
||||
end
|
||||
|
||||
it 'updates the catalog resource if there are no more releases' do
|
||||
subject
|
||||
|
||||
expect(catalog_resource.reload.state).to eq('draft')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when tag does not exist in the repository' do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Requires a context containing:
|
||||
# - user
|
||||
# - params
|
||||
|
||||
RSpec.shared_examples 'search request exceeding rate limit' do
|
||||
include_examples 'rate limited endpoint', rate_limit_key: :search_rate_limit
|
||||
|
||||
it 'allows user in allow-list to search without applying rate limit', :freeze_time,
|
||||
:clean_gitlab_redis_rate_limiting do
|
||||
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1)
|
||||
|
||||
stub_application_setting(search_rate_limit_allowlist: [current_user.username])
|
||||
|
||||
request
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
|
@ -32,3 +32,110 @@ RSpec.shared_examples 'migration that adds widget to work items definitions' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Shared examples for testing migration that adds a single widget to a work item type
|
||||
#
|
||||
# It expects the following variables
|
||||
# - `target_type_enum_value`: Int, enum value for the target work item type, typically defined in the migration
|
||||
# as a constant
|
||||
# - `target_type`: Symbol, the target type's name
|
||||
# - `additional_types`: Hash (optional), name of work item types and their corresponding enum value that are defined
|
||||
# at the time the migration was created but are missing from `base_types`.
|
||||
# - `widgets_for_type`: Hash, name of the widgets included in the target type with their corresponding enum value
|
||||
RSpec.shared_examples 'migration that adds a widget to a work item type' do
|
||||
include MigrationHelpers::WorkItemTypesHelper
|
||||
|
||||
let(:work_item_types) { table(:work_item_types) }
|
||||
let(:work_item_widget_definitions) { table(:work_item_widget_definitions) }
|
||||
let(:additional_base_types) { try(:additional_types) || {} }
|
||||
let(:base_types) do
|
||||
{
|
||||
issue: 0,
|
||||
incident: 1,
|
||||
test_case: 2,
|
||||
requirement: 3,
|
||||
task: 4,
|
||||
objective: 5,
|
||||
key_result: 6,
|
||||
epic: 7
|
||||
}.merge!(additional_base_types)
|
||||
end
|
||||
|
||||
after(:all) do
|
||||
# Make sure base types are recreated after running the migration
|
||||
# because migration specs are not run in a transaction
|
||||
reset_work_item_types
|
||||
end
|
||||
|
||||
before do
|
||||
# Database needs to be in a similar state as when the migration was created
|
||||
reset_db_state_prior_to_migration
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
it "adds widget to work item type", :aggregate_failures do
|
||||
expect do
|
||||
migrate!
|
||||
end.to change { work_item_widget_definitions.count }.by(1)
|
||||
|
||||
work_item_type = work_item_types.find_by(namespace_id: nil, base_type: target_type_enum_value)
|
||||
created_widget = work_item_widget_definitions.last
|
||||
|
||||
expect(created_widget).to have_attributes(
|
||||
widget_type: described_class::WIDGET_ENUM_VALUE,
|
||||
name: described_class::WIDGET_NAME,
|
||||
work_item_type_id: work_item_type.id
|
||||
)
|
||||
end
|
||||
|
||||
context 'when type does not exist' do
|
||||
it 'skips creating the new widget definition' do
|
||||
work_item_types.where(namespace_id: nil, base_type: base_types[target_type]).delete_all
|
||||
|
||||
expect do
|
||||
migrate!
|
||||
end.to not_change(work_item_widget_definitions, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it "removes widget from work item type" do
|
||||
migrate!
|
||||
|
||||
expect { schema_migrate_down! }.to change { work_item_widget_definitions.count }.by(-1)
|
||||
end
|
||||
end
|
||||
|
||||
def reset_db_state_prior_to_migration
|
||||
work_item_types.delete_all
|
||||
|
||||
base_types.each do |type_sym, type_enum|
|
||||
create_work_item_type!(type_sym.to_s.titleize, type_enum)
|
||||
end
|
||||
|
||||
target_type_record = work_item_types.find_by_name(target_type.to_s.titleize)
|
||||
|
||||
widgets = widgets_for_type.map do |widget_name_value, widget_enum_value|
|
||||
{
|
||||
work_item_type_id: target_type_record.id,
|
||||
name: widget_name_value,
|
||||
widget_type: widget_enum_value
|
||||
}
|
||||
end
|
||||
|
||||
# Creating all widgets for the type so the state in the DB is as close as possible to the actual state
|
||||
work_item_widget_definitions.upsert_all(
|
||||
widgets,
|
||||
unique_by: :index_work_item_widget_definitions_on_default_witype_and_name
|
||||
)
|
||||
end
|
||||
|
||||
def create_work_item_type!(type_name, type_enum_value)
|
||||
work_item_types.create!(
|
||||
name: type_name,
|
||||
namespace_id: nil,
|
||||
base_type: type_enum_value
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Ci::InitializePipelinesIidSequenceWorker, feature_category: :continuous_integration do
|
||||
let_it_be_with_refind(:project) { create(:project) }
|
||||
|
||||
let(:project_created_event) do
|
||||
Projects::ProjectCreatedEvent.new(
|
||||
data: {
|
||||
project_id: project.id,
|
||||
namespace_id: project.namespace_id,
|
||||
root_namespace_id: project.root_namespace.id
|
||||
})
|
||||
end
|
||||
|
||||
it_behaves_like 'subscribes to event' do
|
||||
let(:event) { project_created_event }
|
||||
end
|
||||
|
||||
it 'creates an internal_ids sequence for ci_pipelines' do
|
||||
consume_event(subscriber: described_class, event: project_created_event)
|
||||
|
||||
expect(project.internal_ids.ci_pipelines).to be_any
|
||||
expect(project.internal_ids.ci_pipelines).to all be_persisted
|
||||
end
|
||||
|
||||
context 'when the internal_ids sequence is already initialized' do
|
||||
before do
|
||||
create_list(:ci_pipeline, 2, project: project)
|
||||
end
|
||||
|
||||
it 'does not reset the sequence' do
|
||||
expect { consume_event(subscriber: described_class, event: project_created_event) }
|
||||
.not_to change { project.internal_ids.ci_pipelines.pluck(:last_value) }
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue