Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-04-29 09:12:50 +00:00
parent 02c0ca3a07
commit ca3ebabdfc
38 changed files with 331 additions and 201 deletions

View File

@ -745,7 +745,6 @@ Layout/LineLength:
- 'ee/app/workers/geo/destroy_worker.rb'
- 'ee/app/workers/geo/scheduler/scheduler_worker.rb'
- 'ee/app/workers/geo/secondary/registry_consistency_worker.rb'
- 'ee/app/workers/geo/verification_worker.rb'
- 'ee/app/workers/repository_update_mirror_worker.rb'
- 'ee/app/workers/security/orchestration_policy_rule_schedule_namespace_worker.rb'
- 'ee/app/workers/security/orchestration_policy_rule_schedule_worker.rb'
@ -1833,7 +1832,6 @@ Layout/LineLength:
- 'ee/spec/workers/geo/secondary/registry_consistency_worker_spec.rb'
- 'ee/spec/workers/geo/verification_batch_worker_spec.rb'
- 'ee/spec/workers/geo/verification_timeout_worker_spec.rb'
- 'ee/spec/workers/geo/verification_worker_spec.rb'
- 'ee/spec/workers/import_software_licenses_worker_spec.rb'
- 'ee/spec/workers/incident_management/oncall_rotations/persist_all_rotations_shifts_job_spec.rb'
- 'ee/spec/workers/incident_management/oncall_rotations/persist_shifts_job_spec.rb'

View File

@ -173,7 +173,6 @@ RSpec/VerifiedDoubles:
- 'ee/spec/workers/geo/verification_batch_worker_spec.rb'
- 'ee/spec/workers/geo/verification_cron_worker_spec.rb'
- 'ee/spec/workers/geo/verification_timeout_worker_spec.rb'
- 'ee/spec/workers/geo/verification_worker_spec.rb'
- 'ee/spec/workers/iterations/cadences/create_iterations_worker_spec.rb'
- 'ee/spec/workers/iterations/roll_over_issues_worker_spec.rb'
- 'ee/spec/workers/ldap_group_sync_worker_spec.rb'

View File

@ -292,7 +292,6 @@ SidekiqLoadBalancing/WorkerDataConsistency:
- 'ee/app/workers/geo/verification_cron_worker.rb'
- 'ee/app/workers/geo/verification_state_backfill_worker.rb'
- 'ee/app/workers/geo/verification_timeout_worker.rb'
- 'ee/app/workers/geo/verification_worker.rb'
- 'ee/app/workers/gitlab_subscriptions/schedule_refresh_seats_worker.rb'
- 'ee/app/workers/gitlab_subscriptions/trials/apply_trial_worker.rb'
- 'ee/app/workers/group_saml_group_sync_worker.rb'

View File

