Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-10-19 18:07:43 +00:00
parent 3c55affa66
commit 1e1012d3d2
70 changed files with 763 additions and 157 deletions

View File

@ -20,6 +20,7 @@ function createSuggestionPlugin({
limit = 15,
nodeType,
nodeProps = {},
insertionMap = {},
}) {
const fetchData = memoize(
isFunction(dataSource) ? dataSource : async () => (await axios.get(dataSource)).data,
@ -36,7 +37,7 @@ function createSuggestionPlugin({
.focus()
.insertContentAt(range, [
{ type: nodeType, attrs: props },
{ type: 'text', text: ' ' },
{ type: 'text', text: ` ${insertionMap[props.text] || ''}` },
])
.run();
},
@ -217,6 +218,19 @@ export default Node.create({
referenceType: 'command',
},
search: (query) => ({ name }) => find(name, query),
insertionMap: {
'/label': '~',
'/unlabel': '~',
'/relabel': '~',
'/assign': '@',
'/unassign': '@',
'/reassign': '@',
'/cc': '@',
'/assign_reviewer': '@',
'/unassign_reviewer': '@',
'/reassign_reviewer': '@',
'/milestone': '%',
},
}),
createSuggestionPlugin({
editor: this.editor,

View File

@ -2,6 +2,8 @@
import { isEmpty } from 'lodash';
import * as translations from '~/ml/model_registry/routes/models/index/translations';
import Pagination from '~/vue_shared/components/incubation/pagination.vue';
import { BASE_SORT_FIELDS } from '../constants';
import SearchBar from './search_bar.vue';
import ModelRow from './model_row.vue';
export default {
@ -9,6 +11,7 @@ export default {
components: {
Pagination,
ModelRow,
SearchBar,
},
props: {
models: {
@ -26,6 +29,7 @@ export default {
},
},
i18n: translations,
sortableFields: BASE_SORT_FIELDS,
};
</script>
@ -40,6 +44,7 @@ export default {
</div>
<template v-if="hasModels">
<search-bar :sortable-fields="$options.sortableFields" />
<model-row v-for="model in models" :key="model.name" :model="model" />
<pagination v-bind="pageInfo" />
</template>

View File

@ -0,0 +1,71 @@
<script>
import { queryToObject, setUrlParams, visitUrl } from '~/lib/utils/url_utility';
import { LIST_KEY_CREATED_AT } from '~/ml/experiment_tracking/routes/experiments/show/constants';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
export default {
name: 'SearchBar',
components: {
RegistrySearch,
},
props: {
sortableFields: {
type: Array,
required: true,
},
},
data() {
const query = queryToObject(window.location.search);
const filter = query.name ? [{ value: { data: query.name }, type: FILTERED_SEARCH_TERM }] : [];
const orderBy = query.orderBy || LIST_KEY_CREATED_AT;
return {
filters: filter,
sorting: {
orderBy,
sort: (query.sort || 'desc').toLowerCase(),
},
};
},
methods: {
submitFilters() {
return visitUrl(setUrlParams(this.parsedQuery()));
},
parsedQuery() {
const name = this.filters
.map((f) => f.value.data)
.join(' ')
.trim();
const filterByQuery = name === '' ? {} : { name };
return { ...filterByQuery, ...this.sorting };
},
updateFilters(newValue) {
this.filters = newValue;
},
updateSorting(newValue) {
this.sorting = { ...this.sorting, ...newValue };
},
updateSortingAndEmitUpdate(newValue) {
this.updateSorting(newValue);
this.submitFilters();
},
},
};
</script>
<template>
<registry-search
:filters="filters"
:sorting="sorting"
:sortable-fields="sortableFields"
@sorting:changed="updateSortingAndEmitUpdate"
@filter:changed="updateFilters"
@filter:submit="submitFilters"
@filter:clear="filters = []"
/>
</template>

View File

@ -0,0 +1,13 @@
import { s__ } from '~/locale';
export const LIST_KEY_CREATED_AT = 'created_at';
export const BASE_SORT_FIELDS = Object.freeze([
{
orderBy: 'name',
label: s__('MlExperimentTracking|Name'),
},
{
orderBy: LIST_KEY_CREATED_AT,
label: s__('MlExperimentTracking|Created at'),
},
]);

View File

@ -153,9 +153,10 @@ const initForkInfo = () => {
initForkInfo();
const CommitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status');
const statusLink = document.querySelector('.commit-actions .ci-status-link');
if (statusLink) {
statusLink.remove();
const legacyStatusBadge = document.querySelector('.js-ci-status-badge-legacy');
if (legacyStatusBadge) {
legacyStatusBadge.remove();
// eslint-disable-next-line no-new
new Vue({
el: CommitPipelineStatusEl,

View File

@ -91,7 +91,7 @@ export default {
};
</script>
<template>
<div class="ci-status-link">
<div class="gl-ml-5">
<gl-loading-icon v-if="isLoading" size="lg" label="Loading pipeline status" />
<a v-else :href="ciStatus.details_path">
<ci-icon

View File

@ -115,7 +115,7 @@ export default {
class="commit-actions gl-display-flex gl-flex-align gl-align-items-center gl-flex-direction-row"
>
<signature-badge v-if="commit.signature" :signature="commit.signature" />
<div v-if="commit.pipeline" class="ci-status-link">
<div v-if="commit.pipeline" class="gl-ml-5">
<ci-badge-link
:status="commit.pipeline.detailedStatus"
:details-path="commit.pipeline.detailedStatus.detailsPath"

View File

@ -1,5 +1,5 @@
<script>
import { GlToggle, GlBadge } from '@gitlab/ui';
import { GlToggle } from '@gitlab/ui';
import { updateApplicationSettings } from '~/rest_api';
import { createAlert } from '~/alert';
import toast from '~/vue_shared/plugins/global_toast';
@ -13,11 +13,9 @@ export default {
saveError: s__('SilentMode|There was an error updating the Silent Mode Settings.'),
enabled: __('enabled'),
disabled: __('disabled'),
experiment: __('Experiment'),
},
components: {
GlToggle,
GlBadge,
},
props: {
isSilentModeEnabled: {
@ -62,9 +60,5 @@ export default {
:label="$options.i18n.toggleLabel"
:is-loading="isLoading"
@change="updateSilentModeSettings"
>
<template #label
>{{ $options.i18n.toggleLabel }} <gl-badge>{{ $options.i18n.experiment }}</gl-badge></template
>
</gl-toggle>
/>
</template>

View File

@ -34,12 +34,4 @@
margin-left: 8px;
}
}
.ci-status-link {
svg {
position: relative;
top: 2px;
margin: 0 2px 0 3px;
}
}
}

View File

@ -655,6 +655,7 @@ $status-icon-size: 22px;
*/
$discord: #5865f2;
$linkedin: #2867b2;
$mastodon: #6364ff;
$skype: #0078d7;
$twitter: #1d9bf0;

View File

@ -88,20 +88,6 @@ $comparison-empty-state-height: 62px;
.merge-request-title {
margin-bottom: 2px;
.ci-status-link {
svg {
height: 16px;
width: 16px;
position: relative;
top: 3px;
}
&:hover,
&:focus {
text-decoration: none;
}
}
}
}
}
@ -147,10 +133,6 @@ $comparison-empty-state-height: 62px;
padding: 0;
background: transparent;
}
.ci-status-link {
margin-right: 5px;
}
}
.merge-request-select {

View File

@ -242,6 +242,10 @@
color: $discord;
}
.mastodon-icon {
color: $mastodon;
}
.key-created-at {
line-height: 42px;
}

View File

@ -320,10 +320,6 @@
}
}
.ci-status-link {
@include gl-text-decoration-none;
}
&:not(.compact) {
.controls {
@include media-breakpoint-up(lg) {
@ -369,10 +365,6 @@
}
}
}
.ci-status-link {
@include gl-display-inline-flex;
}
}
.icon-container {

View File

@ -144,7 +144,6 @@
vertical-align: text-bottom;
}
> .ci-status-link,
> .btn,
> .commit-sha-group {
margin-left: $gl-padding;

View File

@ -81,17 +81,6 @@ ul.related-merge-requests > li gl-emoji {
}
}
.related-merge-requests {
.ci-status-link {
display: block;
margin-right: 5px;
}
svg {
display: block;
}
}
@include media-breakpoint-down(xs) {
.detail-page-header {
.issuable-meta {

View File

@ -67,7 +67,6 @@ nav.navbar-collapse.collapse,
.nav,
.btn,
ul.notes-form,
.ci-status-link::after,
.issuable-gutter-toggle,
.gutter-toggle,
.issuable-details .content-block-small,

View File

@ -342,6 +342,7 @@ class Admin::UsersController < Admin::ApplicationController
:bio,
:can_create_group,
:color_scheme_id,
:discord,
:email,
:extern_uid,
:external,
@ -350,6 +351,7 @@ class Admin::UsersController < Admin::ApplicationController
:hide_no_ssh_key,
:key_id,
:linkedin,
:mastodon,
:name,
:password_expires_at,
:projects_limit,
@ -358,7 +360,6 @@ class Admin::UsersController < Admin::ApplicationController
:skype,
:theme_id,
:twitter,
:discord,
:username,
:website_url,
:note,

View File

@ -111,6 +111,7 @@ class ProfilesController < Profiles::ApplicationController
[
:avatar,
:bio,
:discord,
:email,
:role,
:gitpod_enabled,
@ -119,12 +120,12 @@ class ProfilesController < Profiles::ApplicationController
:hide_project_limit,
:linkedin,
:location,
:mastodon,
:name,
:public_email,
:commit_email,
:skype,
:twitter,
:discord,
:username,
:website_url,
:organization,

View File

@ -10,7 +10,11 @@ module Projects
MAX_MODELS_PER_PAGE = 20
def index
@paginator = ::Projects::Ml::ModelFinder.new(@project)
find_params = params
.transform_keys(&:underscore)
.permit(:name, :order_by, :sort)
@paginator = ::Projects::Ml::ModelFinder.new(@project, find_params)
.execute
.keyset_paginate(cursor: params[:cursor], per_page: MAX_MODELS_PER_PAGE)
end

View File

@ -44,10 +44,6 @@ class SearchController < ApplicationController
push_frontend_feature_flag(:search_notes_hide_archived_projects, current_user)
end
before_action only: :show do
push_frontend_feature_flag(:search_merge_requests_hide_archived_projects, current_user)
end
rescue_from ActiveRecord::QueryCanceled, with: :render_timeout
layout 'search'

View File

@ -3,16 +3,46 @@
module Projects
module Ml
class ModelFinder
def initialize(project)
VALID_ORDER_BY = %w[name created_at id].freeze
VALID_SORT = %w[asc desc].freeze
def initialize(project, params = {})
@project = project
@params = params
end
def execute
::Ml::Model
.by_project(@project)
@models = ::Ml::Model
.by_project(project)
.including_latest_version
.with_version_count
@models = by_name
ordered
end
private
def by_name
return models unless params[:name].present?
models.by_name(params[:name])
end
def ordered
order_by = valid_or_default(params[:order_by]&.downcase, VALID_ORDER_BY, 'created_at')
sort = valid_or_default(params[:sort]&.downcase, VALID_SORT, 'desc')
models.order_by("#{order_by}_#{sort}").with_order_id_desc
end
def valid_or_default(value, valid_values, default)
return value if valid_values.include?(value)
default
end
attr_reader :params, :project, :models
end
end
end

View File

@ -371,6 +371,14 @@ module ApplicationHelper
"https://discord.com/users/#{user.discord}"
end
def mastodon_url(user)
return '' if user.mastodon.blank?
url = user.mastodon.match UserDetail::MASTODON_VALIDATION_REGEX
"https://#{url[2]}/@#{url[1]}"
end
def collapsed_sidebar?
cookies["sidebar_collapsed"] == "true"
end

View File

@ -77,7 +77,7 @@ module Ci
def render_status_with_link(status, path = nil, type: _('pipeline'), tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16)
variant = badge_variant(status)
klass = "ci-status-link #{ci_icon_class_for_status(status)} d-inline-flex gl-line-height-1 #{cssclass}"
klass = "js-ci-status-badge-legacy #{ci_icon_class_for_status(status)} d-inline-flex gl-line-height-1 #{cssclass}"
title = "#{type.titleize}: #{ci_label_for_status(status)}"
data = { toggle: 'tooltip', placement: tooltip_placement, container: container, testid: 'ci-status-badge-legacy' }
badge_classes = 'gl-px-2 gl-ml-3'

View File

@ -3,6 +3,7 @@
module Ml
class Model < ApplicationRecord
include Presentable
include Sortable
validates :project, :default_experiment, presence: true
validates :name,
@ -24,6 +25,7 @@ module Ml
.select("ml_models.*, count(ml_model_versions.id) as version_count")
.group(:id)
}
scope :by_name, ->(name) { where("ml_models.name LIKE ?", "%#{sanitize_sql_like(name)}%") } # rubocop:disable GitlabSecurity/SqlInjection
scope :by_project, ->(project) { where(project_id: project.id) }
def valid_default_experiment?

View File

@ -417,6 +417,7 @@ class User < MainClusterwide::ApplicationRecord
delegate :pronouns, :pronouns=, to: :user_detail, allow_nil: true
delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true
delegate :registration_objective, :registration_objective=, to: :user_detail, allow_nil: true
delegate :mastodon, :mastodon=, to: :user_detail, allow_nil: true
delegate :linkedin, :linkedin=, to: :user_detail, allow_nil: true
delegate :twitter, :twitter=, to: :user_detail, allow_nil: true
delegate :skype, :skype=, to: :user_detail, allow_nil: true

View File

@ -17,10 +17,24 @@ class UserDetail < MainClusterwide::ApplicationRecord
DEFAULT_FIELD_LENGTH = 500
MASTODON_VALIDATION_REGEX = /
\A # beginning of string
@?\b # optional leading at
([\w\d.%+-]+) # character group to pick up words in user portion of username
@ # separator between user and host
( # beginning of charagter group for host portion
[\w\d.-]+ # character group to pick up words in host portion of username
\.\w{2,} # pick up tld of host domain, 2 chars or more
)\b # end of character group to pick up words in host portion of username
\z # end of string
/x
validates :discord, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validate :discord_format
validates :linkedin, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validates :location, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validates :mastodon, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validate :mastodon_format
validates :organization, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validates :skype, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validates :twitter, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
@ -32,7 +46,7 @@ class UserDetail < MainClusterwide::ApplicationRecord
enum registration_objective: REGISTRATION_OBJECTIVE_PAIRS, _suffix: true
def sanitize_attrs
%i[discord linkedin skype twitter website_url].each do |attr|
%i[discord linkedin mastodon skype twitter website_url].each do |attr|
value = self[attr]
self[attr] = Sanitize.clean(value) if value.present?
end
@ -49,6 +63,7 @@ class UserDetail < MainClusterwide::ApplicationRecord
self.discord = '' if discord.nil?
self.linkedin = '' if linkedin.nil?
self.location = '' if location.nil?
self.mastodon = '' if mastodon.nil?
self.organization = '' if organization.nil?
self.skype = '' if skype.nil?
self.twitter = '' if twitter.nil?
@ -62,4 +77,10 @@ def discord_format
errors.add(:discord, _('must contain only a discord user ID.'))
end
def mastodon_format
return if mastodon.blank? || mastodon =~ UserDetail::MASTODON_VALIDATION_REGEX
errors.add(:mastodon, _('must contain only a mastodon username.'))
end
UserDetail.prepend_mod_with('UserDetail')

View File

@ -3,7 +3,7 @@
- path = local_assigns.fetch(:path, status.has_details? ? status.details_path : nil)
- option_css_classes = local_assigns.fetch(:option_css_classes, '')
- css_classes = "gl-px-2 #{option_css_classes}"
- ci_css_classes = "ci-status-link ci-status-icon ci-status-icon-#{status.group} gl-line-height-1"
- ci_css_classes = "ci-status-icon ci-status-icon-#{status.group} gl-line-height-1"
- title = s_("PipelineStatusTooltip|Pipeline: %{ci_status}") % {ci_status: status.label}
= gl_badge_tag(variant: badge_variant(status), size: :md, href: path, class: css_classes, title: title, data: { toggle: 'tooltip', placement: tooltip_placement, testid: "ci-status-badge" }) do

View File

@ -122,6 +122,10 @@
allow_empty: true}
%small.form-text.text-gl-muted
= external_accounts_docs_link
- if Feature.enabled?(:mastodon_social_ui, @user)
.form-group.gl-form-group
= f.label :mastodon
= f.text_field :mastodon, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: "@robin@example.com"
.form-group.gl-form-group
= f.label :website_url, s_('Profiles|Website url')

View File

@ -16,10 +16,10 @@
%li.list-item{ class: "gl-py-0! gl-border-0!" }
.item-body.gl-display-flex.align-items-center.gl-px-3.gl-pr-2.gl-mx-n2
.item-contents.gl-display-flex.gl-align-items-center.gl-flex-wrap.gl-flex-grow-1.gl-min-h-7
.item-title.gl-display-flex.mb-xl-0.gl-min-w-0
.item-title.gl-display-flex.mb-xl-0.gl-min-w-0.gl-align-items-center
- if branch[:pipeline_status].present?
%span.related-branch-ci-status
= render 'ci/status/icon', status: branch[:pipeline_status]
= render 'ci/status/icon', status: branch[:pipeline_status], option_css_classes: 'gl-display-block gl-mr-3'
%span.related-branch-info
%strong
= link_to branch[:name], branch[:link], class: "ref-name"

View File

@ -95,6 +95,10 @@
= render 'middle_dot_divider', breakpoint: 'sm' do
= link_to discord_url(@user), class: 'gl-hover-text-decoration-none', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow' do
= sprite_icon('discord', css_class: 'discord-icon')
- if Feature.enabled?(:mastodon_social_ui, @user) && @user.mastodon.present?
= render 'middle_dot_divider', breakpoint: 'sm' do
= link_to mastodon_url(@user), class: 'gl-hover-text-decoration-none', title: "Mastodon", target: '_blank', rel: 'noopener noreferrer nofollow' do
= sprite_icon('mastodon', css_class: 'mastodon-icon')
- if @user.website_url.present?
= render 'middle_dot_divider', stacking: true do
- if Feature.enabled?(:security_auto_fix) && @user.bot?

View File

@ -1,8 +1,8 @@
---
name: search_merge_requests_hide_archived_projects
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126024
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/417595
milestone: '16.3'
name: mastodon_social_ui
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132892
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428163
milestone: '16.5'
type: development
group: group::global search
group: group::tenant scale
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: project_tool_filter_with_scanner_name
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131310
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/424509
milestone: '16.5'
type: development
group: group::threat insights
default_enabled: false

View File

@ -1,17 +1,28 @@
# frozen_string_literal: true
GITALY_COORDINATION_MESSAGE = <<~MSG
This merge request requires coordination with gitaly deployments.
Before merging this merge request we should verify that gitaly
running in production already implements the new gRPC interface
included here.
## Changing Gitaly version
Failing to do so will introduce a [non backward compatible
change](https://docs.gitlab.com/ee/development/multi_version_compatibility.html)
during canary depoyment that can cause an incident.
This merge request requires coordination with Gitaly deployments. You must assert why this change is safe.
1. Identify the gitaly MR introducing the new interface
1. Verify that the environment widget contains a `gprd` deployment
If these two assertions can be made, then this change is safe:
1. No Gitaly definitions that have been removed in the version bump are in use on the Rails side.
1. No Gitaly definitions that are not yet part of a released version become used without a feature flag.
In general, we can ignore the first assertion because the specs will fail as needed. If a GitLab Rails spec
exercises a definition that is removed in the new Gitaly version, then that
spec will fail.
You must confirm the second assertion. Failing to do so will introduce a [non
backward compatible change](https://docs.gitlab.com/ee/development/multi_version_compatibility.html),
for example during canary deployment of GitLab.com, which can cause an incident.
This type of problem can also impact customers performing zero-downtime upgrades.
Some options:
- This change does not cause Rails to use a new definition.
- This change causes Rails to use a new definition, but only behind a feature flag which is disabled by default.
This feature flag must only be removed in a subsequent release.
MSG
changed_lines = helper.changed_lines('Gemfile.lock')

View File

@ -1,5 +1,10 @@
# frozen_string_literal: true
# Danger should not comment when inline disables are added in the following files.
no_suggestions_for_extensions = %w[.md]
helper.all_changed_files.each do |filename|
next if filename.end_with?(*no_suggestions_for_extensions)
rubocop.add_suggestions_for(filename)
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class AddMastodonToUserDetails < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
USER_DETAILS_FIELD_LIMIT = 500
def up
with_lock_retries do
add_column :user_details, :mastodon, :text, default: '', null: false, if_not_exists: true
end
add_text_limit :user_details, :mastodon, USER_DETAILS_FIELD_LIMIT
end
def down
with_lock_retries do
remove_column :user_details, :mastodon
end
end
end

View File

@ -0,0 +1 @@
652375e6b7318fe85b4b23eac3cce88618136341cee7721522adacbe52a52c66

View File

@ -24112,6 +24112,7 @@ CREATE TABLE user_details (
enterprise_group_id bigint,
enterprise_group_associated_at timestamp with time zone,
email_reset_offered_at timestamp with time zone,
mastodon text DEFAULT ''::text NOT NULL,
CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100)),
CONSTRAINT check_444573ee52 CHECK ((char_length(skype) <= 500)),
CONSTRAINT check_466a25be35 CHECK ((char_length(twitter) <= 500)),
@ -24123,6 +24124,7 @@ CREATE TABLE user_details (
CONSTRAINT check_8a7fcf8a60 CHECK ((char_length(location) <= 500)),
CONSTRAINT check_a73b398c60 CHECK ((char_length(phone) <= 50)),
CONSTRAINT check_eeeaf8d4f0 CHECK ((char_length(pronouns) <= 50)),
CONSTRAINT check_f1a8a05b9a CHECK ((char_length(mastodon) <= 500)),
CONSTRAINT check_f932ed37db CHECK ((char_length(pronunciation) <= 255))
);

View File

@ -0,0 +1,33 @@
---
stage: Data Stores
group: Database
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# Monitoring and logging setup for external databases
External PostgreSQL database systems have different logging options for monitoring performance and troubleshooting, however they are not enabled by default. In this section we provide the recommendations for self-managed PostgreSQL, and recommendations for some major providers of PostgreSQL managed services.
## Recommended PostgreSQL Logging settings
You should enable the following logging settings:
- `log_statement=ddl`: log changes of database model definition (DDL), such as `CREATE`, `ALTER` or `DROP` of objects. This helps track recent model changes that could be causing performance issues and identify security breaches and human errors.
- `log_lock_waits=on`: log of processes holding [locks](https://www.postgresql.org/docs/current/explicit-locking.html) for long periods, a common cause of poor query performance.
- `log_temp_files=0`: log usage of intense and unusual temporary files that can indicate poor query performance.
- `log_autovacuum_min_duration=0`: log all autovacuum executions. Autovacuum is a key component for overall PostgreSQL engine performance. Essential for troubleshooting and tuning if dead tuples are not being removed from tables.
- `log_min_duration_statement=1000`: log slow queries (slower than 1 second).
The full description of the above parameter settings can be found in
[PostgreSQL error reporting and logging documentation](https://www.postgresql.org/docs/current/runtime-config-logging.html#RUNTIME-CONFIG-LOGGING-WHAT).
## Amazon RDS
The Amazon Relational Database Service (RDS) provides a large number of [monitoring metrics](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Monitoring.html) and [logging interfaces](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Monitor_Logs_Events.html). Here are a few you should configure:
- Change all above [recommended PostgreSQL Logging settings](#recommended-postgresql-logging-settings) through [RDS Parameter Groups](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithDBInstanceParamGroups.html).
- As the recommended logging parameters are [dynamic in RDS](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Appendix.PostgreSQL.CommonDBATasks.Parameters.html) you don't require a reboot after changing these settings.
- The PostgreSQL logs can be observed through the [RDS console](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/logs-events-streams-console.html).
- Enable [RDS performance insight](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_PerfInsights.html) allows you to visualise your database load with many important performance metrics of a PostgreSQL database engine.
- Enable [RDS Enhanced Monitoring](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Monitoring.OS.html) to monitor the operating system metrics. These metrics can indicate bottlenecks in your underlying hardware and OS that are impacting your database performance.
- In production environments set the monitoring interval to 10 seconds (or less) to capture micro bursts of resource usage that can be the cause of many performance issues. Set `Granularity=10` in the console or `monitoring-interval=10` in the CLI.

View File

@ -30,6 +30,10 @@ your own external PostgreSQL server.
Read how to [set up an external PostgreSQL instance](external.md).
When setting up an external database there are some metrics that are useful for monitoring and troubleshooting.
When setting up an external database there are monitoring and logging settings required for troubleshooting various database related issues.
Read more about [monitoring and logging setup for external Databases](external_metrics.md).
### PostgreSQL replication and failover for Linux package installations **(PREMIUM SELF)**
This setup is for when you have installed GitLab using the

View File

@ -4,10 +4,11 @@ group: Geo
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# GitLab Silent Mode **(FREE SELF EXPERIMENT)**
# GitLab Silent Mode **(FREE SELF)**
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/9826) in GitLab 15.11. This feature is an [Experiment](../../policy/experiment-beta-support.md#experiment).
> - Enabling and disabling Silent Mode through the web UI was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131090) in GitLab 16.4
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/9826) in GitLab 15.11. This feature was an [Experiment](../../policy/experiment-beta-support.md#experiment).
> - Enabling and disabling Silent Mode through the web UI was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131090) in GitLab 16.4.
> - Silent Mode was updated to [Generally Available (GA)](../../policy/experiment-beta-support.md#generally-available-ga) in GitLab 16.6.
Silent Mode allows you to silence outbound communication, such as emails, from GitLab. Silent Mode is not intended to be used on environments which are in-use. Two use-cases are:
@ -76,7 +77,7 @@ It may take up to a minute to take effect. [Issue 405433](https://gitlab.com/git
## Behavior of GitLab features in Silent Mode
This section documents the current behavior of GitLab when Silent Mode is enabled. While Silent Mode is an Experiment, the behavior may change without notice. The work for the first iteration of Silent Mode is tracked by [Epic 9826](https://gitlab.com/groups/gitlab-org/-/epics/9826).
This section documents the current behavior of GitLab when Silent Mode is enabled. The work for the first iteration of Silent Mode is tracked by [Epic 9826](https://gitlab.com/groups/gitlab-org/-/epics/9826).
When Silent Mode is enabled, a banner is displayed at the top of the page for all users stating the setting is enabled and **All outbound communications are blocked.**.

View File

@ -1161,6 +1161,10 @@ Example response:
]
```
NOTE:
This endpoint is subject to [Merge requests diff limits](../administration/instance_limits.md#diff-limits).
Merge requests that exceed the diff limits return limited results.
## List merge request pipelines
Get a list of merge request pipelines. The pagination parameters `page` and

View File

@ -38,10 +38,10 @@ context rich definitions around the reason the feature is SaaS-only.
1. Add the new feature to `FEATURE` in `ee/lib/ee/gitlab/saas.rb`.
```ruby
FEATURES = %w[purchases/additional_minutes some_domain/new_feature_name].freeze
FEATURES = %i[purchases_additional_minutes some_domain_new_feature_name].freeze
```
1. Use the new feature in code with `Gitlab::Saas.feature_available?('some_domain/new_feature_name')`.
1. Use the new feature in code with `Gitlab::Saas.feature_available?(:some_domain_new_feature_name)`.
#### SaaS-only feature definition and validation
@ -68,7 +68,7 @@ Each SaaS feature is defined in a separate YAML file consisting of a number of f
Prepend the `ee/lib/ee/gitlab/saas.rb` module and override the `Gitlab::Saas.feature_available?` method.
```ruby
JH_DISABLED_FEATURES = %w[some_domain/new_feature_name].freeze
JH_DISABLED_FEATURES = %i[some_domain_new_feature_name].freeze
override :feature_available?
def feature_available?(feature)
@ -88,30 +88,30 @@ It is strongly advised to include automated tests for all code affected by a Saa
to ensure the feature works properly.
To enable a SaaS-only feature in a test, use the `stub_saas_features`
helper. For example, to globally disable the `purchases/additional_minutes` feature
helper. For example, to globally disable the `purchases_additional_minutes` feature
flag in a test:
```ruby
stub_saas_features('purchases/additional_minutes' => false)
stub_saas_features(purchases_additional_minutes: false)
::Gitlab::Saas.feature_available?('purchases/additional_minutes') # => false
::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => false
```
A common pattern of testing both paths looks like:
```ruby
it 'purchases/additional_minutes is not available' do
# tests assuming purchases/additional_minutes is not enabled by default
::Gitlab::Saas.feature_available?('purchases/additional_minutes') # => false
# tests assuming purchases_additional_minutes is not enabled by default
::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => false
end
context 'when purchases/additional_minutes is available' do
context 'when purchases_additional_minutes is available' do
before do
stub_saas_features('purchases/additional_minutes' => true)
stub_saas_features(purchases_additional_minutes: true)
end
it 'returns true' do
::Gitlab::Saas.feature_available?('purchases/additional_minutes') # => true
::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => true
end
end
```

View File

@ -0,0 +1,145 @@
---
stage: Create
group: Editor Extensions
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# TypeScript
## History with GitLab
TypeScript has been [considered](https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/35),
discussed, promoted, and rejected for years at GitLab. The general
conclusion is that we are unable to integrate TypeScript into the main
project because the costs outweigh the benefits.
- The main project has **a lot** of pre-existing code that is not strongly typed.
- The main contributors to the main project are not all familiar with TypeScript.
Apart from the main project, TypeScript has been profitably employed in
a handful of satellite projects.
## Projects using TypeScript
The following GitLab projects use TypeScript:
- [`gitlab-web-ide`](https://gitlab.com/gitlab-org/gitlab-web-ide/)
- [`gitlab-vscode-extension`](https://gitlab.com/gitlab-org/gitlab-vscode-extension/)
- [`gitlab-language-server-for-code-suggestions`](https://gitlab.com/gitlab-org/editor-extensions/gitlab-language-server-for-code-suggestions)
## Recommended configurations
The [GitLab Workflow Extension](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/blob/main) project is a good model
for a project's TypeScript configuration. Consider copying the `.tsconfig` and `.eslintrc.json` from there.
- In `.tsconfig`, make sure [`"strict": true`](https://www.typescriptlang.org/tsconfig#strict) is set.
- In `.eslintrc.json`, make sure that TypeScript-specific parsing and linting is placed in an `overrides` for `**/*.ts` files.
## Future plans
- Shared ESLint configuration to reuse across TypeScript projects.
## Recommended patterns
### Avoid casting with `<>` or `as`
Avoid casting with `<>` or `as` as much as possible. This circumvents Type safety. Consider using
[type predicates](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates).
```typescript
// Bad
function handler(data: unknown) {
console.log((data as StuffContainer).stuff);
}
// Good :)
function hasStuff(data: unknown): data is StuffContainer {
if (data && typeof data === 'object') {
return 'stuff' in data;
}
return false;
}
function handler(data: unknown) {
if (hasStuff(data)) {
// No casting needed :)
console.log(data.stuff);
}
throw new Error('Expected data to have stuff. Catastrophic consequences might follow...');
}
```
### Prefer `interface` over `type` for new interfaces
Prefer interface over type declaration when describing structures.
```typescript
// Bad
type Fooer = {
foo: () => string;
}
// Good
interface Fooer {
foo: () => string;
}
```
### Use `type` to define aliases for existing types
Use type to define aliases for existing types, classes or interfaces. Use
the TypeScript [Utility Types](https://www.typescriptlang.org/docs/handbook/utility-types.html)
to provide transformations.
```typescript
interface Config = {
foo: string;
isBad: boolean;
}
// Bad
type PartialConfig = {
foo?: string;
isBad?: boolean;
}
// Good
type PartialConfig = Partial<Config>;
```
### Use union types to improve inference
```typescript
// Bad
interface Foo { type: string }
interface FooBar extends Foo { bar: string }
interface FooZed extends Foo { zed: string }
const doThing = (foo: Foo) => {
if (foo.type === 'bar') {
// Casting bad :(
console.log((foo as FooBar).bar);
}
}
// Good :)
interface FooBar { type: 'bar', bar: string };
interface FooZed { type: 'zed', zed: string }
type Foo = FooBar | FooZed;
const doThing = (foo: Foo) => {
if (foo.type === 'bar') {
// No casting needed :) - TS knows we are FooBar now
console.log(foo.bar);
}
}
```
## Related topics
- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html)
- [TypeScript notes in GitLab Workflow Extension](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/blob/main/docs/developer/coding-guidelines.md?ref_type=heads#typescript)

View File

@ -6,9 +6,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Free push limit **(FREE SAAS)**
A 100 MB per-file limit applies when pushing new files to any project in the Free tier.
A 100 MiB per-file limit applies when pushing new files to any project in the Free tier.
If a new file that is 100 MB or large is pushed to a project in the Free tier, an error is displayed. For example:
If a new file that is 100 MiB or large is pushed to a project in the Free tier, an error is displayed. For example:
```shell
Enumerating objects: 3, done.

View File

@ -128,6 +128,8 @@ to match your username.
## Add external accounts to your user profile page
> Mastodon user account [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132892) as a beta feature in 16.5 [with a flag](../feature_flags.md) named `mastodon_social_ui`. Disabled by default.
You can add links to certain other external accounts you might have, like Skype and Twitter.
They can help other users connect with you on other platforms.
@ -138,6 +140,7 @@ To add links to other accounts:
1. In the **Main settings** section, add your:
- Discord [user ID](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-).
- LinkedIn profile name.
- Mastodon username.
- Skype username.
- Twitter @username.

View File

@ -50,7 +50,11 @@ variables:
- gitlab-terraform plan-json
resource_group: ${TF_STATE_NAME}
artifacts:
# The next line, which disables public access to pipeline artifacts, may not be available everywhere.
# Terraform's cache files can include secrets which can be accidentally exposed.
# Please exercise caution when utilizing secrets in your Terraform infrastructure and
# consider limiting access to artifacts or take other security measures to protect sensitive information.
#
# The next line, which disables public access to pipeline artifacts, is not available on GitLab.com.
# See: https://docs.gitlab.com/ee/ci/yaml/#artifactspublic
public: false
paths:

View File

@ -216,9 +216,7 @@ module Gitlab
unless default_project_filter
project_ids = project_ids_relation
if Feature.enabled?(:search_merge_requests_hide_archived_projects, current_user) && !filters[:include_archived]
project_ids = project_ids.non_archived
end
project_ids = project_ids.non_archived unless filters[:include_archived]
merge_requests = merge_requests.of_projects(project_ids)
end

View File

@ -21159,9 +21159,6 @@ msgstr ""
msgid "Geo|Filter by name"
msgstr ""
msgid "Geo|Filter by status"
msgstr ""
msgid "Geo|Geo Settings"
msgstr ""
@ -57157,6 +57154,9 @@ msgstr ""
msgid "must contain only a discord user ID."
msgstr ""
msgid "must contain only a mastodon username."
msgstr ""
msgid "must have a repository"
msgstr ""

View File

@ -17,7 +17,7 @@ module RuboCop
# end
#
# # good
# if Gitlab::Saas.feature_available?('purchases/additional_minutes')
# if Gitlab::Saas.feature_available?(:purchases_additional_minutes)
# Ci::Runner::FORM_EDITABLE + Ci::Runner::MINUTES_COST_FACTOR_FIELDS
# else
# Ci::Runner::FORM_EDITABLE

View File

@ -128,6 +128,16 @@ RSpec.describe ProfilesController, :request_store do
expect(user.reload.discord).to eq(discord_user_id)
expect(response).to have_gitlab_http_status(:found)
end
it 'allows updating user specified mastodon username', :aggregate_failures do
mastodon_username = '@robin@example.com'
sign_in(user)
put :update, params: { user: { mastodon: mastodon_username } }
expect(user.reload.mastodon).to eq(mastodon_username)
expect(response).to have_gitlab_http_status(:found)
end
end
describe 'GET audit_log' do

View File

@ -33,7 +33,9 @@ RSpec.describe "User comments on issue", :js, feature_category: :team_planning d
end
end
it_behaves_like 'edits content using the content editor'
# do not test quick actions here since guest users don't have permission
# to execute all quick actions
it_behaves_like 'edits content using the content editor', { with_quick_actions: false }
it "adds comment with code block" do
code_block_content = "Command [1]: /usr/local/bin/git , see [text](doc/text)"

View File

@ -139,8 +139,6 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
end
end
it_behaves_like 'edits content using the content editor'
context 'dropzone upload file', :js do
before do
visit new_project_issue_path(project)
@ -308,6 +306,21 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
end
end
context 'when signed in as a maintainer', :js do
let_it_be(:project) { create(:project) }
before_all do
project.add_maintainer(user)
end
before do
sign_in(user)
visit(new_project_issue_path(project))
end
it_behaves_like 'edits content using the content editor'
end
context "when signed in as user with special characters in their name" do
let(:user_special) { create(:user, name: "Jon O'Shea") }

View File

@ -54,7 +54,7 @@ RSpec.shared_examples 'Signup name validation' do |field, max_length, label|
end
end
RSpec.describe 'Signup', :js, feature_category: :user_profile do
RSpec.describe 'Signup', :js, feature_category: :user_management do
include TermsHelper
let(:new_user) { build_stubbed(:user) }

View File

@ -6,24 +6,55 @@ RSpec.describe Projects::Ml::ModelFinder, feature_category: :mlops do
let_it_be(:project) { create(:project) }
let_it_be(:model1) { create(:ml_models, :with_versions, project: project) }
let_it_be(:model2) { create(:ml_models, :with_versions, project: project) }
let_it_be(:model3) { create(:ml_models) }
let_it_be(:model3) { create(:ml_models, name: "#{model1.name}_1", project: project) }
let_it_be(:other_model) { create(:ml_models) }
let_it_be(:project_models) { [model1, model2, model3] }
subject(:models) { described_class.new(project).execute.to_a }
let(:params) { {} }
it 'returns models for project' do
is_expected.to match_array([model1, model2])
subject(:models) { described_class.new(project, params).execute.to_a }
describe 'default params' do
it 'returns models for project ordered by id' do
is_expected.to eq([model3, model2, model1])
end
it 'including the latest version', :aggregate_failures do
expect(models[0].association_cached?(:latest_version)).to be(true)
expect(models[1].association_cached?(:latest_version)).to be(true)
end
it 'does not return models belonging to a different project' do
is_expected.not_to include(other_model)
end
it 'includes version count' do
expect(models[0].version_count).to be(models[0].versions.count)
end
end
it 'including the latest version', :aggregate_failures do
expect(models[0].association_cached?(:latest_version)).to be(true)
expect(models[1].association_cached?(:latest_version)).to be(true)
context 'when name is passed' do
let(:params) { { name: model1.name } }
it 'searches by name' do
is_expected.to match_array([model1, model3])
end
end
it 'does not return models belonging to a different project' do
is_expected.not_to include(model3)
end
describe 'sorting' do
using RSpec::Parameterized::TableSyntax
it 'includes version count' do
expect(models[0].version_count).to be(models[0].versions.count)
where(:test_case, :order_by, :direction, :expected_order) do
'default params' | nil | nil | [2, 1, 0]
'ascending order' | 'id' | 'ASC' | [0, 1, 2]
'by column' | 'name' | 'ASC' | [0, 2, 1]
'invalid sort' | nil | 'UP' | [2, 1, 0]
'invalid order by' | 'INVALID' | nil | [2, 1, 0]
end
with_them do
let(:params) { { order_by: order_by, sort: direction } }
it { expect(subject).to eq(project_models.values_at(*expected_order)) }
end
end
end

View File

@ -3,6 +3,8 @@ import MlModelsIndexApp from '~/ml/model_registry/routes/models/index';
import ModelRow from '~/ml/model_registry/routes/models/index/components/model_row.vue';
import { TITLE_LABEL, NO_MODELS_LABEL } from '~/ml/model_registry/routes/models/index/translations';
import Pagination from '~/vue_shared/components/incubation/pagination.vue';
import SearchBar from '~/ml/model_registry/routes/models/index/components/search_bar.vue';
import { BASE_SORT_FIELDS } from '~/ml/model_registry/routes/models/index/constants';
import { mockModels, startCursor, defaultPageInfo } from './mock_data';
let wrapper;
@ -14,6 +16,7 @@ const findModelRow = (index) => wrapper.findAllComponents(ModelRow).at(index);
const findPagination = () => wrapper.findComponent(Pagination);
const findTitle = () => wrapper.findByText(TITLE_LABEL);
const findEmptyLabel = () => wrapper.findByText(NO_MODELS_LABEL);
const findSearchBar = () => wrapper.findComponent(SearchBar);
describe('MlModelsIndex', () => {
describe('empty state', () => {
@ -26,6 +29,10 @@ describe('MlModelsIndex', () => {
it('does not show pagination', () => {
expect(findPagination().exists()).toBe(false);
});
it('does not show search bar', () => {
expect(findSearchBar().exists()).toBe(false);
});
});
describe('with data', () => {
@ -43,6 +50,10 @@ describe('MlModelsIndex', () => {
});
});
it('adds a search bar', () => {
expect(findSearchBar().props()).toMatchObject({ sortableFields: BASE_SORT_FIELDS });
});
describe('model list', () => {
it('displays the models', () => {
expect(findModelRow(0).props('model')).toMatchObject(mockModels[0]);

View File

@ -0,0 +1,86 @@
import { shallowMount } from '@vue/test-utils';
import setWindowLocation from 'helpers/set_window_location_helper';
import * as urlHelpers from '~/lib/utils/url_utility';
import SearchBar from '~/ml/model_registry/routes/models/index/components/search_bar.vue';
import { BASE_SORT_FIELDS } from '~/ml/model_registry/routes/models/index/constants';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
let wrapper;
const makeUrl = ({ filter = 'query', orderBy = 'name', sort = 'asc' } = {}) =>
`https://blah.com/?name=${filter}&orderBy=${orderBy}&sort=${sort}`;
const createWrapper = () => {
wrapper = shallowMount(SearchBar, { propsData: { sortableFields: BASE_SORT_FIELDS } });
};
const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
describe('SearchBar', () => {
beforeEach(() => {
createWrapper();
});
it('passes default filter and sort by to registry search', () => {
expect(findRegistrySearch().props()).toMatchObject({
filters: [],
sorting: {
orderBy: 'created_at',
sort: 'desc',
},
sortableFields: BASE_SORT_FIELDS,
});
});
it('sets the component filters based on the querystring', () => {
const filter = 'A';
setWindowLocation(makeUrl({ filter }));
createWrapper();
expect(findRegistrySearch().props('filters')).toMatchObject([{ value: { data: filter } }]);
});
it('sets the registry search sort based on the querystring', () => {
const orderBy = 'B';
const sort = 'C';
setWindowLocation(makeUrl({ orderBy, sort }));
createWrapper();
expect(findRegistrySearch().props('sorting')).toMatchObject({ orderBy, sort: 'c' });
});
describe('Search submit', () => {
beforeEach(() => {
setWindowLocation(makeUrl());
jest.spyOn(urlHelpers, 'visitUrl').mockImplementation(() => {});
createWrapper();
});
it('On submit, resets the cursor and reloads to correct page', () => {
findRegistrySearch().vm.$emit('filter:submit');
expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
expect(urlHelpers.visitUrl).toHaveBeenCalledWith(makeUrl());
});
it('On sorting changed, resets cursor and reloads to correct page', () => {
const orderBy = 'created_at';
findRegistrySearch().vm.$emit('sorting:changed', { orderBy });
expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
expect(urlHelpers.visitUrl).toHaveBeenCalledWith(makeUrl({ orderBy }));
});
it('On direction changed, reloads to correct page', () => {
const sort = 'asc';
findRegistrySearch().vm.$emit('sorting:changed', { sort });
expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
expect(urlHelpers.visitUrl).toHaveBeenCalledWith(makeUrl({ sort }));
});
});
});

View File

@ -8,7 +8,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
class="commit-actions gl-align-items-center gl-display-flex gl-flex-align gl-flex-direction-row"
>
<div
class="ci-status-link"
class="gl-ml-5"
>
<ci-badge-link-stub
aria-label="Pipeline: failed"

View File

@ -1,4 +1,4 @@
import { GlToggle, GlBadge } from '@gitlab/ui';
import { GlToggle } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
@ -29,19 +29,8 @@ describe('SilentModeSettingsApp', () => {
};
const findGlToggle = () => wrapper.findComponent(GlToggle);
const findGlBadge = () => wrapper.findComponent(GlBadge);
describe('template', () => {
describe('experiment badge', () => {
beforeEach(() => {
createComponent();
});
it('renders properly', () => {
expect(findGlBadge().exists()).toBe(true);
});
});
describe('when silent mode is already enabled', () => {
beforeEach(() => {
createComponent({ isSilentModeEnabled: true });

View File

@ -637,6 +637,21 @@ RSpec.describe ApplicationHelper do
expect(discord).to eq('https://discord.com/users/1234567890123456789')
end
end
context 'when mastodon is set' do
let_it_be(:user) { build(:user) }
let(:mastodon) { mastodon_url(user) }
it 'returns an empty string if mastodon username is not set' do
expect(mastodon).to eq('')
end
it 'returns mastodon url when mastodon username is set' do
user.mastodon = '@robin@example.com'
expect(mastodon).to eq('https://example.com/@robin')
end
end
end
describe '#gitlab_ui_form_for' do

View File

@ -37,7 +37,7 @@ RSpec.describe Ci::StatusHelper do
subject { helper.render_status_with_link("success") }
it "renders a passed status icon" do
is_expected.to include("<span class=\"ci-status-link ci-status-icon-success d-inline-flex")
is_expected.to include("<span class=\"js-ci-status-badge-legacy ci-status-icon-success d-inline-flex")
end
it "has 'Pipeline' as the status type in the title" do
@ -84,7 +84,7 @@ RSpec.describe Ci::StatusHelper do
subject { helper.render_status_with_link("success", cssclass: "extra-class") }
it "has appended extra class to icon classes" do
is_expected.to include('class="ci-status-link ci-status-icon-success d-inline-flex ' \
is_expected.to include('class="js-ci-status-badge-legacy ci-status-icon-success d-inline-flex ' \
'gl-line-height-1 extra-class"')
end
end

View File

@ -48,7 +48,7 @@ RSpec.describe Gitlab::GroupSearchResults, feature_category: :global_search do
end
include_examples 'search results filtered by state'
include_examples 'search results filtered by archived', 'search_merge_requests_hide_archived_projects'
include_examples 'search results filtered by archived'
end
describe 'milestones search' do

View File

@ -197,7 +197,7 @@ RSpec.describe Gitlab::SearchResults, feature_category: :global_search do
let(:query) { 'foo' }
include_examples 'search results filtered by state'
include_examples 'search results filtered by archived', 'search_merge_requests_hide_archived_projects'
include_examples 'search results filtered by archived'
end
context 'ordering' do

View File

@ -59,6 +59,27 @@ RSpec.describe UserDetail do
end
end
describe '#mastodon' do
it { is_expected.to validate_length_of(:mastodon).is_at_most(500) }
context 'when mastodon is set' do
let_it_be(:user_detail) { create(:user_detail) }
it 'accepts a valid mastodon username' do
user_detail.mastodon = '@robin@example.com'
expect(user_detail).to be_valid
end
it 'throws an error when mastodon username format is wrong' do
user_detail.mastodon = '@robin'
expect(user_detail).not_to be_valid
expect(user_detail.errors.full_messages).to match_array([_('Mastodon must contain only a mastodon username.')])
end
end
end
describe '#location' do
it { is_expected.to validate_length_of(:location).is_at_most(500) }
end
@ -97,6 +118,7 @@ RSpec.describe UserDetail do
discord: '1234567890123456789',
linkedin: 'linkedin',
location: 'location',
mastodon: '@robin@example.com',
organization: 'organization',
skype: 'skype',
twitter: 'twitter',
@ -117,6 +139,7 @@ RSpec.describe UserDetail do
it_behaves_like 'prevents `nil` value', :discord
it_behaves_like 'prevents `nil` value', :linkedin
it_behaves_like 'prevents `nil` value', :location
it_behaves_like 'prevents `nil` value', :mastodon
it_behaves_like 'prevents `nil` value', :organization
it_behaves_like 'prevents `nil` value', :skype
it_behaves_like 'prevents `nil` value', :twitter

View File

@ -113,6 +113,9 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to delegate_method(:linkedin).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:linkedin=).to(:user_detail).with_arguments(:args).allow_nil }
it { is_expected.to delegate_method(:mastodon).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:mastodon=).to(:user_detail).with_arguments(:args).allow_nil }
it { is_expected.to delegate_method(:twitter).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:twitter=).to(:user_detail).with_arguments(:args).allow_nil }

View File

@ -33,7 +33,7 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do
end
it 'fetches the models using the finder' do
expect(::Projects::Ml::ModelFinder).to receive(:new).with(project).and_call_original
expect(::Projects::Ml::ModelFinder).to receive(:new).with(project, {}).and_call_original
index_request
end
@ -61,6 +61,22 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do
end
end
context 'with search params' do
let(:params) { { name: 'some_name', order_by: 'name', sort: 'asc' } }
it 'passes down params to the finder' do
expect(Projects::Ml::ModelFinder).to receive(:new).and_call_original do |_exp, params|
expect(params.to_h).to include({
name: 'some_name',
order_by: 'name',
sort: 'asc'
})
end
index_request
end
end
describe 'pagination' do
before do
stub_const("Projects::Ml::ModelsController::MAX_MODELS_PER_PAGE", 2)

View File

@ -17,7 +17,7 @@ RSpec.describe RuboCop::Cop::Gitlab::FeatureAvailableUsage do
end
it 'does not flag the use of Gitlab::Saas.feature_available?' do
expect_no_offenses('Gitlab::Saas.feature_available?("some/feature")')
expect_no_offenses('Gitlab::Saas.feature_available?(:some_feature)')
end
it 'flags the use with a dynamic feature as nil' do

View File

@ -6,9 +6,9 @@ module StubSaasFeatures
# @param [Hash] features where key is feature name and value is boolean whether enabled or not.
#
# Examples
# - `stub_saas_features('onboarding' => false)` ... Disable `onboarding`
# - `stub_saas_features(onboarding: false)` ... Disable `onboarding`
# SaaS feature globally.
# - `stub_saas_features('onboarding' => true)` ... Enable `onboarding`
# - `stub_saas_features(onboarding: true)` ... Enable `onboarding`
# SaaS feature globally.
def stub_saas_features(features)
features.each do |feature_name, value|

View File

@ -2,7 +2,10 @@
require 'spec_helper'
RSpec.shared_examples 'edits content using the content editor' do |params = { with_expanded_references: true }|
RSpec.shared_examples 'edits content using the content editor' do |params = {
with_expanded_references: true,
with_quick_actions: true
}|
include ContentEditorHelpers
let(:content_editor_testid) { '[data-testid="content-editor"] [contenteditable].ProseMirror' }
@ -546,6 +549,39 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
end
end
if params[:with_quick_actions]
it 'shows suggestions for quick actions' do
type_in_content_editor '/a'
expect(find(suggestions_dropdown)).to have_text('/assign')
expect(find(suggestions_dropdown)).to have_text('/label')
end
it 'adds the correct prefix for /assign' do
type_in_content_editor '/assign'
send_keys [:arrow_down, :enter]
expect(page).to have_text('/assign @')
end
it 'adds the correct prefix for /label' do
type_in_content_editor '/label'
send_keys [:arrow_down, :enter]
expect(page).to have_text('/label ~')
end
it 'adds the correct prefix for /milestone' do
type_in_content_editor '/milestone'
send_keys [:arrow_down, :enter]
expect(page).to have_text('/milestone %')
end
end
it 'shows suggestions for members with descriptions' do
type_in_content_editor '@a'

View File

@ -149,7 +149,10 @@ RSpec.shared_examples 'User updates wiki page' do
end
end
it_behaves_like 'edits content using the content editor', { with_expanded_references: false }
it_behaves_like 'edits content using the content editor', {
with_expanded_references: false,
with_quick_actions: false
}
it_behaves_like 'inserts diagrams.net diagram using the content editor'
it_behaves_like 'autocompletes items'
end

View File

@ -6,7 +6,7 @@ RSpec.describe StubSaasFeatures, feature_category: :shared do
describe '#stub_saas_features' do
using RSpec::Parameterized::TableSyntax
let(:feature_name) { '_some_saas_feature_' }
let(:feature_name) { :some_saas_feature }
context 'when checking global state' do
where(:feature_value) do
@ -41,10 +41,10 @@ RSpec.describe StubSaasFeatures, feature_category: :shared do
end
it 'handles multiple features' do
stub_saas_features(feature_name => false, '_some_new_feature_' => true)
stub_saas_features(feature_name => false, some_new_feature: true)
expect(::Gitlab::Saas.feature_available?(feature_name)).to eq(false)
expect(::Gitlab::Saas.feature_available?('_some_new_feature_')).to eq(true)
expect(::Gitlab::Saas.feature_available?(:some_new_feature)).to eq(true)
end
end
end