Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-08-16 09:12:07 +00:00
parent a6529472b0
commit ead25aaac3
43 changed files with 474 additions and 153 deletions

View File

@ -1401,7 +1401,6 @@ Style/InlineDisableAnnotation:
- 'ee/app/services/app_sec/dast/sites/find_or_create_service.rb'
- 'ee/app/services/approval_rules/params_filtering_service.rb'
- 'ee/app/services/billable_members/destroy_service.rb'
- 'ee/app/services/ci/minutes/additional_packs/base_service.rb'
- 'ee/app/services/ci/minutes/additional_packs/create_service.rb'
- 'ee/app/services/ci/minutes/refresh_cached_data_service.rb'
- 'ee/app/services/ci/minutes/reset_usage_service.rb'

View File

@ -1 +1 @@
3d2dea2bdbf379da45781469d044f11dc16bace0
83d81744aa7215577e8b0bef40723a787de26793

View File

@ -571,7 +571,7 @@
{"name":"redis-clustering","version":"5.2.0","platform":"ruby","checksum":"685f388e0bdd81091a96cce9a46e22e727213d5fa14ebfc5111e110440e0038e"},
{"name":"redis-namespace","version":"1.11.0","platform":"ruby","checksum":"e91a1aa2b2d888b6dea1d4ab8d39e1ae6fac3426161feb9d91dd5cca598a2239"},
{"name":"redis-rack","version":"3.0.0","platform":"ruby","checksum":"abb50b82ae10ad4d11ca2e4901bfc2b98256cdafbbd95f80c86fc9e001478380"},
{"name":"redis-store","version":"1.10.0","platform":"ruby","checksum":"f258894f9f7e82834308a3d86242294f0cff2c9db9ae66e5cb4c553a5ec8b09e"},
{"name":"redis-store","version":"1.11.0","platform":"ruby","checksum":"edc4f3e239dcd1fdd9905584e6b1e623a84618e14436e6e8a07c70891008eda4"},
{"name":"regexp_parser","version":"2.6.0","platform":"ruby","checksum":"f163ba463a45ca2f2730e0902f2475bb0eefcd536dfc2f900a86d1e5a7d7a556"},
{"name":"regexp_property_values","version":"1.0.0","platform":"java","checksum":"5e26782b01241616855c4ee7bb8a62fce9387e484f2d3eaf04f2a0633708222e"},
{"name":"regexp_property_values","version":"1.0.0","platform":"ruby","checksum":"162499dc0bba1e66d334273a059f207a61981cc8cc69d2ca743594e7886d080f"},

View File

@ -1553,7 +1553,7 @@ GEM
redis-rack (3.0.0)
rack-session (>= 0.2.0)
redis-store (>= 1.2, < 2)
redis-store (1.10.0)
redis-store (1.11.0)
redis (>= 4, < 6)
regexp_parser (2.6.0)
regexp_property_values (1.0.0)

View File

@ -0,0 +1,60 @@
<script>
import { toSentenceCase } from '../../utils/common';
export default {
name: 'TablePresenter',
inject: ['presenter'],
props: {
data: {
required: true,
type: Object,
validator: ({ nodes }) => Array.isArray(nodes),
},
config: {
required: true,
type: Object,
validator: ({ fields }) => Array.isArray(fields) && fields.length > 0,
},
},
computed: {
items() {
return this.data.nodes;
},
fields() {
return this.config.fields.map((field) => {
return { key: field, label: toSentenceCase(field) };
});
},
},
};
</script>
<template>
<div class="!gl-my-4">
<table class="!gl-mt-0 !gl-mb-2">
<thead>
<tr>
<th v-for="field in fields" :key="field.key">
{{ field.label }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, itemIndex) in items"
:key="itemIndex"
:data-testid="`table-row-${itemIndex}`"
>
<td v-for="(field, fieldIndex) in fields" :key="fieldIndex">
<component :is="presenter.forField(item, field.key)" />
</td>
</tr>
<tr v-if="!items.length">
<td :colspan="fields.length" class="gl-text-center">
<em>{{ __('No data found for this query') }}</em>
</td>
</tr>
</tbody>
</table>
<small>{{ __('Generated by GLQL') }}</small>
</div>
</template>

View File

@ -3,10 +3,13 @@ import LinkPresenter from '../components/presenters/link.vue';
import TextPresenter from '../components/presenters/text.vue';
import ListPresenter from '../components/presenters/list.vue';
import NullPresenter from '../components/presenters/null.vue';
import TablePresenter from '../components/presenters/table.vue';
const presentersByDisplayType = {
list: ListPresenter,
orderedList: ListPresenter,
table: TablePresenter,
};
const olProps = { listType: 'ol' };

View File

