summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/batch_comments/components/diff_file_drafts.vue11
-rw-r--r--app/assets/javascripts/deprecated_notes.js2
-rw-r--r--app/assets/javascripts/design_management/components/design_overlay.vue1
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue31
-rw-r--r--app/assets/javascripts/diffs/components/image_diff_overlay.vue44
-rw-r--r--app/assets/javascripts/image_diff/helpers/badge_helper.js14
-rw-r--r--app/assets/javascripts/image_diff/helpers/dom_helper.js4
-rw-r--r--app/assets/javascripts/image_diff/image_diff.js2
-rw-r--r--app/assets/javascripts/image_diff/replaced_image_diff.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/design_management/design_note_pin.vue28
-rw-r--r--app/assets/stylesheets/components/design_management/design.scss108
-rw-r--r--app/assets/stylesheets/framework/diffs.scss35
-rw-r--r--app/events/members/members_added_event.rb16
-rw-r--r--app/graphql/types/repository/blob_type.rb6
-rw-r--r--app/presenters/blob_presenter.rb10
-rw-r--r--app/presenters/snippet_blob_presenter.rb2
-rw-r--r--app/services/members/create_service.rb12
-rw-r--r--app/views/discussions/_notes.html.haml4
-rw-r--r--app/views/projects/_new_project_fields.html.haml38
-rw-r--r--app/views/projects/_new_project_initialize_with_sast.html.haml16
-rw-r--r--app/views/shared/notes/_note.html.haml2
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/post_migrate/20220201034731_remove_index_clusters_kubernetes_namespaces_on_cluster_id.rb15
-rw-r--r--db/schema_migrations/202202010347311
-rw-r--r--db/structure.sql2
-rw-r--r--doc/api/graphql/reference/index.md22
-rw-r--r--doc/ci/jobs/job_control.md3
-rw-r--r--doc/development/event_store.md13
-rw-r--r--doc/user/project/clusters/protect/container_host_security/index.md11
-rw-r--r--doc/user/project/clusters/protect/container_host_security/quick_start_guide.md7
-rw-r--r--doc/user/project/clusters/protect/container_network_security/index.md11
-rw-r--r--doc/user/project/clusters/protect/container_network_security/quick_start_guide.md7
-rw-r--r--doc/user/project/clusters/protect/index.md9
-rw-r--r--doc/user/project/integrations/webex_teams.md2
-rw-r--r--lib/gitlab/event_store.rb14
-rw-r--r--lib/gitlab/event_store/store.rb2
-rw-r--r--lib/gitlab/i18n.rb20
-rw-r--r--lib/gitlab/search/found_blob.rb4
-rw-r--r--locale/gitlab.pot6
-rw-r--r--qa/qa/page/project/new.rb5
-rw-r--r--spec/features/merge_request/user_creates_image_diff_notes_spec.rb2
-rw-r--r--spec/frontend/batch_comments/components/diff_file_drafts_spec.js9
-rw-r--r--spec/frontend/diffs/components/diff_discussions_spec.js6
-rw-r--r--spec/frontend/diffs/components/image_diff_overlay_spec.js4
-rw-r--r--spec/frontend/image_diff/helpers/badge_helper_spec.js9
-rw-r--r--spec/frontend/image_diff/helpers/dom_helper_spec.js12
-rw-r--r--spec/frontend/image_diff/image_diff_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js68
-rw-r--r--spec/graphql/types/repository/blob_type_spec.rb3
-rw-r--r--spec/lib/gitlab/event_store/store_spec.rb20
-rw-r--r--spec/presenters/blob_presenter_spec.rb36
-rw-r--r--spec/services/members/create_service_spec.rb9
52 files changed, 508 insertions, 222 deletions
diff --git a/app/assets/javascripts/batch_comments/components/diff_file_drafts.vue b/app/assets/javascripts/batch_comments/components/diff_file_drafts.vue
index 570954c7200..2ebde10c229 100644
--- a/app/assets/javascripts/batch_comments/components/diff_file_drafts.vue
+++ b/app/assets/javascripts/batch_comments/components/diff_file_drafts.vue
@@ -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>
diff --git a/app/assets/javascripts/deprecated_notes.js b/app/assets/javascripts/deprecated_notes.js
index ab57cbbc7c0..82bbbe891e2 100644
--- a/app/assets/javascripts/deprecated_notes.js
+++ b/app/assets/javascripts/deprecated_notes.js
@@ -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);
diff --git a/app/assets/javascripts/design_management/components/design_overlay.vue b/app/assets/javascripts/design_management/components/design_overlay.vue
index b058709b316..674415ec449 100644
--- a/app/assets/javascripts/design_management/components/design_overlay.vue
+++ b/app/assets/javascripts/design_management/components/design_overlay.vue
@@ -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)"
/>
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index 5e05ec87f84..47a05ce11cc 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -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>
diff --git a/app/assets/javascripts/diffs/components/image_diff_overlay.vue b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
index eede8e52292..8871be1f9af 100644
--- a/app/assets/javascripts/diffs/components/image_diff_overlay.vue
+++ b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
@@ -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>
diff --git a/app/assets/javascripts/image_diff/helpers/badge_helper.js b/app/assets/javascripts/image_diff/helpers/badge_helper.js
index 8ee72235a23..5ff00394e3b 100644
--- a/app/assets/javascripts/image_diff/helpers/badge_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/badge_helper.js
@@ -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');
}
diff --git a/app/assets/javascripts/image_diff/helpers/dom_helper.js b/app/assets/javascripts/image_diff/helpers/dom_helper.js
index a61e5f01f9b..3468a629f5a 100644
--- a/app/assets/javascripts/image_diff/helpers/dom_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/dom_helper.js
@@ -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;
}
diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js
index a0dd8e6f894..e3ca4327efe 100644
--- a/app/assets/javascripts/image_diff/image_diff.js
+++ b/app/assets/javascripts/image_diff/image_diff.js
@@ -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)
diff --git a/app/assets/javascripts/image_diff/replaced_image_diff.js b/app/assets/javascripts/image_diff/replaced_image_diff.js
index a3d9b8a138a..8b84cc45c21 100644
--- a/app/assets/javascripts/image_diff/replaced_image_diff.js
+++ b/app/assets/javascripts/image_diff/replaced_image_diff.js
@@ -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
diff --git a/app/assets/javascripts/vue_shared/components/design_management/design_note_pin.vue b/app/assets/javascripts/vue_shared/components/design_management/design_note_pin.vue
index cb038a8c4e1..c411496fad1 100644
--- a/app/assets/javascripts/vue_shared/components/design_management/design_note_pin.vue
+++ b/app/assets/javascripts/vue_shared/components/design_management/design_note_pin.vue
@@ -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"
diff --git a/app/assets/stylesheets/components/design_management/design.scss b/app/assets/stylesheets/components/design_management/design.scss
index 377d5130571..a9be1d89495 100644
--- a/app/assets/stylesheets/components/design_management/design.scss
+++ b/app/assets/stylesheets/components/design_management/design.scss
@@ -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;
+ }
+}
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index 0e849f8d635..1f4221e5778 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -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;
- }
}
}
diff --git a/app/events/members/members_added_event.rb b/app/events/members/members_added_event.rb
new file mode 100644
index 00000000000..c174ffebab6
--- /dev/null
+++ b/app/events/members/members_added_event.rb
@@ -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
diff --git a/app/graphql/types/repository/blob_type.rb b/app/graphql/types/repository/blob_type.rb
index 16332b654d4..6a1adc478a5 100644
--- a/app/graphql/types/repository/blob_type.rb
+++ b/app/graphql/types/repository/blob_type.rb
@@ -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
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index efd29b5601b..47b72df32a2 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -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')
diff --git a/app/presenters/snippet_blob_presenter.rb b/app/presenters/snippet_blob_presenter.rb
index 026d442291c..51ce6ccea58 100644
--- a/app/presenters/snippet_blob_presenter.rb
+++ b/app/presenters/snippet_blob_presenter.rb
@@ -33,7 +33,7 @@ class SnippetBlobPresenter < BlobPresenter
blob.container
end
- def language
+ def gitattr_language
nil
end
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index acd00d0d1ec..dc29bb2c6da 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -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
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index beac4946fd7..a35ba12dd52 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -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 }
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index 0b5da84e4e3..ac238878392 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -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: "" }
diff --git a/app/views/projects/_new_project_initialize_with_sast.html.haml b/app/views/projects/_new_project_initialize_with_sast.html.haml
new file mode 100644
index 00000000000..ec12abbf789
--- /dev/null
+++ b/app/views/projects/_new_project_initialize_with_sast.html.haml
@@ -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 }
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 6549c86ab29..3ab8514aebf 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -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
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 6e328c723fd..cea491ff2f2 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -177,6 +177,8 @@
- 1
- - gitlab_shell
- 2
+- - gitlab_subscriptions_notify_seats_exceeded
+ - 1
- - group_destroy
- 1
- - group_export
diff --git a/db/post_migrate/20220201034731_remove_index_clusters_kubernetes_namespaces_on_cluster_id.rb b/db/post_migrate/20220201034731_remove_index_clusters_kubernetes_namespaces_on_cluster_id.rb
new file mode 100644
index 00000000000..0e2ef3b3324
--- /dev/null
+++ b/db/post_migrate/20220201034731_remove_index_clusters_kubernetes_namespaces_on_cluster_id.rb
@@ -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
diff --git a/db/schema_migrations/20220201034731 b/db/schema_migrations/20220201034731
new file mode 100644
index 00000000000..59e4fd59720
--- /dev/null
+++ b/db/schema_migrations/20220201034731
@@ -0,0 +1 @@
+873ff811d4f70c012785297ee8c07eb496994af69d5ae4b266e8f675c88daca4 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index cb5653a896d..195358131f4 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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);
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 9ad28222804..304f2d42ad2 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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. |
diff --git a/doc/ci/jobs/job_control.md b/doc/ci/jobs/job_control.md
index 0423ada4b72..c93dd75285a 100644
--- a/doc/ci/jobs/job_control.md
+++ b/doc/ci/jobs/job_control.md
@@ -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
diff --git a/doc/development/event_store.md b/doc/development/event_store.md
index 001c461cd56..0eb59724d3f 100644
--- a/doc/development/event_store.md
+++ b/doc/development/event_store.md
@@ -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.
diff --git a/doc/user/project/clusters/protect/container_host_security/index.md b/doc/user/project/clusters/protect/container_host_security/index.md
index 98ba4a1f84d..a0297a7a86f 100644
--- a/doc/user/project/clusters/protect/container_host_security/index.md
+++ b/doc/user/project/clusters/protect/container_host_security/index.md
@@ -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
diff --git a/doc/user/project/clusters/protect/container_host_security/quick_start_guide.md b/doc/user/project/clusters/protect/container_host_security/quick_start_guide.md
index 466bcb7916f..4e56b7e5140 100644
--- a/doc/user/project/clusters/protect/container_host_security/quick_start_guide.md
+++ b/doc/user/project/clusters/protect/container_host_security/quick_start_guide.md
@@ -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
diff --git a/doc/user/project/clusters/protect/container_network_security/index.md b/doc/user/project/clusters/protect/container_network_security/index.md
index 06dc6b24620..7176a1cf1b9 100644
--- a/doc/user/project/clusters/protect/container_network_security/index.md
+++ b/doc/user/project/clusters/protect/container_network_security/index.md
@@ -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
diff --git a/doc/user/project/clusters/protect/container_network_security/quick_start_guide.md b/doc/user/project/clusters/protect/container_network_security/quick_start_guide.md
index 340c9397e9c..e6c91302d7b 100644
--- a/doc/user/project/clusters/protect/container_network_security/quick_start_guide.md
+++ b/doc/user/project/clusters/protect/container_network_security/quick_start_guide.md
@@ -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
diff --git a/doc/user/project/clusters/protect/index.md b/doc/user/project/clusters/protect/index.md
index 1314a1948d5..473195f4c17 100644
--- a/doc/user/project/clusters/protect/index.md
+++ b/doc/user/project/clusters/protect/index.md
@@ -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
diff --git a/doc/user/project/integrations/webex_teams.md b/doc/user/project/integrations/webex_teams.md
index de152aabde5..dd4cdb632e6 100644
--- a/doc/user/project/integrations/webex_teams.md
+++ b/doc/user/project/integrations/webex_teams.md
@@ -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**.
diff --git a/lib/gitlab/event_store.rb b/lib/gitlab/event_store.rb
index 3d7b6b27eb0..7dbbcdbb1a7 100644
--- a/lib/gitlab/event_store.rb
+++ b/lib/gitlab/event_store.rb
@@ -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')
diff --git a/lib/gitlab/event_store/store.rb b/lib/gitlab/event_store/store.rb
index ecf3cd7e562..2e5e0215687 100644
--- a/lib/gitlab/event_store/store.rb
+++ b/lib/gitlab/event_store/store.rb
@@ -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
diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb
index f056381b86a..584f7d4aeaf 100644
--- a/lib/gitlab/i18n.rb
+++ b/lib/gitlab/i18n.rb
@@ -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
diff --git a/lib/gitlab/search/found_blob.rb b/lib/gitlab/search/found_blob.rb
index 183e582925d..60d3e360984 100644
--- a/lib/gitlab/search/found_blob.rb
+++ b/lib/gitlab/search/found_blob.rb
@@ -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 }
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f4f7a8d8d0f..50d36aaf7a8 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
index 42baf1f3f87..e061bc52abc 100644
--- a/qa/qa/page/project/new.rb
+++ b/qa/qa/page/project/new.rb
@@ -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
diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
index 47529518ba4..15f186b649a 100644
--- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
@@ -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')
diff --git a/spec/frontend/batch_comments/components/diff_file_drafts_spec.js b/spec/frontend/batch_comments/components/diff_file_drafts_spec.js
index 8805f6bf204..6a5ff1af7c9 100644
--- a/spec/frontend/batch_comments/components/diff_file_drafts_spec.js
+++ b/spec/frontend/batch_comments/components/diff_file_drafts_spec.js
@@ -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);
});
});
diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js
index 8d0c9b01188..2da68adddf6 100644
--- a/spec/frontend/diffs/components/diff_discussions_spec.js
+++ b/spec/frontend/diffs/components/diff_discussions_spec.js
@@ -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');
});
});
});
diff --git a/spec/frontend/diffs/components/image_diff_overlay_spec.js b/spec/frontend/diffs/components/image_diff_overlay_spec.js
index 8c1a8041f6c..70191620eb6 100644
--- a/spec/frontend/diffs/components/image_diff_overlay_spec.js
+++ b/spec/frontend/diffs/components/image_diff_overlay_spec.js
@@ -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() {
diff --git a/spec/frontend/image_diff/helpers/badge_helper_spec.js b/spec/frontend/image_diff/helpers/badge_helper_spec.js
index c970ccc535d..9ac6ebf6fdb 100644
--- a/spec/frontend/image_diff/helpers/badge_helper_spec.js
+++ b/spec/frontend/image_diff/helpers/badge_helper_spec.js
@@ -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', () => {
diff --git a/spec/frontend/image_diff/helpers/dom_helper_spec.js b/spec/frontend/image_diff/helpers/dom_helper_spec.js
index 9357d626bbe..1c5f1cbe3da 100644
--- a/spec/frontend/image_diff/helpers/dom_helper_spec.js
+++ b/spec/frontend/image_diff/helpers/dom_helper_spec.js
@@ -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(),
+ );
});
});
diff --git a/spec/frontend/image_diff/image_diff_spec.js b/spec/frontend/image_diff/image_diff_spec.js
index 16d19f45496..710aa7108a8 100644
--- a/spec/frontend/image_diff/image_diff_spec.js
+++ b/spec/frontend/image_diff/image_diff_spec.js
@@ -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');
diff --git a/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js b/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js
index 984a28c93d6..353d493add9 100644
--- a/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js
+++ b/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js
@@ -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`);
+ });
+ });
+ });
});
diff --git a/spec/graphql/types/repository/blob_type_spec.rb b/spec/graphql/types/repository/blob_type_spec.rb
index 489b617f763..565341d15b9 100644
--- a/spec/graphql/types/repository/blob_type_spec.rb
+++ b/spec/graphql/types/repository/blob_type_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/event_store/store_spec.rb b/spec/lib/gitlab/event_store/store_spec.rb
index 711e1d5b4d5..284018582a7 100644
--- a/spec/lib/gitlab/event_store/store_spec.rb
+++ b/spec/lib/gitlab/event_store/store_spec.rb
@@ -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
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index abeeef69d39..225386d9596 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -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) }
diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb
index 13f56fe7458..b3bfdfbcc4d 100644
--- a/spec/services/members/create_service_spec.rb
+++ b/spec/services/members/create_service_spec.rb
@@ -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