@ -11,11 +11,7 @@ import { isUserBusy } from '~/set_status_modal/utils';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import { currentAssignees, linkedItems } from '~/graphql_shared/issuable_client';
import { state } from '~/sidebar/components/reviewers/sidebar_reviewers.vue';
import {
ISSUABLE_EPIC,
WORK_ITEMS_TYPE_MAP,
WORK_ITEM_TYPE_ENUM_EPIC,
} from '~/work_items/constants';
import { ISSUABLE_EPIC, NAME_TO_ICON_MAP, WORK_ITEM_TYPE_NAME_EPIC } from '~/work_items/constants';
import AjaxCache from './lib/utils/ajax_cache';
import { spriteIcon } from './lib/utils/common_utils';
import { newDate } from './lib/utils/datetime_utility';
@ -1259,7 +1255,7 @@ GfmAutoComplete.Issues = {
},
templateFunction({ id, title, reference, iconName }) {
const mappedIconName =
iconName === ISSUABLE_EPIC ? WORK_ITEMS_TYPE_MAP[WORK_ITEM_TYPE_ENUM_EPIC].icon : iconName;
iconName === ISSUABLE_EPIC ? NAME_TO_ICON_MAP[WORK_ITEM_TYPE_NAME_EPIC] : iconName;
const icon = mappedIconName
? spriteIcon(mappedIconName, 'gl-fill-icon-subtle s16 gl-mr-2')
: '';

View File

@ -14,6 +14,7 @@ import {
WIDGET_TYPE_HIERARCHY,
WIDGET_TYPE_LINKED_ITEMS,
WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_VULNERABILITIES,
} from '~/work_items/constants';
import isExpandedHierarchyTreeChildQuery from '~/work_items/graphql/client/is_expanded_hierarchy_tree_child.query.graphql';
@ -133,6 +134,15 @@ export const config = {
},
},
},
WorkItemWidgetVulnerabilities: {
fields: {
// If we add any key args, the relatedVulnerabilities field becomes relatedVulnerabilities({"first":50,"after":"xyz"}) and
// kills any possibility to handle it on the widget level without hardcoding a string.
relatedVulnerabilities: {
keyArgs: false,
},
},
},
WorkItem: {
fields: {
// Prevent `reference` from being transformed into `reference({"fullPath":true})`
@ -196,6 +206,25 @@ export const config = {
};
}
// we want to concat next page of vulnerabilities work items within Vulnerabilities widget to the existing ones
if (
incomingWidget?.type === WIDGET_TYPE_VULNERABILITIES &&
context.variables.after &&
incomingWidget.relatedVulnerabilities?.nodes
) {
// concatPagination won't work because we were placing new widget here so we have to do this manually
return {
...incomingWidget,
relatedVulnerabilities: {
...incomingWidget.relatedVulnerabilities,
nodes: [
...existingWidget.relatedVulnerabilities.nodes,
...incomingWidget.relatedVulnerabilities.nodes,
],
},
};
}
// this ensures that we dont override linkedItems.workItem when updating parent
if (incomingWidget?.type === WIDGET_TYPE_LINKED_ITEMS) {
if (!incomingWidget.linkedItems) {

View File

@ -131,7 +131,7 @@ export default {
return newWorkItemPath({
fullPath: this.fullPath,
isGroup: this.isGroup,
workItemTypeName: NAME_TO_ENUM_MAP[this.selectedWorkItemTypeName],
workItemType: this.selectedWorkItemTypeName,
query: this.newWorkItemPathQuery,
});
},

View File

@ -53,9 +53,6 @@ export default {
workItemType() {
return this.workItem?.workItemType?.name;
},
workItemIconName() {
return this.workItem?.workItemType?.iconName;
},
workItemMovedToWorkItemUrl() {
return this.workItem?.movedToWorkItemUrl;
},
@ -120,7 +117,6 @@ export default {
<locked-badge v-if="isDiscussionLocked" class="gl-align-middle" :issuable-type="workItemType" />
<work-item-type-icon
class="gl-align-middle"
:work-item-icon-name="workItemIconName"
:work-item-type="workItemType"
show-text
icon-class="gl-fill-icon-subtle"

View File

@ -77,7 +77,7 @@ export default {
v-for="rolledUpCount in filteredRollUpCountsByType"
:key="rolledUpCount.workItemType.name"
>
<work-item-type-icon :work-item-icon-name="rolledUpCount.workItemType.iconName" />
<work-item-type-icon :work-item-type="rolledUpCount.workItemType.name" />
{{ rolledUpCount.countsByState.all }}
</span>
</span>

View File

@ -32,7 +32,7 @@ export default {
:key="rolledUpCount.workItemType.name"
data-testid="rolled-up-type-info"
>
<work-item-type-icon :work-item-icon-name="rolledUpCount.workItemType.iconName" />
<work-item-type-icon :work-item-type="rolledUpCount.workItemType.name" />
<span class="gl-font-bold"
>{{ rolledUpCount.countsByState.closed }}/{{ rolledUpCount.countsByState.all }}</span
>

View File

@ -1,6 +1,7 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { WORK_ITEMS_TYPE_MAP } from '../constants';
import { NAME_TO_ICON_MAP, NAME_TO_TEXT_MAP } from '../constants';
import { convertTypeEnumToName } from '../utils';
export default {
components: {
@ -12,19 +13,13 @@ export default {
props: {
workItemType: {
type: String,
required: false,
default: '',
required: true,
},
showText: {
type: Boolean,
required: false,
default: false,
},
workItemIconName: {
type: String,
required: false,
default: '',
},
showTooltipOnHover: {
type: Boolean,
required: false,
@ -42,26 +37,22 @@ export default {
},
},
computed: {
workItemTypeUppercase() {
return this.workItemType.toUpperCase().split(' ').join('_');
},
iconName() {
// TODO Delete this conditional once we have an `issue-type-epic` icon
if (this.workItemIconName === 'issue-type-epic') {
return 'epic';
}
return (
this.workItemIconName ||
WORK_ITEMS_TYPE_MAP[this.workItemTypeUppercase]?.icon ||
'issue-type-issue'
);
workItemTypeEnum() {
// Since this component is used by work items and legacy issues, workItemType can be
// a legacy issue type or work item name, so normalize it into a work item enum
return this.workItemType.replaceAll(' ', '_').toUpperCase();
},
workItemTypeName() {
return WORK_ITEMS_TYPE_MAP[this.workItemTypeUppercase]?.name;
return convertTypeEnumToName(this.workItemTypeEnum);
},
iconName() {
return NAME_TO_ICON_MAP[this.workItemTypeName] || 'issue-type-issue';
},
workItemTypeText() {
return NAME_TO_TEXT_MAP[this.workItemTypeName];
},
workItemTooltipTitle() {
return this.showTooltipOnHover ? this.workItemTypeName : '';
return this.showTooltipOnHover ? this.workItemTypeText : '';
},
},
};
@ -76,6 +67,6 @@ export default {
:variant="iconVariant"
:class="iconClass"
/>
<span v-if="workItemTypeName" :class="{ 'gl-sr-only': !showText }">{{ workItemTypeName }}</span>
<span v-if="workItemTypeText" :class="{ 'gl-sr-only': !showText }">{{ workItemTypeText }}</span>
</span>
</template>

View File

@ -90,63 +90,6 @@ export const sprintfWorkItem = (msg, workItemTypeArg, parentWorkItemType = '') =
);
};
export const WORK_ITEMS_TYPE_MAP = {
[WORK_ITEM_TYPE_ENUM_INCIDENT]: {
icon: `issue-type-incident`,
name: s__('WorkItem|Incident'),
value: WORK_ITEM_TYPE_NAME_INCIDENT,
},
[WORK_ITEM_TYPE_ENUM_ISSUE]: {
icon: `issue-type-issue`,
name: s__('WorkItem|Issue'),
value: WORK_ITEM_TYPE_NAME_ISSUE,
routeParamName: 'issues',
},
[WORK_ITEM_TYPE_ENUM_TASK]: {
icon: `issue-type-task`,
name: s__('WorkItem|Task'),
value: WORK_ITEM_TYPE_NAME_TASK,
},
[WORK_ITEM_TYPE_ENUM_TEST_CASE]: {
icon: `issue-type-test-case`,
name: s__('WorkItem|Test case'),
value: WORK_ITEM_TYPE_NAME_TEST_CASE,
},
[WORK_ITEM_TYPE_ENUM_REQUIREMENTS]: {
icon: `issue-type-requirements`,
name: s__('WorkItem|Requirements'),
value: WORK_ITEM_TYPE_NAME_REQUIREMENTS,
},
[WORK_ITEM_TYPE_ENUM_OBJECTIVE]: {
icon: `issue-type-objective`,
name: s__('WorkItem|Objective'),
value: WORK_ITEM_TYPE_NAME_OBJECTIVE,
},
[WORK_ITEM_TYPE_ENUM_KEY_RESULT]: {
icon: `issue-type-keyresult`,
name: s__('WorkItem|Key result'),
value: WORK_ITEM_TYPE_NAME_KEY_RESULT,
},
[WORK_ITEM_TYPE_ENUM_EPIC]: {
icon: `epic`,
name: s__('WorkItem|Epic'),
value: WORK_ITEM_TYPE_NAME_EPIC,
routeParamName: 'epics',
},
};
export const NAME_TO_ENUM_MAP = {
[WORK_ITEM_TYPE_NAME_EPIC]: WORK_ITEM_TYPE_ENUM_EPIC,
[WORK_ITEM_TYPE_NAME_INCIDENT]: WORK_ITEM_TYPE_ENUM_INCIDENT,
[WORK_ITEM_TYPE_NAME_ISSUE]: WORK_ITEM_TYPE_ENUM_ISSUE,
[WORK_ITEM_TYPE_NAME_KEY_RESULT]: WORK_ITEM_TYPE_ENUM_KEY_RESULT,
[WORK_ITEM_TYPE_NAME_OBJECTIVE]: WORK_ITEM_TYPE_ENUM_OBJECTIVE,
[WORK_ITEM_TYPE_NAME_REQUIREMENTS]: WORK_ITEM_TYPE_ENUM_REQUIREMENTS,
[WORK_ITEM_TYPE_NAME_TASK]: WORK_ITEM_TYPE_ENUM_TASK,
[WORK_ITEM_TYPE_NAME_TEST_CASE]: WORK_ITEM_TYPE_ENUM_TEST_CASE,
[WORK_ITEM_TYPE_NAME_TICKET]: WORK_ITEM_TYPE_ENUM_TICKET,
};
export const FORM_TYPES = {
create: 'create',
add: 'add',
@ -329,6 +272,35 @@ export const ALLOWED_CONVERSION_TYPES = [
WORK_ITEM_TYPE_NAME_ISSUE,
];
export const NAME_TO_ENUM_MAP = {
[WORK_ITEM_TYPE_NAME_EPIC]: WORK_ITEM_TYPE_ENUM_EPIC,
[WORK_ITEM_TYPE_NAME_INCIDENT]: WORK_ITEM_TYPE_ENUM_INCIDENT,
[WORK_ITEM_TYPE_NAME_ISSUE]: WORK_ITEM_TYPE_ENUM_ISSUE,
[WORK_ITEM_TYPE_NAME_KEY_RESULT]: WORK_ITEM_TYPE_ENUM_KEY_RESULT,
[WORK_ITEM_TYPE_NAME_OBJECTIVE]: WORK_ITEM_TYPE_ENUM_OBJECTIVE,
[WORK_ITEM_TYPE_NAME_REQUIREMENTS]: WORK_ITEM_TYPE_ENUM_REQUIREMENTS,
[WORK_ITEM_TYPE_NAME_TASK]: WORK_ITEM_TYPE_ENUM_TASK,
[WORK_ITEM_TYPE_NAME_TEST_CASE]: WORK_ITEM_TYPE_ENUM_TEST_CASE,
[WORK_ITEM_TYPE_NAME_TICKET]: WORK_ITEM_TYPE_ENUM_TICKET,
};
export const NAME_TO_ICON_MAP = {
[WORK_ITEM_TYPE_NAME_EPIC]: 'epic',
[WORK_ITEM_TYPE_NAME_INCIDENT]: 'issue-type-incident',
[WORK_ITEM_TYPE_NAME_ISSUE]: 'issue-type-issue',
[WORK_ITEM_TYPE_NAME_KEY_RESULT]: 'issue-type-keyresult',
[WORK_ITEM_TYPE_NAME_OBJECTIVE]: 'issue-type-objective',
[WORK_ITEM_TYPE_NAME_REQUIREMENTS]: 'issue-type-requirements',
[WORK_ITEM_TYPE_NAME_TASK]: 'issue-type-task',
[WORK_ITEM_TYPE_NAME_TEST_CASE]: 'issue-type-test-case',
[WORK_ITEM_TYPE_NAME_TICKET]: 'issue-type-ticket',
};
export const NAME_TO_ROUTE_MAP = {
[WORK_ITEM_TYPE_NAME_EPIC]: WORK_ITEM_TYPE_ROUTE_EPIC,
[WORK_ITEM_TYPE_NAME_ISSUE]: WORK_ITEM_TYPE_ROUTE_ISSUE,
};
export const NAME_TO_TEXT_MAP = {
[WORK_ITEM_TYPE_NAME_EPIC]: s__('WorkItem|Epic'),
[WORK_ITEM_TYPE_NAME_INCIDENT]: s__('WorkItem|Incident'),

View File

@ -6,8 +6,9 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import {
DEFAULT_PAGE_SIZE_CHILD_ITEMS,
ISSUABLE_EPIC,
NAME_TO_ENUM_MAP,
NAME_TO_ICON_MAP,
NAME_TO_ROUTE_MAP,
NEW_WORK_ITEM_GID,
NEW_WORK_ITEM_IID,
STATE_CLOSED,
@ -34,9 +35,7 @@ import {
WIDGET_TYPE_TIME_TRACKING,
WIDGET_TYPE_VULNERABILITIES,
WIDGET_TYPE_WEIGHT,
WORK_ITEM_TYPE_ENUM_EPIC,
WORK_ITEM_TYPE_ROUTE_WORK_ITEM,
WORK_ITEMS_TYPE_MAP,
} from './constants';
export const isAssigneesWidget = (widget) => widget.type === WIDGET_TYPE_ASSIGNEES;
@ -129,11 +128,6 @@ export const formatLabelForListbox = (label) => ({
export const convertTypeEnumToName = (workItemTypeEnum) =>
Object.keys(NAME_TO_ENUM_MAP).find((name) => NAME_TO_ENUM_MAP[name] === workItemTypeEnum);
export const getWorkItemIcon = (icon) => {
if (icon === ISSUABLE_EPIC) return WORK_ITEMS_TYPE_MAP[WORK_ITEM_TYPE_ENUM_EPIC].icon;
return icon;
};
/**
* TODO: Remove this method with https://gitlab.com/gitlab-org/gitlab/-/issues/479637
* We're currently setting children count per page based on `DEFAULT_PAGE_SIZE_CHILD_ITEMS`
@ -149,7 +143,7 @@ export const getDefaultHierarchyChildrenCount = () => {
export const formatAncestors = (workItem) =>
findHierarchyWidgetAncestors(workItem).map((ancestor) => ({
...ancestor,
icon: getWorkItemIcon(ancestor.workItemType?.iconName),
icon: NAME_TO_ICON_MAP[ancestor.workItemType?.name],
href: ancestor.webUrl,
}));
@ -279,11 +273,10 @@ export const markdownPreviewPath = ({ fullPath, iid, isGroup = false }) => {
};
// the path for creating a new work item of that type, e.g. /groups/gitlab-org/-/epics/new
export const newWorkItemPath = ({ fullPath, isGroup = false, workItemTypeName, query = '' }) => {
export const newWorkItemPath = ({ fullPath, isGroup = false, workItemType, query = '' }) => {
const domain = gon.relative_url_root || '';
const basePath = isGroup ? `groups/${fullPath}` : fullPath;
const type =
WORK_ITEMS_TYPE_MAP[workItemTypeName]?.routeParamName || WORK_ITEM_TYPE_ROUTE_WORK_ITEM;
const type = NAME_TO_ROUTE_MAP[workItemType] || WORK_ITEM_TYPE_ROUTE_WORK_ITEM;
return `${domain}/${basePath}/-/${type}/new${query}`;
};

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
module NumbersHelper
WORDS = %w[zero one two three four five six seven eight nine].freeze
def limited_counter_with_delimiter(resource, **options)
limit = options.fetch(:limit, 1000).to_i
count = resource.page.total_count_with_limit(:all, limit: limit)
@ -13,4 +15,10 @@ module NumbersHelper
number_with_delimiter(count, options)
end
end
def number_in_words(num)
raise ArgumentError, _('Input must be an integer between 0 and 9') unless num.between?(0, 9)
WORDS[num]
end
end

View File

@ -79,8 +79,12 @@ module Namespaces
end
end
def traversal_path
"#{traversal_ids.join('/')}/"
def traversal_path(with_organization: false)
ids = traversal_ids.clone
ids.prepend(organization_id) if with_organization
"#{ids.join('/')}/"
end
def use_traversal_ids?

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class AddMergeRequestCommitsMetadataIdOnMergeRequestDiffCommits < Gitlab::Database::Migration[2.2]
milestone '18.0'
# rubocop:disable Migration/PreventAddingColumns -- this column is required as
# we will be querying data from `merge_request_commits_metadata` table using
# this column
def change
add_column :merge_request_diff_commits, :merge_request_commits_metadata_id, :bigint, null: true
end
# rubocop:enable Migration/PreventAddingColumns
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class PrepareAsyncIndexOnMergeRequestCommitsMetadataId < Gitlab::Database::Migration[2.2]
milestone '18.0'
INDEX_NAME = 'index_mrdc_on_merge_request_commits_metadata_id'
# TODO: Index to be created synchronously in https://gitlab.com/gitlab-org/gitlab/-/issues/527227
# rubocop:disable Migration/PreventIndexCreation -- this index is required as
# we will be querying data from `merge_request_commits_metadata_id` and joining
# by this column.
def up
prepare_async_index :merge_request_diff_commits, :merge_request_commits_metadata_id, name: INDEX_NAME,
where: "merge_request_commit_metadata_id IS NOT NULL"
end
# rubocop:enable Migration/PreventIndexCreation
def down
unprepare_async_index :merge_request_diff_commits, :merge_request_commits_metadata_id, name: INDEX_NAME
end
end

View File

@ -0,0 +1 @@
b4e384b22ad5ef6a42e6ccaf60a30e7cd7a73736da14d2d871bf64c47eef9ea8

View File

@ -0,0 +1 @@
b2a963c276ecbfb2a9acdcb0506fa19581424499345504dc17c4a1ec3cffc226

View File

@ -17105,7 +17105,8 @@ CREATE TABLE merge_request_diff_commits (
message text,
trailers jsonb DEFAULT '{}'::jsonb,
commit_author_id bigint,
committer_id bigint
committer_id bigint,
merge_request_commits_metadata_id bigint
);
CREATE TABLE merge_request_diff_details (

View File

@ -125,23 +125,23 @@ To check indexing status:
{{< /tabs >}}
## Delete offline nodes automatically
## Pause indexing
Prerequisites:
- You must have administrator access to the instance.
You can automatically delete Zoekt nodes that are offline for more than 12 hours
and their related indices, repositories, and tasks.
To delete offline nodes automatically:
To pause indexing for [exact code search](../../user/search/exact_code_search.md):
1. On the left sidebar, at the bottom, select **Admin**.
1. Select **Settings > Search**.
1. Expand **Exact code search configuration**.
1. Select the **Delete offline nodes after 12 hours** checkbox.
1. Select the **Pause indexing** checkbox.
1. Select **Save changes**.
When you pause indexing for exact code search, all changes in your repository are queued.
To resume indexing, clear the **Pause indexing for exact code search** checkbox.
## Index root namespaces automatically
Prerequisites:
@ -169,22 +169,38 @@ When you disable this setting:
- Existing root namespaces remain indexed.
- New root namespaces are no longer indexed.
## Pause indexing
## Delete offline nodes automatically
Prerequisites:
- You must have administrator access to the instance.
To pause indexing for [exact code search](../../user/search/exact_code_search.md):
You can automatically delete Zoekt nodes that are offline for more than 12 hours
and their related indices, repositories, and tasks.
To delete offline nodes automatically:
1. On the left sidebar, at the bottom, select **Admin**.
1. Select **Settings > Search**.
1. Expand **Exact code search configuration**.
1. Select the **Pause indexing** checkbox.
1. Select the **Delete offline nodes after 12 hours** checkbox.
1. Select **Save changes**.
When you pause indexing for exact code search, all changes in your repository are queued.
To resume indexing, clear the **Pause indexing for exact code search** checkbox.
## Cache search results
Prerequisites:
- You must have administrator access to the instance.
You can cache search results for better performance.
This feature is enabled by default and caches results for five minutes.
To cache search results:
1. On the left sidebar, at the bottom, select **Admin**.
1. Select **Settings > Search**.
1. Expand **Exact code search configuration**.
1. Select the **Cache search results for five minutes** checkbox.
1. Select **Save changes**.
## Set concurrent indexing tasks

View File

@ -157,8 +157,8 @@ for GitLab Ultimate.
## Manage members in personal projects outside a group namespace
Personal projects are not located in top-level group namespaces. You can manage
the users in each of your personal projects, but you cannot have more than five
users in all of your personal projects.
the users in each of your personal projects. You can have more than five
users in your personal projects.
You should [move your personal project to a group](../tutorials/move_personal_project_to_group/_index.md)
so that you can:

View File

@ -2,6 +2,16 @@
Create a file in `ActiveContext::Config.migrations_path`.
## Data types
ActiveContext supports several field types for defining collection schemas:
- `bigint`: For large numeric values (accepts `index: true/false`, defaults to `false`)
- `boolean`: For boolean values (accepts `index: true/false`, defaults to `true`)
- `keyword`: For exact-match searchable string fields (always indexed, no `index` option)
- `text`: For full-text searchable content (accepts `index: true/false`, defaults to `false`)
- `vector`: For embedding vectors (accepts `index: true/false`, defaults to `true`), requires `dimensions:` specification
## Migration to create a collection
```ruby
@ -14,7 +24,9 @@ class CreateMergeRequests < ActiveContext::Migration[1.0]
create_collection :merge_requests, number_of_partitions: 3 do |c|
c.bigint :issue_id, index: true
c.bigint :namespace_id, index: true
c.prefix :traversal_ids
c.boolean :is_draft
c.keyword :traversal_ids
c.text :description
c.vector :embeddings, dimensions: 768
end
end

View File

@ -6,7 +6,7 @@ Migrations are similiar to database migrations: they create collections, update
See [migrations](migrations.md) for more details.
A migration worker will apply migrations for the active connection. See [Migrations](how_it_works.md#migrations).
A migration worker applies migrations for the active connection. See [Migrations](how_it_works.md#migrations).
If you want to run the worker manually, execute:

View File

@ -13,6 +13,10 @@ module ActiveContext
fields << Field::Bigint.new(name, index: index)
end
def boolean(name, index: true)
fields << Field::Boolean.new(name, index: index)
end
def keyword(name)
fields << Field::Keyword.new(name, index: true)
end
@ -35,6 +39,7 @@ module ActiveContext
end
class Bigint < Field; end
class Boolean < Field; end
class Keyword < Field; end
class Text < Field; end
class Vector < Field; end

View File

@ -64,6 +64,8 @@ module ActiveContext
mappings[field.name] = case field
when Field::Bigint
{ type: 'long' }
when Field::Boolean
{ type: 'boolean' }
when Field::Keyword
{ type: 'keyword' }
when Field::Text

View File

@ -72,6 +72,9 @@ module ActiveContext
when Field::Bigint
# Bigint is 8 bytes
fixed_columns << [field, 8]
when Field::Boolean
# Boolean is 1 byte
fixed_columns << [field, 1]
when Field::Keyword, Field::Text
# Text fields are variable width
variable_columns << field
@ -91,6 +94,8 @@ module ActiveContext
case field
when Field::Bigint
table.bigint(field.name, **field.options.except(:index))
when Field::Boolean
table.boolean(field.name, **field.options.except(:index))
when Field::Keyword, Field::Text
table.text(field.name, **field.options.except(:index))
when Field::Vector

View File

@ -11884,6 +11884,9 @@ msgstr ""
msgid "CVS|Only a project maintainer or owner can toggle this feature."
msgstr ""
msgid "Cache search results for %{label}"
msgstr ""
msgid "Cadence"
msgstr ""
@ -32386,6 +32389,9 @@ msgstr[1] ""
msgid "Input host keys manually"
msgstr ""
msgid "Input must be an integer between 0 and 9"
msgstr ""
msgid "Input the remote repository URL"
msgstr ""
@ -53838,9 +53844,6 @@ msgstr ""
msgid "Secrets|Delete secret"
msgstr ""
msgid "Secrets|Description must be 200 characters or less."
msgstr ""
msgid "Secrets|Edit %{id}"
msgstr ""
@ -53910,6 +53913,9 @@ msgstr ""
msgid "Secrets|The name should be unique within this project."
msgstr ""
msgid "Secrets|This field is required and must be 200 characters or less."
msgstr ""
msgid "Secrets|To confirm, enter %{secretName}:"
msgstr ""
@ -69028,9 +69034,6 @@ msgstr ""
msgid "WorkItem|Requirement"
msgstr ""
msgid "WorkItem|Requirements"
msgstr ""
msgid "WorkItem|Reset template"
msgstr ""
@ -69157,6 +69160,12 @@ msgstr ""
msgid "WorkItem|Something went wrong while fetching milestones. Please try again."
msgstr ""
msgid "WorkItem|Something went wrong while fetching more related vulnerabilities."
msgstr ""
msgid "WorkItem|Something went wrong while fetching related vulnerabilities."
msgstr ""
msgid "WorkItem|Something went wrong while fetching the %{workItemType}. Please try again."
msgstr ""

View File

@ -143,7 +143,7 @@ RSpec.describe 'Database schema',
merge_requests_compliance_violations: %w[target_project_id],
merge_request_diffs: %w[project_id],
merge_request_diff_files: %w[project_id],
merge_request_diff_commits: %w[commit_author_id committer_id],
merge_request_diff_commits: %w[commit_author_id committer_id merge_request_commits_metadata_id],
# merge_request_diff_commits_b5377a7a34 is the temporary table for the merge_request_diff_commits partitioning
# backfill. It will get foreign keys after the partitioning is finished.
merge_request_diff_commits_b5377a7a34: %w[merge_request_diff_id commit_author_id committer_id project_id],

View File

@ -7,9 +7,17 @@ import CreateWorkItem from '~/work_items/components/create_work_item.vue';
import CreateWorkItemModal from '~/work_items/components/create_work_item_modal.vue';
import {
WORK_ITEM_TYPE_NAME_EPIC,
WORK_ITEM_TYPE_NAME_INCIDENT,
WORK_ITEM_TYPE_NAME_ISSUE,
WORK_ITEM_TYPE_NAME_KEY_RESULT,
WORK_ITEM_TYPE_NAME_OBJECTIVE,
WORK_ITEM_TYPE_NAME_REQUIREMENTS,
WORK_ITEM_TYPE_NAME_TASK,
WORK_ITEM_TYPE_NAME_TEST_CASE,
WORK_ITEM_TYPE_NAME_TICKET,
WORK_ITEM_TYPE_ROUTE_EPIC,
WORK_ITEM_TYPE_ROUTE_ISSUE,
WORK_ITEM_TYPE_ROUTE_WORK_ITEM,
WORK_ITEMS_TYPE_MAP,
} from '~/work_items/constants';
import CreateWorkItemCancelConfirmationModal from '~/work_items/components/create_work_item_cancel_confirmation_modal.vue';
@ -152,24 +160,38 @@ describe('CreateWorkItemModal', () => {
expect(findCreateModal().props('visible')).toBe(false);
});
for (const values of Object.values(WORK_ITEMS_TYPE_MAP)) {
it(`has link to new work item page in modal header for ${values.value}`, async () => {
createComponent({ preselectedWorkItemType: values.value });
const routeParamName = values.routeParamName || WORK_ITEM_TYPE_ROUTE_WORK_ITEM;
it.each`
workItemType | routeParamName
${WORK_ITEM_TYPE_NAME_EPIC} | ${WORK_ITEM_TYPE_ROUTE_EPIC}
${WORK_ITEM_TYPE_NAME_ISSUE} | ${WORK_ITEM_TYPE_ROUTE_ISSUE}
${WORK_ITEM_TYPE_NAME_INCIDENT} | ${WORK_ITEM_TYPE_ROUTE_WORK_ITEM}
${WORK_ITEM_TYPE_NAME_KEY_RESULT} | ${WORK_ITEM_TYPE_ROUTE_WORK_ITEM}
${WORK_ITEM_TYPE_NAME_OBJECTIVE} | ${WORK_ITEM_TYPE_ROUTE_WORK_ITEM}
${WORK_ITEM_TYPE_NAME_REQUIREMENTS} | ${WORK_ITEM_TYPE_ROUTE_WORK_ITEM}
${WORK_ITEM_TYPE_NAME_TASK} | ${WORK_ITEM_TYPE_ROUTE_WORK_ITEM}
${WORK_ITEM_TYPE_NAME_TEST_CASE} | ${WORK_ITEM_TYPE_ROUTE_WORK_ITEM}
${WORK_ITEM_TYPE_NAME_TICKET} | ${WORK_ITEM_TYPE_ROUTE_WORK_ITEM}
`(
`has link to new work item page in modal header for $workItemType`,
async ({ workItemType, routeParamName }) => {
createComponent({ preselectedWorkItemType: workItemType });
await waitForPromises();
expect(findOpenInFullPageButton().attributes().href).toBe(
`/full-path/-/${routeParamName}/new`,
);
});
}
},
);
describe('when there is a related item', () => {
beforeEach(async () => {
createComponent({
relatedItem: { id: 'gid://gitlab/WorkItem/843', type: 'Epic', reference: 'flightjs#53' },
relatedItem: {
id: 'gid://gitlab/WorkItem/843',
type: 'Epic',
reference: 'flightjs#53',
webUrl: 'http://gdk.test:3000/flightjs/Flight',
},
});
await waitForPromises();
await nextTick();

View File

@ -21,8 +21,12 @@ import {
WORK_ITEM_TYPE_NAME_EPIC,
WORK_ITEM_TYPE_NAME_INCIDENT,
WORK_ITEM_TYPE_NAME_ISSUE,
WORK_ITEM_TYPE_NAME_KEY_RESULT,
WORK_ITEM_TYPE_NAME_OBJECTIVE,
WORK_ITEM_TYPE_NAME_REQUIREMENTS,
WORK_ITEM_TYPE_NAME_TASK,
WORK_ITEMS_TYPE_MAP,
WORK_ITEM_TYPE_NAME_TEST_CASE,
WORK_ITEM_TYPE_NAME_TICKET,
} from '~/work_items/constants';
import { setNewWorkItemCache } from '~/work_items/graphql/cache_utils';
import namespaceWorkItemTypesQuery from '~/work_items/graphql/namespace_work_item_types.query.graphql';
@ -187,14 +191,24 @@ describe('Create work item component', () => {
expect(setNewWorkItemCache).toHaveBeenCalled();
});
it.each(Object.keys(WORK_ITEMS_TYPE_MAP))(
'Clears cache on cancel for workItemType: %s with the correct data',
async (type) => {
const typeName = WORK_ITEMS_TYPE_MAP[type].value;
it.each`
workItemType
${WORK_ITEM_TYPE_NAME_EPIC}
${WORK_ITEM_TYPE_NAME_INCIDENT}
${WORK_ITEM_TYPE_NAME_ISSUE}
${WORK_ITEM_TYPE_NAME_KEY_RESULT}
${WORK_ITEM_TYPE_NAME_OBJECTIVE}
${WORK_ITEM_TYPE_NAME_REQUIREMENTS}
${WORK_ITEM_TYPE_NAME_TASK}
${WORK_ITEM_TYPE_NAME_TEST_CASE}
${WORK_ITEM_TYPE_NAME_TICKET}
`(
'Clears cache on cancel for workItemType=$workItemType with the correct data',
async ({ workItemType }) => {
const expectedWorkItemTypeData = namespaceWorkItemTypes.find(
({ name }) => name === typeName,
({ name }) => name === workItemType,
);
createComponent({ preselectedWorkItemType: typeName });
createComponent({ preselectedWorkItemType: workItemType });
await waitForPromises();
findCancelButton().vm.$emit('click');

View File

@ -115,7 +115,6 @@ describe('WorkItemCreatedUpdated component', () => {
expect(findWorkItemTypeIcon().props()).toMatchObject({
showText: true,
workItemIconName: workItem.workItemType.iconName,
workItemType: workItem.workItemType.name,
});
});

View File

@ -18,32 +18,25 @@ describe('Work Item type component', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
describe.each`
workItemType | workItemIconName | iconName | text | showTooltipOnHover | iconVariant
${'TASK'} | ${''} | ${'issue-type-task'} | ${'Task'} | ${false} | ${'default'}
${''} | ${'issue-type-task'} | ${'issue-type-task'} | ${''} | ${true} | ${'default'}
${'ISSUE'} | ${''} | ${'issue-type-issue'} | ${'Issue'} | ${true} | ${'default'}
${''} | ${'issue-type-issue'} | ${'issue-type-issue'} | ${''} | ${true} | ${'default'}
${'REQUIREMENT'} | ${''} | ${'issue-type-requirements'} | ${'Requirements'} | ${true} | ${'default'}
${'INCIDENT'} | ${''} | ${'issue-type-incident'} | ${'Incident'} | ${false} | ${'default'}
${'TEST_CASE'} | ${''} | ${'issue-type-test-case'} | ${'Test case'} | ${true} | ${'default'}
${'random-issue-type'} | ${''} | ${'issue-type-issue'} | ${''} | ${true} | ${'default'}
${'Task'} | ${''} | ${'issue-type-task'} | ${'Task'} | ${false} | ${'default'}
${'Issue'} | ${''} | ${'issue-type-issue'} | ${'Issue'} | ${true} | ${'default'}
${'Requirement'} | ${''} | ${'issue-type-requirements'} | ${'Requirements'} | ${true} | ${'default'}
${'Incident'} | ${''} | ${'issue-type-incident'} | ${'Incident'} | ${false} | ${'default'}
${'Test_case'} | ${''} | ${'issue-type-test-case'} | ${'Test case'} | ${true} | ${'default'}
${'Objective'} | ${''} | ${'issue-type-objective'} | ${'Objective'} | ${true} | ${'default'}
${'Key Result'} | ${''} | ${'issue-type-keyresult'} | ${'Key result'} | ${true} | ${'subtle'}
workItemType | iconName | text | showTooltipOnHover | iconVariant
${'TASK'} | ${'issue-type-task'} | ${'Task'} | ${false} | ${'default'}
${'ISSUE'} | ${'issue-type-issue'} | ${'Issue'} | ${true} | ${'default'}
${'REQUIREMENT'} | ${'issue-type-requirements'} | ${'Requirement'} | ${true} | ${'default'}
${'INCIDENT'} | ${'issue-type-incident'} | ${'Incident'} | ${false} | ${'default'}
${'TEST_CASE'} | ${'issue-type-test-case'} | ${'Test case'} | ${true} | ${'default'}
${'random-issue-type'} | ${'issue-type-issue'} | ${''} | ${true} | ${'default'}
${'Task'} | ${'issue-type-task'} | ${'Task'} | ${false} | ${'default'}
${'Issue'} | ${'issue-type-issue'} | ${'Issue'} | ${true} | ${'default'}
${'Requirement'} | ${'issue-type-requirements'} | ${'Requirement'} | ${true} | ${'default'}
${'Incident'} | ${'issue-type-incident'} | ${'Incident'} | ${false} | ${'default'}
${'Test_case'} | ${'issue-type-test-case'} | ${'Test case'} | ${true} | ${'default'}
${'Objective'} | ${'issue-type-objective'} | ${'Objective'} | ${true} | ${'default'}
${'Key Result'} | ${'issue-type-keyresult'} | ${'Key result'} | ${true} | ${'subtle'}
`(
'with workItemType set to "$workItemType" and workItemIconName set to "$workItemIconName"',
({ workItemType, workItemIconName, iconName, text, showTooltipOnHover, iconVariant }) => {
'with workItemType set to "$workItemType"',
({ workItemType, iconName, text, showTooltipOnHover, iconVariant }) => {
beforeEach(() => {
createComponent({
workItemType,
workItemIconName,
showTooltipOnHover,
iconVariant,
});
createComponent({ workItemType, showTooltipOnHover, iconVariant });
});
it(`renders icon with name '${iconName}'`, () => {

View File

@ -27,7 +27,6 @@ import {
markdownPreviewPath,
newWorkItemPath,
isReference,
getWorkItemIcon,
workItemRoadmapPath,
saveToggleToLocalStorage,
getToggleFromLocalStorage,
@ -185,7 +184,7 @@ describe('newWorkItemPath', () => {
it('returns correct path for workItemType', () => {
expect(
newWorkItemPath({ fullPath: 'group/project', workItemTypeName: WORK_ITEM_TYPE_ENUM_ISSUE }),
newWorkItemPath({ fullPath: 'group/project', workItemType: WORK_ITEM_TYPE_NAME_ISSUE }),
).toBe('/foobar/group/project/-/issues/new');
});
@ -194,7 +193,7 @@ describe('newWorkItemPath', () => {
newWorkItemPath({
fullPath: 'group',
isGroup: true,
workItemTypeName: WORK_ITEM_TYPE_ENUM_EPIC,
workItemType: WORK_ITEM_TYPE_NAME_EPIC,
}),
).toBe('/foobar/groups/group/-/epics/new');
});
@ -223,12 +222,6 @@ describe('convertTypeEnumToName', () => {
});
});
describe('getWorkItemIcon', () => {
it.each(['epic', 'issue-type-epic'])('returns epic icon in case of %s', (icon) => {
expect(getWorkItemIcon(icon)).toBe('epic');
});
});
describe('isReference', () => {
it.each`
referenceId | result

View File

@ -33,4 +33,17 @@ RSpec.describe NumbersHelper do
it { is_expected.to eq(expected_result) }
end
end
describe '#number_in_words' do
it 'returns the correct word for the given number' do
expect(number_in_words(0)).to eq('zero')
expect(number_in_words(1)).to eq('one')
expect(number_in_words(9)).to eq('nine')
end
it 'raises an error for numbers outside the range 0-9' do
expect { number_in_words(-1) }.to raise_error(ArgumentError, _('Input must be an integer between 0 and 9'))
expect { number_in_words(10) }.to raise_error(ArgumentError, _('Input must be an integer between 0 and 9'))
end
end
end

View File

@ -995,6 +995,26 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
end
end
describe 'traversal_path' do
it 'formats the traversal ids with slashes' do
expect(namespace.traversal_path).to eq("#{namespace.id}/")
end
context 'for subgroup' do
let(:subgroup) { Group.new(traversal_ids: [1, 2, 3], organization_id: 1111) }
it 'formats the traversal ids with slashes' do
expect(subgroup.traversal_path).to eq("1/2/3/")
end
context 'when with_organization option is enabled' do
it 'prepends the organization id' do
expect(subgroup.traversal_path(with_organization: true)).to eq("1111/1/2/3/")
end
end
end
end
describe "after_commit :expire_child_caches" do
let(:namespace) { create(:group, organization: organization) }

View File

@ -4,18 +4,25 @@ module ClickHouseHelpers
extend ActiveRecord::ConnectionAdapters::Quoting
def insert_events_into_click_house(events = Event.all)
clickhouse_fixture(:events, events.map do |event|
{
id: event.id,
path: event.project.reload.project_namespace.traversal_path,
author_id: event.author_id,
target_id: event.target_id,
target_type: event.target_type,
action: Event.actions[event.action],
created_at: event.created_at,
updated_at: event.updated_at
}
end)
# Insert into both events table until legacy table is removed
%i[events events_new].each do |clickhouse_table_name|
clickhouse_fixture(clickhouse_table_name, events.map do |event|
project_namespace = event.project.reload.project_namespace
include_organization_on_path = clickhouse_table_name == :events_new
path = project_namespace.traversal_path(with_organization: include_organization_on_path)
{
id: event.id,
path: path,
author_id: event.author_id,
target_id: event.target_id,
target_type: event.target_type,
action: Event.actions[event.action],
created_at: event.created_at,
updated_at: event.updated_at
}
end)
end
end
# rubocop:disable Metrics/CyclomaticComplexity -- the method is straightforward, just a lot of &.

View File

@ -2394,7 +2394,6 @@
- './ee/spec/workers/geo/verification_state_backfill_service_spec.rb'
- './ee/spec/workers/geo/verification_state_backfill_worker_spec.rb'
- './ee/spec/workers/geo/verification_timeout_worker_spec.rb'
- './ee/spec/workers/geo/verification_worker_spec.rb'
- './ee/spec/workers/group_saml_group_sync_worker_spec.rb'
- './ee/spec/workers/groups/create_event_worker_spec.rb'
- './ee/spec/workers/groups/export_memberships_worker_spec.rb'

View File

@ -251,7 +251,6 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'Geo::VerificationBatchWorker' => 0,
'Geo::VerificationStateBackfillWorker' => false,
'Geo::VerificationTimeoutWorker' => false,
'Geo::VerificationWorker' => 3,
'Gitlab::BitbucketImport::AdvanceStageWorker' => 6,
'Gitlab::BitbucketImport::Stage::FinishImportWorker' => 6,
'Gitlab::BitbucketImport::Stage::ImportIssuesWorker' => 6,