Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
9d67de388b
commit
dcdf2392b9
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export class DiffFileMounted extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.parentElement.mount();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
// required for easier DiffFile Web Component testing
|
||||
export default IntersectionObserver;
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
@import './diff_file_component';
|
||||
@import './expand_lines';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
@import 'mixins_and_variables_and_functions';
|
||||
@import 'components/rapid_diffs/diff_file_component';
|
||||
@import 'components/rapid_diffs';
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
@import 'mixins_and_variables_and_functions';
|
||||
@import 'components/rapid_diffs/diff_file_component';
|
||||
@import 'components/rapid_diffs';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
- variant ||= :info
|
||||
|
||||
= gl_badge_tag yield, variant: variant, size: :sm
|
||||
= gl_badge_tag yield, variant: variant
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
f946cbd278cb57f87cf53400042783eea25d08140930d4a7f20a909b7d8bacb6
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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 |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
1. Optional. Select a trace to view its span.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
|
@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
1. Optional. Select a trace to view its span.
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ module Sidebars
|
|||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'git-merge'
|
||||
'merge-request'
|
||||
end
|
||||
|
||||
override :render?
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ module Sidebars
|
|||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'git-merge'
|
||||
'merge-request'
|
||||
end
|
||||
|
||||
override :render?
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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') }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue