Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3c55affa66
commit
1e1012d3d2
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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'),
|
||||
},
|
||||
]);
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -34,12 +34,4 @@
|
|||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-link {
|
||||
svg {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin: 0 2px 0 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -655,6 +655,7 @@ $status-icon-size: 22px;
|
|||
*/
|
||||
$discord: #5865f2;
|
||||
$linkedin: #2867b2;
|
||||
$mastodon: #6364ff;
|
||||
$skype: #0078d7;
|
||||
$twitter: #1d9bf0;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -242,6 +242,10 @@
|
|||
color: $discord;
|
||||
}
|
||||
|
||||
.mastodon-icon {
|
||||
color: $mastodon;
|
||||
}
|
||||
|
||||
.key-created-at {
|
||||
line-height: 42px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -144,7 +144,6 @@
|
|||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
> .ci-status-link,
|
||||
> .btn,
|
||||
> .commit-sha-group {
|
||||
margin-left: $gl-padding;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
652375e6b7318fe85b4b23eac3cce88618136341cee7721522adacbe52a52c66
|
||||
|
|
@ -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))
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.**.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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") }
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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 }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue