Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-06-20 18:23:36 +00:00
parent 21d9258e42
commit 6d391a8669
71 changed files with 1426 additions and 302 deletions

View File

@ -80,8 +80,13 @@ export default {
return formatPipelinesGraphQLDataToREST(data?.project) || [];
},
result({ data }) {
const pipelineCount = data?.project?.mergeRequest?.pipelines?.count;
this.isInitialLoading = false;
this.pageInfo = data?.project?.mergeRequest?.pipelines?.pageInfo || {};
if (pipelineCount) {
this.updateBadgeCount(pipelineCount);
}
},
error() {
this.hasError = true;
@ -214,6 +219,16 @@ export default {
this.$refs.modal.show();
}
},
updateBadgeCount(pipelineCount) {
const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
detail: { pipelineCount },
});
// Event to update the count in tabs in app/assets/javascripts/commit/pipelines/utils.js
if (this.$el?.parentElement) {
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
}
},
},
modal: {
id: 'create-pipeline-for-uork-merge-request-modal',

View File

@ -541,3 +541,23 @@ export const humanizeBranchValidationErrors = (invalidChars = []) => {
* @returns {String} String without any enclosure
*/
export const stripQuotes = (value) => value.replace(/^('|")(.*)('|")$/, '$2');
/**
* Converts a sentence to title case inspite of it being in any case
* e.g. Hello world => Hello World
* e.g HELLO WORLD => Hello World
* e.g. hello World => Hello World
* e.g. Hello world => Hello World
* e.g. Hello World => Hello World
*
* @param {String} string
* @returns {String}
*/
export const convertEachWordToTitleCase = (str) => {
return str
.toLowerCase()
.split(' ')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};

View File

@ -8,7 +8,7 @@ import {
GlBadge,
GlLink,
} from '@gitlab/ui';
import { formatDate } from '~/lib/utils/datetime_utility';
import { localeDateFormat } from '~/lib/utils/datetime_utility';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@ -116,14 +116,11 @@ export default {
// remove sha256: from the string, and show only the first 7 char
return this.tag.digest?.substring(7, 14) ?? NOT_AVAILABLE_TEXT;
},
publishDate() {
publishDateTime() {
return this.tag.publishedAt || this.tag.createdAt;
},
publishedDate() {
return formatDate(this.publishDate, 'isoDate');
},
publishedTime() {
return formatDate(this.publishDate, 'HH:MM:ss Z');
publishedDateTime() {
return localeDateFormat.asDateTimeFull.format(this.publishDateTime);
},
formattedRevision() {
// to be removed when API response is adjusted
@ -210,7 +207,7 @@ export default {
<span data-testid="time">
<gl-sprintf :message="$options.i18n.CREATED_AT_LABEL">
<template #timeInfo>
<time-ago-tooltip :time="publishDate" />
<time-ago-tooltip :time="publishDateTime" />
</template>
</gl-sprintf>
</span>
@ -243,11 +240,8 @@ export default {
<template #repositoryPath>
<i>{{ tagLocation }}</i>
</template>
<template #time>
{{ publishedTime }}
</template>
<template #date>
{{ publishedDate }}
<template #dateTime>
{{ publishedDateTime }}
</template>
</gl-sprintf>
</details-row>

View File

@ -23,7 +23,7 @@ export const TAGS_LIST_TITLE = s__('ContainerRegistry|Image tags');
export const DIGEST_LABEL = s__('ContainerRegistry|Digest: %{imageId}');
export const CREATED_AT_LABEL = s__('ContainerRegistry|Published %{timeInfo}');
export const PUBLISHED_DETAILS_ROW_TEXT = s__(
'ContainerRegistry|Published to the %{repositoryPath} image repository at %{time} on %{date}',
'ContainerRegistry|Published to the %{repositoryPath} image repository on %{dateTime}',
);
export const MANIFEST_DETAILS_ROW_TEST = s__('ContainerRegistry|Manifest digest: %{digest}');
export const CONFIGURATION_DETAILS_ROW_TEST = s__(

View File

@ -12,6 +12,7 @@ import HelpPageLink from '~/vue_shared/components/help_page_link/help_page_link.
import createProtectionRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/create_container_protection_rule.mutation.graphql';
import { s__, __ } from '~/locale';
const GRAPHQL_ACCESS_LEVEL_VALUE_NULL = null;
const GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER = 'MAINTAINER';
const GRAPHQL_ACCESS_LEVEL_VALUE_OWNER = 'OWNER';
const GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN = 'ADMIN';
@ -70,6 +71,7 @@ export default {
},
minimumAccessLevelOptions() {
return [
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_NULL, text: __('Developer (default)') },
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER, text: __('Maintainer') },
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_OWNER, text: __('Owner') },
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN, text: __('Admin') },
@ -164,7 +166,6 @@ export default {
v-model="protectionRuleFormData.minimumAccessLevelForPush"
:options="minimumAccessLevelOptions"
:disabled="isFieldDisabled"
required
/>
</gl-form-group>
@ -178,7 +179,6 @@ export default {
v-model="protectionRuleFormData.minimumAccessLevelForDelete"
:options="minimumAccessLevelOptions"
:disabled="isFieldDisabled"
required
/>
</gl-form-group>

View File

@ -134,6 +134,7 @@ export default {
},
minimumAccessLevelOptions() {
return [
{ value: undefined, text: __('Developer (default)') },
{ value: 'MAINTAINER', text: __('Maintainer') },
{ value: 'OWNER', text: __('Owner') },
{ value: 'ADMIN', text: __('Admin') },

View File

@ -14,7 +14,7 @@ import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { fetchPolicies } from '~/lib/graphql';
import { setNewWorkItemCache } from '~/work_items/graphql/cache_utils';
import { findWidget } from '~/issues/list/utils';
import { newWorkItemFullPath } from '~/work_items/utils';
import { newWorkItemFullPath, isWorkItemItemValidEnum } from '~/work_items/utils';
import {
I18N_WORK_ITEM_CREATE_BUTTON_LABEL,
I18N_WORK_ITEM_ERROR_CREATING,
@ -229,7 +229,7 @@ export default {
this.isTitleValid = Boolean(title.trim());
},
updateCache() {
if (!this.selectedWorkItemTypeId) {
if (!this.selectedWorkItemTypeId || !isWorkItemItemValidEnum(this.workItemType)) {
return;
}
setNewWorkItemCache(

View File

@ -3,6 +3,7 @@ import { GlButton, GlModal, GlDisclosureDropdownItem } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { setNewWorkItemCache } from '~/work_items/graphql/cache_utils';
import { isWorkItemItemValidEnum } from '~/work_items/utils';
import {
I18N_NEW_WORK_ITEM_BUTTON_LABEL,
I18N_WORK_ITEM_CREATED,
@ -57,6 +58,11 @@ export default {
if (!this.workItemTypes || this.workItemTypes.length === 0) {
return;
}
// We need a valid enum of fetching workItemTypes which otherwise causes issues in cache
if (!isWorkItemItemValidEnum(this.workItemTypeName)) {
return;
}
await setNewWorkItemCache(
this.isGroup,
this.fullPath,

View File

@ -5,6 +5,7 @@ import { issuesListClient } from '~/issues/list';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { getBaseURL } from '~/lib/utils/url_utility';
import { convertEachWordToTitleCase } from '~/lib/utils/text_utility';
import { findHierarchyWidgetChildren, isNotesWidget, newWorkItemFullPath } from '../utils';
import {
WIDGET_TYPE_ASSIGNEES,
@ -221,6 +222,7 @@ export const setNewWorkItemCache = async (
return;
}
const workItemTitleCase = convertEachWordToTitleCase(workItemType.split('_').join(' '));
const availableWidgets = widgetDefinitions?.flatMap((i) => i.type);
const currentUserId = convertToGraphQLId(TYPENAME_USER, gon?.current_user_id);
const baseURL = getBaseURL();
@ -436,7 +438,7 @@ export const setNewWorkItemCache = async (
},
workItemType: {
id: workItemTypeId || 'mock-work-item-type-id',
name: workItemType,
name: workItemTitleCase,
iconName: 'issue-type-epic',
__typename: 'WorkItemType',
},

View File

@ -13,6 +13,13 @@ import {
ISSUABLE_EPIC,
WORK_ITEMS_TYPE_MAP,
WORK_ITEM_TYPE_ENUM_EPIC,
WORK_ITEM_TYPE_ENUM_INCIDENT,
WORK_ITEM_TYPE_ENUM_ISSUE,
WORK_ITEM_TYPE_ENUM_TASK,
WORK_ITEM_TYPE_ENUM_TEST_CASE,
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
WORK_ITEM_TYPE_ENUM_KEY_RESULT,
WORK_ITEM_TYPE_ENUM_REQUIREMENTS,
} from './constants';
export const isAssigneesWidget = (widget) => widget.type === WIDGET_TYPE_ASSIGNEES;
@ -145,7 +152,34 @@ export const workItemRoadmapPath = (fullPath, iid) => {
return `${domain}/groups/${fullPath}/-/roadmap?epic_iid=${iid}`;
};
/**
* Builds unique path for new work item
*
* @param {string} fullPath the path to the namespace
*/
export const newWorkItemFullPath = (fullPath) => {
// eslint-disable-next-line @gitlab/require-i18n-strings
return `${fullPath}-id`;
};
/**
* Checks whether the work item type is a valid enum
*
* @param {string} workItemType
*/
export const isWorkItemItemValidEnum = (workItemType) => {
return (
[
WORK_ITEM_TYPE_ENUM_EPIC,
WORK_ITEM_TYPE_ENUM_INCIDENT,
WORK_ITEM_TYPE_ENUM_ISSUE,
WORK_ITEM_TYPE_ENUM_TASK,
WORK_ITEM_TYPE_ENUM_TEST_CASE,
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
WORK_ITEM_TYPE_ENUM_KEY_RESULT,
WORK_ITEM_TYPE_ENUM_REQUIREMENTS,
].indexOf(workItemType) >= 0
);
};

View File

@ -68,88 +68,5 @@ so that utilities can overwrite values
@apply gl-mt-0;
@apply gl-mb-5;
}
/*
our gl-border shorthands do a little more than tailwinds.
in order not to conflict with other border related properties
(color, width), we need to define them _before_ the tailwind utils.
*/
.gl-border {
@apply gl-border-gray-100;
@apply gl-border-solid;
}
.gl-border\! {
@apply gl-border !important;
@apply gl-border-gray-100 !important;
@apply gl-border-solid !important;
}
.gl-border-b {
@apply gl-border-b-gray-100;
@apply gl-border-b-solid;
}
.gl-border-b\! {
@apply gl-border-b !important;
@apply gl-border-b-gray-100 !important;
@apply gl-border-b-solid !important;
}
.gl-border-l {
@apply gl-border-l-gray-100;
@apply gl-border-l-solid;
}
.gl-border-l\! {
@apply gl-border-l !important;
@apply gl-border-l-gray-100 !important;
@apply gl-border-l-solid !important;
}
.gl-border-r {
@apply gl-border-r-gray-100;
@apply gl-border-r-solid;
}
.gl-border-r\! {
@apply gl-border-r !important;
@apply gl-border-r-gray-100 !important;
@apply gl-border-r-solid !important;
}
.gl-border-t {
@apply gl-border-t-gray-100;
@apply gl-border-t-solid;
}
.gl-border-t\! {
@apply gl-border-t !important;
@apply gl-border-t-gray-100 !important;
@apply gl-border-t-solid !important;
}
}
/*
Utilities layer:
https://tailwindcss.com/docs/adding-custom-styles#adding-custom-utilities
*/
@layer utilities {
/* the border-style utils in tailwind do not allow for top, bottom, right, left specific values */
.gl-border-b-solid {
border-bottom-style: solid;
}
.gl-border-l-solid {
border-left-style: solid;
}
.gl-border-r-solid {
border-right-style: solid;
}
.gl-border-t-solid {
border-top-style: solid;
}
}

View File

@ -5,11 +5,12 @@ module Organizations
include ::Groups::Params
feature_category :cell
urgency :low, [:create, :new]
urgency :low, [:create, :new, :destroy]
before_action :authorize_create_group!, only: [:new]
before_action :authorize_read_organization!, only: [:edit]
before_action :authorize_view_edit_page!, only: [:edit]
before_action :authorize_remove_group!, only: :destroy
def new; end
@ -26,6 +27,13 @@ module Organizations
end
end
def destroy
Groups::DestroyService.new(group, current_user).async_execute
render json: { message: format(_("Group '%{group_name}' is being deleted."), group_name: group.full_name) }
rescue Groups::DestroyService::DestroyError => error
render json: { message: error.message }, status: :unprocessable_entity
end
private
def group
@ -42,6 +50,12 @@ module Organizations
access_denied! unless can?(current_user, :view_edit_page, group)
end
def authorize_remove_group!
return render_404 if group.nil?
access_denied! unless can?(current_user, :remove_group, group)
end
end
end

View File

@ -21,23 +21,27 @@ module Mutations
argument :repository_path_pattern,
GraphQL::Types::String,
required: true,
description:
'Container repository path pattern protected by the protection rule. ' \
'For example, `my-project/my-container-*`. Wildcard character `*` allowed.'
argument :minimum_access_level_for_push,
Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
required: true,
description:
'Minimum GitLab access level to allow to push container images to the container registry. ' \
'For example, `MAINTAINER`, `OWNER`, or `ADMIN`.'
validates: { allow_blank: false },
description: copy_field_description(
Types::ContainerRegistry::Protection::RuleType,
:repository_path_pattern
)
argument :minimum_access_level_for_delete,
Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
required: true,
description:
'Minimum GitLab access level to allow to delete container images in the container registry. ' \
'For example, `MAINTAINER`, `OWNER`, or `ADMIN`.'
required: false,
description: copy_field_description(
Types::ContainerRegistry::Protection::RuleType,
:minimum_access_level_for_delete
)
argument :minimum_access_level_for_push,
Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
required: false,
description: copy_field_description(
Types::ContainerRegistry::Protection::RuleType,
:minimum_access_level_for_push
)
field :container_registry_protection_rule,
Types::ContainerRegistry::Protection::RuleType,

View File

@ -22,28 +22,28 @@ module Mutations
required: false,
validates: { allow_blank: false },
alpha: { milestone: '16.7' },
description:
'Container\'s repository path pattern of the protection rule. ' \
'For example, `my-scope/my-project/container-dev-*`. ' \
'Wildcard character `*` allowed.'
description: copy_field_description(
Types::ContainerRegistry::Protection::RuleType,
:repository_path_pattern
)
argument :minimum_access_level_for_delete,
Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
required: false,
validates: { allow_blank: false },
alpha: { milestone: '16.7' },
description:
'Minimum GitLab access level allowed to delete container images to the container registry. ' \
'For example, `MAINTAINER`, `OWNER`, or `ADMIN`.'
description: copy_field_description(
Types::ContainerRegistry::Protection::RuleType,
:minimum_access_level_for_delete
)
argument :minimum_access_level_for_push,
Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
required: false,
validates: { allow_blank: false },
alpha: { milestone: '16.7' },
description:
'Minimum GitLab access level allowed to push container images to the container registry. ' \
'For example, `MAINTAINER`, `OWNER`, or `ADMIN`.'
description: copy_field_description(
Types::ContainerRegistry::Protection::RuleType,
:minimum_access_level_for_push
)
field :container_registry_protection_rule,
Types::ContainerRegistry::Protection::RuleType,

View File

@ -24,21 +24,25 @@ module Types
'Container repository path pattern protected by the protection rule. ' \
'For example, `my-project/my-container-*`. Wildcard character `*` allowed.'
field :minimum_access_level_for_push,
Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
null: false,
alpha: { milestone: '16.6' },
description:
'Minimum GitLab access level to allow to push container images to the container registry. ' \
'For example, `MAINTAINER`, `OWNER`, or `ADMIN`.'
field :minimum_access_level_for_delete,
Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
null: false,
null: true,
alpha: { milestone: '16.6' },
description:
'Minimum GitLab access level to allow to delete container images from the container registry. ' \
'For example, `MAINTAINER`, `OWNER`, or `ADMIN`. ' \
'If the value is `nil`, the minimum access level for delete is ignored. ' \
'Users with at least the Developer role are allowed to delete container images.'
field :minimum_access_level_for_push,
Types::ContainerRegistry::Protection::RuleAccessLevelEnum,
null: true,
alpha: { milestone: '16.6' },
description:
'Minimum GitLab access level to allow to push container images to the container registry. ' \
'For example, `MAINTAINER`, `OWNER`, or `ADMIN`.'
'For example, `MAINTAINER`, `OWNER`, or `ADMIN`. ' \
'If the value is `nil`, the minimum access level for push is ignored. ' \
'Users with at least the Developer role are allowed to push container images.'
end
end
end

View File

@ -25,10 +25,9 @@ module ContainerRegistry
message:
->(_object, _data) { _('should be a valid container repository path with optional wildcard characters.') }
}
validates :minimum_access_level_for_delete, presence: true
validates :minimum_access_level_for_push, presence: true
validate :path_pattern_starts_with_project_full_path, if: :repository_path_pattern_changed?
validate :at_least_one_minimum_access_level_must_be_present
scope :for_repository_path, ->(repository_path) do
return none if repository_path.blank?
@ -47,11 +46,19 @@ module ContainerRegistry
.exists?
end
private
def path_pattern_starts_with_project_full_path
return if repository_path_pattern.downcase.starts_with?(project.full_path.downcase)
errors.add(:repository_path_pattern, :does_not_start_with_project_full_path)
end
def at_least_one_minimum_access_level_must_be_present
return unless minimum_access_level_for_delete.blank? && minimum_access_level_for_push.blank?
errors.add(:base, _('A rule must have at least a minimum access role for push or delete.'))
end
end
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
resources(:organizations, only: [:show, :index, :new, :create], param: :organization_path, module: :organizations) do
resources(:organizations, only: [:show, :index, :new], param: :organization_path, module: :organizations) do
collection do
post :preview_markdown
end
@ -14,7 +14,7 @@ resources(:organizations, only: [:show, :index, :new, :create], param: :organiza
get :general
end
resource :groups, only: [:new, :create], as: :groups_organization
resource :groups, only: [:new, :create, :destroy], as: :groups_organization
scope(
path: 'groups/*id',

View File

@ -24,7 +24,30 @@ try {
delete require.cache[path.resolve(__filename)];
}
function gitLabUIUtilities({ addUtilities }) {
function gitLabUIUtilities({ addComponents, addUtilities }) {
addComponents({
'.border': {
'border-style': 'solid',
'border-color': 'var(--gray-100, #dcdcde)',
},
'.border-t': {
'border-top-style': 'solid',
'border-top-color': 'var(--gray-100, #dcdcde)',
},
'.border-r': {
'border-right-style': 'solid',
'border-right-color': 'var(--gray-100, #dcdcde)',
},
'.border-b': {
'border-bottom-style': 'solid',
'border-bottom-color': 'var(--gray-100, #dcdcde)',
},
'.border-l': {
'border-left-style': 'solid',
'border-left-color': 'var(--gray-100, #dcdcde)',
},
});
addUtilities({
'.font-monospace': {
'font-family':
@ -38,6 +61,18 @@ function gitLabUIUtilities({ addUtilities }) {
'.wrap-anywhere': {
'overflow-wrap': 'anywhere',
},
'.border-b-solid': {
'border-bottom-style': 'solid',
},
'.border-l-solid': {
'border-left-style': 'solid',
},
'.border-r-solid': {
'border-right-style': 'solid',
},
'.border-t-solid': {
'border-top-style': 'solid',
},
});
}

View File

@ -104,7 +104,6 @@
published_at: 2024-05-16
release: 17.0
- name: New usage overview panel in the Value Streams Dashboard
description: |
We enhanced the Value Streams Dashboard with an Overview panel. This new visualization addresses the need for executive-level insights into software delivery performance, and gives a clear picture of GitLab usage in the context of software development life cycle (SDLC).

View File

@ -0,0 +1,113 @@
- name: Model registry available in beta
description: |
GitLab now officially supports model registry in beta as a first-class concept. You can add and edit models directly via the UI, or use the MLflow integration to use GitLab as a model registry backend.
A model registry is a hub that helps data science teams manage machine learning models and their related metadata. It serves as a centralized location for organizations to store, version, document, and discover trained machine learning models. It ensures better collaboration, reproducibility, and governance over the entire model lifecycle.
We think of the model registry as a cornerstone concept that enables teams to collaborate, deploy, monitor, and continuously train models, and are very interested in your feedback. Please feel free to drop us a note in our [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/465405) and we'll get back in touch!
stage: modelops
self-managed: true
gitlab-com: true
available_in: [Free, Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/project/ml/model_registry/
image_url: https://img.youtube.com/vi/8uyxk0vhifE/hqdefault.jpg
published_at: 2024-06-20
release: 17.1
- name: See multiple GitLab Duo Code Suggestions in VS Code
description: |
GitLab Duo Code Suggestions in VS Code will now show you if there are multiple suggestions available. Simply hover over the suggestion and use the arrows or keyboard shortcut to cycle through the suggestions.
stage: create
self-managed: true
gitlab-com: true
available_in: [Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/project/repository/code_suggestions/supported_extensions.html#view-multiple-code-suggestions
image_url: https://about.gitlab.com/images/17_1/multiple-code-suggestions-vs-code.png
published_at: 2024-06-20
release: 17.1
- name: Secret Push Protection available in beta
description: |
If a secret, like a key or an API token, is accidentally committed to a Git repository, anyone with repository access can impersonate the user of the secret for malicious purposes. To address this risk, most organizations require exposed secrets to be revoked and replaced, but you can save remediation time and reduce risk by preventing secrets from being pushed in the first place.
Secret push protection checks the content of each commit pushed to GitLab. [If any secrets are detected](https://docs.gitlab.com/ee/user/application_security/secret_detection/secret_push_protection/detected_secrets.html), the push is blocked and displays information about the commit, including:
- The commit ID that contains the secret.
- The filename and line number that contains the secret.
- The type of secret.
Need to bypass secret push protection for testing? When you skip secret push detection, GitLab logs an audit event so you can investigate.
Secret push protection is available on GitLab.com and for Dedicated customers as a [Beta](https://docs.gitlab.com/ee/policy/experiment-beta-support.html#beta) feature and can be enabled on a [per project basis](https://docs.gitlab.com/ee/user/application_security/secret_detection/secret_push_protection/index.html#enable-secret-push-protection-in-a-project). You can help us improve secret push protection by providing feedback in [issue 467408](https://gitlab.com/gitlab-org/gitlab/-/issues/467408).
stage: secure
self-managed: false
gitlab-com: true
available_in: [Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/application_security/secret_detection/secret_push_protection
image_url: https://img.youtube.com/vi/2jBC3uBUlyU/hqdefault.jpg
published_at: 2024-06-20
release: 17.1
- name: GitLab Runner Autoscaler is generally available
description: |
In earlier versions of GitLab, some customers needed an autoscaling solution for GitLab Runner on virtual machine instances on public cloud platforms. These customers had to rely on the legacy [Docker Machine executor](https://docs.gitlab.com/runner/configuration/autoscale.html) or custom solutions stitched together by using cloud provider technologies.
Today, we're pleased to announce the general availability of the GitLab Runner Autoscaler. The GitLab Runner Autoscaler is composed of GitLab-developed taskscaler and [fleeting](https://docs.gitlab.com/runner/fleet_scaling/fleeting.html) technologies and the cloud provider plugin for Google Compute Engine.
stage: verify
self-managed: true
gitlab-com: true
available_in: [Free, Premium, Ultimate]
documentation_link: https://docs.gitlab.com/runner/runner_autoscale/
image_url: https://about.gitlab.com/images/17_1/runner_fleeting_ga.png
published_at: 2024-06-20
release: 17.1
- name: Improved wiki user experience
description: |
The wiki feature in GitLab 17.1 provides a more unified and efficient workflow:
- [Easier and quicker cloning](https://gitlab.com/gitlab-org/gitlab/-/issues/281830) with a new repository clone button. This improves collaboration, and speeds up access to the wiki content for editing or viewing.
- [A more obvious delete option](https://gitlab.com/gitlab-org/gitlab/-/issues/335169) in a more discoverable location. This reduces the time spent searching for it, and minimizes potential errors or confusion when managing wiki pages.
- [Allowing empty pages to be valid](https://gitlab.com/gitlab-org/gitlab/-/issues/221061), improving flexibility. Create empty placeholders when you need them. Focus on better planning and organization of wiki content, and fill in the empty pages later.
These enhancements improve ease of use, discoverability, and content management in your wiki's workflow. We want your wiki experience to be efficient and user-friendly. By making cloning repositories more accessible, relocating key options for better visibility, and allowing for the creation of empty placeholders, we're refining our platform to better meet your users' needs.
stage: plan
self-managed: true
gitlab-com: true
available_in: [Free, Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/project/wiki/
image_url: https://img.youtube.com/vi/t2z7sZoJ6oE/hqdefault.jpg
published_at: 2024-06-20
release: 17.1
- name: Container images linked to signatures
description: |
The GitLab container registry now associates signed container images with their signatures. With this improvement, users can more easily:
- Identify which images are signed and which are not.
- Find and validate the signatures that are associated with a container image.
This improvement is generally available only on GitLab.com. Self-managed support is in beta and requires users to enable the
[next-generation container registry](https://docs.gitlab.com/ee/administration/packages/container_registry_metadata_database.html), which is also in beta.
stage: package
self-managed: false
gitlab-com: true
available_in: [Free, Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/packages/container_registry#container-image-signatures
image_url: https://about.gitlab.com/images/17_1/container-registry-signatures.png
published_at: 2024-06-20
release: 17.1
- name: Runner fleet dashboard for groups
description: |
Operators of self-managed runner fleets at the group level need observability and the ability to quickly answer critical questions about their runner fleet infrastructure at a glance. With the runner fleet dashboard for groups, you directly have runner fleet observability and actionable insights in the GitLab UI. You can now quickly determine the runner health, and gain insights into runner usage metrics as well as CI/CD job queue service capabilities, in your organization's target service-level objectives.
Customers on GitLab.com can use all of the fleet dashboard metrics available for groups today. Self-managed customers can use most of the fleet dashboard metrics, but must configure the ClickHouse analytics database to use the **Runner usage** and **Wait time to pick a job** metrics.
stage: verify
self-managed: true
gitlab-com: true
available_in: [Ultimate]
documentation_link: https://docs.gitlab.com/ee/ci/runners/runner_fleet_dashboard_groups.html
image_url: https://about.gitlab.com/images/17_1/runners-fleet-dashboard-groups-beta.png
published_at: 2024-06-20
release: 17.1

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
class AddMultiNotNullConstraintOnContainerProtectionRulesMinimumAccessLevels < Gitlab::Database::Migration[2.2]
milestone '17.1'
disable_ddl_transaction!
TABLE_NAME = :container_registry_protection_rules
def up
add_multi_column_not_null_constraint(TABLE_NAME,
:minimum_access_level_for_push,
:minimum_access_level_for_delete,
operator: '>',
limit: 0
)
end
def down
remove_multi_column_not_null_constraint(TABLE_NAME,
:minimum_access_level_for_push,
:minimum_access_level_for_delete
)
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class RemoveNotNullConstraintOnContainerProtectionRulesMinimumAccessLevels < Gitlab::Database::Migration[2.2]
milestone '17.1'
disable_ddl_transaction!
TABLE_NAME = :container_registry_protection_rules
def up
remove_not_null_constraint TABLE_NAME, :minimum_access_level_for_push
remove_not_null_constraint TABLE_NAME, :minimum_access_level_for_delete
end
def down
add_not_null_constraint TABLE_NAME, :minimum_access_level_for_push
add_not_null_constraint TABLE_NAME, :minimum_access_level_for_delete
end
end

View File

@ -0,0 +1 @@
99ee3b2175a18c0199b0080bc199da7c1ddbb9e28ac1fdaa7e97da3382e17c67

View File

@ -0,0 +1 @@
1e15c96cb7a9b2e2055a5f551a835cb4d5c9b337858ff1a5afb73c1deee549c0

View File

@ -8597,10 +8597,9 @@ CREATE TABLE container_registry_protection_rules (
repository_path_pattern text,
minimum_access_level_for_push smallint,
minimum_access_level_for_delete smallint,
CONSTRAINT check_0dc4ab5f43 CHECK ((num_nonnulls(minimum_access_level_for_delete, minimum_access_level_for_push) > 0)),
CONSTRAINT check_3658b31291 CHECK ((repository_path_pattern IS NOT NULL)),
CONSTRAINT check_d53a270af5 CHECK ((char_length(repository_path_pattern) <= 255)),
CONSTRAINT check_d82c1eb825 CHECK ((minimum_access_level_for_delete IS NOT NULL)),
CONSTRAINT check_f684912b48 CHECK ((minimum_access_level_for_push IS NOT NULL))
CONSTRAINT check_d53a270af5 CHECK ((char_length(repository_path_pattern) <= 255))
);
CREATE SEQUENCE container_registry_protection_rules_id_seq

View File

@ -113,7 +113,7 @@ four standard [pagination arguments](#pagination-arguments):
### `Query.aiSelfHostedModels`
List of Self-Hosted LLM servers.
List of self-hosted LLM servers.
DETAILS:
**Introduced** in GitLab 17.1.
@ -1610,6 +1610,32 @@ Input type: `AiAgentUpdateInput`
| <a id="mutationaiagentupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationaiagentupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.aiSelfHostedModelCreate`
DETAILS:
**Introduced** in GitLab 17.1.
**Status**: Experiment.
Input type: `AiSelfHostedModelCreateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationaiselfhostedmodelcreateapitoken"></a>`apiToken` | [`String`](#string) | API token to access the self-hosted model, if any. |
| <a id="mutationaiselfhostedmodelcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationaiselfhostedmodelcreateendpoint"></a>`endpoint` | [`String!`](#string) | Endpoint of the self-hosted model. |
| <a id="mutationaiselfhostedmodelcreatemodel"></a>`model` | [`AiAcceptedSelfHostedModels!`](#aiacceptedselfhostedmodels) | AI model deployed. |
| <a id="mutationaiselfhostedmodelcreatename"></a>`name` | [`String!`](#string) | Deployment name of the self-hosted model. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationaiselfhostedmodelcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationaiselfhostedmodelcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationaiselfhostedmodelcreateselfhostedmodel"></a>`selfHostedModel` | [`AiSelfHostedModel`](#aiselfhostedmodel) | Created self-hosted model. |
### `Mutation.alertSetAssignees`
Input type: `AlertSetAssigneesInput`
@ -3216,10 +3242,10 @@ Input type: `CreateContainerRegistryProtectionRuleInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationcreatecontainerregistryprotectionruleclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationcreatecontainerregistryprotectionruleminimumaccesslevelfordelete"></a>`minimumAccessLevelForDelete` | [`ContainerRegistryProtectionRuleAccessLevel!`](#containerregistryprotectionruleaccesslevel) | Minimum GitLab access level to allow to delete container images in the container registry. For example, `MAINTAINER`, `OWNER`, or `ADMIN`. |
| <a id="mutationcreatecontainerregistryprotectionruleminimumaccesslevelforpush"></a>`minimumAccessLevelForPush` | [`ContainerRegistryProtectionRuleAccessLevel!`](#containerregistryprotectionruleaccesslevel) | Minimum GitLab access level to allow to push container images to the container registry. For example, `MAINTAINER`, `OWNER`, or `ADMIN`. |
| <a id="mutationcreatecontainerregistryprotectionruleminimumaccesslevelfordelete"></a>`minimumAccessLevelForDelete` | [`ContainerRegistryProtectionRuleAccessLevel`](#containerregistryprotectionruleaccesslevel) | Minimum GitLab access level to allow to delete container images from the container registry. For example, `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, the minimum access level for delete is ignored. Users with at least the Developer role are allowed to delete container images. Introduced in GitLab 16.6: **Status**: Experiment. |
| <a id="mutationcreatecontainerregistryprotectionruleminimumaccesslevelforpush"></a>`minimumAccessLevelForPush` | [`ContainerRegistryProtectionRuleAccessLevel`](#containerregistryprotectionruleaccesslevel) | Minimum GitLab access level to allow to push container images to the container registry. For example, `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, the minimum access level for push is ignored. Users with at least the Developer role are allowed to push container images. Introduced in GitLab 16.6: **Status**: Experiment. |
| <a id="mutationcreatecontainerregistryprotectionruleprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project where a protection rule is located. |
| <a id="mutationcreatecontainerregistryprotectionrulerepositorypathpattern"></a>`repositoryPathPattern` | [`String!`](#string) | Container repository path pattern protected by the protection rule. For example, `my-project/my-container-*`. Wildcard character `*` allowed. |
| <a id="mutationcreatecontainerregistryprotectionrulerepositorypathpattern"></a>`repositoryPathPattern` | [`String!`](#string) | Container repository path pattern protected by the protection rule. For example, `my-project/my-container-*`. Wildcard character `*` allowed. Introduced in GitLab 16.6: **Status**: Experiment. |
#### Fields
@ -16465,13 +16491,13 @@ Self-hosted LLM servers.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="aiselfhostedmodelcreatedat"></a>`createdAt` | [`Time!`](#time) | Date of creation. |
| <a id="aiselfhostedmodelendpoint"></a>`endpoint` | [`String!`](#string) | Endpoint of the Self-Hosted model server. |
| <a id="aiselfhostedmodelhasapitoken"></a>`hasApiToken` | [`Boolean!`](#boolean) | Indicates if an API key is set for the Self-Hosted model server. |
| <a id="aiselfhostedmodelid"></a>`id` | [`AiSelfHostedModelID!`](#aiselfhostedmodelid) | ID of the Self-Hosted model server. |
| <a id="aiselfhostedmodelmodel"></a>`model` | [`String!`](#string) | Model running the Self-Hosted model server. |
| <a id="aiselfhostedmodelmodifiedat"></a>`modifiedAt` | [`Time!`](#time) | Date of last modification. |
| <a id="aiselfhostedmodelname"></a>`name` | [`String!`](#string) | Given name of the Self-Hosted model server. |
| <a id="aiselfhostedmodelcreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of creation. |
| <a id="aiselfhostedmodelendpoint"></a>`endpoint` | [`String!`](#string) | Endpoint of the self-hosted model server. |
| <a id="aiselfhostedmodelhasapitoken"></a>`hasApiToken` | [`Boolean!`](#boolean) | Indicates if an API key is set for the self-hosted model server. |
| <a id="aiselfhostedmodelid"></a>`id` | [`AiSelfHostedModelID!`](#aiselfhostedmodelid) | ID of the self-hosted model server. |
| <a id="aiselfhostedmodelmodel"></a>`model` | [`String!`](#string) | AI model deployed. |
| <a id="aiselfhostedmodelname"></a>`name` | [`String!`](#string) | Deployment name of the self-hosted model. |
| <a id="aiselfhostedmodelupdatedat"></a>`updatedAt` | [`Time`](#time) | Timestamp of last update. |
### `AlertManagementAlert`
@ -18812,8 +18838,8 @@ A container registry protection rule designed to prevent users with a certain ac
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="containerregistryprotectionruleid"></a>`id` **{warning-solid}** | [`ContainerRegistryProtectionRuleID!`](#containerregistryprotectionruleid) | **Introduced** in GitLab 16.6. **Status**: Experiment. ID of the container registry protection rule. |
| <a id="containerregistryprotectionruleminimumaccesslevelfordelete"></a>`minimumAccessLevelForDelete` **{warning-solid}** | [`ContainerRegistryProtectionRuleAccessLevel!`](#containerregistryprotectionruleaccesslevel) | **Introduced** in GitLab 16.6. **Status**: Experiment. Minimum GitLab access level to allow to push container images to the container registry. For example, `MAINTAINER`, `OWNER`, or `ADMIN`. |
| <a id="containerregistryprotectionruleminimumaccesslevelforpush"></a>`minimumAccessLevelForPush` **{warning-solid}** | [`ContainerRegistryProtectionRuleAccessLevel!`](#containerregistryprotectionruleaccesslevel) | **Introduced** in GitLab 16.6. **Status**: Experiment. Minimum GitLab access level to allow to push container images to the container registry. For example, `MAINTAINER`, `OWNER`, or `ADMIN`. |
| <a id="containerregistryprotectionruleminimumaccesslevelfordelete"></a>`minimumAccessLevelForDelete` **{warning-solid}** | [`ContainerRegistryProtectionRuleAccessLevel`](#containerregistryprotectionruleaccesslevel) | **Introduced** in GitLab 16.6. **Status**: Experiment. Minimum GitLab access level to allow to delete container images from the container registry. For example, `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, the minimum access level for delete is ignored. Users with at least the Developer role are allowed to delete container images. |
| <a id="containerregistryprotectionruleminimumaccesslevelforpush"></a>`minimumAccessLevelForPush` **{warning-solid}** | [`ContainerRegistryProtectionRuleAccessLevel`](#containerregistryprotectionruleaccesslevel) | **Introduced** in GitLab 16.6. **Status**: Experiment. Minimum GitLab access level to allow to push container images to the container registry. For example, `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, the minimum access level for push is ignored. Users with at least the Developer role are allowed to push container images. |
| <a id="containerregistryprotectionrulerepositorypathpattern"></a>`repositoryPathPattern` **{warning-solid}** | [`String!`](#string) | **Introduced** in GitLab 16.6. **Status**: Experiment. Container repository path pattern protected by the protection rule. For example, `my-project/my-container-*`. Wildcard character `*` allowed. |
### `ContainerRepository`
@ -33144,6 +33170,16 @@ Agent token statuses.
| <a id="agenttokenstatusactive"></a>`ACTIVE` | Active agent token. |
| <a id="agenttokenstatusrevoked"></a>`REVOKED` | Revoked agent token. |
### `AiAcceptedSelfHostedModels`
LLMs supported by the self-hosted model features.
| Value | Description |
| ----- | ----------- |
| <a id="aiacceptedselfhostedmodelscodegemma"></a>`CODEGEMMA` | CodeGemma 2b or 7b model. |
| <a id="aiacceptedselfhostedmodelsmistral"></a>`MISTRAL` | Mistral7B model from Mistral AI. |
| <a id="aiacceptedselfhostedmodelsmixtral"></a>`MIXTRAL` | Mixtral8x22B model from Mistral AI. |
### `AiAction`
Action to subscribe to.

View File

@ -0,0 +1,17 @@
---
stage: Solutions Architecture
group: Solutions Architecture
info: This page is owned by the Solutions Architecture team.
---
# Coding Languages
This section provides resource indexes and enablements around individual coding languages including any ecosystem technologies that surround them (e.g. dependency packaging frameworks)
## Relationship to documentation
While information in this section gives valuable and qualified guidance on ways to solve problems by using the GitLab platform, the product documentation is the authoritative reference for product features and functions.
## Solutions categories
[Rust Language Solutions](rust/index.md)

View File

@ -0,0 +1,45 @@
---
stage: Solutions Architecture
group: Solutions Architecture
info: This page is owned by the Solutions Architecture team.
---
# Rust Language and EcoSytem Solutions Index
Learn how to GitLab supports the Rust ecosystem.
Unless otherwise noted, all of this content applies to both GitLab.com and self-managed instances.
This page attempts to index the ways in which GitLab supports Rust. It does so whether the integration is the result of configuring general functionality, was built in to Rust or GitLab or is provided as a solution.
| Text Tag | Configuration / Built / Solution | Support/Maintenance |
| ------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| `[Rust Configuration]` | Integration accomplished by Configuring Existing Rust Functionality | Rust |
| `[GitLab Configuration]` | Integration accomplished by Configuring Existing GitLab Functionality | GitLab |
| `[Rust Built]` | Built into Rust by Product Team to Address Rust Integration | Rust |
| `[GitLab Built]` | Built into GitLab by Product Team to Address Rust Integration | GitLab |
| `[Rust Solution]` | Built as Solution Example by Rust or Rust Partners | Community/Example |
| `[GitLab Solution]` | Built as Solution Example by GitLab or GitLab Partners | Community/Example |
| `[CI Solution]` | Built using GitLab CI and therefore <br />more customer customizable. | Items tagged `[CI Solution]` will <br />also carry one of the other tags <br />that indicate the maintenance status. |
## Rust SCM
- [GitLab DUO Code Suggestions](../../../user/project/repository/code_suggestions/supported_extensions.md#supported-languages) `[GitLab Built]`
## Rust CI
- [Unit Testing Results](../../../ci/testing/unit_test_report_examples.md#rust) `[GitLab Built]`
- [GitLab CICD Rust Component](https://gitlab.com/explore/catalog/components/rust) `[GitLab Built]`
- [Using Rust Component](../../../ci/components/examples.md#example-test-a-rust-language-cicd-component) `[GitLab Built]`
## Rust CD
- GitLab Package Registry Support for Cargo - [Open for Contributions](https://gitlab.com/gitlab-org/gitlab/-/issues/33060)
- [GitLab CICD Rust Component (Currently in Prerelease)](https://gitlab.com/explore/catalog/components/rust) `[GitLab Built]`
- [How To Use the Rust Component](../../../ci/components/examples.md#example-test-a-rust-language-cicd-component) `[GitLab Built]`
## Rust Security and SBOM
- [Testing Code Coverage](../../../ci/testing/code_coverage.md#test-coverage-examples) `[GitLab Built]`
- [GitLab SAST Scanning](../../../user/application_security/sast/index.md#supported-languages-and-frameworks) `[GitLab Built]`- requires custom ruleset be created.
- [Rust License Scanning (Currently in Prerelease)](https://gitlab.com/groups/gitlab-org/-/epics/13093) `[GitLab Built]`

View File

@ -104,7 +104,8 @@ When secret push protection blocks a push, you can either:
### Remove the secret
Remove a blocked secret to allow the commit to be pushed to GitLab. The method of removing the
secret depends on how recently it was committed.
secret depends on how recently it was committed. The instructions below use the Git CLI client,
but you can achieve the same result by using another Git client.
If the blocked secret was added with the most recent commit on your branch:

View File

@ -44,6 +44,38 @@ Visual Studio support is
You can express interest in other IDE extension support
[in this issue](https://gitlab.com/gitlab-org/editor-extensions/meta/-/issues/78).
## The context Chat is aware of
GitLab Duo Chat is sometimes aware of the context you're working in.
Other times, you must be more specific with your request.
In the GitLab UI, GitLab Duo Chat knows about these areas:
| Area | How to ask Chat |
|---------|------------------|
| Epics | From the epic, ask about `this epic`, `this`, or the URL. From any UI area, ask about the URL. |
| Issues | From the issue, ask about `this issue`, `this`, or the URL. From any UI area, ask about the URL. |
| Code files | From the single file, ask about `this code` or `this file`. From any UI area, ask about the URL. |
In the IDEs, GitLab Duo Chat knows about these areas:
| Area | How to ask Chat |
|---------|------------------|
| Selected lines in the editor | With the lines selected, ask about `this code` or `this file`. Chat is not aware of the file; you must select the lines you want to ask about. |
| Epics | Ask about the URL. |
| Issues | Ask about the URL. |
In addition, in the IDEs, when you use any of the slash commands,
like `/explain`, `/refactor`, or `/tests,` Duo Chat has access to the
code you selected.
Duo Chat always has access to:
- GitLab documentation.
- General programming and coding knowledge.
We are continuously working to expand contextual awareness of Chat to include more types of content.
## Use GitLab Duo Chat in the GitLab UI
1. In the upper-right corner, select **GitLab Duo Chat**. A drawer opens on the right side of your screen.

View File

@ -56,6 +56,9 @@ To add previously merged commits to a merge request for more context:
1. Select the commits that you want to add.
1. Select **Save changes**.
Previously merged commits are referred to as **Context Commits** in
the [API](../../../api/merge_request_context_commits.md).
## Add a comment to a commit
WARNING:

View File

@ -367,12 +367,9 @@ To create a new branch with protections:
base the new branch on. If you [require everyone to submit merge requests for a protected branch](#require-everyone-to-submit-merge-requests-for-a-protected-branch), only existing protected branches and commits
that are already in protected branches are accepted.
You can also use these methods to create a branch with protections:
You can also use [the Branches API](../../api/branches.md) to create a branch with protections.
- [The Branches API](../../api/branches.md), if
[everyone is required to submit merge requests for a protected branch](#require-everyone-to-submit-merge-requests-for-a-protected-branch).
- From the command line or from a Git client application, if branch protection is configured to
[allow everyone to push directly to a protected branch](#allow-everyone-to-push-directly-to-a-protected-branch).
If branch protection is configured to [allow everyone to push directly to a protected branch](#allow-everyone-to-push-directly-to-a-protected-branch), a branch with protections can also be created from the command line or a Git client application.
## Delete a protected branch

View File

@ -31,7 +31,7 @@ module Gitlab
end
end
def build
def build_order
order = if order_values.empty?
primary_key_descending_order
elsif Gitlab::Pagination::Keyset::Order.keyset_aware?(scope)
@ -56,7 +56,12 @@ module Gitlab
columns_with_tie_breaker_order(order_values[0...-1], tie_breaker_column_order)
end
order ? [scope.reorder!(order), true] : [scope, false] # [scope, success]
order ? [order, true] : [nil, false]
end
def build
keyset_order, success = build_order
success ? [scope.reorder!(keyset_order), success] : [scope, false]
end
private

View File

@ -0,0 +1,176 @@
# frozen_string_literal: true
module Gitlab
module Pagination
module Offset
# rubocop: disable CodeReuse/ActiveRecord -- Generic code for pagination
#
# This class can build optimized offset queries where we try to force the DB to use
# index-only-scan for skipping the OFFSET rows by selecting the columns in the ORDER BY clause explicitly.
# The selected rows will be fully loaded from the table using a LATERAL SELECT.
#
# The class can be used with any ActiveRecord scope however, the optimization will be only applied when:
# - `ORDER BY` clause is present
# - The columns in an `ORDER BY` point to one distinct record -> the `ORDER BY` clause can be keyset paginated
#
# Usage:
#
# scope = Issue.where(project_id: 1).order(:id)
#
# records = PaginationWithIndexOnlyScan.new(scope: scope, page: 5, per_page: 100).pageinate_with_kaminari
# puts records.to_a
# puts records.total_count
class PaginationWithIndexOnlyScan
CTE_NAME = :index_only_scan_pagination_cte
SUBQUERY_NAME = :index_only_scan_subquery
def initialize(scope:, page:, per_page:)
@scope = scope
@page = page
@per_page = per_page
@model = scope.model
@original_order_values = scope.order_values
end
def pageinate_with_kaminari
original_kaminari_query = scope.page(page).per(per_page)
# Check for keyset pagination support
if keyset_aware_scope && (keyset_order_by_columns.size == original_order_by_columns.size)
original_kaminari_query.extend(build_module_for_load)
end
original_kaminari_query
end
private
attr_reader :scope, :page, :per_page, :model, :original_order_values
def build_module_for_load
optimized_scope = build_optimized_scope
.includes(scope.includes_values)
.preload(scope.preload_values)
.eager_load(scope.eager_load_values)
.select(scope.select_values)
# Extend the current ActiveRecord::Relation and override the load method where we
# use our optimized query to load the actual records.
#
# Reason 1: total_count query
#
# Kaminari uses the given scope to build the count query for calculating the total
# number of pages. This data will be used on the UI and also in the REST API for providing
# pagination headers. By only modifying the `load` method, the `COUNT` query is not
# going to change which is desired since the "original" `COUNT` query is more efficient.
#
# Reason 2: preserving offset value
#
# The optimized query has the ORDER BY, LIMIT and OFFSET clauses in a subquery which
# makes building the correct pagination headers impossible. Kaminari calls offset_value
# and limit_value on the original scope.
Module.new do
define_method :load do |&block|
if !loaded? || scheduled?
@records = optimized_scope.to_a
@loaded = true
end
super(&block)
end
end
end
def build_optimized_scope
index_only_scan_query = Arel::Table.new(CTE_NAME)
.project(*keyset_order_by_columns.map(&:attribute_name))
.as(SUBQUERY_NAME.to_s)
.to_sql
row_loader_query = select_rows_from_cte
from = [
index_only_scan_query,
"LATERAL (#{row_loader_query.to_sql}) #{model.quoted_table_name}"
].join(', ')
build_cte
.apply_to(model.where({}))
.from(from)
end
def select_rows_from_cte
inner_table = Arel::Table.new(SUBQUERY_NAME)
lateral_scope = model.limit(1).select(scope.select_values)
keyset_order_by_columns.each do |column|
eq_condition = column.column_expression.eq(inner_table[column.attribute_name])
# If the column is nullable we have to do two lookups:
# - use = filter to handle the case where the selected value is not NULL
# - use IS NULL filter to handle the case where the selected value is NULL
lateral_scope = if column.nullable?
not_null_condition = Arel::Nodes::And.new([
eq_condition,
inner_table[column.attribute_name].not_eq(nil)
])
null_condition = Arel::Nodes::And.new([
column.column_expression.eq(nil),
inner_table[column.attribute_name].eq(nil)
])
conditions = Arel::Nodes::Grouping.new([
Arel::Nodes::Or.new(not_null_condition, null_condition)
])
lateral_scope.where(conditions)
else
lateral_scope.where(eq_condition)
end
end
lateral_scope
end
def build_cte
select_list = keyset_order_by_columns.map do |column|
column.column_expression.as(column.attribute_name)
end
inner_scope = keyset_aware_scope.reselect(*select_list)
# Build kaminari-based offset-pagination, ideally this should make an index only scan
inner_scope = inner_scope.page(page).per(per_page)
Gitlab::SQL::CTE.new(CTE_NAME, inner_scope)
end
def keyset_aware_scope
return @keyset_aware_scope if defined?(@keyset_aware_scope)
order, success = Gitlab::Pagination::Keyset::SimpleOrderBuilder
.new(scope: scope)
.build_order
@keyset_aware_scope = scope.reorder(order) if success
end
def keyset_order
@keyset_order ||= Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(keyset_aware_scope)
end
def keyset_order_by_columns
keyset_order.column_definitions
end
def original_order_by_columns
if original_order_values.first.is_a?(Gitlab::Pagination::Keyset::Order) && original_order_values.one?
original_order_values.first.column_definitions
else
original_order_values
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
end

View File

@ -1480,6 +1480,9 @@ msgstr ""
msgid "'%{data_pointer}' must be a valid '%{type}'"
msgstr ""
msgid "'%{group_name}' has been scheduled for removal on %{removal_time}."
msgstr ""
msgid "'%{level}' is not a valid visibility level"
msgstr ""
@ -2034,6 +2037,9 @@ msgstr ""
msgid "A release with a date in the future is labeled as an %{linkStart}Upcoming Release%{linkEnd}."
msgstr ""
msgid "A rule must have at least a minimum access role for push or delete."
msgstr ""
msgid "A sign-in to your account has been made from the following IP address: %{ip}"
msgstr ""
@ -14370,7 +14376,7 @@ msgstr ""
msgid "ContainerRegistry|Published %{timeInfo}"
msgstr ""
msgid "ContainerRegistry|Published to the %{repositoryPath} image repository at %{time} on %{date}"
msgid "ContainerRegistry|Published to the %{repositoryPath} image repository on %{dateTime}"
msgstr ""
msgid "ContainerRegistry|Push an image"
@ -18646,6 +18652,9 @@ msgstr ""
msgid "Developer"
msgstr ""
msgid "Developer (default)"
msgstr ""
msgid "Development widget is not enabled for this work item type"
msgstr ""

View File

@ -65,7 +65,7 @@
"@gitlab/cluster-client": "^2.2.0",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
"@gitlab/ui": "81.1.0",
"@gitlab/ui": "82.0.1",
"@gitlab/svgs": "3.103.0",
"@gitlab/web-ide": "^0.0.1-dev-20240613133550",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",

View File

@ -114,7 +114,7 @@ module QA
payload = post_body.is_a?(String) ? { query: post_body } : post_body
graphql_response = post(Runtime::API::Request.new(api_client, post_path).url, payload, args)
body = flatten_hash(parse_body(graphql_response))
body = extract_graphql_body(graphql_response)
unless graphql_response.code == HTTP_STATUS_OK && (body[:errors].nil? || body[:errors].empty?)
action = post_body =~ /mutation {\s+destroy/ ? 'Deletion' : 'Fabrication'
@ -124,7 +124,7 @@ module QA
MSG
end
body[:id] = body.fetch(:id).split('/').last if body.key?(:id)
body[:id] = extract_graphql_id(body) if body.key?(:id)
body.deep_transform_keys { |key| key.to_s.underscore.to_sym }
else
@ -220,10 +220,10 @@ module QA
end.join('&').prepend('?').chomp('?') # prepend `?` unless the string is blank
end
def flatten_hash(param)
param.each_pair.reduce({}) do |a, (k, v)|
v.is_a?(Hash) ? a.merge(flatten_hash(v)) : a.merge(k.to_sym => v)
end
def extract_graphql_body(graphql_response)
parsed_body = parse_body(graphql_response)
data = Hash[parsed_body.values[0]]
Hash[data.values[0]]
end
# Given a URL, wait for the given URL to return 200
@ -241,6 +241,16 @@ module QA
response_check.code == HTTP_STATUS_OK
end
end
def extract_graphql_id(item)
item.fetch(:id).split('/').last
end
def extract_graphql_resource(response, key)
resource = response[key.to_sym]
resource[:id] = extract_graphql_id(resource) if resource.key?(:id)
resource
end
end
end
end

View File

@ -108,6 +108,24 @@ module QA
api_resource.key?(:designs) ? api_resource[:designs].first : api_resource
end
def process_api_response(parsed_response)
design_response = if parsed_response.key?(:designs)
response = parsed_response
response[:designs].each do |design|
design[:id] = extract_graphql_id(design)
end
response
elsif parsed_response.key?(:design_collection)
response = parsed_response[:design_collection][:design]
response[:id] = extract_graphql_id(response)
response
else
parsed_response
end
super(design_response)
end
private
def filepath

View File

@ -29,6 +29,45 @@ RSpec.describe QA::Resource::ApiFabricator do
end
end
let(:graphql_resource) do
Class.new do
def self.name
'FooBarResource'
end
def api_get_path
'/graphql'
end
def api_post_path
api_get_path
end
def api_post_body
<<~GQL
mutation {
createFooBar(
input: {
name: "name"
}
) {
name
id
}
errors
}
GQL
end
def process_api_response(parsed_response)
response = parsed_response[:foobar]
response[:id] = extract_graphql_id(response)
super(response)
end
end
end
before do
allow(subject).to receive(:current_url).and_return('')
end
@ -203,6 +242,64 @@ RSpec.describe QA::Resource::ApiFabricator do
end
end
end
context 'when graphql resource supports fabrication via the API' do
let(:resource) { graphql_resource }
let(:api_request) { spy('Runtime::API::Request') }
let(:resource_web_url) { 'http://example.org/api/v4/foo' }
let(:response) do
{
data: {
foobarCreate: {
foobar: {
id: "gid://gitlab/FooBar/4096",
title: "testing 1",
dueDate: "2024-08-11",
webUrl: resource_web_url
},
errors: []
}
}
}
end
let(:parsed_resource) do
{
id: "4096",
title: "testing 1",
due_date: "2024-08-11",
web_url: resource_web_url
}
end
let(:raw_post) { double('Raw POST response', code: 200, body: response.to_json) }
before do
stub_const('QA::Runtime::API::Request', api_request)
allow(api_request).to receive(:new).and_return(double(url: resource_web_url))
allow(subject).to receive(:get).and_return(double("Raw GET response", code: 200, body: {}.to_json))
end
context 'when creating a resource' do
before do
allow(subject).to receive(:post).with(resource_web_url, { query: subject.api_post_body }, {}).and_return(raw_post)
end
it 'returns the resource URL' do
expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url))
expect(subject).to receive(:post).with(resource_web_url, { query: subject.api_post_body }, {}).and_return(raw_post)
expect(subject.fabricate_via_api!).to eq(resource_web_url)
end
it 'populates api_resource with the extracted graphql body' do
subject.fabricate_via_api!
expect(subject.api_resource).to eq(parsed_resource)
end
end
end
end
describe '#exists?' do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Blob button line permalinks (BlobLinePermalinkUpdater)', :js, feature_category: :groups_and_projects do
RSpec.describe 'Blob button line permalinks (BlobLinePermalinkUpdater)', :js, feature_category: :source_code_management do
include TreeHelper
let(:project) { create(:project, :public, :repository) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > User wants to add a Dockerfile file', :js, feature_category: :groups_and_projects do
RSpec.describe 'Projects > Files > User wants to add a Dockerfile file', :js, feature_category: :source_code_management do
include Features::SourceEditorSpecHelpers
before do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > Download buttons in files tree', feature_category: :groups_and_projects do
RSpec.describe 'Projects > Files > Download buttons in files tree', feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
let(:user) { project.creator }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > User uses soft wrap while editing file', :js, feature_category: :groups_and_projects do
RSpec.describe 'Projects > Files > User uses soft wrap while editing file', :js, feature_category: :source_code_management do
before do
project = create(:project, :repository)
user = project.first_owner

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > User wants to edit a file', feature_category: :groups_and_projects do
RSpec.describe 'Projects > Files > User wants to edit a file', feature_category: :source_code_management do
include ProjectForksHelper
let(:project) { create(:project, :repository, :public) }
let(:user) { project.first_owner }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > User views files page', feature_category: :groups_and_projects do
RSpec.describe 'Projects > Files > User views files page', feature_category: :source_code_management do
let(:project) { create(:forked_project_with_submodules) }
let(:user) { project.first_owner }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > User wants to add a .gitignore file', :js, feature_category: :groups_and_projects do
RSpec.describe 'Projects > Files > User wants to add a .gitignore file', :js, feature_category: :source_code_management do
include Features::SourceEditorSpecHelpers
before do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file', :js, feature_category: :groups_and_projects do
RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file', :js, feature_category: :source_code_management do
include Features::SourceEditorSpecHelpers
let(:params) { {} }

View File

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > Project owner sees a link to create a license file in empty project', :js,
feature_category: :groups_and_projects do
feature_category: :source_code_management do
include Features::WebIdeSpecHelpers
let(:project) { create(:project_empty_repo) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > Template Undo Button', :js, feature_category: :groups_and_projects do
RSpec.describe 'Projects > Files > Template Undo Button', :js, feature_category: :team_planning do
let(:project) { create(:project, :repository) }
let(:user) { project.first_owner }

View File

@ -4,7 +4,7 @@ require 'spec_helper'
# This is a regression test for https://gitlab.com/gitlab-org/gitlab-foss/issues/37569
RSpec.describe 'Projects > Files > User browses a tree with a folder containing only a folder', :js,
feature_category: :groups_and_projects do
feature_category: :source_code_management do
let(:project) { create(:project, :empty_repo) }
let(:user) { project.first_owner }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > User uploads files', feature_category: :groups_and_projects do
RSpec.describe 'Projects > Files > User uploads files', feature_category: :source_code_management do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, name: 'Shop', creator: user) }
let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'listing forks of a project', feature_category: :groups_and_projects do
RSpec.describe 'listing forks of a project', feature_category: :source_code_management do
include ProjectForksHelper
include ExternalAuthorizationServiceHelpers

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Hook logs', feature_category: :groups_and_projects do
RSpec.describe 'Hook logs', feature_category: :webhooks do
let(:project) { create(:project) }
let(:project_hook) { create(:project_hook, project: project) }
let(:web_hook_log) { create(:web_hook_log, web_hook: project_hook, response_body: 'Hello World') }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Project Jobs Permissions', feature_category: :groups_and_projects do
RSpec.describe 'Project Jobs Permissions', feature_category: :continuous_integration do
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:group) { create(:group, name: 'some group') }

View File

@ -177,6 +177,24 @@ describe('PipelinesTableWrapper component', () => {
expect(findEmptyState().text()).toContain('To run a merge request pipeline');
});
});
describe('pipeline badge counts', () => {
it('should receive update-pipelines-count event', () => {
const element = document.createElement('div');
document.body.appendChild(element);
return new Promise((resolve) => {
element.addEventListener('update-pipelines-count', (event) => {
expect(event.detail.pipelineCount).toEqual(1);
resolve();
});
createComponent();
element.appendChild(wrapper.vm.$el);
});
});
});
});
describe('polling', () => {

View File

@ -408,4 +408,21 @@ describe('text_utility', () => {
},
);
});
describe('convertEachWordToTitleCase', () => {
it.each`
inputValue | outputValue
${'Foo Bar'} | ${'Foo Bar'}
${'Foo bar'} | ${'Foo Bar'}
${'foo bar'} | ${'Foo Bar'}
${'FOO BAr'} | ${'Foo Bar'}
${'FOO BAR'} | ${'Foo Bar'}
${'fOO bar'} | ${'Foo Bar'}
`(
'returns string $outputValue when called with string $inputValue',
({ inputValue, outputValue }) => {
expect(textUtils.convertEachWordToTitleCase(inputValue)).toBe(outputValue);
},
);
});
});

View File

@ -362,10 +362,10 @@ describe('tags list row', () => {
});
describe.each`
name | finderFunction | text | icon | clipboard
${'published date detail'} | ${findPublishedDateDetail} | ${'Published to the gitlab-org/gitlab-test/rails-12009 image repository at 13:29:38 UTC on 2020-11-05'} | ${'clock'} | ${false}
${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:2cf3d2fdac1b04a14301d47d51cb88dcd26714c74f91440eeee99ce399089062'} | ${'log'} | ${true}
${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:c2613843ab33aabf847965442b13a8b55a56ae28837ce182627c0716eb08c02b'} | ${'cloud-gear'} | ${true}
name | finderFunction | text | icon | clipboard
${'published date detail'} | ${findPublishedDateDetail} | ${'Published to the gitlab-org/gitlab-test/rails-12009 image repository on November 5, 2020 at 1:29:38 PM GMT'} | ${'clock'} | ${false}
${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:2cf3d2fdac1b04a14301d47d51cb88dcd26714c74f91440eeee99ce399089062'} | ${'log'} | ${true}
${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:c2613843ab33aabf847965442b13a8b55a56ae28837ce182627c0716eb08c02b'} | ${'cloud-gear'} | ${true}
`('$name details row', ({ finderFunction, text, icon, clipboard }) => {
it(`has ${text} as text`, async () => {
mountComponent();
@ -407,7 +407,7 @@ describe('tags list row', () => {
it('name details row has correct text', () => {
expect(findPublishedDateDetail().text()).toMatchInterpolatedText(
'Published to the gitlab-org/gitlab-test/rails-12009 image repository at 13:29:38 UTC on 2020-11-03',
'Published to the gitlab-org/gitlab-test/rails-12009 image repository on November 3, 2020 at 1:29:38 PM GMT',
);
});
});

View File

@ -58,12 +58,15 @@ describe('container Protection Rule Form', () => {
.findAll('option')
.wrappers.map((option) => option.element.value);
it.each(['MAINTAINER', 'OWNER', 'ADMIN'])('includes the %s access level', (accessLevel) => {
mountComponent();
it.each(['', 'MAINTAINER', 'OWNER', 'ADMIN'])(
'includes the access level "%s" as an option',
(accessLevel) => {
mountComponent();
expect(findMinimumAccessLevelForPushSelect().exists()).toBe(true);
expect(minimumAccessLevelForPushOptions()).toContain(accessLevel);
});
expect(findMinimumAccessLevelForPushSelect().exists()).toBe(true);
expect(minimumAccessLevelForPushOptions()).toContain(accessLevel);
},
);
});
describe('when graphql mutation is in progress', () => {
@ -162,6 +165,29 @@ describe('container Protection Rule Form', () => {
});
});
it('dispatches correct apollo mutation when no minimumAccessLevelForPush is selected', async () => {
const mutationResolver = jest
.fn()
.mockResolvedValue(createContainerProtectionRuleMutationPayload());
mountComponentWithApollo({ mutationResolver });
await findRepositoryPathPatternInput().setValue(
createContainerProtectionRuleMutationInput.repositoryPathPattern,
);
await findMinimumAccessLevelForPushSelect().setValue('');
await submitForm();
expect(mutationResolver).toHaveBeenCalledWith({
input: {
projectPath: 'path',
...createContainerProtectionRuleMutationInput,
minimumAccessLevelForPush: null,
},
});
});
it('emits event "submit" when apollo mutation successful', async () => {
const mutationResolver = jest
.fn()

View File

@ -116,6 +116,23 @@ describe('Container protection rules project settings', () => {
);
});
it('renders table with container protection rule with blank minimumAccessLevelForDelete', async () => {
const containerProtectionRuleQueryResolver = jest.fn().mockResolvedValue(
containerProtectionRuleQueryPayload({
nodes: [{ ...containerProtectionRulesData[0], minimumAccessLevelForDelete: null }],
}),
);
createComponent({ containerProtectionRuleQueryResolver });
await waitForPromises();
expect(findTableRowCell(0, 0).text()).toBe(
containerProtectionRulesData[0].repositoryPathPattern,
);
expect(findTableRowCellComboboxSelectedOption(0, 1).text).toBe('Maintainer');
expect(findTableRowCellComboboxSelectedOption(0, 2).text).toBe('Developer (default)');
});
it('displays table in busy state and shows loading icon inside table', async () => {
createComponent();
@ -305,7 +322,12 @@ describe('Container protection rules project settings', () => {
.findAllComponents('option')
.wrappers.map((w) => w.text());
expect(accessLevelOptions).toEqual(['Maintainer', 'Owner', 'Admin']);
expect(accessLevelOptions).toEqual([
'Developer (default)',
'Maintainer',
'Owner',
'Admin',
]);
});
describe('when value changes', () => {

View File

@ -1,8 +1,10 @@
import { nextTick } from 'vue';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlDisclosureDropdownItem, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import projectWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/project_work_item_types.query.graphql.json';
import groupWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/group_work_item_types.query.graphql.json';
import { setNewWorkItemCache } from '~/work_items/graphql/cache_utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CreateWorkItem from '~/work_items/components/create_work_item.vue';
@ -11,9 +13,15 @@ import groupWorkItemTypesQuery from '~/work_items/graphql/group_work_item_types.
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
const showToast = jest.fn();
jest.mock('~/work_items/graphql/cache_utils', () => ({
setNewWorkItemCache: jest.fn(),
}));
Vue.use(VueApollo);
describe('CreateWorkItemModal', () => {
let wrapper;
let apolloProvider;
const findTrigger = () => wrapper.find('[data-testid="new-epic-button"]');
const findDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
@ -50,14 +58,32 @@ describe('CreateWorkItemModal', () => {
data: groupSingleWorkItemQueryResponse.data,
});
const createComponent = (propsData = { workItemTypeName: 'issue' }) => {
const apolloProvider = createMockApollo([
[projectWorkItemTypesQuery, workItemTypesQueryHandler],
const workItemTypesEmptyQueryHandler = jest.fn().mockResolvedValue({
data: {
workspace: {
workItemTypes: {
nodes: [],
__typename: 'WorkItemType',
},
},
},
});
const createComponent = ({
workItemTypeName = 'EPIC',
projectWorkItemTypesQueryHandler = workItemTypesQueryHandler,
asDropdownItem = false,
} = {}) => {
apolloProvider = createMockApollo([
[projectWorkItemTypesQuery, projectWorkItemTypesQueryHandler],
[groupWorkItemTypesQuery, groupWorkItemTypesQueryHandler],
]);
wrapper = shallowMount(CreateWorkItemModal, {
propsData,
propsData: {
workItemTypeName,
asDropdownItem,
},
apolloProvider,
provide: {
fullPath: 'full-path',
@ -71,10 +97,14 @@ describe('CreateWorkItemModal', () => {
});
};
it('passes workItemTypeName to CreateWorkItem', () => {
it('passes workItemTypeName to CreateWorkItem and sets the cache', async () => {
createComponent();
expect(findForm().props('workItemTypeName')).toBe('issue');
expect(findForm().props('workItemTypeName')).toBe('EPIC');
await waitForPromises();
expect(setNewWorkItemCache).toHaveBeenCalled();
});
it('shows toast on workItemCreated', async () => {
@ -83,7 +113,7 @@ describe('CreateWorkItemModal', () => {
await waitForPromises();
findForm().vm.$emit('workItemCreated', { webUrl: '/' });
expect(showToast).toHaveBeenCalledWith('Issue created', expect.any(Object));
expect(showToast).toHaveBeenCalledWith('Epic created', expect.any(Object));
});
describe('default trigger', () => {
@ -119,4 +149,20 @@ describe('CreateWorkItemModal', () => {
expect(findModal().props('visible')).toBe(false);
});
it('when there are no work item types it does not set the cache', async () => {
createComponent({ projectWorkItemTypesQueryHandler: workItemTypesEmptyQueryHandler });
await waitForPromises();
expect(setNewWorkItemCache).not.toHaveBeenCalled();
});
it('when the work item type is invalid it does not set the cache', async () => {
createComponent({ workItemTypeName: 'INVALID' });
await waitForPromises();
expect(setNewWorkItemCache).not.toHaveBeenCalled();
});
});

View File

@ -5,30 +5,10 @@ require 'spec_helper'
RSpec.describe Resolvers::GroupResolver do
include GraphqlHelpers
let_it_be(:group1) { create(:group) }
let_it_be(:group2) { create(:group) }
describe '#resolve' do
it 'batch-resolves groups by full path' do
paths = [group1.full_path, group2.full_path]
result = batch_sync(max_queries: 3) do
paths.map { |path| resolve_group(path) }
end
expect(result).to contain_exactly(group1, group2)
end
it 'resolves an unknown full_path to nil' do
result = batch_sync { resolve_group('unknown/group') }
expect(result).to be_nil
end
it 'treats group full path as case insensitive' do
result = batch_sync { resolve_group(group1.full_path.upcase) }
expect(result).to eq group1
end
it_behaves_like 'a resolver that batch resolves by full path' do
let_it_be(:entity1) { create(:group) }
let_it_be(:entity2) { create(:group) }
let_it_be(:resolve_method) { :resolve_group }
end
def resolve_group(full_path)

View File

@ -5,31 +5,10 @@ require 'spec_helper'
RSpec.describe Resolvers::ProjectResolver do
include GraphqlHelpers
let_it_be(:project1) { create(:project) }
let_it_be(:project2) { create(:project) }
let_it_be(:other_project) { create(:project) }
describe '#resolve' do
it 'batch-resolves projects by full path' do
paths = [project1.full_path, project2.full_path]
result = batch_sync(max_queries: 3) do
paths.map { |path| resolve_project(path) }
end
expect(result).to contain_exactly(project1, project2)
end
it 'resolves an unknown full_path to nil' do
result = batch_sync { resolve_project('unknown/project') }
expect(result).to be_nil
end
it 'treats project full path as case insensitive' do
result = batch_sync { resolve_project(project1.full_path.upcase) }
expect(result).to eq project1
end
it_behaves_like 'a resolver that batch resolves by full path' do
let_it_be(:entity1) { create(:project) }
let_it_be(:entity2) { create(:project) }
let_it_be(:resolve_method) { :resolve_project }
end
it 'does not increase complexity depending on number of load limits' do

View File

@ -24,12 +24,12 @@ RSpec.describe GitlabSchema.types['ContainerRegistryProtectionRule'], feature_ca
describe 'minimum_access_level_for_push' do
subject { described_class.fields['minimumAccessLevelForPush'] }
it { is_expected.to have_non_null_graphql_type(Types::ContainerRegistry::Protection::RuleAccessLevelEnum) }
it { is_expected.to have_nullable_graphql_type(Types::ContainerRegistry::Protection::RuleAccessLevelEnum) }
end
describe 'minimum_access_level_for_delete' do
subject { described_class.fields['minimumAccessLevelForDelete'] }
it { is_expected.to have_non_null_graphql_type(Types::ContainerRegistry::Protection::RuleAccessLevelEnum) }
it { is_expected.to have_nullable_graphql_type(Types::ContainerRegistry::Protection::RuleAccessLevelEnum) }
end
end

View File

@ -0,0 +1,144 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Pagination::Offset::PaginationWithIndexOnlyScan, feature_category: :database do
let_it_be(:project) { create(:project) }
let_it_be(:issue1) { create(:issue, project: project, closed_at: 3.days.ago) }
let_it_be(:issue2) { create(:issue, project: project, closed_at: 2.days.ago) }
let_it_be(:issue3) { create(:issue, project: project, closed_at: 5.days.ago) }
let_it_be(:issue4) { create(:issue, project: project, closed_at: nil) }
let_it_be(:outside_issue) { create(:issue, project: project) }
let!(:offset_paginated_result) { scope.page(page).per(per) }
let(:optimization) { described_class.new(scope: scope, page: page, per_page: per) }
let(:per) { 2 }
let(:page) { 1 }
subject(:result) { optimization.pageinate_with_kaminari }
RSpec::Matchers.define :match_pagination_results do |expected|
match do |actual|
expect(expected.to_a).to eq(actual.to_a)
expect(expected.offset_value).to eq(actual.offset_value)
expect(expected.limit_value).to eq(actual.limit_value)
expect(expected.select_values).to eq(actual.select_values)
expect(expected.includes_values).to eq(actual.includes_values)
expect(expected.preload_values).to eq(actual.preload_values)
expect(expected.eager_load_values).to eq(actual.eager_load_values)
end
end
context 'when sorting by id' do
let!(:scope) { Issue.where(project: project).order(id: :desc) }
it { is_expected.to match_pagination_results(offset_paginated_result) }
it 'calls the optimized code path' do
expect(optimization).to receive(:build_module_for_load).and_call_original
result
end
context 'when requesting the 2nd page' do
let(:page) { 2 }
it { is_expected.to match_pagination_results(offset_paginated_result) }
end
context 'when selecting specific columns' do
let!(:scope) { Issue.where(project: project).order(id: :desc).select(:id, :title) }
it 'only loads the selected columns' do
expect(result).to match_pagination_results(offset_paginated_result)
expect(result.first).not_to have_attribute(:description)
end
end
end
context 'when sorting by closed at' do
let!(:scope) { Issue.where(project: project).order(:closed_at, :id) }
it { is_expected.to match_pagination_results(offset_paginated_result) }
context 'when one of the closed_at value is null' do
let(:page) { 2 }
it { is_expected.to match_pagination_results(offset_paginated_result) }
end
context 'when no records are returned' do
let(:page) { 15 }
it { is_expected.to match_pagination_results(offset_paginated_result) }
end
end
context 'when sorting by an SQL expression' do
context 'when the SQL expression is not keyset-pagination aware' do
let!(:scope) { Issue.where(project: project).order(Arel.sql('id + 1'), :id) }
it 'does not apply the optimization' do
expect(optimization).not_to receive(:build_module_for_load)
result
end
end
context 'when the SQL expression supports keyset-pagination' do
let(:order) do
Gitlab::Pagination::Keyset::Order.build([
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id_plus_one',
order_expression: Arel.sql('id + 1').asc
),
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id',
order_expression: Issue.arel_table[:id].asc
)
])
end
let!(:scope) { Issue.where(project: project).order(order) }
it { is_expected.to match_pagination_results(offset_paginated_result) }
it 'calls the optimized code path' do
expect(optimization).to receive(:build_module_for_load).and_call_original
result
end
end
end
context 'when the scope is not sorted' do
let!(:scope) { Issue.where(project: project) }
it 'does not apply the optimization' do
expect(optimization).not_to receive(:build_module_for_load)
result
end
end
context 'when STI scope is used' do
let!(:build1) { create(:ci_build) }
let!(:build2) { create(:ci_build) }
let!(:scope) { Ci::Build.where({}).order(:id) }
let(:per) { 1 }
let(:page) { 2 }
it { is_expected.to match_pagination_results(offset_paginated_result) }
it 'calls the optimized code path' do
expect(optimization).to receive(:build_module_for_load).and_call_original
result
end
end
end

View File

@ -90,12 +90,31 @@ RSpec.describe ContainerRegistry::Protection::Rule, type: :model, feature_catego
end
end
describe '#minimum_access_level_for_delete' do
it { is_expected.to validate_presence_of(:minimum_access_level_for_delete) }
end
describe '#at_least_one_minimum_access_level_must_be_present' do
where(:minimum_access_level_for_delete, :minimum_access_level_for_push, :valid) do
:maintainer | :maintainer | true
:maintainer | nil | true
nil | :maintainer | true
nil | nil | false
end
describe '#minimum_access_level_for_push' do
it { is_expected.to validate_presence_of(:minimum_access_level_for_push) }
with_them do
subject(:container_registry_protection_rule) {
build(:container_registry_protection_rule, minimum_access_level_for_delete: minimum_access_level_for_delete,
minimum_access_level_for_push: minimum_access_level_for_push)
}
if params[:valid]
it { is_expected.to be_valid }
else
it 'is invalid' do
expect(container_registry_protection_rule).not_to be_valid
expect(container_registry_protection_rule.errors[:base]).to include(
'A rule must have at least a minimum access role for push or delete.'
)
end
end
end
end
end

View File

@ -12,7 +12,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai
build_stubbed(:container_registry_protection_rule, project: project)
end
let(:kwargs) do
let(:input) do
{
project_path: project.full_path,
repository_path_pattern: container_registry_protection_rule_attributes.repository_path_pattern,
@ -22,7 +22,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai
end
let(:mutation) do
graphql_mutation(:create_container_registry_protection_rule, kwargs,
graphql_mutation(:create_container_registry_protection_rule, input,
<<~QUERY
containerRegistryProtectionRule {
id
@ -36,7 +36,9 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai
let(:mutation_response) { graphql_mutation_response(:create_container_registry_protection_rule) }
subject { post_graphql_mutation(mutation, current_user: user) }
subject(:post_graphql_mutation_create_container_registry_protection_rule) {
post_graphql_mutation(mutation, current_user: user)
}
shared_examples 'a successful response' do
it { subject.tap { expect_graphql_errors_to_be_empty } }
@ -48,7 +50,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai
'errors' => be_blank,
'containerRegistryProtectionRule' => {
'id' => be_present,
'repositoryPathPattern' => kwargs[:repository_path_pattern]
'repositoryPathPattern' => input[:repository_path_pattern]
}
)
end
@ -57,7 +59,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai
expect { subject }.to change { ::ContainerRegistry::Protection::Rule.count }.by(1)
expect(::ContainerRegistry::Protection::Rule.where(project: project,
repository_path_pattern: kwargs[:repository_path_pattern])).to exist
repository_path_pattern: input[:repository_path_pattern])).to exist
end
end
@ -68,10 +70,10 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai
it_behaves_like 'a successful response'
context 'with invalid input fields `minimumAccessLevelForPush` and `minimumAccessLevelForDelete`' do
let(:kwargs) do
let(:input) do
super().merge(
minimum_access_level_for_push: 'UNKNOWN_ACCESS_LEVEL',
minimum_access_level_for_delete: 'UNKNOWN_ACCESS_LEVEL'
minimum_access_level_for_push: 'INVALID_ACCESS_LEVEL',
minimum_access_level_for_delete: 'INVALID_ACCESS_LEVEL'
)
end
@ -84,24 +86,45 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai
}
end
context 'with invalid input field `repositoryPathPattern`' do
let(:kwargs) do
super().merge(repository_path_pattern: '')
end
context 'with blank input fields `minimumAccessLevelForPush` and `minimumAccessLevelForDelete`' do
let(:input) { super().merge(minimum_access_level_for_push: nil, minimum_access_level_for_delete: nil) }
it_behaves_like 'an erroneous response'
it { subject.tap { expect_graphql_errors_to_be_empty } }
it 'returns error with correct error message' do
subject
it {
subject.tap do
expect(mutation_response['errors']).to eq [
"Repository path pattern can't be blank, " \
"Repository path pattern should be a valid container repository path with optional wildcard characters., " \
"and Repository path pattern should start with the project's full path"
]
end
}
expect(mutation_response['errors']).to eq ['A rule must have at least a minimum access role for push or delete.']
end
end
context 'with blank input field `repositoryPathPattern`' do
let(:input) { super().merge(repository_path_pattern: '') }
it_behaves_like 'an erroneous response'
it 'returns error from endpoint implementation (not from grapqhl framework)' do
post_graphql_mutation_create_container_registry_protection_rule
expect_graphql_errors_to_include([/repositoryPathPattern can't be blank/])
end
end
context 'with invalid input field `repositoryPathPattern`' do
let(:input) { super().merge(repository_path_pattern: "prefix-#{project.full_path}-invalid-character-!") }
it_behaves_like 'an erroneous response'
it 'returns error from endpoint implementation (not from grapqhl framework)' do
post_graphql_mutation_create_container_registry_protection_rule
expect_graphql_errors_to_be_empty
expect(mutation_response['errors']).to eq [
"Repository path pattern should be a valid container repository path with optional wildcard characters. and " \
"Repository path pattern should start with the project's full path"
]
end
end
context 'with existing containers protection rule' do
@ -111,7 +134,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai
end
context 'when container name pattern is slightly different' do
let(:kwargs) do
let(:input) do
# The field `repository_path_pattern` is unique; this is why we change the value in a minimum way
super().merge(
repository_path_pattern: "#{existing_container_registry_protection_rule.repository_path_pattern}-unique"
@ -126,7 +149,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai
end
context 'when field `repository_path_pattern` is taken' do
let(:kwargs) do
let(:input) do
super().merge(repository_path_pattern: existing_container_registry_protection_rule.repository_path_pattern,
minimum_access_level_for_push: 'OWNER')
end
@ -143,7 +166,7 @@ RSpec.describe 'Creating the container registry protection rule', :aggregate_fai
it 'does not create new container protection rules' do
expect(::ContainerRegistry::Protection::Rule.where(project: project,
repository_path_pattern: kwargs[:repository_path_pattern],
repository_path_pattern: input[:repository_path_pattern],
minimum_access_level_for_push: Gitlab::Access::OWNER)).not_to exist
end
end

View File

@ -41,7 +41,9 @@ RSpec.describe 'Updating the container registry protection rule', :aggregate_fai
let(:mutation_response) { graphql_mutation_response(:update_container_registry_protection_rule) }
subject { post_graphql_mutation(mutation, current_user: current_user) }
subject(:post_graphql_mutation_update_container_registry_protection_rule) do
post_graphql_mutation(mutation, current_user: current_user)
end
shared_examples 'a successful response' do
it { subject.tap { expect_graphql_errors_to_be_empty } }
@ -97,19 +99,34 @@ RSpec.describe 'Updating the container registry protection rule', :aggregate_fai
end
context 'with invalid input param `minimumAccessLevelForPush`' do
let(:input) { super().merge(minimum_access_level_for_push: nil) }
let(:input) { super().merge(minimum_access_level_for_push: 'INVALID_ACCESS_LEVEL') }
it_behaves_like 'an erroneous response'
it { is_expected.tap { expect_graphql_errors_to_include(/invalid value for minimumAccessLevelForPush/) } }
it { is_expected.tap { expect_graphql_errors_to_include(/minimumAccessLevelForPush can't be blank/) } }
it do
expect { post_graphql_mutation_update_container_registry_protection_rule }
.not_to(change { container_registry_protection_rule.reload.updated_at })
end
end
context 'with invalid input param `repositoryPathPattern`' do
let(:input) { super().merge(repository_path_pattern: '') }
it_behaves_like 'an erroneous response'
it 'returns error with correct error message' do
post_graphql_mutation_update_container_registry_protection_rule
it { is_expected.tap { expect_graphql_errors_to_include(/repositoryPathPattern can't be blank/) } }
expect_graphql_errors_to_include(/repositoryPathPattern can't be blank/)
end
end
context 'with blank input fields `minimumAccessLevelForPush` and `minimumAccessLevelForDelete`' do
let(:input) { super().merge(minimum_access_level_for_push: nil, minimum_access_level_for_delete: nil) }
it 'returns error with correct error message' do
post_graphql_mutation_update_container_registry_protection_rule
expect(mutation_response['errors']).to eq ['A rule must have at least a minimum access role for push or delete.']
end
end
context 'when current_user does not have permission' do

View File

@ -194,4 +194,150 @@ RSpec.describe Organizations::GroupsController, feature_category: :cell do
end
end
end
describe 'DELETE #destroy' do
let_it_be(:group) { create(:group, organization: organization) }
shared_examples 'deletes the group' do
specify do
expect_next_instance_of(Groups::DestroyService) do |instance|
expect(instance).to receive(:async_execute)
end
gitlab_request
end
end
shared_examples 'unable to delete the group' do
specify do
expect_any_instance_of(Groups::DestroyService) do |instance|
expect(instance).not_to receive(:async_execute)
end
gitlab_request
end
end
context 'when group exists' do
subject(:gitlab_request) do
delete groups_organization_path(organization, id: group.to_param)
end
context 'when the user is not signed in' do
it_behaves_like 'organization - redirects to sign in page'
context 'when `ui_for_organizations` feature flag is disabled' do
before do
stub_feature_flags(ui_for_organizations: false)
end
it_behaves_like 'organization - redirects to sign in page'
it_behaves_like 'unable to delete the group'
end
end
context 'when the user is signed in' do
let_it_be(:user) { create(:user) }
before do
sign_in(user)
end
context 'as as admin', :enable_admin_mode do
let_it_be(:user) { create(:admin) }
it_behaves_like 'organization - successful response'
it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag'
it_behaves_like 'deletes the group'
end
context 'as a group owner' do
before_all do
group.add_owner(user)
end
it_behaves_like 'organization - successful response'
it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag'
it_behaves_like 'deletes the group'
context 'when destroy service raises DestroyError' do
let(:error_message) { "Error deleting group" }
before do
allow_next_instance_of(Groups::DestroyService) do |instance|
allow(instance).to receive(:async_execute)
.and_raise(Groups::DestroyService::DestroyError, error_message)
end
end
it 'returns the error message' do
gitlab_request
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to eq(error_message)
end
end
end
context 'as a user that is not an owner' do
it_behaves_like 'organization - not found response'
it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag'
it_behaves_like 'unable to delete the group'
end
context 'as an organization owner' do
let_it_be(:user) do
organization_user = create(:organization_owner, organization: organization)
organization_user.user
end
it_behaves_like 'organization - successful response'
it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag'
it_behaves_like 'deletes the group'
end
end
end
context 'when group is not in organization' do
let_it_be(:user) { create(:user) }
let_it_be(:organization_2) { create(:organization) }
subject(:gitlab_request) do
delete groups_organization_path(organization_2, id: group.to_param)
end
before_all do
group.add_owner(user)
end
before do
sign_in(user)
end
it_behaves_like 'organization - not found response'
it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag'
it_behaves_like 'unable to delete the group'
end
context 'when group does not exist' do
subject(:gitlab_request) do
delete groups_organization_path(organization, id: 'group-that-does-not-exist')
end
context 'when the user is not signed in' do
it_behaves_like 'organization - redirects to sign in page'
end
context 'when the user is signed in' do
let_it_be(:user) { create(:user) }
before do
sign_in(user)
end
it_behaves_like 'organization - not found response'
it_behaves_like 'unable to delete the group'
end
end
end
end

View File

@ -93,6 +93,13 @@ RSpec.describe ContainerRegistry::Protection::CreateRuleService, '#execute', fea
it_behaves_like 'an erroneous service response without side effects',
message: "'1000' is not a valid minimum_access_level_for_push"
end
context 'when minimum_access_level_for_delete and minimum_access_level_for_push are blank' do
let(:params) { super().merge(minimum_access_level_for_delete: nil, minimum_access_level_for_push: nil) }
it_behaves_like 'an erroneous service response without side effects',
message: 'A rule must have at least a minimum access role for push or delete.'
end
end
context 'with existing container registry protection rule in the database' do

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
RSpec.shared_examples 'a resolver that batch resolves by full path' do
describe '#resolve' do
it 'batch-resolves entities by full path' do
paths = [entity1.full_path, entity2.full_path]
result = batch_sync(max_queries: 3) do
paths.map { |path| send(resolve_method, path) }
end
expect(result).to contain_exactly(entity1, entity2)
end
it 'resolves an unknown full_path to nil' do
result = batch_sync { send(resolve_method, 'unknown/entity') }
expect(result).to be_nil
end
it 'treats entity full path as case insensitive' do
result = batch_sync { send(resolve_method, entity1.full_path.upcase) }
expect(result).to eq entity1
end
end
end

View File

@ -1331,10 +1331,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.103.0.tgz#af61387481100eadef2bea8fe8605250311ac582"
integrity sha512-jVWCrRVRF6nw2A+Aowc0quXV2bdRPl2v08ElCPSestfdKjQ92tSlCrIsLB8GvdW5aI0eFsD1vJ1w2qkzZdpA4A==
"@gitlab/ui@81.1.0":
version "81.1.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-81.1.0.tgz#42eb5cb47b38681985b8a3623938adb05cb04ed6"
integrity sha512-vrFRihmNWrG7LP7ds9nLPtYZlReojM1ZL+tFeXumsgsKQ0C66VfnJ0UInsaAAz1DhczA6DfdDdmAtqGog6f4HA==
"@gitlab/ui@82.0.1":
version "82.0.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-82.0.1.tgz#de461d5844264c4ec5b8be832ba84db3d8d62d4b"
integrity sha512-/Brp8KIM5/x5FczH/d9Ftvob9Jyu/ByWSXNKy0/VD/MCqXVWmVB6oPBsDjybif3CzSzNb78ilB0nQmlQ/4HBLQ==
dependencies:
"@floating-ui/dom" "1.4.3"
bootstrap-vue "2.23.1"