@ -1,8 +1,7 @@
import jsYaml from 'js-yaml';
import { uniq } from 'lodash';
import { uniq, upperFirst, lowerCase } from 'lodash';
export const extractGroupOrProject = () => {
const url = window.location.href;
export const extractGroupOrProject = (url = window.location.href) => {
let fullPath = url
.replace(window.location.origin, '')
.split('/-/')[0]
@ -31,3 +30,8 @@ export const parseFrontmatter = (frontmatter, defaults = {}) => {
config.display = config.display || 'list';
return config;
};
export const toSentenceCase = (str) => {
if (str === 'id' || str === 'iid') return str.toUpperCase();
return upperFirst(lowerCase(str));
};

View File

@ -27,9 +27,8 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
// eslint-disable-next-line local-rules/require-valid-help-page-path
projectCreationHelp: helpPagePath('user/group/import/index', {
anchor: 'ensure-projects-can-be-imported',
projectCreationHelp: helpPagePath('user/group/import/direct_transfer_migrations', {
anchor: 'configuration',
}),
props: {
id: {

View File

@ -182,7 +182,6 @@ export default {
<cleanup-status
v-if="item.expirationPolicyCleanupStatus"
class="gl-ml-2"
:status="item.expirationPolicyCleanupStatus"
:expiration-policy="expirationPolicy"
/>
@ -191,7 +190,6 @@ export default {
v-if="showBadgeProtected"
v-gl-tooltip
:title="$options.i18n.badgeProtectedTooltipText"
class="gl-ml-3"
size="sm"
variant="neutral"
>

View File

@ -140,10 +140,9 @@ export default {
{{ packageEntity.name }}
</span>
<div v-if="showTags || showBadgeProtected" class="gl-display-flex gl-gap-2">
<div v-if="showTags || showBadgeProtected" class="gl-flex gl-gap-3">
<package-tags
v-if="showTags"
class="gl-ml-2"
:tags="packageEntity.tags.nodes"
hide-label
:tag-display-limit="1"
@ -152,7 +151,6 @@ export default {
<gl-badge
v-if="showBadgeProtected"
v-gl-tooltip="{ title: $options.i18n.badgeProtectedTooltipText }"
class="gl-ml-2"
icon-size="sm"
variant="neutral"
>

View File

@ -98,7 +98,7 @@ export default {
'left-secondary'
]
"
class="gl-flex gl-min-h-6 gl-min-w-0 gl-grow gl-items-center gl-text-sm gl-text-secondary"
class="gl-flex gl-min-h-6 gl-min-w-0 gl-grow gl-items-center gl-text-sm gl-text-subtle gl-gap-3"
>
<slot name="left-secondary"></slot>
</div>

View File

@ -26,6 +26,7 @@ class ProjectAutoDevops < ApplicationRecord
def create_gitlab_deploy_token
project.deploy_tokens.create!(
name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME,
project_id: project_id,
read_registry: true
)
end

View File

@ -434,6 +434,7 @@ class User < ApplicationRecord
:achievements_enabled, :achievements_enabled=,
:enabled_following, :enabled_following=,
:home_organization, :home_organization_id, :home_organization_id=,
:dpop_enabled, :dpop_enabled=,
to: :user_preference
delegate :path, to: :namespace, allow_nil: true, prefix: true

View File

@ -44,6 +44,7 @@ class UserPreference < ApplicationRecord
attribute :project_shortcut_buttons, default: true
attribute :keyboard_shortcuts_enabled, default: true
attribute :use_web_ide_extension_marketplace, default: false
attribute :dpop_enabled, default: false
enum :visibility_pipeline_id_type, { id: 0, iid: 1 }, scopes: false
@ -149,6 +150,15 @@ class UserPreference < ApplicationRecord
self.extensions_marketplace_opt_in_status = status
end
def dpop_enabled=(value)
if value.nil?
default = self.class.column_defaults['dpop_enabled']
super(default)
else
super(value)
end
end
private
def user_belongs_to_home_organization

View File

@ -22,7 +22,7 @@ module VirtualRegistries
:content_type,
length: { maximum: 255 }
validates :downloads_count, numericality: { greater_than: 0, only_integer: true }
validates :relative_path, uniqueness: { scope: :upstream_id }
validates :relative_path, uniqueness: { scope: :upstream_id }, if: :upstream
validates :file, presence: true
mount_file_store_uploader ::VirtualRegistries::CachedResponseUploader

View File

@ -87,6 +87,7 @@ module Gitlab
require_dependency Rails.root.join('lib/gitlab/runtime')
require_dependency Rails.root.join('lib/gitlab/patch/database_config')
require_dependency Rails.root.join('lib/gitlab/patch/redis_cache_store')
require_dependency Rails.root.join('lib/gitlab/patch/old_redis_cache_store')
require_dependency Rails.root.join('lib/gitlab/exceptions_app')
config.exceptions_app = Gitlab::ExceptionsApp.new(Gitlab.jh? ? Rails.root.join('jh/public') : Rails.public_path)
@ -535,7 +536,12 @@ module Gitlab
end
# Use caching across all environments
ActiveSupport::Cache::RedisCacheStore.prepend(Gitlab::Patch::RedisCacheStore)
if ::Gitlab.next_rails?
ActiveSupport::Cache::RedisCacheStore.prepend(Gitlab::Patch::RedisCacheStore)
else
ActiveSupport::Cache::RedisCacheStore.prepend(Gitlab::Patch::OldRedisCacheStore)
end
config.cache_store = :redis_cache_store, Gitlab::Redis::Cache.active_support_config
config.active_job.queue_adapter = :sidekiq

View File

@ -41,7 +41,19 @@ module ActionDispatch
val = match_data[i + 1]
path_parameters[name.to_sym] = Utils.unescape_uri(val) if val
end
[match_data, path_parameters, r]
# This is the minimal version to support both Rails 7.0 and Rails 7.1
#
# - https://github.com/rails/rails/blob/v7.1.3.4/actionpack/lib/action_dispatch/journey/router.rb#L131
#
# - https://github.com/rails/rails/blob/v7.0.8.4/actionpack/lib/action_dispatch/journey/router.rb#L130
#
# After the upgrade, this method can be more like the v7.1.3.4 version
if Gitlab.next_rails?
yield [match_data, path_parameters, r]
else
[match_data, path_parameters, r]
end
end.compact!
routes

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
class AllowNullForUpstreamIdInVirtualRegistriesPackagesMavenCachedResponses < Gitlab::Database::Migration[2.2]
enable_lock_retries!
milestone '17.4'
def change
change_column_null :virtual_registries_packages_maven_cached_responses, :upstream_id, true
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddDpopEnabledToUserPreferences < Gitlab::Database::Migration[2.2]
milestone '17.4'
def change
add_column :user_preferences, :dpop_enabled, :boolean, default: false, null: false, if_not_exists: true
end
end

View File

@ -0,0 +1 @@
c7671c356fd7343b8800a995810a1adee1333f57da2cede316b6d1770b0edc32

View File

@ -0,0 +1 @@
0e8f60bc7e3767525dccadd4e5bbc3ae6c2c1e13c8f55ed78e351caadae23b6f

View File

@ -19116,6 +19116,7 @@ CREATE TABLE user_preferences (
extensions_marketplace_opt_in_status smallint DEFAULT 0 NOT NULL,
organization_groups_projects_sort text,
organization_groups_projects_display smallint DEFAULT 0 NOT NULL,
dpop_enabled boolean DEFAULT false NOT NULL,
CONSTRAINT check_1d670edc68 CHECK ((time_display_relative IS NOT NULL)),
CONSTRAINT check_89bf269f41 CHECK ((char_length(diffs_deletion_color) <= 7)),
CONSTRAINT check_b1306f8875 CHECK ((char_length(organization_groups_projects_sort) <= 64)),
@ -19278,7 +19279,7 @@ ALTER SEQUENCE value_stream_dashboard_counts_id_seq OWNED BY value_stream_dashbo
CREATE TABLE virtual_registries_packages_maven_cached_responses (
id bigint NOT NULL,
group_id bigint NOT NULL,
upstream_id bigint NOT NULL,
upstream_id bigint,
upstream_checked_at timestamp with time zone DEFAULT now() NOT NULL,
downloaded_at timestamp with time zone DEFAULT now() NOT NULL,
created_at timestamp with time zone NOT NULL,

View File

@ -2,6 +2,7 @@
stage: Systems
group: Geo
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
ignore_in_report: true
---
# Back up and Restore GitLab with `gitlab-backup-cli`

View File

@ -282,7 +282,14 @@ module Gitlab
return unless connection.respond_to?(:pool) &&
connection.pool.respond_to?(:db_config)
connection.pool.db_config
db_config = connection.pool.db_config
db_config unless empty_config?(db_config)
end
def self.empty_config?(db_config)
return true unless db_config
::Gitlab.next_rails? && db_config.is_a?(ActiveRecord::ConnectionAdapters::NullPool::NullConfig)
end
# At the moment, the connection can only be retrieved by
@ -353,10 +360,14 @@ module Gitlab
::Gitlab::Database::Metrics.subtransactions_increment(self.name) if transaction_type == :sub_transaction
payload = { connection: connection, transaction_type: transaction_type }
ActiveSupport::Notifications.instrument('transaction.active_record', payload) do
if ::Gitlab.next_rails?
super(**options, &block)
else
payload = { connection: connection, transaction_type: transaction_type }
ActiveSupport::Notifications.instrument('transaction.active_record', payload) do
super(**options, &block)
end
end
end

View File

@ -11,7 +11,7 @@ module Gitlab
@writer = Oj::StreamWriter.new(@file, {})
@writer.push_array
@subscriber = ActiveSupport::Notifications.subscribe('transaction.active_record') do |*args|
record_sql_event(*args)
record_event(*args)
end
end
@ -26,14 +26,26 @@ module Gitlab
# no-op
end
def record_sql_event(_name, started, finished, _unique_id, payload)
return if payload[:transaction_type] == :fake_transaction
private
@writer.push_value({
start_time: started.iso8601(6),
end_time: finished.iso8601(6),
transaction_type: payload[:transaction_type]
})
def record_event(_name, started, finished, _unique_id, payload)
if ::Gitlab.next_rails?
stack_count = payload[:connection].open_transactions
@writer.push_value({
start_time: started.iso8601(6),
end_time: finished.iso8601(6),
transaction_type: stack_count == 0 ? :real_transaction : :sub_transaction
})
else
return if payload[:transaction_type] == :fake_transaction
@writer.push_value({
start_time: started.iso8601(6),
end_time: finished.iso8601(6),
transaction_type: payload[:transaction_type]
})
end
end
end
end

View File

@ -27,7 +27,7 @@ module Gitlab
@primary_keys = if @model_class.primary_key.nil?
@model_class.connection.primary_keys(@model_class.table_name)
else
[@model_class.primary_key]
Array.wrap(@model_class.primary_key)
end
end

View File

@ -0,0 +1,70 @@
# frozen_string_literal: true
module Gitlab
module Patch
module OldRedisCacheStore
# We will try keep patched code explicit and matching the original signature in
# https://github.com/rails/rails/blob/v6.1.7.2/activesupport/lib/active_support/cache/redis_cache_store.rb#L361
def read_multi_mget(*names) # rubocop:disable Style/ArgumentsForwarding -- Overridden patch
return super unless enable_rails_cache_pipeline_patch?
return super unless use_patched_mget?
patched_read_multi_mget(*names) # rubocop:disable Style/ArgumentsForwarding -- Overridden patch
end
# `delete_multi_entries` in Rails runs a multi-key `del` command
# patch will run pipelined single-key `del` for Redis Cluster compatibility
def delete_multi_entries(entries, **options)
return super unless enable_rails_cache_pipeline_patch?
redis.with do |conn|
::Gitlab::Redis::ClusterUtil.batch_del(entries, conn)
end
end
# Copied from https://github.com/rails/rails/blob/v6.1.6.1/activesupport/lib/active_support/cache/redis_cache_store.rb
# re-implements `read_multi_mget` using a pipeline of `get`s rather than an `mget`
#
def patched_read_multi_mget(*names)
options = names.extract_options!
options = merged_options(options)
return {} if names == []
raw = options&.fetch(:raw, false)
keys = names.map { |name| normalize_key(name, options) }
values = failsafe(:patched_read_multi_mget, returning: {}) do
redis.with do |c|
::Gitlab::Redis::ClusterUtil.batch_get(keys, c)
end
end
names.zip(values).each_with_object({}) do |(name, value), results|
if value # rubocop:disable Style/Next -- Overridden patch
entry = deserialize_entry(value, raw: raw)
unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
results[name] = entry.value
end
end
end
end
private
def enable_rails_cache_pipeline_patch?
redis.with { |c| ::Gitlab::Redis::ClusterUtil.cluster?(c) }
end
# MultiStore reads ONLY from the default store (no fallback), hence we can use `mget`
# if the default store is not a Redis::Cluster. We should do that as pipelining gets on a single redis is slow
def use_patched_mget?
redis.with do |conn|
next true unless conn.is_a?(Gitlab::Redis::MultiStore)
::Gitlab::Redis::ClusterUtil.cluster?(conn.default_store)
end
end
end
end
end

View File

@ -4,12 +4,12 @@ module Gitlab
module Patch
module RedisCacheStore
# We will try keep patched code explicit and matching the original signature in
# https://github.com/rails/rails/blob/v6.1.7.2/activesupport/lib/active_support/cache/redis_cache_store.rb#L361
def read_multi_mget(*names) # rubocop:disable Style/ArgumentsForwarding
# https://github.com/rails/rails/blob/v7.1.3.4/activesupport/lib/active_support/cache/redis_cache_store.rb#L324
def read_multi_entries(...)
return super unless enable_rails_cache_pipeline_patch?
return super unless use_patched_mget?
patched_read_multi_mget(*names) # rubocop:disable Style/ArgumentsForwarding
patched_read_multi_entries(...)
end
# `delete_multi_entries` in Rails runs a multi-key `del` command
@ -22,11 +22,19 @@ module Gitlab
end
end
# Copied from https://github.com/rails/rails/blob/v6.1.6.1/activesupport/lib/active_support/cache/redis_cache_store.rb
# re-implements `read_multi_mget` using a pipeline of `get`s rather than an `mget`
#
def patched_read_multi_mget(*names)
options = names.extract_options!
# `pipeline_entries` is used by Rails for multi-key writes
# patch will run pipelined single-key for Redis Cluster compatibility
def pipeline_entries(entries, &block)
return super unless enable_rails_cache_pipeline_patch?
redis.with do |conn|
::Gitlab::Redis::ClusterUtil.batch(entries, conn, &block)
end
end
# Copied from https://github.com/rails/rails/blob/v7.1.3.4/activesupport/lib/active_support/cache/redis_cache_store.rb#L324
# re-implements `read_multi_entries` using a pipeline of `get`s rather than an `mget`
def patched_read_multi_entries(names, **options)
options = merged_options(options)
return {} if names == []
@ -34,18 +42,18 @@ module Gitlab
keys = names.map { |name| normalize_key(name, options) }
values = failsafe(:patched_read_multi_mget, returning: {}) do
values = failsafe(:patched_read_multi_entries, returning: {}) do
redis.with do |c|
::Gitlab::Redis::ClusterUtil.batch_get(keys, c)
end
end
names.zip(values).each_with_object({}) do |(name, value), results|
if value # rubocop:disable Style/Next
entry = deserialize_entry(value, raw: raw)
unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
results[name] = entry.value
end
next unless value
entry = deserialize_entry(value, raw: raw)
unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
results[name] = entry.value
end
end
end

View File

@ -18,30 +18,28 @@ module Gitlab
end
def batch_unlink(keys, redis)
expired_count = 0
keys.each_slice(pipeline_batch_size) do |subset|
expired_count += redis.pipelined do |pipeline|
subset.each { |key| pipeline.unlink(key) }
end.sum
end
expired_count
batch(keys, redis) do |pipeline, subset|
subset.each { |key| pipeline.unlink(key) }
end.sum
end
def batch_del(keys, redis)
expired_count = 0
keys.each_slice(pipeline_batch_size) do |subset|
expired_count += redis.pipelined do |pipeline|
subset.each { |key| pipeline.del(key) }
end.sum
end
expired_count
batch(keys, redis) do |pipeline, subset|
subset.each { |key| pipeline.del(key) }
end.sum
end
# Redis cluster alternative to mget
def batch_get(keys, redis)
keys.each_slice(pipeline_batch_size).flat_map do |subset|
batch(keys, redis) do |pipeline, subset|
subset.map { |key| pipeline.get(key) }
end.flatten
end
def batch(entries, redis)
entries.each_slice(pipeline_batch_size).flat_map do |subset|
redis.pipelined do |pipeline|
subset.map { |key| pipeline.get(key) }
yield pipeline, subset
end
end
end

View File

@ -14,6 +14,8 @@ module Gitlab
end
end
end
alias_method :then, :with
end
end
end

View File

@ -0,0 +1,62 @@
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import TablePresenter from '~/glql/components/presenters/table.vue';
import TextPresenter from '~/glql/components/presenters/text.vue';
import LinkPresenter from '~/glql/components/presenters/link.vue';
import Presenter from '~/glql/core/presenter';
import { MOCK_ISSUES, MOCK_FIELDS } from '../../mock_data';
describe('TablePresenter', () => {
let wrapper;
const createWrapper = ({ data, config, ...moreProps }, mountFn = shallowMountExtended) => {
wrapper = mountFn(TablePresenter, {
provide: {
presenter: new Presenter().init({ data, config }),
},
propsData: { data, config, ...moreProps },
});
};
it('renders header rows with sentence cased field names', () => {
createWrapper({ data: MOCK_ISSUES, config: { fields: MOCK_FIELDS } });
const headerRow = wrapper.find('thead tr');
const headerCells = headerRow.findAll('th').wrappers.map((th) => th.text());
expect(headerCells).toEqual(['Title', 'Author', 'State']);
});
it('renders a row of items presented by appropriate presenters', () => {
createWrapper({ data: MOCK_ISSUES, config: { fields: MOCK_FIELDS } }, mountExtended);
const tableRow1 = wrapper.findByTestId('table-row-0');
const tableRow2 = wrapper.findByTestId('table-row-1');
const linkPresenters1 = tableRow1.findAllComponents(LinkPresenter);
const linkPresenters2 = tableRow2.findAllComponents(LinkPresenter);
const textPresenter1 = tableRow1.findComponent(TextPresenter);
const textPresenter2 = tableRow2.findComponent(TextPresenter);
expect(linkPresenters1).toHaveLength(2);
expect(linkPresenters2).toHaveLength(2);
expect(linkPresenters1.at(0).props('data')).toBe(MOCK_ISSUES.nodes[0]);
expect(linkPresenters1.at(1).props('data')).toBe(MOCK_ISSUES.nodes[0].author);
expect(linkPresenters2.at(0).props('data')).toBe(MOCK_ISSUES.nodes[1]);
expect(linkPresenters2.at(1).props('data')).toBe(MOCK_ISSUES.nodes[1].author);
expect(textPresenter1.props('data')).toBe(MOCK_ISSUES.nodes[0].state);
expect(textPresenter2.props('data')).toBe(MOCK_ISSUES.nodes[1].state);
const getCells = (row) => row.findAll('td').wrappers.map((td) => td.text());
expect(getCells(tableRow1)).toEqual(['Issue 1', 'foobar', 'opened']);
expect(getCells(tableRow2)).toEqual(['Issue 2', 'janedoe', 'closed']);
});
it('shows a "No data" message if the list of items provided is empty', () => {
createWrapper({ data: { nodes: [] }, config: { fields: MOCK_FIELDS } });
expect(wrapper.text()).toContain('No data found for this query');
});
});

View File

@ -2,6 +2,7 @@ import Presenter, { componentForField } from '~/glql/core/presenter';
import LinkPresenter from '~/glql/components/presenters/link.vue';
import TextPresenter from '~/glql/components/presenters/text.vue';
import ListPresenter from '~/glql/components/presenters/list.vue';
import TablePresenter from '~/glql/components/presenters/table.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { MOCK_FIELDS, MOCK_ISSUES } from '../mock_data';
@ -18,12 +19,13 @@ describe('componentForField', () => {
describe('Presenter', () => {
it.each`
displayType | additionalProps
${'list'} | ${{ listType: 'ul' }}
${'orderedList'} | ${{ listType: 'ol' }}
displayType | additionalProps | PresenterComponent
${'list'} | ${{ listType: 'ul' }} | ${ListPresenter}
${'orderedList'} | ${{ listType: 'ol' }} | ${ListPresenter}
${'table'} | ${{}} | ${TablePresenter}
`(
'inits a ListPresenter for displayType: $displayType with additionalProps: $additionalProps',
async ({ displayType, additionalProps }) => {
'inits appropriate presenter component for displayType: $displayType with additionalProps: $additionalProps',
async ({ displayType, additionalProps, PresenterComponent }) => {
const element = document.createElement('div');
element.innerHTML =
'<pre><code data-canonical-lang="glql">assignee = currentUser()</code></pre>';
@ -32,12 +34,12 @@ describe('Presenter', () => {
const { component } = await new Presenter().init({ data, config });
const wrapper = mountExtended(component);
const listPresenter = wrapper.findComponent(ListPresenter);
const presenter = wrapper.findComponent(PresenterComponent);
expect(listPresenter.exists()).toBe(true);
expect(listPresenter.props('data')).toBe(data);
expect(listPresenter.props('config')).toBe(config);
expect(listPresenter.props()).toMatchObject(additionalProps);
expect(presenter.exists()).toBe(true);
expect(presenter.props('data')).toBe(data);
expect(presenter.props('config')).toBe(config);
expect(presenter.props()).toMatchObject(additionalProps);
},
);
});

View File

@ -1,4 +1,9 @@
import { extractGroupOrProject, parseQueryText, parseFrontmatter } from '~/glql/utils/common';
import {
extractGroupOrProject,
parseQueryText,
parseFrontmatter,
toSentenceCase,
} from '~/glql/utils/common';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
describe('extractGroupOrProject', () => {
@ -72,3 +77,16 @@ describe('parseFrontmatter', () => {
});
});
});
describe('toSentenceCase', () => {
it.each`
str | expected
${'title'} | ${'Title'}
${'camelCasedExample'} | ${'Camel cased example'}
${'snake_case_example'} | ${'Snake case example'}
${'id'} | ${'ID'}
${'iid'} | ${'IID'}
`('returns $expected for $str', ({ str, expected }) => {
expect(toSentenceCase(str)).toBe(expected);
});
});

View File

@ -33,7 +33,7 @@ exports[`packages_list_row renders 1`] = `
</div>
</div>
<div
class="gl-flex gl-grow gl-items-center gl-min-h-6 gl-min-w-0 gl-text-secondary gl-text-sm"
class="gl-flex gl-gap-3 gl-grow gl-items-center gl-min-h-6 gl-min-w-0 gl-text-sm gl-text-subtle"
>
<div
class="gl-display-flex"

View File

@ -38,7 +38,7 @@ exports[`packages_list_row renders 1`] = `
</div>
</div>
<div
class="gl-flex gl-grow gl-items-center gl-min-h-6 gl-min-w-0 gl-text-secondary gl-text-sm"
class="gl-flex gl-gap-3 gl-grow gl-items-center gl-min-h-6 gl-min-w-0 gl-text-sm gl-text-subtle"
>
<div
class="gl-align-items-center gl-display-flex"

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::Observers::TransactionDuration do
subject(:transaction_duration_observer) { described_class.new(observation, directory_path, connection) }
let!(:transaction_duration_observer) { described_class.new(observation, directory_path, connection) }
let(:connection) { ActiveRecord::Migration.connection }
let(:observation) { Gitlab::Database::Migrations::Observation.new(version: migration_version, name: migration_name) }
@ -80,22 +80,35 @@ RSpec.describe Gitlab::Database::Migrations::Observers::TransactionDuration do
def run_real_transactions
ApplicationRecord.transaction do
User.first
end
end
def run_sub_transactions
ApplicationRecord.transaction(requires_new: true) do
User.first
end
end
def run_transaction
ApplicationRecord.connection_pool.with_connection do |connection|
Gitlab::Database::SharedModel.using_connection(connection) do
User.first
Gitlab::Database::SharedModel.transaction do
User.first
Gitlab::Database::SharedModel.transaction(requires_new: true) do
User.first
Gitlab::Database::SharedModel.transaction do
User.first
Gitlab::Database::SharedModel.transaction do
User.first
Gitlab::Database::SharedModel.transaction(requires_new: true) do
User.first
end
end
end

View File

@ -550,27 +550,6 @@ RSpec.describe Gitlab::Database, feature_category: :database do
event = events.first
expect(event).not_to be_nil
expect(event.duration).to be > 0.0
expect(event.payload).to a_hash_including(
connection: be_a(Gitlab::Database::LoadBalancing::ConnectionProxy)
)
end
end
context 'within an empty transaction block' do
it 'publishes a transaction event' do
events = subscribe_events do
ApplicationRecord.transaction {}
Ci::ApplicationRecord.transaction {}
end
expect(events.length).to be(2)
event = events.first
expect(event).not_to be_nil
expect(event.duration).to be > 0.0
expect(event.payload).to a_hash_including(
connection: be_a(Gitlab::Database::LoadBalancing::ConnectionProxy)
)
end
end
@ -578,8 +557,12 @@ RSpec.describe Gitlab::Database, feature_category: :database do
it 'publishes multiple transaction events' do
events = subscribe_events do
ApplicationRecord.transaction do
ApplicationRecord.transaction do
ApplicationRecord.transaction do
User.first
ApplicationRecord.transaction(requires_new: true) do
User.first
ApplicationRecord.transaction(requires_new: true) do
User.first
end
end
@ -591,9 +574,6 @@ RSpec.describe Gitlab::Database, feature_category: :database do
events.each do |event|
expect(event).not_to be_nil
expect(event.duration).to be > 0.0
expect(event.payload).to a_hash_including(
connection: be_a(Gitlab::Database::LoadBalancing::ConnectionProxy)
)
end
end
end
@ -612,9 +592,6 @@ RSpec.describe Gitlab::Database, feature_category: :database do
event = events.first
expect(event).not_to be_nil
expect(event.duration).to be > 0.0
expect(event.payload).to a_hash_including(
connection: be_a(Gitlab::Database::LoadBalancing::ConnectionProxy)
)
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe ProjectAutoDevops do
RSpec.describe ProjectAutoDevops, feature_category: :auto_devops do
let_it_be(:project) { build(:project) }
it_behaves_like 'having unique enum values'
@ -65,69 +65,66 @@ RSpec.describe ProjectAutoDevops do
describe '#create_gitlab_deploy_token' do
let(:auto_devops) { build(:project_auto_devops, project: project) }
shared_examples 'does not create a gitlab deploy token' do
it do
expect { auto_devops.save! }.not_to change { project.deploy_tokens.count }
end
end
shared_examples 'creates a gitlab deploy token' do
it do
expect { auto_devops.save! }.to change { project.deploy_tokens.count }.by(1)
token = project.deploy_tokens.last
expect(token).to have_attributes(
name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME,
read_registry: true,
project_id: project.id
)
end
end
context 'when the project is public' do
let(:project) { create(:project, :repository, :public) }
it 'does not create a gitlab deploy token' do
expect do
auto_devops.save!
end.not_to change { DeployToken.count }
end
include_examples 'does not create a gitlab deploy token'
end
context 'when the project is internal' do
let(:project) { create(:project, :repository, :internal) }
it 'creates a gitlab deploy token' do
expect do
auto_devops.save!
end.to change { DeployToken.count }.by(1)
end
include_examples 'creates a gitlab deploy token'
end
context 'when the project is private' do
let(:project) { create(:project, :repository, :private) }
it 'creates a gitlab deploy token' do
expect do
auto_devops.save!
end.to change { DeployToken.count }.by(1)
end
include_examples 'creates a gitlab deploy token'
end
context 'when autodevops is enabled at project level' do
let(:project) { create(:project, :repository, :internal) }
let(:auto_devops) { build(:project_auto_devops, project: project) }
it 'creates a deploy token' do
expect do
auto_devops.save!
end.to change { DeployToken.count }.by(1)
end
include_examples 'creates a gitlab deploy token'
end
context 'when autodevops is enabled at instance level' do
let(:project) { create(:project, :repository, :internal) }
let(:auto_devops) { build(:project_auto_devops, enabled: nil, project: project) }
it 'creates a deploy token' do
before do
allow(Gitlab::CurrentSettings).to receive(:auto_devops_enabled?).and_return(true)
expect do
auto_devops.save!
end.to change { DeployToken.count }.by(1)
end
include_examples 'creates a gitlab deploy token'
end
context 'when autodevops is disabled' do
let(:project) { create(:project, :repository, :internal) }
let(:auto_devops) { build(:project_auto_devops, :disabled, project: project) }
it 'does not create a deploy token' do
expect do
auto_devops.save!
end.not_to change { DeployToken.count }
end
include_examples 'does not create a gitlab deploy token'
end
context 'when the project already has an active gitlab-deploy-token' do
@ -135,11 +132,7 @@ RSpec.describe ProjectAutoDevops do
let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, projects: [project]) }
let(:auto_devops) { build(:project_auto_devops, project: project) }
it 'does not create a deploy token' do
expect do
auto_devops.save!
end.not_to change { DeployToken.count }
end
include_examples 'does not create a gitlab deploy token'
end
context 'when the project already has a revoked gitlab-deploy-token' do
@ -147,11 +140,7 @@ RSpec.describe ProjectAutoDevops do
let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, :expired, projects: [project]) }
let(:auto_devops) { build(:project_auto_devops, project: project) }
it 'does not create a deploy token' do
expect do
auto_devops.save!
end.not_to change { DeployToken.count }
end
include_examples 'does not create a gitlab deploy token'
end
end
end

View File

@ -385,4 +385,32 @@ RSpec.describe UserPreference, feature_category: :user_profile do
end
end
end
describe '#dpop_enabled' do
let(:pref) { described_class.new(args) }
context 'when no arguments are provided' do
let(:args) { {} }
it 'is set to false by default' do
expect(pref.dpop_enabled).to eq(false)
end
end
context 'when dpop_enabled is set to nil' do
let(:args) { { dpop_enabled: nil } }
it 'returns default value' do
expect(pref.dpop_enabled).to eq(false)
end
end
context 'when dpop_enabled is set to true' do
let(:args) { { dpop_enabled: true } }
it 'returns assigned value' do
expect(pref.dpop_enabled).to eq(true)
end
end
end
end

View File

@ -108,6 +108,9 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to delegate_method(:home_organization_id).to(:user_preference) }
it { is_expected.to delegate_method(:home_organization_id=).to(:user_preference).with_arguments(:args) }
it { is_expected.to delegate_method(:dpop_enabled).to(:user_preference) }
it { is_expected.to delegate_method(:dpop_enabled=).to(:user_preference).with_arguments(:args) }
it { is_expected.to delegate_method(:job_title).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:job_title=).to(:user_detail).with_arguments(:args).allow_nil }

View File

@ -23,6 +23,20 @@ RSpec.describe VirtualRegistries::Packages::Maven::CachedResponse, type: :model,
end
it { is_expected.to validate_uniqueness_of(:relative_path).scoped_to(:upstream_id) }
context 'when upstream_id is nil' do
let(:new_cached_response) { build(:virtual_registries_packages_maven_cached_response) }
before do
cached_response.update!(upstream_id: nil)
new_cached_response.upstream = nil
end
it 'does not validate uniqueness of relative_path' do
new_cached_response.validate
expect(new_cached_response.errors.messages_for(:relative_path)).not_to include 'has already been taken'
end
end
end
end

View File

@ -29,10 +29,10 @@ require (
gitlab.com/gitlab-org/labkit v1.21.0
go.uber.org/goleak v1.3.0
gocloud.dev v0.38.0
golang.org/x/image v0.18.0
golang.org/x/image v0.19.0
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616
golang.org/x/net v0.28.0
golang.org/x/oauth2 v0.21.0
golang.org/x/oauth2 v0.22.0
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.34.2

View File

@ -550,8 +550,8 @@ golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -643,8 +643,8 @@ golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=