Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-07-17 21:26:24 +00:00
parent 9d67de388b
commit dcdf2392b9
101 changed files with 1040 additions and 201 deletions

View File

@ -15,7 +15,7 @@ export const JOB_GRAPHQL_ERRORS = {
export const ICONS = {
TAG: 'tag',
MR: 'git-merge',
MR: 'merge-request',
BRANCH: 'branch',
RETRY: 'retry',
SUCCESS: 'success',

View File

@ -7,6 +7,7 @@ import cancelJobMutation from './graphql/mutations/job_cancel.mutation.graphql';
import playJobMutation from './graphql/mutations/job_play.mutation.graphql';
import retryJobMutation from './graphql/mutations/job_retry.mutation.graphql';
import unscheduleJobMutation from './graphql/mutations/job_unschedule.mutation.graphql';
import JobActionModal from './job_action_modal.vue';
export const i18n = {
errors: {
@ -46,6 +47,7 @@ export default {
GlButton,
GlIcon,
GlLoadingIcon,
JobActionModal,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -74,10 +76,17 @@ export default {
actionType() {
return this.jobAction.icon;
},
hasConfirmationModal() {
return this.jobAction?.confirmationMessage !== null;
},
},
methods: {
onActionButtonClick() {
this.executeJobAction();
if (this.hasConfirmationModal) {
this.showConfirmationModal = true;
} else {
this.executeJobAction();
}
},
async executeJobAction() {
try {
@ -117,5 +126,12 @@ export default {
<gl-loading-icon v-if="isLoading" size="sm" class="gl-m-2" />
<gl-icon v-else :name="jobAction.icon" :size="12" />
</gl-button>
<job-action-modal
v-if="hasConfirmationModal"
v-model="showConfirmationModal"
:job-name="jobName"
:custom-message="jobAction.confirmationMessage"
@confirm="executeJobAction"
/>
</div>
</template>

View File

@ -0,0 +1,69 @@
<script>
import { GlModal } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
export default {
name: 'JobActionModal',
i18n: {
title: s__('PipelineGraph|Are you sure you want to run %{jobName}?'),
confirmationText: s__('PipelineGraph|Do you want to continue?'),
actionCancel: { text: __('Cancel') },
},
components: {
GlModal,
},
model: {
prop: 'visible',
event: 'change',
},
props: {
customMessage: {
type: String,
required: true,
},
visible: {
type: Boolean,
required: false,
default: false,
},
jobName: {
type: String,
required: true,
},
},
computed: {
modalText() {
return {
confirmButton: {
text: sprintf(__('Yes, run %{jobName}'), {
jobName: this.jobName,
}),
},
message: sprintf(__('Custom confirmation message: %{message}'), {
message: this.customMessage,
}),
title: sprintf(this.$options.i18n.title, {
jobName: this.jobName,
}),
};
},
},
};
</script>
<template>
<gl-modal
modal-id="job-action-modal"
:action-cancel="$options.i18n.actionCancel"
:action-primary="modalText.confirmButton"
:title="modalText.title"
:visible="visible"
@primary="$emit('confirm')"
@change="$emit('change', $event)"
>
<div>
<p>{{ modalText.message }}</p>
<span>{{ $options.i18n.confirmationText }}</span>
</div>
</gl-modal>
</template>

View File

@ -20,5 +20,9 @@ export default {
</script>
<template>
<contribution-event-base :event="event" :message="$options.i18n.message" icon-name="git-merge" />
<contribution-event-base
:event="event"
:message="$options.i18n.message"
icon-name="merge-request"
/>
</template>

View File

@ -16,7 +16,7 @@ export default {
v-gl-tooltip
:title="__('Part of merge request changes')"
:size="12"
name="git-merge"
name="merge-request"
class="gl-mr-3"
/>
</template>

View File

@ -118,7 +118,7 @@ export const MR_FILTER_OPTIONS = [
{
text: __('Merge request status'),
value: 'status',
systemNoteIcons: ['git-merge', 'issue-close', 'issues', 'merge-request-close'],
systemNoteIcons: ['merge-request', 'issue-close', 'issues', 'merge-request-close'],
},
{
text: __('Tracking'),

View File

@ -35,7 +35,7 @@ export default {
<template>
<div class="gl-display-flex gl-align-items-center">
<template v-if="hasPipeline">
<gl-icon name="git-merge" class="gl-mr-2" />
<gl-icon name="merge-request" class="gl-mr-2" />
<span data-testid="pipeline-ref" class="gl-mr-2">{{ pipeline.ref }}</span>
<gl-icon name="commit" class="gl-mr-2" />

View File

@ -38,7 +38,7 @@ export default {
<template>
<div class="gl-display-flex gl-align-items-center">
<template v-if="hasPipeline">
<gl-icon name="git-merge" class="gl-mr-2" />
<gl-icon name="merge-request" class="gl-mr-2" />
<span data-testid="pipeline-ref" class="gl-mr-2">{{ packageEntity.pipeline.ref }}</span>
<gl-icon name="commit" class="gl-mr-2" />

View File

@ -1,3 +1,7 @@
import { initMrPage } from '~/pages/projects/merge_requests/page';
import { DiffFile } from '~/rapid_diffs/diff_file';
import { DiffFileMounted } from '~/rapid_diffs/diff_file_mounted';
initMrPage();
customElements.define('diff-file', DiffFile);
customElements.define('diff-file-mounted', DiffFileMounted);

View File

@ -0,0 +1,34 @@
import { ExpandLinesAdapter } from '~/rapid_diffs/expand_lines/adapter';
/** @module RapidDiffs */
const RAPID_DIFFS_VIEWERS = {
text_inline: 'text_inline',
text_parallel: 'text_parallel',
};
export const VIEWER_ADAPTERS = {
[RAPID_DIFFS_VIEWERS.text_inline]: [ExpandLinesAdapter],
[RAPID_DIFFS_VIEWERS.text_parallel]: [ExpandLinesAdapter],
};
/** @typedef {HTMLDivElement} diffElement */
/** @typedef {string} viewer */
/**
* @typedef {Object} adapterContext
* @property {viewer} viewer
* @property {diffElement} diffElement
*/
/**
* @typedef {PointerEvent} PointerEventWithTarget
* @property {HTMLElement} target
*/
/**
* @typedef {Object} diffFileAdapter
* @property {function(this: adapterContext, event: PointerEventWithTarget): void} [onClick] - Handle click events that happen on the diff file.
* @property {function(this: adapterContext): void} [onVisible] - Executes when diff files becomes visible.
* @property {function(this: adapterContext): void} [onInvisible] - Executes when diff files becomes invisible.
*/

View File

@ -0,0 +1,63 @@
import { VIEWER_ADAPTERS } from './adapters';
// required for easier mocking in tests
import IntersectionObserver from './intersection_observer';
/** @module RapidDiffs */
const sharedObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.onVisible();
} else {
entry.target.onInvisible();
}
});
});
export class DiffFile extends HTMLElement {
/** @type {diffElement} */
diffElement;
/** @type {viewer} */
viewer;
adapterConfig = VIEWER_ADAPTERS;
constructor() {
super();
this.boundOnClick = this.onClick.bind(this);
}
mount() {
const [diffElement] = this.children;
this.diffElement = diffElement;
this.viewer = this.dataset.viewer;
sharedObserver.observe(this);
}
onVisible() {
this.diffElement.addEventListener('click', this.boundOnClick);
this.adapters.forEach((adapter) => adapter.onVisible?.call?.(this.adapterContext));
}
onInvisible() {
this.adapters.forEach((adapter) => adapter.onInvisible?.call?.(this.adapterContext));
this.diffElement.removeEventListener('click', this.boundOnClick);
}
onClick(event) {
this.adapters.forEach((adapter) => adapter.onClick?.call?.(this.adapterContext, event));
}
/** @returns {adapterContext} */
get adapterContext() {
return {
diffElement: this.diffElement,
viewer: this.viewer,
};
}
/** @returns {diffFileAdapter[]} */
get adapters() {
return this.adapterConfig[this.viewer];
}
}

View File

@ -0,0 +1,5 @@
export class DiffFileMounted extends HTMLElement {
connectedCallback() {
this.parentElement.mount();
}
}

View File

@ -0,0 +1,42 @@
import { getLines } from '~/rapid_diffs/expand_lines/get_lines';
const getLineNumber = (el) => parseInt(el.dataset.linenumber, 10);
const collectLineData = (element) => {
const buttons = element.querySelectorAll('[data-linenumber]');
const lineNumbers = Array.from(buttons).map(getLineNumber);
const previousEl = element.previousElementSibling;
const prevNewLine = previousEl?.querySelector('[data-linenumber]:last-child');
const prevNewLineNumber = prevNewLine ? getLineNumber(prevNewLine) : 0;
return [...lineNumbers, prevNewLineNumber];
};
const viewersMap = {
text_inline: 'text',
text_parallel: 'parallel',
};
/** @type {diffFileAdapter} */
export const ExpandLinesAdapter = {
async onClick(event) {
const { target } = event;
const { expandPrevLine, expandNextLine } = target.dataset;
const parent = target.closest('tr');
if (parent.dataset.loading || (!expandPrevLine && !expandNextLine)) return;
parent.dataset.loading = true;
const { blobDiffPath } = this.diffElement.dataset;
const lines = await getLines({
expandPrevLine,
lineData: collectLineData(parent),
blobDiffPath,
view: viewersMap[this.viewer],
});
const method = expandPrevLine ? 'beforebegin' : 'afterend';
// eslint-disable-next-line no-unsanitized/method
parent.insertAdjacentHTML(method, lines);
parent.remove();
},
};

View File

@ -0,0 +1,34 @@
import axios from '~/lib/utils/axios_utils';
const UNFOLD_COUNT = 20;
const getRequestParams = (expandPrevLine, oldLineNumber, newLineNumber, prevNewLineNumber) => {
const offset = newLineNumber - oldLineNumber;
let since;
let to;
let unfold = true;
if (!expandPrevLine) {
const lineNumber = newLineNumber + 1;
since = lineNumber;
to = lineNumber + UNFOLD_COUNT;
} else {
const lineNumber = newLineNumber - 1;
since = lineNumber - UNFOLD_COUNT;
to = lineNumber;
// make sure we aren't loading more than we need
if (since <= prevNewLineNumber + 1) {
since = prevNewLineNumber + 1;
unfold = false;
}
}
return { since, to, bottom: !expandPrevLine, offset, unfold };
};
export const getLines = async ({ expandPrevLine, lineData, blobDiffPath, view }) => {
const params = getRequestParams(expandPrevLine, ...lineData);
const { data: lines } = await axios.get(blobDiffPath, { params: { ...params, view } });
return lines;
};

View File

@ -0,0 +1,2 @@
// required for easier DiffFile Web Component testing
export default IntersectionObserver;

View File

@ -148,7 +148,7 @@ export default {
<template v-if="shouldShowRefInfo">
<div class="icon-container gl-display-inline-block">
<gl-icon v-if="tag" name="tag" />
<gl-icon v-else-if="mergeRequestRef" name="git-merge" />
<gl-icon v-else-if="mergeRequestRef" name="merge-request" />
<gl-icon v-else name="branch" />
</div>

View File

@ -374,7 +374,7 @@ export default {
:aria-label="$options.i18n.mergeRequests"
class="gl-text-secondary"
>
<gl-icon name="git-merge" />
<gl-icon name="merge-request" />
<span>{{ openMergeRequestsCount }}</span>
</gl-link>
<gl-link

View File

@ -1 +1,2 @@
@import './diff_file_component';
@import './expand_lines';

View File

@ -0,0 +1,7 @@
.rd-diff-file [data-loading] [data-visible-when-loading=true] {
display: unset;
}
.rd-diff-file [data-loading] [data-visible-when-loading=false] {
display: none;
}

View File

@ -1,2 +1,2 @@
@import 'mixins_and_variables_and_functions';
@import 'components/rapid_diffs/diff_file_component';
@import 'components/rapid_diffs';

View File

@ -1,2 +1,2 @@
@import 'mixins_and_variables_and_functions';
@import 'components/rapid_diffs/diff_file_component';
@import 'components/rapid_diffs';

View File

@ -1,6 +1,14 @@
- if link?
%a{ href: @href, **html_options }><
= badge_content
- if has_icon?
= sprite_icon(@icon, css_class: icon_classes)
- if text
%span.gl-badge-content<
= text
- else
%span{ **html_options }><
= badge_content
- if has_icon?
= sprite_icon(@icon, css_class: icon_classes)
- if text
%span.gl-badge-content<
= text

View File

@ -8,7 +8,6 @@ module Pajamas
icon_classes: [],
icon_only: false,
href: nil,
size: :md,
variant: :muted,
**html_options
)
@ -17,12 +16,10 @@ module Pajamas
@icon_classes = Array.wrap(icon_classes)
@icon_only = @icon && icon_only
@href = href.presence
@size = filter_attribute(size.to_sym, SIZE_OPTIONS, default: :md)
@variant = filter_attribute(variant.to_sym, VARIANT_OPTIONS, default: :muted)
@html_options = html_options
end
SIZE_OPTIONS = [:sm, :md, :lg].freeze
VARIANT_OPTIONS = [:muted, :neutral, :info, :success, :warning, :danger, :tier].freeze
private
@ -30,12 +27,14 @@ module Pajamas
delegate :sprite_icon, to: :helpers
def badge_classes
["gl-badge", "badge", "badge-pill", "badge-#{@variant}", @size.to_s]
classes = ["gl-badge", "badge", "badge-pill", "badge-#{@variant}"]
classes.push('!gl-px-2') if icon_only?
classes.join(" ")
end
def icon_classes
classes = %w[gl-icon gl-badge-icon] + @icon_classes
classes.push("gl-mr-2") unless icon_only?
classes.push("-gl-ml-2") if circular_icon?
classes.join(" ")
end
@ -53,14 +52,12 @@ module Pajamas
content || @text
end
def badge_content
if icon_only?
sprite_icon(@icon, css_class: icon_classes)
elsif @icon.present?
sprite_icon(@icon, css_class: icon_classes) + text
else
text
end
def has_icon?
icon_only? || @icon.present?
end
def circular_icon?
%w[issue-open-m issue-close].include?(@icon)
end
def html_options

View File

@ -1,7 +1,9 @@
-# TODO: add fork suggestion (commits only)
.rd-diff-file
= render RapidDiffs::DiffFileHeaderComponent.new(diff_file: @diff_file)
.rd-diff-file-body
-# TODO: replace with ViewComponents for each viewer
= render 'projects/diffs/viewer', viewer: viewer
%diff-file{ data: web_component_context }
.rd-diff-file{ id: id, data: server_data }
= render RapidDiffs::DiffFileHeaderComponent.new(diff_file: @diff_file)
.rd-diff-file-body
-# TODO: replace with ViewComponents for each viewer
= render 'projects/diffs/viewer', viewer: viewer
%diff-file-mounted

View File

@ -2,12 +2,38 @@
module RapidDiffs
class DiffFileComponent < ViewComponent::Base
def initialize(diff_file:)
include TreeHelper
def initialize(diff_file:, parallel_view: false)
@diff_file = diff_file
@parallel_view = parallel_view
end
def id
@diff_file.file_identifier_hash
end
def server_data
project = @diff_file.repository.project
params = tree_join(@diff_file.content_sha, @diff_file.file_path)
{
blob_diff_path: project_blob_diff_path(project, params)
}
end
def web_component_context
viewer_name = viewer.partial_name
if viewer_name == 'text'
viewer_name = @parallel_view ? 'text_parallel' : 'text_inline'
end
{
viewer: viewer_name
}
end
def viewer
@diff_file.has_renderable? ? @diff_file.rendered.viewer : @diff_file.viewer
@diff_file.view_component_viewer
end
end
end

View File

@ -10,9 +10,6 @@ module BadgesHelper
# # Danger variant
# gl_badge_tag("foo", variant: :danger)
#
# # Small size
# gl_badge_tag("foo", size: :sm)
#
# # With icon
# gl_badge_tag("foo", icon: "question-o")
#

View File

@ -78,7 +78,7 @@ module Ci
icon_wrapper_class = "js-ci-status-badge-legacy ci-icon-gl-icon-wrapper"
gl_badge_tag(variant: variant, size: :md, href: path, class: badge_classes, title: title, data: data) do
gl_badge_tag(variant: variant, href: path, class: badge_classes, title: title, data: data) do
if show_status_text
content_tag(:span, ci_icon_for_status(status), { class: icon_wrapper_class }) + content_tag(:span, status.label, { class: 'gl-mx-2 gl-whitespace-nowrap', data: { testid: 'ci-icon-text' } })
else

View File

@ -45,6 +45,12 @@ module DiffHelper
options
end
def rapid_diffs?
return false unless defined? current_user
::Feature.enabled?(:rapid_diffs, current_user, type: :wip)
end
def diff_match_line(old_pos, new_pos, text: '', view: :inline, bottom: false)
content_line_class = %w[line_content match]
content_line_class << 'parallel' if view == :parallel
@ -54,13 +60,28 @@ module DiffHelper
html = []
expand_data = {}
if bottom
expand_data[:expand_next_line] = true
else
expand_data[:expand_prev_line] = true
end
if rapid_diffs?
expand_button = content_tag(:button, '...', class: 'gl-bg-none gl-border-0 gl-p-0', data: { visible_when_loading: false, **expand_data })
spinner = render(Pajamas::SpinnerComponent.new(size: :sm, class: 'gl-display-none gl-text-align-right', data: { visible_when_loading: true }))
expand_html = content_tag(:div, [expand_button, spinner].join.html_safe, data: { expand_wrapper: true })
else
expand_html = content_tag(:div, '...', data: { visible_when_loading: false, **expand_data })
end
if old_pos
html << content_tag(:td, '...', class: [*line_num_class, 'old_line'], data: { linenumber: old_pos })
html << content_tag(:td, expand_html, class: [*line_num_class, 'old_line'], data: { linenumber: old_pos })
html << content_tag(:td, text, class: [*content_line_class, 'left-side']) if view == :parallel
end
if new_pos
html << content_tag(:td, '...', class: [*line_num_class, 'new_line'], data: { linenumber: new_pos })
html << content_tag(:td, expand_html, class: [*line_num_class, 'new_line'], data: { linenumber: new_pos })
html << content_tag(:td, text, class: [*content_line_class, ('right-side' if view == :parallel)])
end

View File

@ -22,7 +22,6 @@ module GroupsHelper
end
def can_set_group_diff_preview_in_email?(group)
return false unless Feature.enabled?(:diff_preview_in_email, group)
return false if group.parent&.show_diff_preview_in_email?.equal?(false)
can?(current_user, :set_show_diff_preview_in_email, group)

View File

@ -129,7 +129,7 @@ module IssuablesHelper
count = issuables_count_for_state(issuable_type, state)
if count != -1
html << " " << gl_badge_tag(format_count(issuable_type, count, Gitlab::IssuablesCountForState::THRESHOLD), { variant: :muted, size: :sm }, { class: "gl-tab-counter-badge gl-hidden sm:gl-inline-flex" })
html << " " << gl_badge_tag(format_count(issuable_type, count, Gitlab::IssuablesCountForState::THRESHOLD), { variant: :muted }, { class: "gl-tab-counter-badge gl-hidden sm:gl-inline-flex" })
end
html.html_safe

View File

@ -197,7 +197,6 @@ module ProjectsHelper
end
def can_set_diff_preview_in_email?(project, current_user)
return false unless Feature.enabled?(:diff_preview_in_email, project.group)
return false if project.group&.show_diff_preview_in_email?.equal?(false)
can?(current_user, :set_show_diff_preview_in_email, project)

View File

@ -441,7 +441,7 @@ module SearchHelper
link_to search_path(search_params) do
concat label
concat ' '
concat gl_badge_tag(count, { size: :sm }, { class: badge_class, data: badge_data })
concat gl_badge_tag(count, { class: badge_class, data: badge_data })
end
end
end

View File

@ -70,7 +70,6 @@ module TabHelper
def gl_tab_counter_badge(count, html_options = {})
gl_badge_tag(
count,
{ size: :sm },
html_options.merge(
class: ['gl-tab-counter-badge', *html_options[:class]]
)

View File

@ -144,7 +144,7 @@ module TodosHelper
end
content_tag(:span, class: 'todo-target-state') do
gl_badge_tag(raw_state_to_i18n[state] || state.capitalize, { variant: variant, size: 'sm' })
gl_badge_tag(raw_state_to_i18n[state] || state.capitalize, { variant: variant })
end
end

View File

@ -9,7 +9,7 @@
- else
= _('Unknown')
%td{ role: 'cell', data: { label: _('Status') } }
= gl_badge_tag migration.status_name.to_s.humanize, { size: :sm, variant: batched_migration_status_badge_variant(migration) }
= gl_badge_tag migration.status_name.to_s.humanize, { variant: batched_migration_status_badge_variant(migration) }
%td{ role: 'cell', data: { label: _('Action') } }
- if migration.active?
= render Pajamas::ButtonComponent.new(icon: 'pause',

View File

@ -18,4 +18,4 @@
%td{ role: 'cell', data: { label: _('Last updated') } }
= @migration.updated_at
%td{ role: 'cell', data: { label: _('Status') } }
= gl_badge_tag @migration.status_name.to_s.humanize, { size: :sm, variant: batched_migration_status_badge_variant(@migration) }
= gl_badge_tag @migration.status_name.to_s.humanize, { variant: batched_migration_status_badge_variant(@migration) }

View File

@ -14,7 +14,7 @@
= markdown_field(group, :description)
.stats.gl-text-gray-500.gl-flex-shrink-0.gl-hidden.sm:gl-flex
= gl_badge_tag storage_counter(group.storage_size), size: :sm
= gl_badge_tag storage_counter(group.storage_size)
= render_if_exists 'admin/namespace_plan_badge', namespace: group, css_class: 'gl-ml-5 gl-mr-0'
= render_if_exists 'admin/groups/marked_for_deletion_badge', group: group, css_class: 'gl-ml-5'

View File

@ -5,10 +5,9 @@
s_('GroupSettings|Enable email notifications'),
checkbox_options: { checked: @group.emails_enabled?, disabled: !can_disable_group_emails?(@group) },
help_text: s_('GroupSettings|Enable sending email notifications for this group and all its subgroups and projects')
- if Feature.enabled?(:diff_preview_in_email, @group)
.gl-px-7
= f.gitlab_ui_checkbox_component :show_diff_preview_in_email,
s_('GroupSettings|Include diff previews'),
checkbox_options: { checked: @group.show_diff_preview_in_email? & @group.emails_enabled?,
disabled: !@group.emails_enabled? || !can_set_group_diff_preview_in_email?(@group) },
help_text: s_('GroupSettings|Emails are not encrypted. Concerned administrators may want to disable diff previews.')
.gl-px-7
= f.gitlab_ui_checkbox_component :show_diff_preview_in_email,
s_('GroupSettings|Include diff previews'),
checkbox_options: { checked: @group.show_diff_preview_in_email? & @group.emails_enabled?,
disabled: !@group.emails_enabled? || !can_set_group_diff_preview_in_email?(@group) },
help_text: s_('GroupSettings|Emails are not encrypted. Concerned administrators may want to disable diff previews.')

View File

@ -4,4 +4,4 @@
- if text_only
%span.has-tooltip.gl-leading-normal.gl-font-sm{ title: tooltip_title }= _('Imported')
- else
= render Pajamas::BadgeComponent.new(_('Imported'), size: :sm, title: tooltip_title, class: 'has-tooltip')
= render Pajamas::BadgeComponent.new(_('Imported'), title: tooltip_title, class: 'has-tooltip')

View File

@ -7,7 +7,7 @@
= link_to root_path, title: _('Homepage'), id: 'logo', class: 'header-logged-out-logo has-tooltip', aria: { label: _('Homepage') }, **tracking_attrs('main_navigation', 'click_gitlab_logo_link', 'navigation_top') do
= brand_header_logo
- if Gitlab.com_and_canary?
= gl_badge_tag({ variant: :success, size: :sm }, { href: Gitlab::Saas.canary_toggle_com_url, data: { testid: 'canary_badge_link' }, target: :_blank, rel: 'noopener noreferrer', class: 'canary-badge' }) do
= gl_badge_tag({ variant: :success }, { href: Gitlab::Saas.canary_toggle_com_url, data: { testid: 'canary_badge_link' }, target: :_blank, rel: 'noopener noreferrer', class: 'canary-badge' }) do
= s_('GitLab Next|Next')
%ul.gl-list-none.gl-p-0.gl-m-0.gl-display-flex.gl-gap-3.gl-align-items-center.gl-flex-grow-1

View File

@ -49,10 +49,10 @@
%ul.controls
- if issue.closed? && issue.moved?
%li
= render Pajamas::BadgeComponent.new(_('Closed (moved)'), size: 'sm', variant: 'info')
= render Pajamas::BadgeComponent.new(_('Closed (moved)'), variant: 'info')
- elsif issue.closed?
%li
= render Pajamas::BadgeComponent.new(_('Closed'), size: 'sm', variant: 'info')
= render Pajamas::BadgeComponent.new(_('Closed'), variant: 'info')
- if issue.assignees.any?
%li.gl-display-flex
= render 'shared/issuable/assignees', project: @project, issuable: issue

View File

@ -11,9 +11,9 @@
= branch.name
= clipboard_button(text: branch.name, title: _("Copy branch name"))
- if is_default_branch
= gl_badge_tag s_('DefaultBranchLabel|default'), { variant: :info, size: :sm }, { class: 'gl-ml-2' }
= gl_badge_tag s_('DefaultBranchLabel|default'), { variant: :info }, { class: 'gl-ml-2' }
- if protected_branch?(@project, branch)
= gl_badge_tag s_('Branches|protected'), { variant: :neutral, size: :sm }, { class: 'gl-ml-2' }
= gl_badge_tag s_('Branches|protected'), { variant: :neutral }, { class: 'gl-ml-2' }
= render_if_exists 'projects/branches/diverged_from_upstream', branch: branch
@ -38,7 +38,7 @@
- if mr_status.present? && can?(current_user, :read_merge_request, related_merge_request)
.issuable-reference.gl-display-flex.gl-justify-content-end.gl-overflow-hidden
= gl_badge_tag issuable_reference(related_merge_request),
{ icon: mr_status[:icon], variant: mr_status[:variant], size: :md, href: merge_request_path(related_merge_request) },
{ icon: mr_status[:icon], variant: mr_status[:variant], href: merge_request_path(related_merge_request) },
{ class: 'gl-block gl-text-truncate', title: mr_status[:title], data: { toggle: 'tooltip', container: 'body' } }
- elsif mr_status.nil? && create_mr_button?(from: branch.name, source_project: @project)

View File

@ -50,15 +50,15 @@
.label-container
- if job.tags.any?
- job.tags.each do |tag|
= gl_badge_tag tag, variant: :info, size: :sm
= gl_badge_tag tag, variant: :info
- if job.try(:trigger_request)
= gl_badge_tag _('triggered'), variant: :info, size: :sm
= gl_badge_tag _('triggered'), variant: :info
- if job.try(:allow_failure) && !job.success?
= gl_badge_tag _('allowed to fail'), variant: :warning, size: :sm
= gl_badge_tag _('allowed to fail'), variant: :warning
- if job.schedulable?
= gl_badge_tag s_('DelayedJobs|delayed'), variant: :info, size: :sm
= gl_badge_tag s_('DelayedJobs|delayed'), variant: :info
- elsif job.action?
= gl_badge_tag _('manual'), variant: :info, size: :sm
= gl_badge_tag _('manual'), variant: :info
- if pipeline_link
%td

View File

@ -11,4 +11,4 @@
.container-fluid{ class: [container_class] }
= render "commit_box"
= render "ci_menu"
= render RapidDiffs::DiffFileComponent.with_collection(@diffs.diff_files)
= render RapidDiffs::DiffFileComponent.with_collection(@diffs.diff_files, parallel_view: diff_view == :parallel)

View File

@ -39,9 +39,9 @@
.label-container
- if generic_commit_status.tags.any?
- generic_commit_status.tags.each do |tag|
= gl_badge_tag tag, variant: :info, size: :sm
= gl_badge_tag tag, variant: :info
- if retried
= gl_badge_tag retried, variant: :warning, size: :sm
= gl_badge_tag retried, variant: :warning
- if pipeline_link
%td

View File

@ -10,4 +10,4 @@
- badge_variant = :success
- badge_label = _("Approved")
%li.gl-display-flex{ class: 'gl-mr-0!' }= render Pajamas::BadgeComponent.new(badge_label, size: 'sm', variant: badge_variant, icon: approval_icon, title: approval_tooltip, class: 'has-tooltip', data: { 'testid': 'mr-appovals' })
%li.gl-display-flex{ class: 'gl-mr-0!' }= render Pajamas::BadgeComponent.new(badge_label, variant: badge_variant, icon: approval_icon, title: approval_tooltip, class: 'has-tooltip', data: { 'testid': 'mr-appovals' })

View File

@ -48,10 +48,10 @@
- merged_at = merge_request.merged_at ? l(merge_request.merged_at.to_time) : _("Merge date & time could not be determined")
%li.gl-flex{ class: 'gl-mr-0!' }
%a.has-tooltip{ href: "#{merge_request_path(merge_request)}#widget-state", title: merged_at }
= render Pajamas::BadgeComponent.new(_('Merged'), size: 'sm', variant: 'info')
= render Pajamas::BadgeComponent.new(_('Merged'), variant: 'info')
- elsif merge_request.closed?
%li.gl-flex{ class: 'gl-mr-0!' }
= render Pajamas::BadgeComponent.new(_('Closed'), size: 'sm', variant: 'danger')
= render Pajamas::BadgeComponent.new(_('Closed'), variant: 'danger')
- if merge_request.open? && merge_request.broken?
%li.issuable-pipeline-broken.gl-flex{ class: 'gl-mr-0!' }
= link_to merge_request_path(merge_request), class: "has-tooltip", title: _('Cannot be merged automatically') do

View File

@ -29,21 +29,21 @@
= render "projects/merge_requests/tabs/tab", class: "notes-tab", testid: "notes-tab" do
= tab_link_for @merge_request, :show, force_link: @commit.present? do
= _("Overview")
= gl_badge_tag @merge_request.related_notes.user.count, { size: :sm }, { class: 'js-discussions-count' }
= gl_badge_tag @merge_request.related_notes.user.count, { class: 'js-discussions-count' }
- if @merge_request.source_project
= render "projects/merge_requests/tabs/tab", name: "commits", class: "commits-tab", testid: "commits-tab" do
= tab_link_for @merge_request, :commits do
= _("Commits")
= gl_badge_tag tab_count_display(@merge_request, @commits_count), { size: :sm }, { class: 'js-commits-count' }
= gl_badge_tag tab_count_display(@merge_request, @commits_count), { class: 'js-commits-count' }
- if @project.builds_enabled?
= render "projects/merge_requests/tabs/tab", name: "pipelines", class: "pipelines-tab" do
= tab_link_for @merge_request, :pipelines do
= _("Pipelines")
= gl_badge_tag @number_of_pipelines, { size: :sm }, { class: 'js-pipelines-mr-count' }
= gl_badge_tag @number_of_pipelines, { class: 'js-pipelines-mr-count' }
= render "projects/merge_requests/tabs/tab", name: "diffs", class: "diffs-tab js-diffs-tab", id: "diffs-tab", testid: "diffs-tab" do
= tab_link_for @merge_request, :diffs do
= _("Changes")
= gl_badge_tag tab_count_display(@merge_request, @diffs_count), { size: :sm, class: 'js-changes-tab-count', data: { gid: @merge_request.to_gid.to_s } }
= gl_badge_tag tab_count_display(@merge_request, @diffs_count), { class: 'js-changes-tab-count', data: { gid: @merge_request.to_gid.to_s } }
.gl-flex.gl-flex-wrap.gl-items-center.justify-content-lg-end
#js-vue-discussion-counter{ data: { blocks_merge: @project.only_allow_merge_if_all_discussions_are_resolved?.to_s } }
- if !!@issuable_sidebar.dig(:current_user, :id)

View File

@ -23,7 +23,7 @@
%li.commits-tab.new-tab
= link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tabvue'} do
= _("Commits")
= gl_badge_tag @total_commit_count, { size: :sm }, { class: 'gl-tab-counter-badge' }
= gl_badge_tag @total_commit_count, { class: 'gl-tab-counter-badge' }
#diff-notes-app.tab-content
#new.commits.tab-pane.active
@ -42,16 +42,16 @@
%li.commits-tab.new-tab
= link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tabvue'} do
= _("Commits")
= gl_badge_tag @total_commit_count, { size: :sm }, { class: 'gl-tab-counter-badge' }
= gl_badge_tag @total_commit_count, { class: 'gl-tab-counter-badge' }
- if @pipelines.any?
%li.builds-tab
= link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tabvue'} do
= _("Pipelines")
= gl_badge_tag @pipelines.size, { size: :sm }, { class: 'gl-tab-counter-badge' }
= gl_badge_tag @pipelines.size, { class: 'gl-tab-counter-badge' }
%li.diffs-tab
= link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tabvue', testid: 'diffs-tab'} do
= _("Changes")
= gl_badge_tag @merge_request.diff_size, { size: :sm }, { class: 'gl-tab-counter-badge' }
= gl_badge_tag @merge_request.diff_size, { class: 'gl-tab-counter-badge' }
#diff-notes-app.tab-content
#new.commits.tab-pane.active

View File

@ -15,8 +15,8 @@
- if @merge_request.for_fork? && !@merge_request.source_project
= err_fork_project_removed
- elsif !@merge_request.source_branch_exists?
= err_source_branch.html_safe % { branch_badge: gl_badge_tag(@merge_request.source_branch, variant: :info, size: :sm), path_badge: gl_badge_tag(@merge_request.source_project_path, variant: :info, size: :sm) }
= err_source_branch.html_safe % { branch_badge: gl_badge_tag(@merge_request.source_branch, variant: :info), path_badge: gl_badge_tag(@merge_request.source_project_path, variant: :info) }
- elsif !@merge_request.target_branch_exists?
= err_target_branch.html_safe % { branch_badge: gl_badge_tag(@merge_request.target_branch, variant: :info, size: :sm), path_badge: gl_badge_tag(@merge_request.source_project_path, variant: :info, size: :sm) }
= err_target_branch.html_safe % { branch_badge: gl_badge_tag(@merge_request.target_branch, variant: :info), path_badge: gl_badge_tag(@merge_request.source_project_path, variant: :info) }
- else
= err_internal

View File

@ -2,4 +2,4 @@
- add_page_specific_style 'page_bundles/merge_request_rapid_diffs'
= render 'page'
= render RapidDiffs::DiffFileComponent.with_collection(@merge_request.diffs.diff_files)
= render RapidDiffs::DiffFileComponent.with_collection(@merge_request.diffs.diff_files, parallel_view: diff_view == :parallel)

View File

@ -34,4 +34,4 @@
- if runner.tags.present?
.gl-my-2
- runner.tags.map(&:name).sort.each do |tag|
= gl_badge_tag tag, variant: :info, size: :sm
= gl_badge_tag tag, variant: :info

View File

@ -13,7 +13,7 @@
%span.cgray= starrer.user.to_reference
- if starrer.user == current_user
= gl_badge_tag _("It's you"), variant: :success, size: :sm, class: 'gl-ml-2'
= gl_badge_tag _("It's you"), variant: :success, class: 'gl-ml-2'
.block-truncated.gl-text-secondary.gl-font-sm
= time_ago_with_tooltip(starrer.starred_since)

View File

@ -8,7 +8,7 @@
= link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name'
- if protected_tag?(@project, tag)
= gl_badge_tag s_('TagsPage|protected'), variant: :neutral, size: :sm, class: 'gl-ml-2'
= gl_badge_tag s_('TagsPage|protected'), variant: :neutral, class: 'gl-ml-2'
- if commit
.block-truncated

View File

@ -3,7 +3,7 @@
.search-result-row.row.gl-display-flex.gl-sm-flex-direction-row.gl-flex-direction-column.gl-mt-5{ class: 'gl-pb-5! gl-mb-0!' }
.col-sm-9
%span.gl-display-flex.gl-align-items-center
= gl_badge_tag issuable_state_text(issuable), variant: issuable_state_to_badge_class(issuable), size: :sm
= gl_badge_tag issuable_state_text(issuable), variant: issuable_state_to_badge_class(issuable)
= sprite_icon('eye-slash', css_class: 'gl-text-secondary gl-ml-2') if issuable.respond_to?(:confidential?) && issuable.confidential?
= link_to issuable_path(issuable), data: { event_tracking: 'click_search_result', event_label: @scope, event_value: position, event_property: @search_term }, class: 'gl-w-full' do
%span.term.str-truncated.gl-font-bold.gl-ml-2= simple_search_highlight_and_truncate(issuable.title, @search_term)

View File

@ -4,4 +4,4 @@
- if email
%span.gl-mr-2
= email
= gl_badge_tag text, { variant: variant, size: :sm }
= gl_badge_tag text, { variant: variant }

View File

@ -1,8 +1,8 @@
%span.gl-display-flex.gl-align-items-center
%h5
= gl_badge_tag "POST", { size: :sm }, { variant: :info }
= gl_badge_tag "POST", { variant: :info }
= hook_log.url
= gl_badge_tag hook_log.trigger.singularize.titleize, { size: :sm }, { class: 'gl-ml-3' }
= gl_badge_tag hook_log.trigger.singularize.titleize, { class: 'gl-ml-3' }
%p
= _('Completed in %{duration_seconds} seconds (%{relative_time})').html_safe % { duration_seconds: number_with_precision(hook_log.execution_duration, precision: 2), relative_time: time_ago_with_tooltip(hook_log.created_at) }

View File

@ -13,7 +13,7 @@
%td
= render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
%td.gl-hidden.sm:gl-table-cell
= gl_badge_tag hook_log.trigger.singularize.titleize, { size: :sm }
= gl_badge_tag hook_log.trigger.singularize.titleize
%td
#{number_with_precision(hook_log.execution_duration, precision: 2)} sec
%td

View File

@ -1,4 +1,4 @@
- badge_variant = hook_log.success? ? :success : :danger
- badge_text = hook_log.internal_error? ? _('Error') : hook_log.response_status
= gl_badge_tag badge_text, { variant: badge_variant, size: :sm }
= gl_badge_tag badge_text, { variant: badge_variant }

View File

@ -1,3 +1,3 @@
- variant ||= :info
= gl_badge_tag yield, variant: variant, size: :sm
= gl_badge_tag yield, variant: variant

View File

@ -79,7 +79,7 @@
%span= milestone.issues_visible_to_user(current_user).count
.title.hide-collapsed
= s_('MilestoneSidebar|Issues')
= gl_badge_tag milestone.issues_visible_to_user(current_user).count, variant: :muted, size: :sm
= gl_badge_tag milestone.issues_visible_to_user(current_user).count, variant: :muted
- if show_new_issue_link?(project)
= link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "gl-float-right", title: s_('MilestoneSidebar|New Issue') do
= s_('MilestoneSidebar|New issue')
@ -111,7 +111,7 @@
%span= milestone.merge_requests.count
.title.hide-collapsed
= s_('MilestoneSidebar|Merge requests')
= gl_badge_tag milestone.merge_requests.count, variant: :muted, size: :sm
= gl_badge_tag milestone.merge_requests.count, variant: :muted
.value.hide-collapsed.bold
- if !project || can?(current_user, :read_merge_request, project)
%span.milestone-stat

View File

@ -107,7 +107,7 @@
= badge_count(project.forks_count)
- if show_count?(disabled: !merge_requests, compact_mode: compact_mode)
= link_to project_merge_requests_path(project), class: "#{css_metadata_classes} merge-requests", title: _('Merge requests'), data: { container: 'body', placement: 'top' } do
= sprite_icon('git-merge', size: 14, css_class: 'gl-mr-2')
= sprite_icon('merge-request', size: 14, css_class: 'gl-mr-2')
= badge_count(project.open_merge_requests_count)
- if show_count?(disabled: !issues, compact_mode: compact_mode)
= link_to project_issues_path(project), class: "#{css_metadata_classes} issues", title: _('Issues'), data: { container: 'body', placement: 'top' } do

View File

@ -69,7 +69,7 @@
= badge_count(project.forks_count)
- if show_count?(disabled: !merge_requests, compact_mode: compact_mode)
= link_to project_merge_requests_path(project), class: "#{css_metadata_classes} merge-requests", title: _('Merge requests'), data: { container: 'body', placement: 'top' } do
= sprite_icon('git-merge', size: 14, css_class: 'gl-mr-2')
= sprite_icon('merge-request', size: 14, css_class: 'gl-mr-2')
= badge_count(project.open_merge_requests_count)
- if show_count?(disabled: !issues, compact_mode: compact_mode)
= link_to project_issues_path(project), class: "#{css_metadata_classes} issues", title: _('Issues'), data: { container: 'body', placement: 'top' } do

View File

@ -11,10 +11,10 @@
%div
%ul
%li
= gl_badge_tag s_("Runners|active"), variant: :success, size: :sm
= gl_badge_tag s_("Runners|active"), variant: :success
= _('- Available to run jobs.')
%li
= gl_badge_tag s_("Runners|paused"), variant: :danger, size: :sm
= gl_badge_tag s_("Runners|paused"), variant: :danger
= _('- Not available to run jobs.')
%p

View File

@ -11,17 +11,17 @@
= hook.url
- if hook.rate_limited?
= gl_badge_tag(_('Disabled'), variant: :danger, size: :sm)
= gl_badge_tag(_('Disabled'), variant: :danger)
- elsif hook.permanently_disabled?
= gl_badge_tag(s_('Webhooks|Failed to connect'), variant: :danger, size: :sm)
= gl_badge_tag(s_('Webhooks|Failed to connect'), variant: :danger)
- elsif hook.temporarily_disabled?
= gl_badge_tag(s_('Webhooks|Fails to connect'), variant: :warning, size: :sm)
= gl_badge_tag(s_('Webhooks|Fails to connect'), variant: :warning)
%div
- hook.class.triggers.each_value do |trigger|
- if hook.public_send(trigger)
= gl_badge_tag(integration_webhook_event_human_name(trigger), variant: :neutral, size: :sm)
= gl_badge_tag(sslBadgeText, variant: :neutral, size: :sm)
= gl_badge_tag(integration_webhook_event_human_name(trigger), variant: :neutral)
= gl_badge_tag(sslBadgeText, variant: :neutral)
.gl-font-sm
= truncate(hook.description, length: 200)

View File

@ -30,7 +30,7 @@
= link_to avatar_icon_for_user(@user, 400, current_user: current_user), target: '_blank', rel: 'noopener noreferrer', title: s_('UserProfile|View large avatar') do
= render Pajamas::AvatarComponent.new(@user, alt: s_('UserProfile|User profile picture'), size: 96, avatar_options: { itemprop: "image" })
- if @user.status&.busy?
= render Pajamas::BadgeComponent.new(s_('UserProfile|Busy'), size: 'sm', variant: 'warning', class: 'gl-absolute gl-display-flex gl-justify-content-center gl-align-items-center gl-left-1/2 gl-bg-gray-50 gl-border gl-border-white -gl-translate-x-1/2 gl-top-full -gl-mt-3')
= render Pajamas::BadgeComponent.new(s_('UserProfile|Busy'), variant: 'warning', class: 'gl-absolute gl-display-flex gl-justify-content-center gl-align-items-center gl-left-1/2 gl-bg-gray-50 gl-border gl-border-white -gl-translate-x-1/2 gl-top-full -gl-mt-3')
%div
%h1.gl-heading-1.gl-leading-1.gl-mr-2{ class: 'gl-my-0!', itemprop: 'name' }
= user_display_name(@user)
@ -142,9 +142,9 @@
%li.js-followers-tab
= link_to user_followers_path, data: { target: 'div#followers', action: 'followers', toggle: 'tab', endpoint: user_followers_path(format: :json) } do
= s_('UserProfile|Followers')
= gl_badge_tag @user.followers.count, size: :sm
= gl_badge_tag @user.followers.count
- if profile_tab?(:following)
%li.js-following-tab
= link_to user_following_path, data: { target: 'div#following', action: 'following', toggle: 'tab', endpoint: user_following_path(format: :json), testid: 'following_tab' } do
= s_('UserProfile|Following')
= gl_badge_tag @user.followees.count, size: :sm
= gl_badge_tag @user.followees.count

View File

@ -1,8 +0,0 @@
---
name: diff_preview_in_email
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60007
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/382055
milestone: '15.6'
type: beta
group: group::code review
default_enabled: true

View File

@ -0,0 +1,27 @@
import path from 'node:path';
import { readdir } from 'node:fs/promises';
const GITLAB_SVG_PATH = '@gitlab/svgs/dist';
export async function IconsPlugin() {
return {
name: 'vite-plugin-gitlab-icons',
async config() {
const iconsPath = path.resolve(__dirname, '../..', 'node_modules', GITLAB_SVG_PATH);
const files = await readdir(iconsPath, { withFileTypes: true });
const alias = files
.filter(file => file.isDirectory() || path.extname(file.name) === '.svg')
.map((file) => {
return {
find: file.name,
replacement: `${iconsPath}/${file.name}`,
}
});
return {
resolve: {
alias,
}
}
}
};
}

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class AddIndexOwaspTop10ForGroupLevelReports < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '17.3'
INDEX_NAME = 'index_for_owasp_top_10_group_level_reports'
def up
add_concurrent_index :vulnerability_reads, [:owasp_top_10, :state, :report_type,
:severity, :traversal_ids, :vulnerability_id, :resolved_on_default_branch],
where: 'archived = false',
name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :vulnerability_reads, INDEX_NAME
end
end

View File

@ -0,0 +1 @@
f946cbd278cb57f87cf53400042783eea25d08140930d4a7f20a909b7d8bacb6

View File

@ -27361,6 +27361,8 @@ CREATE UNIQUE INDEX index_feature_gates_on_feature_key_and_key_and_value ON feat
CREATE UNIQUE INDEX index_features_on_key ON features USING btree (key);
CREATE INDEX index_for_owasp_top_10_group_level_reports ON vulnerability_reads USING btree (owasp_top_10, state, report_type, severity, traversal_ids, vulnerability_id, resolved_on_default_branch) WHERE (archived = false);
CREATE INDEX index_for_security_scans_scan_type ON security_scans USING btree (scan_type, project_id, pipeline_id) WHERE (status = 1);
CREATE INDEX index_for_status_per_branch_per_project ON merge_trains USING btree (target_project_id, target_branch, status);

View File

@ -0,0 +1,234 @@
---
stage: Systems
group: Gitaly
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# Gitaly on Kubernetes
DETAILS:
**Tier:** Free, Premium, Ultimate
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
**Status:** Experiment
Running Gitaly on Kubernetes has availability trade-offs, so consider these trade-offs when planing a production environment and set expectations accordingly.
This document describes and provides guidance on how to minimize, and plan for existing limitations.
Gitaly Cluster (Praefect) is unsupported. For more information on running Gitaly on Kubernetes, see [epic 6127](https://gitlab.com/groups/gitlab-org/-/epics/6127).
## Context
By design, Gitaly (non-Cluster) is a single point of failure service (SPoF). Data is sourced and served from a single instance.
For Kubernetes, when the StatefulSet pod rotates (for example, during upgrades, node maintenance, or eviction), the rotation causes service disruption for data served by the pod or instance.
In a [Cloud Native Hybrid](../reference_architectures/1k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts) setup (Gitaly VM), the Linux package (Omnibus)
masks the problem by:
1. Upgrading the Gitaly binary in-place.
1. Performing a graceful reload.
The same approach doesn't fit a container-based lifecycle where a container or pod needs to fully shutdown and start as a new container or pod.
Gitaly Cluster (Praefect) solves the data and service high-availability aspect by replicating data across instances. However, Gitaly Cluster is unsuited to run in Kubernetes
because of [existing issues and design constraints](index.md#known-issues) that are augmented by a container-based platform.
To support a Cloud Native deployment, Gitaly (non-Cluster) is the only option.
By leveraging the right Kubernetes and Gitaly features and configuration, you can minimize service disruption and provide a good user experience.
## Guidance
When running Gitaly in Kubernetes, you must:
- [Address pod disruption](#address-pod-disruption).
- [Address resource contention and saturation](#address-resource-contention-and-saturation).
- [Optimize pod start time](#optimize-pod-start-time).
### Address pod disruption
A pod can rotate for many reasons. Understanding and planing the service lifecycle helps minimize disruption.
For example, in Gitaly's case, a Kubernetes `StatefulSet` rotates on `spec.template` object changes, which can happen during Helm Chart upgrades (labels, or image tag) or pod resource requests or limits updates.
This section focuses on common pod disruption cases and how to address them.
#### Schedule maintenance windows
Because the service is not highly available, certain operations can cause brief service outages. Scheduling maintenance windows signals potential
service disruption and helps set expectations. You should use maintenance windows for:
- GitLab Helm chart upgrades and reconfiguration.
- Gitaly configuration changes.
- Kubernetes node maintenance windows. For example, upgrades and patching. Isolating Gitaly into its own dedicated node pool might help.
#### Use [PriorityClass](https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/#priorityclass)
Assign Gitaly pods higher priority compared to other pods, to help with node saturation pressure, eviction priority, and scheduling latency:
1. Create a priority class:
```yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: gitlab-gitaly
value: 1000000
globalDefault: false
description: "GitLab Gitaly priority class"
```
1. Assign the priority class to Gitaly pods:
```yaml
gitlab:
gitaly:
priorityClassName: gitlab-gitaly
```
#### Signal node autoscaling to prevent eviction
Node autoscaling tooling adds and removes Kubernetes nodes as needed to schedule pods and optimize cost.
During downscaling events, the Gitaly pod can be evicted to optimize resource usage. Annotations are usually available to control this behavior and
exclude workloads. For example, with Cluster Autoscaler:
```yaml
gitlab:
gitaly:
annotations:
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
```
### Address resource contention and saturation
Gitaly service resource usage can be unpredictable because of the indeterminable nature of Git operations. Not all repositories are the same and size
heavily influences performance and resource usage, especially for [monorepos](../../user/project/repository/monorepos/index.md).
In Kubernetes, uncontrolled resource usage can lead to Out Of Memory (OOM) events, which forces the platform to terminate the pod and kill all its processes.
Pod termination raises two important concerns:
- Data/Repository corruption
- Service disruption
This section focuses on reducing the blast radius and protecting the service as a whole.
#### Constrain Git processes resource usage
Isolating Git processes provides safety in guaranteeing that a single Git call can't consume all service and pod resources.
Gitaly can use Linux [Control Groups (cgroups)](configure_gitaly.md#control-groups) to impose smaller, per repository quotas on resource usage.
You should maintain cgroup quotas below the overall pod resource allocation.
CPU is not critical because it only slows down the service. However, memory saturation can lead to pod termination. A 1 GiB memory buffer between pod request and Git cgroup
allocation is a safe starting point. Sizing the buffer depends on traffic patterns and repository data.
For example, with a pod memory request of 15 GiB, 14 GiB is allocated to Git calls:
```yaml
gitlab:
gitaly:
cgroups:
enabled: true
# Total limit across all repository cgroups, excludes Gitaly process
memoryBytes: 15032385536 # 14GiB
cpuShares: 1024
cpuQuotaUs: 400000 # 4 cores
# Per repository limits, 50 repository cgroups
repositories:
count: 50
memoryBytes: 7516192768 # 7GiB
cpuShares: 512
cpuQuotaUs: 200000 # 2 cores
```
For more information, see [Gitaly configuration documentation](configure_gitaly.md#control-groups).
#### Right size Pod resources
Sizing the Gitaly pod is critical and [reference architectures](../reference_architectures/index.md#cloud-native-hybrid) provide some guidance as a starting
point. However, different repositories and usage patterns consume varying degrees of resources.
You should monitor resource usage and adjust accordingly over time.
Memory is the most sensitive resource in Kubernetes because running out of memory can trigger pod termination.
[Isolating Git calls with cgroups](#constrain-git-processes-resource-usage) helps to restrict resource usage for repository operations, but that doesn't include the Gitaly service itself.
In line with the previous recommendation on cgroup quotas, add a buffer between overall Git cgroup memory allocation and pod memory request to improve safety.
A pod `Guaranteed` [Quality of Service](https://kubernetes.io/docs/tasks/configure-pod-container/quality-service-pod/) class is preferred
(resource requests match limits). With this setting, the pod is less susceptible to resource contention and is guaranteed to never be evicted based on consumption from other pods.
Example resource configuration:
```yaml
gitlab:
gitaly:
resources:
requests:
cpu: 4000m
memory: 15Gi
limits:
cpu: 4000m
memory: 15Gi
init:
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 50m
memory: 32Mi
```
#### Configure concurrency rate limiting
As well as using cgroups, you can use concurrency limits to further help protect the service from abnormal traffic patterns. For more information, see
[concurrency configuration documentation](concurrency_limiting.md) and [how to monitor limits](monitoring.md#monitor-gitaly-concurrency-limiting).
#### Isolate Gitaly pods
When running multiple Gitaly pods, you should schedule them in different nodes to spread out the failure domain. This can be enforced using pod anti affinity.
For example:
```yaml
gitlab:
gitaly:
antiAffinity: hard
```
### Optimize pod start time
This section covers areas of optimization to reduce downtime during maintenance events or unplanned infrastructure events by reducing the time it takes the pod to start serving traffic.
#### Persistent Volume permissions
As the size of data grows (Git history and more repositories), the pod takes more and more time to start and become ready.
During pod initialization, as part of the persistent volume mount, the file system permissions and ownership are explicitly set to the container `uid` and `gid`.
This operation runs by default and can significantly slow down pod startup time because the stored Git data contains many small files.
This behavior is configurable with the
[`fsGroupChangePolicy`](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#configure-volume-permission-and-ownership-change-policy-for-pods)
attribute. Use this attribute to perform the operation only if the volume root `uid` or `gid` mismatches the container spec:
```yaml
gitlab:
gitaly:
securityContext:
fsGroupChangePolicy: OnRootMismatch
```
#### Health probes
The Gitaly pod starts serving traffic after the readiness probe succeeds. The default probe times are conservative to cover most use cases.
Reducing the `readinessProbe` `initialDelaySeconds` attribute triggers probes earlier, which accelerates pod readiness. For example:
```yaml
gitlab:
gitaly:
statefulset:
readinessProbe:
initialDelaySeconds: 2
periodSeconds: 10
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 3
```

View File

@ -771,7 +771,7 @@ The following are some available Rake tasks:
| Task | Description |
|:--------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`sudo gitlab-rake gitlab:elastic:info`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Outputs debugging information for the advanced search integration. |
| [`sudo gitlab-rake gitlab:elastic:index`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | In GitLab 17.0 and earlier, enables Elasticsearch indexing and runs `gitlab:elastic:recreate_index`, `gitlab:elastic:clear_index_status`, `gitlab:elastic:index_group_entities`, `gitlab:elastic:index_projects`, `gitlab:elastic:index_snippets`, and `gitlab:elastic:index_users`.<br>In GitLab 17.1 and later, queues a Sidekiq job in the background. First, the job enables Elasticsearch indexing and pauses indexing to ensure all indices are created. Then, the job re-creates all indices, clears indexing status, and queues additional Sidekiq jobs to index project and group data, snippets, and users. Finally, Elasticsearch indexing is resumed to complete. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421298) in GitLab 17.1 [with a flag](../../administration/feature_flags.md) named `elastic_index_use_trigger_indexing`. Enabled by default. |
| [`sudo gitlab-rake gitlab:elastic:index`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | In GitLab 17.0 and earlier, enables Elasticsearch indexing and runs `gitlab:elastic:recreate_index`, `gitlab:elastic:clear_index_status`, `gitlab:elastic:index_group_entities`, `gitlab:elastic:index_projects`, `gitlab:elastic:index_snippets`, and `gitlab:elastic:index_users`.<br>In GitLab 17.1 and later, queues a Sidekiq job in the background. First, the job enables Elasticsearch indexing and pauses indexing to ensure all indices are created. Then, the job re-creates all indices, clears indexing status, and queues additional Sidekiq jobs to index project and group data, snippets, and users. Finally, Elasticsearch indexing is resumed to complete. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421298) in GitLab 17.1 [with a flag](../../administration/feature_flags.md) named `elastic_index_use_trigger_indexing`. Enabled by default. [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/434580) in GitLab 17.3. Feature flag `elastic_index_use_trigger_indexing` removed. |
| [`sudo gitlab-rake gitlab:elastic:pause_indexing`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Pauses Elasticsearch indexing. Changes are still tracked. Useful for cluster/index migrations. |
| [`sudo gitlab-rake gitlab:elastic:resume_indexing`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Resumes Elasticsearch indexing. |
| [`sudo gitlab-rake gitlab:elastic:index_projects`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Iterates over all projects, and queues Sidekiq jobs to index them in the background. It can only be used after the index is created. |

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,118 @@
---
stage: Monitor
group: Observability
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# Tutorial: Use GitLab Observability with a NodeJS application
DETAILS:
**Tier:** Ultimate
**Offering:** GitLab.com
**Status:** Beta
> - Observability tracing [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124966) in GitLab 16.2 [with a flag](../../administration/feature_flags.md) named `observability_tracing`. Disabled by default.
FLAG:
The availability of this feature is controlled by a feature flag.
For more information, see the history.
This feature is available for testing, but not ready for production use.
In this tutorial, you'll learn how to configure, instrument, and monitor a NodeJS application using GitLab Observability features.
## Before you begin
Take a moment and make sure you have the following:
- A GitLab Ultimate subscription for GitLab.com
- A local installation of NodeJS
- Basic knowledge of Git, NodeJS, JavaScript, and the core concepts of [OpenTelemetry](https://opentelemetry.io/)
## Create a new GitLab project
First, create a new GitLab project and a corresponding access token.
This tutorial uses the project name `nodejs-O11y-tutorial`.
1. On the left sidebar, at the top, select **Create new** (**{plus}**) and **New project/repository**.
1. Select **Create from template**.
1. Select **Use template** for NodeJS Express.
1. Enter the project details.
- In the **Project name** field, enter `nodejs-O11y-tutorial`.
1. Select **Create project**.
1. In the `nodejs-O11y-tutorial` project, on the left sidebar, select **Settings > Access tokens**.
1. Create a new access token with the Owner role and the `read_api` and `write_observability` scopes. Store the token value somewhere safe—you'll need it later.
## Instrument your NodeJS application
Next, we need to instrument the NodeJS application.
1. Ensure you have [NodeJS](https://nodejs.org/en) installed by running the following:
```shell
node -v
```
1. Clone the `nodejs-O11y-tutorial` project and `cd` to the `nodejs-O11y-tutorial` directory.
1. Install the dependencies by running:
```shell
npm install
```
1. Run the application:
```shell
PORT=8080 node server.js
```
1. In a web browser, visit `http://localhost:8080` and make sure the application is running correctly.
1. Add the OpenTelemetry packages:
```shell
npm install --save @opentelemetry/api \
@opentelemetry/auto-instrumentations-node
```
1. Find your group ID:
1. On the left sidebar, select **Search or go to** and find the top-level group with the `nodejs-O11y-tutorial` project. For example, if your project URL is `https://gitlab.com/tankui/observability/nodejs-O11y-tutorial`, the top-level group is `tanuki`.
1. On the group overview page, in the upper-right corner, select **Actions** (**{ellipsis_v}**).
1. Select **Copy group ID**. Save the copied ID for later.
1. Find your project ID:
1. On the `nodejs-O11y-tutorial` project overview page, in the upper-right corner, select **Actions** (**{ellipsis_v}**).
1. Select **Copy project ID**. Save the copied ID for later.
1. Configure and run your project with instrumentation:
```shell
env OTEL_TRACES_EXPORTER="otlp" \
OTEL_EXPORTER_OTLP_ENDPOINT="https://observe.gitlab.com/v3/{{GROUP_ID}}/{{PROJECT_ID}}/ingest" \
OTEL_EXPORTER_OTLP_HEADERS="PRIVATE-TOKEN={{ACCESS_TOKEN}}" \
OTEL_SERVICE_NAME="nodejs-O11y-tutorial" \
OTEL_LOG_LEVEL="debug" \
NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register" \
PORT=8080 node server.js
```
Be sure to replace the `GROUP_ID`, `PROJECT_ID`, and `ACCESS_TOKEN` with the values you obtained earlier.
## View traces
Now that you have an application configured to use Observability tracing,
you can view exported traces on GitLab.com.
To view exported traces:
1. Start the `nodejs-O11y-tutorial` application with instrumentation again.
1. Visit `http://localhost:8080/` and perform some actions in the application.
1. In the `nodejs-O11y-tutorial` project, on the left sidebar, select **Monitor > Traces**.
If everything is working correctly, you should see a trace for each request.
![Metrics UI](img/nodejs_metrics_ui.png)
1. Optional. Select a trace to view its span.
![Traces UI](img/nodejs_single_trace.png)
Congratulations! You successfully created an application, configured it to use GitLab Observability features, and examined the traces the application created. You can continue to experiment with this application, or try configuring a more complex application to export traces.
Remember that Observability Tracing is not yet ready for production use. There is no official support for logs or metrics using the OpenTelemetry collector with a NodeJS application.

View File

@ -126,11 +126,11 @@ To view exported traces:
1. In the `animals` project, on the left sidebar, select **Monitor > Traces**.
If everything is working correctly, you should see a trace for each controller action.
![Metrics UI](img/metrics_ui.png)
![Metrics UI](img/rails_metrics_ui.png)
1. Optional. Select a trace to view its span.
![Traces UI](img/single_trace.png)
![Traces UI](img/rails_single_trace.png)
Congratulations! You successfully created an application, configured it to use GitLab Observability features, and examined the traces the application created. You can continue to experiment with this toy application, or try configuring a more complex application to export traces.

View File

@ -357,6 +357,11 @@ module Gitlab
@rich_viewer = rich_viewer_class&.new(self)
end
# This is going to be updated with viewer components
def view_component_viewer
has_renderable? ? rendered.viewer : viewer
end
def alternate_viewer
alternate_viewer_class&.new(self)
end

View File

@ -18,7 +18,7 @@ module Sidebars
override :sprite_icon
def sprite_icon
'git-merge'
'merge-request'
end
override :render?

View File

@ -23,7 +23,7 @@ module Sidebars
override :sprite_icon
def sprite_icon
'git-merge'
'merge-request'
end
override :render?

View File

@ -53,12 +53,12 @@ class ReleaseEnvironmentsModel
@environment_base ||= if release_tag_match
"#{release_tag_match[1]}-#{release_tag_match[2]}-stable"
else
ENV['CI_COMMIT_REF_SLUG'].delete_suffix('-ee')
ENV['CI_COMMIT_REF_NAME'].delete_suffix('-ee')
end
end
def release_tag_match
@release_tag_match ||= ENV['CI_COMMIT_REF_SLUG'].match(/^v?([\d]+)\.([\d]+)\.[\d]+[\d\w-]*-ee$/)
@release_tag_match ||= ENV['CI_COMMIT_REF_NAME'].match(/^v?([\d]+)\.([\d]+)\.[\d]+[\d\w-]*-ee$/)
end
def security_project?

View File

@ -38,7 +38,7 @@ RSpec.describe Pajamas::BadgeComponent, type: :component do
let(:options) { { icon: :tanuki } }
it "adds the correct icon and margin" do
expect(page).to have_css ".gl-icon.gl-badge-icon.gl-mr-2[data-testid='tanuki-icon']"
expect(page).to have_css ".gl-icon.gl-badge-icon[data-testid='tanuki-icon']"
end
end
@ -50,7 +50,7 @@ RSpec.describe Pajamas::BadgeComponent, type: :component do
it "combines custom classes and component classes" do
expect(page).to have_css \
".gl-icon.gl-badge-icon.gl-mr-2.js-special-badge-icon.js-extra-special[data-testid='tanuki-icon']"
".gl-icon.gl-badge-icon.js-special-badge-icon.js-extra-special[data-testid='tanuki-icon']"
end
end
@ -59,7 +59,7 @@ RSpec.describe Pajamas::BadgeComponent, type: :component do
it "combines custom classes and component classes" do
expect(page).to have_css \
".gl-icon.gl-badge-icon.gl-mr-2.js-special-badge-icon.js-extra-special[data-testid='tanuki-icon']"
".gl-icon.gl-badge-icon.js-special-badge-icon.js-extra-special[data-testid='tanuki-icon']"
end
end
end
@ -67,10 +67,6 @@ RSpec.describe Pajamas::BadgeComponent, type: :component do
describe "icon_only" do
let(:options) { { icon: :tanuki, icon_only: true } }
it "adds no extra margin to the icon" do
expect(page).not_to have_css ".gl-icon.gl-mr-2"
end
it "adds the text as ARIA label" do
expect(page).to have_css ".gl-badge[aria-label='#{text}'][role='img']"
end
@ -84,26 +80,6 @@ RSpec.describe Pajamas::BadgeComponent, type: :component do
end
end
describe "size" do
where(:size) { [:sm, :md, :lg] }
with_them do
let(:options) { { size: size } }
it "adds size class" do
expect(page).to have_css ".gl-badge.#{size}"
end
end
context "with unknown size" do
let(:options) { { size: :xxl } }
it "adds the default size class" do
expect(page).to have_css ".gl-badge.md"
end
end
end
describe "variant" do
where(:variant) { [:muted, :neutral, :info, :success, :warning, :danger] }
@ -133,8 +109,8 @@ RSpec.describe Pajamas::BadgeComponent, type: :component do
end
it "can be combined with component options in no particular order" do
render_inline(described_class.new(text, id: "badge-34", variant: :success, data: { foo: "baz" }, size: :sm))
expect(page).to have_css ".gl-badge.badge-success.sm#badge-34[data-foo='baz']"
render_inline(described_class.new(text, id: "badge-34", variant: :success, data: { foo: "baz" }))
expect(page).to have_css ".gl-badge.badge-success#badge-34[data-foo='baz']"
end
context "with custom CSS classes" do

View File

@ -10,16 +10,14 @@ module Pajamas
# @param icon select [~, star-o, issue-closed, tanuki]
# @param icon_only toggle
# @param href url
# @param size select {{ Pajamas::BadgeComponent::SIZE_OPTIONS }}
# @param text text
# @param variant select {{ Pajamas::BadgeComponent::VARIANT_OPTIONS }}
def default(icon: :tanuki, icon_only: false, href: nil, size: :md, text: "Tanuki", variant: :muted)
def default(icon: :tanuki, icon_only: false, href: nil, text: "Tanuki", variant: :muted)
render Pajamas::BadgeComponent.new(
text,
icon: icon,
icon_only: icon_only,
href: href,
size: size,
variant: variant
)
end
@ -28,9 +26,9 @@ module Pajamas
# ---
#
# Use the content slot instead of the `text` param when things get more complicated than a plain string.
# All other options (`icon`, `size`, etc.) work as usual.
# All other options (`icon`, etc.) work as usual.
def slot
render Pajamas::BadgeComponent.new(size: :lg, variant: :info) do
render Pajamas::BadgeComponent.new(variant: :info) do
"!ereht olleh".reverse.capitalize
end
end
@ -57,5 +55,27 @@ module Pajamas
variant: :success
)
end
# Circular issuable status icons
# ---
#
# Circular icons 'issue-open-m' and 'issue-close'
def circular_icons
render Pajamas::BadgeComponent.new(variant: :success, icon: 'issue-open-m') do
'With status open'
end
render Pajamas::BadgeComponent.new(variant: :info, icon: 'issue-close') do
'With status closed'
end
end
# Icon only
# ---
#
# Uses an icon only.
def icon_only
render Pajamas::BadgeComponent.new(variant: :success, icon: 'calendar')
end
end
end

View File

@ -62,7 +62,7 @@ RSpec.describe "Admin::Projects", feature_category: :groups_and_projects do
expect(page).to have_content(project.name)
expect(page).to have_content(archived_project.name)
expect(page).to have_xpath("//span[@class='gl-badge badge badge-pill badge-info md gl-mr-3']", text: 'Archived')
expect(page).to have_xpath("//span[@class='gl-badge badge badge-pill badge-info gl-mr-3']", text: 'Archived')
end
it 'renders only archived projects', :js do

View File

@ -0,0 +1,58 @@
import { shallowMount } from '@vue/test-utils';
import { GlModal, GlSprintf } from '@gitlab/ui';
import JobActionModal from '~/ci/pipeline_mini_graph/job_action_modal.vue';
describe('JobActionModal', () => {
let wrapper;
const defaultProps = {
customMessage: 'This is a custom message.',
jobName: 'test_job',
};
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(JobActionModal, {
propsData: {
...defaultProps,
...props,
},
stubs: {
GlSprintf,
},
});
};
const findModal = () => wrapper.findComponent(GlModal);
beforeEach(() => {
createComponent();
});
it('shows modal', () => {
expect(findModal().props()).toMatchObject({
actionCancel: { text: 'Cancel' },
actionPrimary: { text: 'Yes, run test_job' },
modalId: 'job-action-modal',
title: 'Are you sure you want to run test_job?',
});
});
it('displays the custom message', () => {
expect(findModal().text()).toContain(defaultProps.customMessage);
});
it('emits change event when modal visibility changes', async () => {
await findModal().vm.$emit('change', true);
expect(wrapper.emitted('change')).toEqual([[true]]);
});
it('passes visible prop to gl-modal', () => {
createComponent({
props: {
visible: true,
},
});
expect(findModal().props('visible')).toBe(true);
});
});

View File

@ -23,7 +23,7 @@ describe('ContributionEventMerged', () => {
it('renders `ContributionEventBase`', () => {
expect(wrapper.findComponent(ContributionEventBase).props()).toEqual({
event: defaultPropsData.event,
iconName: 'git-merge',
iconName: 'merge-request',
message: ContributionEventMerged.i18n.message,
});
});

View File

@ -128,7 +128,7 @@ describe('IDE extra file row component', () => {
});
describe('merge request icon', () => {
const findMergeRequestIcon = () => wrapper.find('[data-testid="git-merge-icon"]');
const findMergeRequestIcon = () => wrapper.find('[data-testid="merge-request-icon"]');
it('hides when not a merge request change', () => {
createComponent();

View File

@ -6,7 +6,7 @@ exports[`publish_method renders 1`] = `
>
<gl-icon-stub
class="gl-mr-2"
name="git-merge"
name="merge-request"
size="16"
variant="current"
/>

View File

@ -6,7 +6,7 @@ exports[`publish_method renders 1`] = `
>
<gl-icon-stub
class="gl-mr-2"
name="git-merge"
name="merge-request"
size="16"
variant="current"
/>

View File

@ -0,0 +1,79 @@
import { DiffFile } from '~/rapid_diffs/diff_file';
import IS from '~/rapid_diffs/intersection_observer';
// We have to use var here because jest hoists mock calls, so let would be uninitialized at this point
// eslint-disable-next-line no-var
var trigger;
// We can't apply useMockIntersectionObserver here because IS is called immediately when DiffFile is imported
jest.mock('~/rapid_diffs/intersection_observer', () => {
class Observer {
constructor(callback) {
trigger = callback;
}
}
Observer.prototype.observe = jest.fn();
return Observer;
});
describe('DiffFile Web Component', () => {
const html = `<diff-file data-viewer="current"><div id="foo"></div></diff-file>`;
let adapter;
const getDiffElement = () => document.querySelector('[id=foo]');
const getWebComponentElement = () => document.querySelector('diff-file');
const triggerVisibility = (isIntersecting) =>
trigger([{ isIntersecting, target: getWebComponentElement() }]);
const assignAdapter = (customAdapter) => {
adapter = customAdapter;
getWebComponentElement().adapterConfig = { current: [customAdapter] };
};
const getContext = () => ({
diffElement: getDiffElement(),
viewer: 'current',
});
beforeAll(() => {
customElements.define('diff-file', DiffFile);
});
beforeEach(() => {
document.body.innerHTML = html;
getWebComponentElement().mount();
});
it('observes diff element', () => {
expect(IS.prototype.observe).toHaveBeenCalledWith(getWebComponentElement());
});
describe('when visible', () => {
beforeEach(() => {
assignAdapter({
onClick: jest.fn(),
onVisible: jest.fn(),
onInvisible: jest.fn(),
});
});
it('handles clicks', () => {
triggerVisibility(true);
getDiffElement().click();
expect(adapter.onClick).toHaveBeenCalledWith(expect.any(MouseEvent));
expect(adapter.onClick.mock.instances[0]).toStrictEqual(getContext());
});
it('handles visible event', () => {
triggerVisibility(true);
expect(adapter.onVisible).toHaveBeenCalled();
expect(adapter.onVisible.mock.instances[0]).toStrictEqual(getContext());
});
it('handles invisible event', () => {
triggerVisibility(false);
expect(adapter.onInvisible).toHaveBeenCalled();
expect(adapter.onInvisible.mock.instances[0]).toStrictEqual(getContext());
});
});
});

View File

@ -189,7 +189,7 @@ describe('Commit component', () => {
expect(refEl.attributes('href')).toBe(props.mergeRequestRef.path);
expect(findIcon('git-merge').exists()).toBe(true);
expect(findIcon('merge-request').exists()).toBe(true);
});
});

View File

@ -212,7 +212,7 @@ describe('ProjectsListItem', () => {
expect(tooltip.value).toBe(ProjectsListItem.i18n.mergeRequests);
expect(mergeRequestsLink.attributes('href')).toBe(`${project.webUrl}/-/merge_requests`);
expect(mergeRequestsLink.text()).toBe('5');
expect(mergeRequestsLink.findComponent(GlIcon).props('name')).toBe('git-merge');
expect(mergeRequestsLink.findComponent(GlIcon).props('name')).toBe('merge-request');
});
});

View File

@ -29,7 +29,7 @@ RSpec.describe BadgesHelper do
end
it 'adds style classes' do
expect(helper.gl_badge_tag(label)).to match(%r{class="gl-badge badge badge-pill badge-muted md"})
expect(helper.gl_badge_tag(label)).to match(%r{class="gl-badge badge badge-pill badge-muted"})
end
it 'adds custom classes' do
@ -64,32 +64,12 @@ RSpec.describe BadgesHelper do
end
end
describe 'sizes' do
where(:size) do
[[:sm], [:md], [:lg]]
end
with_them do
it 'sets the size class' do
expect(helper.gl_badge_tag(label, size: size)).to match(%r{class=".*#{size}.*"})
end
end
it 'defaults to md' do
expect(helper.gl_badge_tag(label)).to match(%r{class=".*md.*"})
end
it 'falls back to default given an unknown size' do
expect(helper.gl_badge_tag(label, size: :foo)).to match(%r{class=".*md.*"})
end
end
it 'applies custom html attributes' do
expect(helper.gl_badge_tag(label, nil, data: { foo: "bar" })).to match(%r{<span .*data-foo="bar".*>})
end
describe 'icons' do
let(:spacing_class_regex) { %r{<svg .*class=".*my-icon-class gl-mr-2".*>.*</svg>} }
let(:spacing_class_regex) { %r{<svg .*class=".*my-icon-class".*>.*</svg>} }
describe 'with text' do
subject { helper.gl_badge_tag(label, icon: "question-o", icon_classes: 'my-icon-class') }

View File

@ -312,7 +312,6 @@ RSpec.describe GroupsHelper, feature_category: :groups_and_projects do
before do
group.update_attribute(:show_diff_preview_in_email, true)
stub_feature_flags(diff_preview_in_email: true)
end
it 'returns true for an owner of the group' do

View File

@ -132,13 +132,13 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do
it 'returns navigation with badges' do
expect(helper.issuables_state_counter_text(:issues, :opened, true))
.to eq('<span>Open</span> <span class="gl-badge badge badge-pill badge-muted sm gl-tab-counter-badge gl-hidden sm:gl-inline-flex">42</span>')
.to eq('<span>Open</span> <span class="gl-badge badge badge-pill badge-muted gl-tab-counter-badge gl-hidden sm:gl-inline-flex"><span class="gl-badge-content">42</span></span>')
expect(helper.issuables_state_counter_text(:issues, :closed, true))
.to eq('<span>Closed</span> <span class="gl-badge badge badge-pill badge-muted sm gl-tab-counter-badge gl-hidden sm:gl-inline-flex">42</span>')
.to eq('<span>Closed</span> <span class="gl-badge badge badge-pill badge-muted gl-tab-counter-badge gl-hidden sm:gl-inline-flex"><span class="gl-badge-content">42</span></span>')
expect(helper.issuables_state_counter_text(:merge_requests, :merged, true))
.to eq('<span>Merged</span> <span class="gl-badge badge badge-pill badge-muted sm gl-tab-counter-badge gl-hidden sm:gl-inline-flex">42</span>')
.to eq('<span>Merged</span> <span class="gl-badge badge badge-pill badge-muted gl-tab-counter-badge gl-hidden sm:gl-inline-flex"><span class="gl-badge-content">42</span></span>')
expect(helper.issuables_state_counter_text(:merge_requests, :all, true))
.to eq('<span>All</span> <span class="gl-badge badge badge-pill badge-muted sm gl-tab-counter-badge gl-hidden sm:gl-inline-flex">42</span>')
.to eq('<span>All</span> <span class="gl-badge badge badge-pill badge-muted gl-tab-counter-badge gl-hidden sm:gl-inline-flex"><span class="gl-badge-content">42</span></span>')
end
end
@ -170,7 +170,7 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do
it 'returns truncated count' do
expect(helper.issuables_state_counter_text(:issues, :opened, true))
.to eq('<span>Open</span> <span class="gl-badge badge badge-pill badge-muted sm gl-tab-counter-badge gl-hidden sm:gl-inline-flex">1.1k</span>')
.to eq('<span>Open</span> <span class="gl-badge badge badge-pill badge-muted gl-tab-counter-badge gl-hidden sm:gl-inline-flex"><span class="gl-badge-content">1.1k</span></span>')
end
end
end

View File

@ -119,7 +119,6 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
end
describe '#can_set_diff_preview_in_email?' do
stub_feature_flags(diff_preview_in_email: true)
let_it_be(:user) { create(:project_member, :maintainer, user: create(:user), project: project).user }
it 'returns true for the project owner' do

View File

@ -167,14 +167,14 @@ RSpec.describe TabHelper do
describe 'gl_tab_counter_badge' do
it 'creates a tab counter badge' do
expect(helper.gl_tab_counter_badge(1)).to eq(
'<span class="gl-badge badge badge-pill badge-muted sm gl-tab-counter-badge">1</span>'
'<span class="gl-badge badge badge-pill badge-muted gl-tab-counter-badge"><span class="gl-badge-content">1</span></span>'
)
end
context 'with extra classes' do
it 'creates a tab counter badge with the correct class attribute' do
expect(helper.gl_tab_counter_badge(1, { class: 'js-test' })).to eq(
'<span class="gl-badge badge badge-pill badge-muted sm gl-tab-counter-badge js-test">1</span>'
'<span class="gl-badge badge badge-pill badge-muted gl-tab-counter-badge js-test"><span class="gl-badge-content">1</span></span>'
)
end
end
@ -182,7 +182,7 @@ RSpec.describe TabHelper do
context 'with data attributes' do
it 'creates a tab counter badge with the data attributes' do
expect(helper.gl_tab_counter_badge(1, { data: { some_attribute: 'foo' } })).to eq(
'<span class="gl-badge badge badge-pill badge-muted sm gl-tab-counter-badge" data-some-attribute="foo">1</span>'
'<span class="gl-badge badge badge-pill badge-muted gl-tab-counter-badge" data-some-attribute="foo"><span class="gl-badge-content">1</span></span>'
)
end
end

View File

@ -9,7 +9,7 @@ RSpec.describe ReleaseEnvironmentsModel, feature_category: :delivery do
describe '#generate_json' do
it 'generates the correct JSON' do
stub_env('CI_COMMIT_SHORT_SHA', 'abcdef')
stub_env('CI_COMMIT_REF_SLUG', '15-10-stable')
stub_env('CI_COMMIT_REF_NAME', '15-10-stable')
expected_json = {
'gitaly' => '15-10-stable-abcdef',
'registry' => '15-10-stable-abcdef',
@ -49,21 +49,21 @@ RSpec.describe ReleaseEnvironmentsModel, feature_category: :delivery do
context 'for stable branch' do
it 'returns the correct environment' do
stub_env('CI_COMMIT_REF_SLUG', '15-10-stable-ee')
stub_env('CI_COMMIT_REF_NAME', '15-10-stable-ee')
expect(model.environment).to eq('15-10-stable')
end
end
context 'for RC tag' do
it 'returns the correct environment' do
stub_env('CI_COMMIT_REF_SLUG', 'v15.10.3-rc42-ee')
stub_env('CI_COMMIT_REF_NAME', 'v15.10.3-rc42-ee')
expect(model.environment).to eq('15-10-stable')
end
end
context 'for release tag' do
it 'returns the correct environment' do
stub_env('CI_COMMIT_REF_SLUG', 'v15.10.3-ee')
stub_env('CI_COMMIT_REF_NAME', 'v15.10.3-ee')
expect(model.environment).to eq('15-10-stable')
end
end
@ -72,7 +72,7 @@ RSpec.describe ReleaseEnvironmentsModel, feature_category: :delivery do
context 'when CI_PROJECT_PATH is gitlab-org/security/gitlab' do
before do
stub_env('CI_PROJECT_PATH', 'gitlab-org/security/gitlab')
stub_env('CI_COMMIT_REF_SLUG', '15-10-stable-ee')
stub_env('CI_COMMIT_REF_NAME', '15-10-stable-ee')
end
it 'returns the environment with -security' do

Some files were not shown because too many files have changed in this diff Show More