Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-09-01 12:09:50 +00:00
parent 5ffb2b7bcd
commit 02f6aecd47
44 changed files with 751 additions and 259 deletions

View File

@ -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

View File

@ -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

View File

@ -1 +1 @@
4.3.8
4.3.9

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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" />

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
9dd2bca54c0f9560719887dae87b54edd10f01ea3a831141b80a3d2364a4eddf

View File

@ -0,0 +1 @@
800d27ba92b45c193bbc8d487bec380fd06ef8660c41b2bef04b52f574ded406

View File

@ -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)),

View File

@ -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. |

View File

@ -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)

View File

@ -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">

View File

@ -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". |

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -141,7 +141,8 @@ module Gitlab
:health_status,
:notifications,
:current_user_todos,
:award_emoji
:award_emoji,
:linked_items
]
}.freeze

View File

@ -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

View File

@ -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)

View File

@ -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...');
});
});

View File

@ -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' }],
},
});
};

View File

@ -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' });

View File

@ -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', () => {

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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