Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-02-03 12:18:57 +00:00
parent ffe7c069a4
commit 584ccdaf68
52 changed files with 508 additions and 222 deletions

View File

@ -1,11 +1,13 @@
<script>
import { mapGetters } from 'vuex';
import imageDiff from '~/diffs/mixins/image_diff';
import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
import DraftNote from './draft_note.vue';
export default {
components: {
DraftNote,
DesignNotePin,
},
mixins: [imageDiff],
props: {
@ -31,9 +33,12 @@ export default {
class="discussion-notes diff-discussions position-relative"
>
<div class="notes">
<span class="d-block btn-transparent badge badge-pill is-draft js-diff-notes-index">
{{ toggleText(draft, index) }}
</span>
<design-note-pin
:label="toggleText(draft, index)"
is-draft
class="js-diff-notes-index gl-translate-x-n50"
size="sm"
/>
<draft-note :draft="draft" />
</div>
</div>

View File

@ -694,7 +694,7 @@ export default class Notes {
// Convert returned HTML to a jQuery object so we can modify it further
const $noteEntityEl = $(noteEntity.html);
const $noteAvatar = $noteEntityEl.find('.image-diff-avatar-link');
const $targetNoteBadge = $targetNote.find('.badge');
const $targetNoteBadge = $targetNote.find('.design-note-pin');
$noteAvatar.append($targetNoteBadge);
this.revertNoteEditForm($targetNote);

View File

@ -286,6 +286,7 @@ export default {
"
:is-inactive="isNoteInactive(note)"
:is-resolved="note.resolved"
is-on-image
@mousedown.stop="onNoteMousedown($event, note)"
@mouseup.stop="onNoteMouseup(note)"
/>

View File

@ -1,12 +1,14 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { mapActions } from 'vuex';
import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
export default {
components: {
noteableDiscussion,
GlIcon,
DesignNotePin,
},
props: {
discussions: {
@ -62,20 +64,22 @@ export default {
<ul :data-discussion-id="discussion.id" class="notes">
<template v-if="shouldCollapseDiscussions">
<button
:class="{
'diff-notes-collapse': discussion.expanded,
'btn-transparent badge badge-pill': !discussion.expanded,
}"
v-if="discussion.expanded"
class="diff-notes-collapse js-diff-notes-toggle"
type="button"
class="js-diff-notes-toggle"
:aria-label="__('Show comments')"
@click="toggleDiscussion({ discussionId: discussion.id })"
>
<gl-icon v-if="discussion.expanded" name="collapse" class="collapse-icon" />
<template v-else>
{{ index + 1 }}
</template>
<gl-icon name="collapse" class="collapse-icon" />
</button>
<design-note-pin
v-else
:label="index + 1"
:is-resolved="discussion.resolved"
size="sm"
class="js-diff-notes-toggle gl-translate-x-n50"
@click="toggleDiscussion({ discussionId: discussion.id })"
/>
</template>
<noteable-discussion
v-show="isExpanded(discussion)"
@ -87,9 +91,12 @@ export default {
@noteDeleted="deleteNoteHandler"
>
<template v-if="renderAvatarBadge" #avatar-badge>
<span class="badge badge-pill">
{{ index + 1 }}
</span>
<design-note-pin
:label="index + 1"
class="user-avatar"
:is-resolved="discussion.resolved"
size="sm"
/>
</template>
</noteable-discussion>
</ul>

View File

@ -1,8 +1,8 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { isArray } from 'lodash';
import { mapActions, mapGetters } from 'vuex';
import imageDiffMixin from 'ee_else_ce/diffs/mixins/image_diff';
import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
function calcPercent(pos, renderedSize) {
return (100 * pos) / renderedSize;
@ -11,7 +11,7 @@ function calcPercent(pos, renderedSize) {
export default {
name: 'ImageDiffOverlay',
components: {
GlIcon,
DesignNotePin,
},
mixins: [imageDiffMixin],
props: {
@ -36,7 +36,7 @@ export default {
badgeClass: {
type: String,
required: false,
default: 'badge badge-pill',
default: '',
},
shouldToggleDiscussion: {
type: Boolean,
@ -114,30 +114,28 @@ export default {
>
<span class="sr-only"> {{ __('Add image comment') }} </span>
</button>
<button
<design-note-pin
v-for="(discussion, index) in allDiscussions"
:key="discussion.id"
:style="getPosition(discussion)"
:class="[badgeClass, { 'is-draft': discussion.isDraft }]"
:disabled="!shouldToggleDiscussion"
class="js-image-badge"
type="button"
:label="showCommentIcon ? null : toggleText(discussion, index)"
:position="getPosition(discussion)"
:aria-label="__('Show comments')"
class="js-image-badge"
:class="badgeClass"
:is-draft="discussion.isDraft"
:is-resolved="discussion.resolved"
is-on-image
:disabled="!shouldToggleDiscussion"
@click="clickedToggle(discussion)"
>
<gl-icon v-if="showCommentIcon" name="image-comment-dark" :size="24" />
<template v-else>
{{ toggleText(discussion, index) }}
</template>
</button>
<button
/>
<design-note-pin
v-if="canComment && currentCommentForm"
:style="{ left: `${currentCommentForm.xPercent}%`, top: `${currentCommentForm.yPercent}%` }"
:aria-label="__('Comment form position')"
class="btn-transparent comment-indicator position-absolute"
type="button"
>
<gl-icon name="image-comment-dark" :size="24" />
</button>
:position="{
left: `${currentCommentForm.xPercent}%`,
top: `${currentCommentForm.yPercent}%`,
}"
/>
</div>
</template>

View File

@ -14,7 +14,15 @@ export function createImageBadge(noteId, { x, y }, classNames = []) {
}
export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) {
const buttonEl = createImageBadge(noteId, coordinate, ['badge', 'badge-pill']);
const buttonEl = createImageBadge(noteId, coordinate, [
'gl-display-flex',
'gl-align-items-center',
'gl-justify-content-center',
'gl-font-sm',
'design-note-pin',
'on-image',
'gl-absolute',
]);
buttonEl.textContent = badgeText;
containerEl.appendChild(buttonEl);
@ -30,8 +38,8 @@ export function addImageCommentBadge(containerEl, { coordinate, noteId }) {
export function addAvatarBadge(el, event) {
const { noteId, badgeNumber } = event.detail;
// Add badge to new comment
const avatarBadgeEl = el.querySelector(`#${noteId} .badge`);
// Add design pin to new comment
const avatarBadgeEl = el.querySelector(`#${noteId} .design-note-pin`);
avatarBadgeEl.textContent = badgeNumber;
avatarBadgeEl.classList.remove('hidden');
}

View File

@ -10,12 +10,12 @@ export function setPositionDataAttribute(el, options) {
}
export function updateDiscussionAvatarBadgeNumber(discussionEl, newBadgeNumber) {
const avatarBadgeEl = discussionEl.querySelector('.image-diff-avatar-link .badge');
const avatarBadgeEl = discussionEl.querySelector('.image-diff-avatar-link .design-note-pin');
avatarBadgeEl.textContent = newBadgeNumber;
}
export function updateDiscussionBadgeNumber(discussionEl, newBadgeNumber) {
const discussionBadgeEl = discussionEl.querySelector('.badge');
const discussionBadgeEl = discussionEl.querySelector('.design-note-pin');
discussionBadgeEl.textContent = newBadgeNumber;
}

View File

@ -118,7 +118,7 @@ export default class ImageDiff {
removeBadge(event) {
const { badgeNumber } = event.detail;
const indexToRemove = badgeNumber - 1;
const imageBadgeEls = this.imageFrameEl.querySelectorAll('.badge');
const imageBadgeEls = this.imageFrameEl.querySelectorAll('.design-note-pin');
if (this.imageBadges.length !== badgeNumber) {
// Cascade badges count numbers for (avatar badges + image badges)

View File

@ -61,7 +61,7 @@ export default class ReplacedImageDiff extends ImageDiff {
this.currentView = newView;
// Clear existing badges on new view
const existingBadges = this.imageFrameEl.querySelectorAll('.badge');
const existingBadges = this.imageFrameEl.querySelectorAll('.design-note-pin');
[...existingBadges].map((badge) => badge.remove());
// Remove existing references to old view image badges

View File

@ -28,12 +28,37 @@ export default {
required: false,
default: false,
},
isOnImage: {
type: Boolean,
required: false,
default: false,
},
isDraft: {
type: Boolean,
required: false,
default: false,
},
size: {
type: String,
required: false,
default: 'md',
validator: (value) => ['sm', 'md'].includes(value),
},
ariaLabel: {
type: String,
required: false,
default: null,
},
},
computed: {
isNewNote() {
return this.label === null;
},
pinLabel() {
if (this.ariaLabel) {
return this.ariaLabel;
}
return this.isNewNote
? __('Comment form position')
: sprintf(__("Comment '%{label}' position"), { label: this.label });
@ -51,7 +76,10 @@ export default {
'js-image-badge design-note-pin': !isNewNote,
resolved: isResolved,
inactive: isInactive,
draft: isDraft,
'on-image': isOnImage,
'gl-absolute': position,
small: size === 'sm',
}"
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-font-sm"
type="button"

View File

@ -1,4 +1,5 @@
$design-pin-diameter: 28px;
$design-pin-diameter-sm: 24px;
$t-gray-a-16-design-pin: rgba($black, 0.16);
.layout-page.design-detail-layout {
@ -12,24 +13,6 @@ $t-gray-a-16-design-pin: rgba($black, 0.16);
top: 35px;
}
.design-note-pin {
display: flex;
height: $design-pin-diameter;
width: $design-pin-diameter;
box-sizing: content-box;
background-color: $purple-500;
color: $white;
font-weight: $gl-font-weight-bold;
border-radius: 50%;
z-index: 1;
padding: 0;
border: 0;
&.resolved {
background-color: $gray-500;
}
}
.comment-indicator {
border-radius: 50%;
}
@ -40,35 +23,6 @@ $t-gray-a-16-design-pin: rgba($black, 0.16);
cursor: grabbing;
}
}
/**
* Design pin that overlays the design
*/
.frame .design-note-pin {
box-shadow: 0 2px 4px $t-gray-a-08, 0 0 1px $t-gray-a-24;
border: $white 2px solid;
will-change: transform, box-shadow, opacity;
// NOTE: verbose transition property required for Safari
transition: transform $general-hover-transition-duration linear, box-shadow $general-hover-transition-duration linear, opacity $general-hover-transition-duration linear;
transform-origin: 0 0;
transform: translate(-50%, -50%);
&:hover {
transform: scale(1.2) translate(-50%, -50%);
}
&:active {
box-shadow: 0 0 4px $t-gray-a-16-design-pin, 0 4px 12px $t-gray-a-16-design-pin;
}
&.inactive {
@include gl-opacity-5;
&:hover {
@include gl-opacity-10;
}
}
}
}
.design-scaler-wrapper {
@ -177,3 +131,63 @@ $t-gray-a-16-design-pin: rgba($black, 0.16);
.design-card-header {
background: transparent;
}
.design-note-pin {
display: flex;
height: $design-pin-diameter;
width: $design-pin-diameter;
box-sizing: content-box;
background-color: $purple-500;
color: $white;
font-weight: $gl-font-weight-bold;
border-radius: 50%;
z-index: 1;
padding: 0;
border: 0;
&.draft {
background-color: $orange-500;
}
&.resolved {
background-color: $gray-500;
}
&.on-image {
box-shadow: 0 2px 4px $t-gray-a-08, 0 0 1px $t-gray-a-24;
border: $white 2px solid;
will-change: transform, box-shadow, opacity;
// NOTE: verbose transition property required for Safari
transition: transform $general-hover-transition-duration linear, box-shadow $general-hover-transition-duration linear, opacity $general-hover-transition-duration linear;
transform-origin: 0 0;
transform: translate(-50%, -50%);
&:hover {
transform: scale(1.2) translate(-50%, -50%);
}
&:active {
box-shadow: 0 0 4px $t-gray-a-16-design-pin, 0 4px 12px $t-gray-a-16-design-pin;
}
&.inactive {
@include gl-opacity-5;
&:hover {
@include gl-opacity-10;
}
}
}
&.small {
position: absolute;
border: 1px solid $white;
height: $design-pin-diameter-sm;
width: $design-pin-diameter-sm;
}
&.user-avatar {
top: 25px;
right: 8px;
}
}

View File

@ -1072,24 +1072,6 @@ table.code {
}
}
.frame .badge.badge-pill,
.image-diff-avatar-link .badge.badge-pill,
.user-avatar-link .badge.badge-pill,
.notes > .badge.badge-pill {
position: absolute;
background-color: $blue-400;
color: $white;
border: $white 1px solid;
min-height: $gl-padding;
padding: 5px 8px;
border-radius: 12px;
&:focus {
outline: none;
}
}
.frame .badge.badge-pill,
.frame .image-comment-badge,
.frame .comment-indicator {
// Center align badges on the frame
@ -1121,11 +1103,6 @@ table.code {
}
}
.notes > .badge.badge-pill {
display: none;
left: -13px;
}
.discussion-notes {
min-height: 35px;
@ -1134,18 +1111,22 @@ table.code {
min-height: 25px;
}
.diff-notes-expand {
display: none;
}
&.collapsed {
background-color: $white;
.diff-notes-expand {
display: initial;
}
.diff-notes-collapse,
.note,
.discussion-reply-holder {
display: none;
}
.notes > .badge.badge-pill {
display: block;
}
}
}

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Members
class MembersAddedEvent < ::Gitlab::EventStore::Event
def schema
{
'type' => 'object',
'required' => %w[source_id source_type],
'properties' => {
'source_id' => { 'type' => 'integer' },
'source_type' => { 'type' => 'string' }
}
}
end
end
end

View File

@ -125,6 +125,12 @@ module Types
field :archived, GraphQL::Types::Boolean, null: true, method: :archived?,
description: 'Whether the current project is archived.'
field :language, GraphQL::Types::String,
description: 'Blob language.',
method: :blob_language,
null: true,
calls_gitaly: true
def raw_text_blob
object.data unless object.binary?
end

View File

@ -32,7 +32,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
end
def blob_language
@_blob_language ||= Gitlab::Diff::CustomDiff.transformed_blob_language(blob) || language
@_blob_language ||= Gitlab::Diff::CustomDiff.transformed_blob_language(blob) || gitattr_language || detect_language
end
def raw_plain_data
@ -166,9 +166,15 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
@all_lines ||= blob.data.lines
end
def language
def gitattr_language
blob.language_from_gitattributes
end
def detect_language
return if blob.binary?
Rouge::Lexer.guess(filename: blob.path, source: blob_data(nil)) { |lex| lex.min_by(&:tag) }.tag
end
end
BlobPresenter.prepend_mod_with('BlobPresenter')

View File

@ -33,7 +33,7 @@ class SnippetBlobPresenter < BlobPresenter
blob.container
end
def language
def gitattr_language
nil
end

View File

@ -24,6 +24,9 @@ module Members
add_members
enqueue_onboarding_progress_action
publish_event!
result
rescue BlankInvitesError, TooManyInvitesError, MembershipLockedError => e
error(e.message)
@ -144,6 +147,15 @@ module Members
def formatted_errors
errors.to_sentence
end
def publish_event!
Gitlab::EventStore.publish(
Members::MembersAddedEvent.new(data: {
source_id: source.id,
source_type: source.class.name
})
)
end
end
end

View File

@ -9,9 +9,9 @@
-# to the first note position when we click on a badge diff discussion
%ul.notes{ id: "discussion_#{discussion.id}", data: { discussion_id: discussion.id, position: discussion.notes[0].position.to_json } }
- if discussion.try(:on_image?) && show_toggle
%button.gl-button.diff-notes-collapse.js-diff-notes-toggle{ type: 'button' }
%button.comment-indicator.gl-display-flex.gl-align-items-center.gl-justify-content-center.gl-font-sm.diff-notes-collapse.js-diff-notes-toggle{ type: 'button' }
= sprite_icon('collapse', css_class: 'collapse-icon')
%button.gl-button.btn-transparent.badge.badge-pill.js-diff-notes-toggle{ type: 'button' }
%button.gl-align-items-center.gl-justify-content-center.gl-font-sm.small.gl-translate-x-n50.design-note-pin.js-diff-notes-toggle.diff-notes-expand{ type: 'button' }
= badge_counter
= render partial: "shared/notes/note", collection: discussion.notes, as: :note, locals: { badge_counter: badge_counter, show_image_comment_badge: show_image_comment_badge }

View File

@ -64,43 +64,13 @@
- experiment(:new_project_sast_enabled, user: current_user) do |e|
- e.try(:candidate) do
.form-group
.form-check.gl-mb-3
= check_box_tag 'project[initialize_with_sast]', '1', true, class: 'form-check-input', data: { qa_selector: 'initialize_with_sast_checkbox', track_experiment: e.name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
= label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
= s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
.form-text.text-muted
= s_('ProjectsNew|Analyze your source code for known security vulnerabilities.')
= link_to _('Learn more.'), help_page_path('user/application_security/sast/index'), target: '_blank', rel: 'noopener noreferrer', data: { track_action: 'followed', track_experiment: e.name }
= render 'new_project_initialize_with_sast', experiment_name: e.name, track_label: track_label, checked: true, with_free_badge: false
- e.try(:unchecked_candidate) do
.form-group
.form-check.gl-mb-3
= check_box_tag 'project[initialize_with_sast]', '1', false, class: 'form-check-input', data: { qa_selector: 'initialize_with_sast_checkbox', track_experiment: e.name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
= label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
= s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
.form-text.text-muted
= s_('ProjectsNew|Analyze your source code for known security vulnerabilities.')
= link_to _('Learn more.'), help_page_path('user/application_security/sast/index'), target: '_blank', rel: 'noopener noreferrer', data: { track_action: 'followed', track_experiment: e.name }
= render 'new_project_initialize_with_sast', experiment_name: e.name, track_label: track_label, checked: false, with_free_badge: false
- e.try(:free_indicator) do
.form-group
.form-check.gl-mb-3
= check_box_tag 'project[initialize_with_sast]', '1', true, class: 'form-check-input', data: { qa_selector: 'initialize_with_sast_checkbox', track_experiment: e.name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
= label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
= s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
= gl_badge_tag _('Free'), variant: :info, size: :sm
.form-text.text-muted
= s_('ProjectsNew|Analyze your source code for known security vulnerabilities.')
= link_to _('Learn more.'), help_page_path('user/application_security/sast/index'), target: '_blank', rel: 'noopener noreferrer', data: { track_action: 'followed', track_experiment: e.name }
= render 'new_project_initialize_with_sast', experiment_name: e.name, track_label: track_label, checked: true, with_free_badge: true
- e.try(:unchecked_free_indicator) do
.form-group
.form-check.gl-mb-3
= check_box_tag 'project[initialize_with_sast]', '1', false, class: 'form-check-input', data: { qa_selector: 'initialize_with_sast_checkbox', track_experiment: e.name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
= label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
= s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
= gl_badge_tag _('Free'), variant: :info, size: :sm
.form-text.text-muted
= s_('ProjectsNew|Analyze your source code for known security vulnerabilities.')
= link_to _('Learn more.'), help_page_path('user/application_security/sast/index'), target: '_blank', rel: 'noopener noreferrer', data: { track_action: 'followed', track_experiment: e.name }
= render 'new_project_initialize_with_sast', experiment_name: e.name, track_label: track_label, checked: false, with_free_badge: true
= f.submit _('Create project'), class: "btn gl-button btn-confirm", data: { track_label: "#{track_label}", track_action: "click_button", track_property: "create_project", track_value: "" }
= link_to _('Cancel'), dashboard_projects_path, class: 'btn gl-button btn-default btn-cancel', data: { track_label: "#{track_label}", track_action: "click_button", track_property: "cancel", track_value: "" }

View File

@ -0,0 +1,16 @@
- experiment_name = local_assigns.fetch(:experiment_name)
- track_label = local_assigns.fetch(:track_label)
- with_free_badge = local_assigns.fetch(:with_free_badge, false)
- checked = local_assigns.fetch(:checked, false)
.form-group
.form-check.gl-mb-3
= check_box_tag 'project[initialize_with_sast]', '1', checked, class: 'form-check-input', data: { qa_selector: 'initialize_with_sast_checkbox', track_experiment: experiment_name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
= label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
= s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
- if with_free_badge
= gl_badge_tag _('Free'), variant: :info, size: :sm
.form-text.text-muted
= s_('ProjectsNew|Analyze your source code for known security vulnerabilities.')
= link_to _('Learn more.'), help_page_path('user/application_security/sast/index'), target: '_blank', rel: 'noopener noreferrer', data: { track_action: 'followed', track_experiment: experiment_name }

View File

@ -25,7 +25,7 @@
- elsif note_counter == 0
- counter = badge_counter if local_assigns[:badge_counter]
- badge_class = "hidden" if @fresh_discussion || counter.nil?
%span.badge.badge-pill{ class: badge_class }
%span.gl-display-flex.gl-align-items-center.gl-justify-content-center.gl-font-sm.design-note-pin.small.user-avatar{ class: badge_class }
= counter
.timeline-content
.note-header

View File

@ -177,6 +177,8 @@
- 1
- - gitlab_shell
- 2
- - gitlab_subscriptions_notify_seats_exceeded
- 1
- - group_destroy
- 1
- - group_export

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class RemoveIndexClustersKubernetesNamespacesOnClusterId < Gitlab::Database::Migration[1.0]
INDEX = 'index_clusters_kubernetes_namespaces_on_cluster_id'
disable_ddl_transaction!
def up
remove_concurrent_index_by_name :clusters_kubernetes_namespaces, INDEX
end
def down
add_concurrent_index :clusters_kubernetes_namespaces, :cluster_id, name: INDEX
end
end

View File

@ -0,0 +1 @@
873ff811d4f70c012785297ee8c07eb496994af69d5ae4b266e8f675c88daca4

View File

@ -25991,8 +25991,6 @@ CREATE INDEX index_clusters_integration_elasticstack_enabled ON clusters_integra
CREATE INDEX index_clusters_integration_prometheus_enabled ON clusters_integration_prometheus USING btree (enabled, created_at, cluster_id);
CREATE INDEX index_clusters_kubernetes_namespaces_on_cluster_id ON clusters_kubernetes_namespaces USING btree (cluster_id);
CREATE INDEX index_clusters_kubernetes_namespaces_on_cluster_project_id ON clusters_kubernetes_namespaces USING btree (cluster_project_id);
CREATE INDEX index_clusters_kubernetes_namespaces_on_environment_id ON clusters_kubernetes_namespaces USING btree (environment_id);

View File

@ -4362,6 +4362,27 @@ Input type: `TerraformStateUnlockInput`
| <a id="mutationterraformstateunlockclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationterraformstateunlockerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.timelineEventCreate`
Input type: `TimelineEventCreateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationtimelineeventcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationtimelineeventcreateincidentid"></a>`incidentId` | [`IssueID!`](#issueid) | Incident ID of the timeline event. |
| <a id="mutationtimelineeventcreatenote"></a>`note` | [`String!`](#string) | Text note of the timeline event. |
| <a id="mutationtimelineeventcreateoccurredat"></a>`occurredAt` | [`Time!`](#time) | Timestamp of when the event occurred. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationtimelineeventcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationtimelineeventcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationtimelineeventcreatetimelineevent"></a>`timelineEvent` | [`TimelineEventType`](#timelineeventtype) | Timeline event. |
### `Mutation.timelineEventDestroy`
Input type: `TimelineEventDestroyInput`
@ -14759,6 +14780,7 @@ Returns [`Tree`](#tree).
| <a id="repositoryblobid"></a>`id` | [`ID!`](#id) | ID of the blob. |
| <a id="repositoryblobideeditpath"></a>`ideEditPath` | [`String`](#string) | Web path to edit this blob in the Web IDE. |
| <a id="repositoryblobideforkandeditpath"></a>`ideForkAndEditPath` | [`String`](#string) | Web path to edit this blob in the Web IDE using a forked project. |
| <a id="repositorybloblanguage"></a>`language` | [`String`](#string) | Blob language. |
| <a id="repositorybloblfsoid"></a>`lfsOid` | [`String`](#string) | LFS OID of the blob. |
| <a id="repositoryblobmode"></a>`mode` | [`String`](#string) | Blob mode. |
| <a id="repositoryblobname"></a>`name` | [`String`](#string) | Blob name. |

View File

@ -263,6 +263,9 @@ Other commonly used variables for `if` clauses:
branch. Use when you want to have the same configuration in multiple
projects with different default branches.
- `if: '$CI_COMMIT_BRANCH =~ /regex-expression/'`: If the commit branch matches a regular expression.
- `if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_COMMIT_TITLE =~ /Merge branch.*/`:
If the commit branch is the default branch and the commit message title matches a regular expression.
For example, the default commit message for a merge commit starts with `Merge branch`.
- `if: '$CUSTOM_VARIABLE !~ /regex-expression/'`: If the [custom variable](../variables/index.md#custom-cicd-variables)
`CUSTOM_VARIABLE` does **not** match a regular expression.
- `if: '$CUSTOM_VARIABLE == "value1"'`: If the custom variable `CUSTOM_VARIABLE` is

View File

@ -252,19 +252,20 @@ add a line like this to the `Gitlab::EventStore.configure!` method:
```ruby
module Gitlab
module EventStore
def self.configure!
Store.new.tap do |store|
# ...
def self.configure!(store)
# ...
store.subscribe ::MergeRequests::UpdateHeadPipelineWorker, to: ::Ci::PipelineCreatedEvent
store.subscribe ::MergeRequests::UpdateHeadPipelineWorker, to: ::Ci::PipelineCreatedEvent
# ...
end
# ...
end
end
end
```
A worker that is only defined in the EE codebase can subscribe to an event in the same way by
declaring the subscription in `ee/lib/ee/gitlab/event_store.rb`.
Subscriptions are stored in memory when the Rails app is loaded and they are immediately frozen.
It's not possible to modify subscriptions at runtime.

View File

@ -6,11 +6,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Container Host Security **(FREE)**
NOTE:
In GitLab 14.5, using a certificate to connect GitLab to a Kubernetes cluster is [deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8).
You can continue using Container Host Security, even though it relies on this certificate-based
method. The work to allow all aspects of Container Host Security to function through the [GitLab Agent](../../../../clusters/agent/index.md)
instead of the certificate-based method can be tracked [in this GitLab issue](https://gitlab.com/gitlab-org/gitlab/-/issues/299350).
> [Deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476) in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477) in GitLab 15.0.
WARNING:
Container Host Security is in its end-of-life process. It's [deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476)
for use in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477)
in GitLab 15.0.
Container Host Security in GitLab provides Intrusion Detection and Prevention capabilities that can
monitor and (optionally) block activity inside the containers themselves. This is done by leveraging

View File

@ -6,6 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Getting started with Container Host Security **(FREE)**
> [Deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476) in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477) in GitLab 15.0.
WARNING:
Container Host Security is in its end-of-life process. It's [deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476)
for use in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477)
in GitLab 15.0.
The following steps are recommended for installing Container Host Security.
## Installation steps

View File

@ -6,11 +6,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Container Network Security **(FREE)**
NOTE:
In GitLab 14.5, using a certificate to connect GitLab to a Kubernetes cluster is [deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8).
You can continue using Container Network Security, even though it relies on this certificate-based
method. The work to allow all aspects of Container Network Security to function through the [GitLab Agent](../../../../clusters/agent/index.md)
instead of the certificate-based method can be tracked [in this GitLab issue](https://gitlab.com/gitlab-org/gitlab/-/issues/299350) and [this GitLab Epic](https://gitlab.com/groups/gitlab-org/-/epics/7057).
> [Deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476) in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477) in GitLab 15.0.
WARNING:
Container Network Security is in its end-of-life process. It's [deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476)
for use in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477)
in GitLab 15.0.
Container Network Security in GitLab provides basic firewall functionality by leveraging Cilium
NetworkPolicies to filter traffic going in and out of the cluster as well as traffic between pods

View File

@ -6,6 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Getting started with Container Network Security **(FREE)**
> [Deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476) in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477) in GitLab 15.0.
WARNING:
Container Network Security is in its end-of-life process. It's [deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476)
for use in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477)
in GitLab 15.0.
The following steps are recommended for installing Container Network Security.
## Installation steps

View File

@ -6,6 +6,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Protecting your deployed applications **(FREE)**
> [Deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476) in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477) in GitLab 15.0.
WARNING:
The Container Network Security and Container Host Security features are in their end-of-life
processes. They're
[deprecated](https://gitlab.com/groups/gitlab-org/-/epics/7476)
for use in GitLab 14.8, and planned for [removal](https://gitlab.com/groups/gitlab-org/-/epics/7477)
in GitLab 15.0.
GitLab makes it straightforward to protect applications deployed in [connected Kubernetes clusters](index.md).
These protections are available in the Kubernetes network layer and in the container itself. At
the network layer, the Container Network Security capabilities in GitLab provide basic firewall

View File

@ -13,7 +13,7 @@ You can configure GitLab to send notifications to a Webex Teams space:
## Create a webhook for the space
1. Go to the [Incoming Webhooks app page](https://apphub.webex.com/applications/incoming-webhooks-cisco-systems-38054).
1. Go to the [Incoming Webhooks app page](https://apphub.webex.com/applications/incoming-webhooks-cisco-systems-38054-23307).
1. Select **Connect** and log in to Webex Teams, if required.
1. Enter a name for the webhook and select the space to receive the notifications.
1. Select **ADD**.

View File

@ -18,7 +18,7 @@ module Gitlab
end
def self.instance
@instance ||= configure!
@instance ||= Store.new { |store| configure!(store) }
end
# Define all event subscriptions using:
@ -29,14 +29,14 @@ module Gitlab
#
# store.subscribe(DomainA::SomeWorker, to: DomainB::SomeEvent), if: ->(event) { event.data == :some_value }
#
def self.configure!
Store.new do |store|
###
# Add subscriptions here:
def self.configure!(store)
###
# Add subscriptions here:
store.subscribe ::MergeRequests::UpdateHeadPipelineWorker, to: ::Ci::PipelineCreatedEvent
end
store.subscribe ::MergeRequests::UpdateHeadPipelineWorker, to: ::Ci::PipelineCreatedEvent
end
private_class_method :configure!
end
end
Gitlab::EventStore.prepend_mod_with('Gitlab::EventStore')

View File

@ -29,7 +29,7 @@ module Gitlab
raise InvalidEvent, "Event being published is not an instance of Gitlab::EventStore::Event: got #{event.inspect}"
end
subscriptions[event.class].each do |subscription|
subscriptions.fetch(event.class, []).each do |subscription|
subscription.consume_event(event)
end
end

View File

@ -43,29 +43,29 @@ module Gitlab
TRANSLATION_LEVELS = {
'bg' => 0,
'cs_CZ' => 0,
'da_DK' => 49,
'da_DK' => 48,
'de' => 15,
'en' => 100,
'eo' => 0,
'es' => 38,
'es' => 39,
'fil_PH' => 0,
'fr' => 11,
'gl_ES' => 0,
'id_ID' => 0,
'it' => 2,
'ja' => 36,
'ko' => 12,
'nb_NO' => 32,
'ja' => 35,
'ko' => 13,
'nb_NO' => 31,
'nl_NL' => 0,
'pl_PL' => 5,
'pl_PL' => 4,
'pt_BR' => 50,
'ro_RO' => 22,
'ru' => 26,
'ru' => 32,
'tr_TR' => 14,
'uk' => 45,
'zh_CN' => 98,
'uk' => 44,
'zh_CN' => 96,
'zh_HK' => 2,
'zh_TW' => 3
'zh_TW' => 2
}.freeze
private_constant :TRANSLATION_LEVELS

View File

@ -80,6 +80,10 @@ module Gitlab
super(presenter_class: BlobPresenter)
end
def binary?
false
end
def fetch_blob
path = [ref, blob_path]
missing_blob = { binary_path: blob_path }

View File

@ -33432,6 +33432,9 @@ msgstr ""
msgid "Slack logo"
msgstr ""
msgid "SlackIntegration|Are you sure you want to remove this project from the Slack application?"
msgstr ""
msgid "SlackIntegration|GitLab for Slack"
msgstr ""
@ -33441,6 +33444,9 @@ msgstr ""
msgid "SlackIntegration|Project alias"
msgstr ""
msgid "SlackIntegration|Remove project"
msgstr ""
msgid "SlackIntegration|Select a GitLab project to link with your Slack workspace."
msgstr ""

View File

@ -13,7 +13,6 @@ module QA
view 'app/views/projects/_new_project_fields.html.haml' do
element :initialize_with_readme_checkbox
element :initialize_with_sast_checkbox
element :project_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
element :project_path, 'text_field :path' # rubocop:disable QA/ElementWithPattern
element :project_description, 'text_area :description' # rubocop:disable QA/ElementWithPattern
@ -21,6 +20,10 @@ module QA
element :visibility_radios, 'visibility_level:' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/projects/_new_project_initialize_with_sast.html.haml' do
element :initialize_with_sast_checkbox
end
view 'app/views/projects/project_templates/_template.html.haml' do
element :use_template_button
element :template_option_row

View File

@ -28,7 +28,7 @@ RSpec.describe 'Merge request > User creates image diff notes', :js do
it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do
indicator = find('.js-image-badge')
badge = find('.image-diff-avatar-link .badge')
badge = find('.image-diff-avatar-link .design-note-pin')
expect(indicator).to have_content('1')
expect(badge).to have_content('1')

View File

@ -3,6 +3,7 @@ import Vue from 'vue';
import Vuex from 'vuex';
import DiffFileDrafts from '~/batch_comments/components/diff_file_drafts.vue';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
Vue.use(Vuex);
@ -40,10 +41,12 @@ describe('Batch comments diff file drafts component', () => {
it('renders index of draft note', () => {
factory();
expect(vm.findAll('.js-diff-notes-index').length).toEqual(2);
const elements = vm.findAll(DesignNotePin);
expect(vm.findAll('.js-diff-notes-index').at(0).text()).toEqual('1');
expect(elements.length).toEqual(2);
expect(vm.findAll('.js-diff-notes-index').at(1).text()).toEqual('2');
expect(elements.at(0).props('label')).toEqual(1);
expect(elements.at(1).props('label')).toEqual(2);
});
});

View File

@ -71,7 +71,7 @@ describe('DiffDiscussions', () => {
expect(diffNotesToggle.text().trim()).toBe('1');
expect(diffNotesToggle.classes()).toEqual(
expect.arrayContaining(['btn-transparent', 'badge', 'badge-pill']),
expect.arrayContaining(['js-diff-notes-toggle', 'gl-translate-x-n50', 'design-note-pin']),
);
});
@ -87,8 +87,8 @@ describe('DiffDiscussions', () => {
createComponent({ renderAvatarBadge: true });
const noteableDiscussion = wrapper.find(NoteableDiscussion);
expect(noteableDiscussion.find('.badge-pill').exists()).toBe(true);
expect(noteableDiscussion.find('.badge-pill').text().trim()).toBe('1');
expect(noteableDiscussion.find('.design-note-pin').exists()).toBe(true);
expect(noteableDiscussion.find('.design-note-pin').text().trim()).toBe('1');
});
});
});

View File

@ -1,5 +1,5 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import { createStore } from '~/mr_notes/stores';
import { imageDiffDiscussions } from '../mock_data/diff_discussions';
@ -19,7 +19,7 @@ describe('Diffs image diff overlay component', () => {
extendStore(store);
dispatch = jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMount(ImageDiffOverlay, {
wrapper = mount(ImageDiffOverlay, {
store,
parentComponent: {
data() {

View File

@ -62,7 +62,10 @@ describe('badge helper', () => {
});
it('should add badge classes', () => {
expect(buttonEl.className).toContain('badge badge-pill');
const classes = buttonEl.className.split(' ');
expect(classes).toEqual(
expect.arrayContaining(['design-note-pin', 'on-image', 'gl-absolute']),
);
});
it('should set the badge text', () => {
@ -105,7 +108,7 @@ describe('badge helper', () => {
beforeEach(() => {
containerEl.innerHTML = `
<div id="${noteId}">
<div class="badge hidden">
<div class="design-note-pin hidden">
</div>
</div>
`;
@ -116,7 +119,7 @@ describe('badge helper', () => {
badgeNumber,
},
});
avatarBadgeEl = containerEl.querySelector(`#${noteId} .badge`);
avatarBadgeEl = containerEl.querySelector(`#${noteId} .design-note-pin`);
});
it('should update badge number', () => {

View File

@ -37,14 +37,16 @@ describe('domHelper', () => {
discussionEl = document.createElement('div');
discussionEl.innerHTML = `
<a href="#" class="image-diff-avatar-link">
<div class="badge"></div>
<div class="design-note-pin"></div>
</a>
`;
domHelper.updateDiscussionAvatarBadgeNumber(discussionEl, badgeNumber);
});
it('should update avatar badge number', () => {
expect(discussionEl.querySelector('.badge').textContent).toEqual(badgeNumber.toString());
expect(discussionEl.querySelector('.design-note-pin').textContent).toEqual(
badgeNumber.toString(),
);
});
});
@ -54,13 +56,15 @@ describe('domHelper', () => {
beforeEach(() => {
discussionEl = document.createElement('div');
discussionEl.innerHTML = `
<div class="badge"></div>
<div class="design-note-pin"></div>
`;
domHelper.updateDiscussionBadgeNumber(discussionEl, badgeNumber);
});
it('should update discussion badge number', () => {
expect(discussionEl.querySelector('.badge').textContent).toEqual(badgeNumber.toString());
expect(discussionEl.querySelector('.design-note-pin').textContent).toEqual(
badgeNumber.toString(),
);
});
});

View File

@ -15,9 +15,9 @@ describe('ImageDiff', () => {
<div class="js-image-frame">
<img src="${TEST_HOST}/image.png">
<div class="comment-indicator"></div>
<div id="badge-1" class="badge">1</div>
<div id="badge-2" class="badge">2</div>
<div id="badge-3" class="badge">3</div>
<div id="badge-1" class="design-note-pin">1</div>
<div id="badge-2" class="design-note-pin">2</div>
<div id="badge-3" class="design-note-pin">3</div>
</div>
<div class="note-container">
<div class="discussion-notes">
@ -335,7 +335,7 @@ describe('ImageDiff', () => {
describe('cascade badge count', () => {
it('should update next imageBadgeEl value', () => {
const imageBadgeEls = imageDiff.imageFrameEl.querySelectorAll('.badge');
const imageBadgeEls = imageDiff.imageFrameEl.querySelectorAll('.design-note-pin');
expect(imageBadgeEls[0].textContent).toEqual('1');
expect(imageBadgeEls[1].textContent).toEqual('2');

View File

@ -39,4 +39,72 @@ describe('Design note pin component', () => {
createComponent({ position: null });
expect(wrapper.element).toMatchSnapshot();
});
it('applies `on-image` class when isOnImage is true', () => {
createComponent({ isOnImage: true });
expect(wrapper.find('.on-image').exists()).toBe(true);
});
it('applies `draft` class when isDraft is true', () => {
createComponent({ isDraft: true });
expect(wrapper.find('.draft').exists()).toBe(true);
});
describe('size', () => {
it('is `sm` it applies `small` class', () => {
createComponent({ size: 'sm' });
expect(wrapper.find('.small').exists()).toBe(true);
});
it('is `md` it applies no size class', () => {
createComponent({ size: 'md' });
expect(wrapper.find('.small').exists()).toBe(false);
expect(wrapper.find('.medium').exists()).toBe(false);
});
it('throws when passed any other value except `sm` or `md`', () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
createComponent({ size: 'lg' });
// eslint-disable-next-line no-console
expect(console.error).toHaveBeenCalled();
});
});
describe('ariaLabel', () => {
describe('when value is passed', () => {
it('overrides default aria-label', () => {
const ariaLabel = 'Aria Label';
createComponent({ ariaLabel });
const button = wrapper.find('button');
expect(button.attributes('aria-label')).toBe(ariaLabel);
});
});
describe('when no value is passed', () => {
it('shows new note label as aria-label when label is absent', () => {
createComponent({ label: null });
const button = wrapper.find('button');
expect(button.attributes('aria-label')).toBe('Comment form position');
});
it('shows label position as aria-label when label is present', () => {
const label = 1;
createComponent({ label, isNewNote: false });
const button = wrapper.find('button');
expect(button.attributes('aria-label')).toBe(`Comment '${label}' position`);
});
});
});
});

View File

@ -41,7 +41,8 @@ RSpec.describe Types::Repository::BlobType do
:ide_edit_path,
:external_storage_url,
:fork_and_edit_path,
:ide_fork_and_edit_path
:ide_fork_and_edit_path,
:language
)
end
end

View File

@ -224,6 +224,26 @@ RSpec.describe Gitlab::EventStore::Store do
store.publish(event)
end
end
context 'when the event does not have any subscribers' do
let(:store) do
described_class.new do |s|
s.subscribe unrelated_worker, to: another_event_klass
end
end
let(:event) { event_klass.new(data: data) }
it 'returns successfully' do
expect { store.publish(event) }.not_to raise_error
end
it 'does not dispatch the event to another subscription' do
expect(unrelated_worker).not_to receive(:perform_async)
store.publish(event)
end
end
end
describe 'subscriber' do

View File

@ -170,13 +170,13 @@ RSpec.describe BlobPresenter do
let(:git_blob) { blob.__getobj__ }
it 'returns highlighted content' do
expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: nil)
expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: 'ruby')
presenter.highlight
end
it 'returns plain content when :plain is true' do
expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: true, language: nil)
expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: true, language: 'ruby')
presenter.highlight(plain: true)
end
@ -189,7 +189,7 @@ RSpec.describe BlobPresenter do
end
it 'returns limited highlighted content' do
expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', "line one\n", plain: nil, language: nil)
expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', "line one\n", plain: nil, language: 'ruby')
presenter.highlight(to: 1)
end
@ -247,6 +247,36 @@ RSpec.describe BlobPresenter do
end
end
describe '#blob_language' do
subject { presenter.blob_language }
it { is_expected.to eq('ruby') }
context 'gitlab-language contains a match' do
before do
allow(blob).to receive(:language_from_gitattributes).and_return('cpp')
end
it { is_expected.to eq('cpp') }
end
context 'when blob is ipynb' do
let(:blob) { repository.blob_at('f6b7a707', 'files/ipython/markdown-table.ipynb') }
before do
allow(Gitlab::Diff::CustomDiff).to receive(:transformed_for_diff?).and_return(true)
end
it { is_expected.to eq('md') }
end
context 'when blob is binary' do
let(:blob) { repository.blob_at('HEAD', 'Gemfile.zip') }
it { is_expected.to be_nil }
end
end
describe '#raw_plain_data' do
let(:blob) { repository.blob_at('HEAD', file) }

View File

@ -39,6 +39,15 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
expect(source.users).to include member
expect(OnboardingProgress.completed?(source, :user_added)).to be(true)
end
it 'triggers a members added event' do
expect(Gitlab::EventStore)
.to receive(:publish)
.with(an_instance_of(Members::MembersAddedEvent))
.and_call_original
expect(execute_service[:status]).to eq(:success)
end
end
end