Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue11
-rw-r--r--app/assets/javascripts/diffs/components/diff_row.vue79
-rw-r--r--app/assets/javascripts/diffs/components/diff_row_utils.js6
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue39
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue7
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_table_row.vue24
-rw-r--r--app/assets/javascripts/diffs/store/utils.js9
-rw-r--r--app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue4
-rw-r--r--app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue2
-rw-r--r--app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue4
-rw-r--r--app/assets/javascripts/members/components/modals/leave_modal.vue4
-rw-r--r--app/assets/javascripts/members/components/table/member_source.vue2
-rw-r--r--app/assets/javascripts/notes/components/multiline_comment_form.vue7
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js2
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue242
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app_legacy.vue164
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue176
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss3
-rw-r--r--app/controllers/projects/ci/pipeline_editor_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/helpers/emails_helper.rb23
-rw-r--r--app/helpers/groups/group_members_helper.rb80
-rw-r--r--app/mailers/emails/members.rb17
-rw-r--r--app/models/commit_status.rb2
-rw-r--r--app/models/member.rb2
-rw-r--r--app/models/members/group_member.rb4
-rw-r--r--app/presenters/packages/nuget/service_index_presenter.rb4
-rw-r--r--app/serializers/diffs_metadata_entity.rb22
-rw-r--r--app/serializers/member_entity.rb54
-rw-r--r--app/serializers/member_serializer.rb5
-rw-r--r--app/serializers/member_user_entity.rb28
-rw-r--r--app/services/notification_service.rb6
-rw-r--r--app/views/admin/dev_ops_report/show.html.haml3
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml4
-rw-r--r--app/views/notify/member_expiration_date_updated_email.html.haml6
-rw-r--r--app/views/notify/member_expiration_date_updated_email.text.erb5
-rw-r--r--app/views/projects/issues/_design_management.html.haml2
-rw-r--r--changelogs/unreleased/291027-extend-diffs_metadata-with-project-and-user-names.yml5
-rw-r--r--changelogs/unreleased/294422-follow-up-from-fire-webhook-when-updating-or-removing-a-group-memb.yml5
-rw-r--r--changelogs/unreleased/astoicescu-remove-limit-of-features-on-billing-page.yml5
-rw-r--r--changelogs/unreleased/ff-enable-ci-vis-by_-default.yml5
-rw-r--r--changelogs/unreleased/ld-fix-typo-on-designs-lfs-notice.yml5
-rw-r--r--changelogs/unreleased/lm-update-regex-group-ame.yml5
-rw-r--r--changelogs/unreleased/yo-gl-button-feature-highlight.yml5
-rw-r--r--config/feature_flags/development/ci_config_visualization_tab.yml2
-rw-r--r--config/feature_flags/development/drag_comment_selection.yml8
-rw-r--r--doc/administration/geo/disaster_recovery/index.md73
-rw-r--r--doc/administration/housekeeping.md17
-rw-r--r--doc/api/epic_links.md2
-rw-r--r--doc/api/groups.md22
-rw-r--r--doc/api/personal_access_tokens.md3
-rw-r--r--doc/ci/environments/deployment_safety.md69
-rw-r--r--doc/ci/examples/semantic-release.md2
-rw-r--r--doc/ci/pipeline_editor/index.md19
-rw-r--r--doc/development/agent/identity.md4
-rw-r--r--doc/development/packages.md18
-rw-r--r--doc/development/usage_ping.md104
-rw-r--r--doc/user/group/iterations/index.md16
-rw-r--r--doc/user/packages/dependency_proxy/index.md3
-rw-r--r--doc/user/packages/nuget_repository/index.md8
-rw-r--r--doc/user/profile/notifications.md3
-rw-r--r--doc/user/project/quick_actions.md3
-rw-r--r--doc/user/project/web_ide/index.md39
-rw-r--r--doc/user/search/index.md46
-rw-r--r--doc/user/snippets.md26
-rw-r--r--lib/api/nuget_group_packages.rb2
-rw-r--r--locale/gitlab.pot76
-rw-r--r--package.json2
-rw-r--r--spec/features/projects/issues/design_management/user_uploads_designs_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/entities/member.json (renamed from spec/fixtures/api/schemas/group_member.json)49
-rw-r--r--spec/fixtures/api/schemas/entities/member_user.json22
-rw-r--r--spec/fixtures/api/schemas/members.json (renamed from spec/fixtures/api/schemas/group_members.json)2
-rw-r--r--spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap186
-rw-r--r--spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap2
-rw-r--r--spec/frontend/diffs/components/app_spec.js30
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js5
-rw-r--r--spec/frontend/diffs/components/diff_line_note_form_spec.js37
-rw-r--r--spec/frontend/diffs/components/diff_row_spec.js47
-rw-r--r--spec/frontend/diffs/components/diff_row_utils_spec.js16
-rw-r--r--spec/frontend/diffs/components/diff_view_spec.js74
-rw-r--r--spec/frontend/diffs/store/utils_spec.js15
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap1
-rw-r--r--spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap164
-rw-r--r--spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js4
-rw-r--r--spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js2
-rw-r--r--spec/frontend/members/components/action_buttons/user_action_buttons_spec.js4
-rw-r--r--spec/frontend/members/components/modals/leave_modal_spec.js4
-rw-r--r--spec/frontend/members/components/table/member_source_spec.js2
-rw-r--r--spec/frontend/members/mock_data.js2
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap1
-rw-r--r--spec/frontend/notes/components/multiline_comment_form_spec.js89
-rw-r--r--spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap1
-rw-r--r--spec/frontend/projects/pipelines/charts/components/app_legacy_spec.js46
-rw-r--r--spec/frontend/projects/pipelines/charts/components/app_spec.js90
-rw-r--r--spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js78
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap1
-rw-r--r--spec/frontend_integration/ide/ide_integration_spec.js4
-rw-r--r--spec/helpers/groups/group_members_helper_spec.rb12
-rw-r--r--spec/mailers/notify_spec.rb82
-rw-r--r--spec/models/commit_status_spec.rb3
-rw-r--r--spec/models/members/group_member_spec.rb12
-rw-r--r--spec/requests/api/nuget_group_packages_spec.rb28
-rw-r--r--spec/serializers/diffs_metadata_entity_spec.rb1
-rw-r--r--spec/serializers/member_entity_spec.rb71
-rw-r--r--spec/serializers/member_serializer_spec.rb32
-rw-r--r--spec/serializers/member_user_entity_spec.rb38
-rw-r--r--spec/services/notification_service_spec.rb12
-rw-r--r--yarn.lock8
111 files changed, 2009 insertions, 932 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index cf7e7cefb17..3d9fc97d6b6 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-3083074640633df94cbeee611795a6fc6d8c5607
+e9743661a588b99e51cf7f806eaaf137573e5c02
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index 6e22e181c02..463b7f5cff4 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -56,10 +56,11 @@ export default {
},
computed: {
...mapState({
- noteableData: (state) => state.notes.noteableData,
- diffViewType: (state) => state.diffs.diffViewType,
+ diffViewType: ({ diffs }) => diffs.diffViewType,
+ showSuggestPopover: ({ diffs }) => diffs.showSuggestPopover,
+ noteableData: ({ notes }) => notes.noteableData,
+ selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
}),
- ...mapState('diffs', ['showSuggestPopover']),
...mapGetters('diffs', ['getDiffFileByHash', 'diffLines']),
...mapGetters([
'isLoggedIn',
@@ -126,6 +127,10 @@ export default {
this.initAutoSave(this.noteableData, keys);
}
+
+ if (this.selectedCommentPosition) {
+ this.commentLineStart = this.selectedCommentPosition.start;
+ }
},
methods: {
...mapActions('diffs', [
diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue
index 9e1385853b6..9054a8aec04 100644
--- a/app/assets/javascripts/diffs/components/diff_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_row.vue
@@ -10,6 +10,7 @@ import {
CONFLICT_THEIR,
CONFLICT_MARKER,
} from '../constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
@@ -22,6 +23,7 @@ export default {
GlTooltip: GlTooltipDirective,
SafeHtml,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
fileHash: {
type: String,
@@ -45,6 +47,15 @@ export default {
required: false,
default: false,
},
+ index: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ dragging: false,
+ };
},
computed: {
...mapGetters('diffs', ['fileLineCoverage']),
@@ -52,26 +63,35 @@ export default {
...mapState({
isHighlighted(state) {
const line = this.line.left?.line_code ? this.line.left : this.line.right;
- return utils.isHighlighted(state, line, this.isCommented);
+ return utils.isHighlighted(state, line, false);
},
}),
classNameMap() {
return {
[CONTEXT_LINE_CLASS_NAME]: this.line.isContextLineLeft,
[PARALLEL_DIFF_VIEW_TYPE]: !this.inline,
+ commented: this.isCommented,
};
},
parallelViewLeftLineType() {
- return utils.parallelViewLeftLineType(this.line, this.isHighlighted);
+ return utils.parallelViewLeftLineType(this.line, this.isHighlighted || this.isCommented);
},
coverageState() {
return this.fileLineCoverage(this.filePath, this.line.right.new_line);
},
classNameMapCellLeft() {
- return utils.classNameMapCell(this.line.left, this.isHighlighted, this.isLoggedIn);
+ return utils.classNameMapCell({
+ line: this.line.left,
+ hll: this.isHighlighted || this.isCommented,
+ isLoggedIn: this.isLoggedIn,
+ });
},
classNameMapCellRight() {
- return utils.classNameMapCell(this.line.right, this.isHighlighted, this.isLoggedIn);
+ return utils.classNameMapCell({
+ line: this.line.right,
+ hll: this.isHighlighted || this.isCommented,
+ isLoggedIn: this.isLoggedIn,
+ });
},
addCommentTooltipLeft() {
return utils.addCommentTooltip(this.line.left);
@@ -131,6 +151,22 @@ export default {
? this.$options.THEIR_CHANGES
: this.$options.OUR_CHANGES;
},
+ onDragEnd() {
+ this.dragging = false;
+ if (!this.glFeatures.dragCommentSelection) return;
+
+ this.$emit('stopdragging');
+ },
+ onDragEnter(line, index) {
+ if (!this.glFeatures.dragCommentSelection) return;
+
+ this.$emit('enterdragging', { ...line, index });
+ },
+ onDragStart(line) {
+ this.$root.$emit('bv::hide::tooltip');
+ this.dragging = true;
+ this.$emit('startdragging', line);
+ },
},
OUR_CHANGES: 'HEAD//our changes',
THEIR_CHANGES: 'origin//their changes',
@@ -143,7 +179,13 @@ export default {
<template>
<div :class="classNameMap" class="diff-grid-row diff-tr line_holder">
- <div class="diff-grid-left left-side">
+ <div
+ data-testid="left-side"
+ class="diff-grid-left left-side"
+ @dragover.prevent
+ @dragenter="onDragEnter(line.left, index)"
+ @dragend="onDragEnd"
+ >
<template v-if="line.left && line.left.type !== $options.CONFLICT_MARKER">
<div
:class="classNameMapCellLeft"
@@ -159,10 +201,13 @@ export default {
:title="addCommentTooltipLeft"
>
<button
+ :draggable="glFeatures.dragCommentSelection"
type="button"
class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
+ :class="{ 'gl-cursor-grab': dragging }"
:disabled="line.left.commentsDisabled"
@click="handleCommentButton(line.left)"
+ @dragstart="onDragStart({ ...line.left, index })"
>
<gl-icon :size="12" name="comment" />
</button>
@@ -234,7 +279,14 @@ export default {
></div>
</template>
</div>
- <div v-if="!inline" class="diff-grid-right right-side">
+ <div
+ v-if="!inline"
+ data-testid="right-side"
+ class="diff-grid-right right-side"
+ @dragover.prevent
+ @dragenter="onDragEnter(line.right, index)"
+ @dragend="onDragEnd"
+ >
<template v-if="line.right">
<div :class="classNameMapCellRight" class="diff-td diff-line-num new_line">
<template v-if="line.right.type !== $options.CONFLICT_MARKER_THEIR">
@@ -246,10 +298,13 @@ export default {
:title="addCommentTooltipRight"
>
<button
+ :draggable="glFeatures.dragCommentSelection"
type="button"
class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
+ :class="{ 'gl-cursor-grab': dragging }"
:disabled="line.right.commentsDisabled"
@click="handleCommentButton(line.right)"
+ @dragstart="onDragStart({ ...line.right, index })"
>
<gl-icon :size="12" name="comment" />
</button>
@@ -279,13 +334,21 @@ export default {
<div
v-gl-tooltip.hover
:title="coverageState.text"
- :class="[line.right.type, coverageState.class, { hll: isHighlighted }]"
+ :class="[line.right.type, coverageState.class, { hll: isHighlighted, hll: isCommented }]"
class="diff-td line-coverage right-side"
></div>
<div
:id="line.right.line_code"
:key="line.right.rich_text"
- :class="[line.right.type, { hll: isHighlighted, parallel: !inline }]"
+ v-safe-html="line.right.rich_text"
+ :class="[
+ line.right.type,
+ {
+ hll: isHighlighted,
+ hll: isCommented,
+ parallel: !inline,
+ },
+ ]"
class="diff-td line_content with-coverage right-side"
@mousedown="handleParallelLineMouseDown"
>
diff --git a/app/assets/javascripts/diffs/components/diff_row_utils.js b/app/assets/javascripts/diffs/components/diff_row_utils.js
index 3ef271ddb8f..7606c39ad37 100644
--- a/app/assets/javascripts/diffs/components/diff_row_utils.js
+++ b/app/assets/javascripts/diffs/components/diff_row_utils.js
@@ -35,7 +35,7 @@ export const lineCode = (line) => {
return line.line_code || line.left?.line_code || line.right?.line_code;
};
-export const classNameMapCell = (line, hll, isLoggedIn, isHover) => {
+export const classNameMapCell = ({ line, hll, isLoggedIn, isHover }) => {
if (!line) return [];
const { type } = line;
@@ -54,7 +54,9 @@ export const addCommentTooltip = (line) => {
let tooltip;
if (!line) return tooltip;
- tooltip = __('Add a comment to this line');
+ tooltip = gon.drag_comment_selection
+ ? __('Add a comment to this line or drag for multiple lines')
+ : __('Add a comment to this line');
const brokenSymlinks = line.commentsDisabled;
if (brokenSymlinks) {
diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue
index 84429f62a1c..79800f835f4 100644
--- a/app/assets/javascripts/diffs/components/diff_view.vue
+++ b/app/assets/javascripts/diffs/components/diff_view.vue
@@ -1,5 +1,5 @@
<script>
-import { mapGetters, mapState } from 'vuex';
+import { mapGetters, mapState, mapActions } from 'vuex';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import DiffRow from './diff_row.vue';
@@ -35,6 +35,12 @@ export default {
default: false,
},
},
+ data() {
+ return {
+ dragStart: null,
+ updatedLineRange: null,
+ };
+ },
computed: {
...mapGetters('diffs', ['commitId']),
...mapState({
@@ -52,12 +58,39 @@ export default {
},
},
methods: {
+ ...mapActions(['setSelectedCommentPosition']),
+ ...mapActions('diffs', ['showCommentForm']),
showCommentLeft(line) {
return !this.inline || line.left;
},
showCommentRight(line) {
return !this.inline || (line.right && !line.left);
},
+ onStartDragging(line) {
+ this.dragStart = line;
+ },
+ onDragOver(line) {
+ if (line.chunk !== this.dragStart.chunk) return;
+
+ let start = this.dragStart;
+ let end = line;
+
+ if (this.dragStart.index >= line.index) {
+ start = line;
+ end = this.dragStart;
+ }
+
+ this.updatedLineRange = { start, end };
+
+ this.setSelectedCommentPosition(this.updatedLineRange);
+ },
+ onStopDragging() {
+ this.showCommentForm({
+ lineCode: this.updatedLineRange?.end?.line_code,
+ fileHash: this.diffFile.file_hash,
+ });
+ this.dragStart = null;
+ },
},
userColorScheme: window.gon.user_color_scheme,
};
@@ -94,6 +127,10 @@ export default {
:is-bottom="index + 1 === diffLinesLength"
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
:inline="inline"
+ :index="index"
+ @enterdragging="onDragOver"
+ @startdragging="onStartDragging"
+ @stopdragging="onStopDragging"
/>
<div
v-if="line.renderCommentRow"
diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
index 2d8ffb047ca..014b1ebe54b 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -72,7 +72,12 @@ export default {
return this.fileLineCoverage(this.filePath, this.line.new_line);
},
classNameMapCell() {
- return classNameMapCell(this.line, this.isHighlighted, this.isLoggedIn, this.isHover);
+ return classNameMapCell({
+ line: this.line,
+ hll: this.isHighlighted,
+ isLoggedIn: this.isLoggedIn,
+ isHover: this.isHover,
+ });
},
addCommentTooltip() {
return addCommentTooltip(this.line);
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
index b1618fb0688..47eecef2385 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
@@ -68,20 +68,20 @@ export default {
return this.fileLineCoverage(this.filePath, this.line.right.new_line);
},
classNameMapCellLeft() {
- return utils.classNameMapCell(
- this.line.left,
- this.isHighlighted,
- this.isLoggedIn,
- this.isLeftHover,
- );
+ return utils.classNameMapCell({
+ line: this.line.left,
+ hll: this.isHighlighted,
+ isLoggedIn: this.isLoggedIn,
+ isHover: this.isLeftHover,
+ });
},
classNameMapCellRight() {
- return utils.classNameMapCell(
- this.line.right,
- this.isHighlighted,
- this.isLoggedIn,
- this.isRightHover,
- );
+ return utils.classNameMapCell({
+ line: this.line.right,
+ hll: this.isHighlighted,
+ isLoggedIn: this.isLoggedIn,
+ isHover: this.isRightHover,
+ });
},
addCommentTooltipLeft() {
return utils.addCommentTooltip(this.line.left);
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 8712220b25e..c52da558be2 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -55,8 +55,17 @@ export const parallelizeDiffLines = (diffLines, inline) => {
let conflictStartIndex = -1;
const lines = [];
+ // `chunk` is used for dragging to select diff lines
+ // we are restricting commenting to only lines that appear between
+ // "expansion rows". Here equal chunks are lines grouped together
+ // inbetween expansion rows.
+ let chunk = 0;
+
for (let i = 0, diffLinesLength = diffLines.length, index = 0; i < diffLinesLength; i += 1) {
const line = diffLines[i];
+ line.chunk = chunk;
+
+ if (isMeta(line)) chunk += 1;
if (isRemoved(line) || isConflictOur(line) || inline) {
lines.push({
diff --git a/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue
index 10078d5cd64..fcb70dd45a6 100644
--- a/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue
@@ -28,13 +28,13 @@ export default {
if (this.isCurrentUser) {
return sprintf(
s__('Members|Are you sure you want to withdraw your access request for "%{source}"'),
- { source: source.name },
+ { source: source.fullName },
);
}
return sprintf(
s__('Members|Are you sure you want to deny %{usersName}\'s request to join "%{source}"'),
- { usersName: user.name, source: source.name },
+ { usersName: user.name, source: source.fullName },
);
},
},
diff --git a/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue
index 2b0a75640e2..9a27348f146 100644
--- a/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue
@@ -25,7 +25,7 @@ export default {
s__(
'Members|Are you sure you want to revoke the invitation for %{inviteEmail} to join "%{source}"',
),
- { inviteEmail: invite.email, source: source.name },
+ { inviteEmail: invite.email, source: source.fullName },
);
},
},
diff --git a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
index f2bc9c7e876..0e5df961782 100644
--- a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
@@ -36,7 +36,7 @@ export default {
s__('Members|Are you sure you want to remove %{usersName} from "%{source}"'),
{
usersName: user.name,
- source: source.name,
+ source: source.fullName,
},
);
}
@@ -44,7 +44,7 @@ export default {
return sprintf(
s__('Members|Are you sure you want to remove this orphaned member from "%{source}"'),
{
- source: source.name,
+ source: source.fullName,
},
);
},
diff --git a/app/assets/javascripts/members/components/modals/leave_modal.vue b/app/assets/javascripts/members/components/modals/leave_modal.vue
index 57a5da774e3..d231c7eabfa 100644
--- a/app/assets/javascripts/members/components/modals/leave_modal.vue
+++ b/app/assets/javascripts/members/components/modals/leave_modal.vue
@@ -35,7 +35,7 @@ export default {
return this.memberPath.replace(/:id$/, 'leave');
},
modalTitle() {
- return sprintf(s__('Members|Leave "%{source}"'), { source: this.member.source.name });
+ return sprintf(s__('Members|Leave "%{source}"'), { source: this.member.source.fullName });
},
},
methods: {
@@ -59,7 +59,7 @@ export default {
<gl-form ref="form" :action="leavePath" method="post">
<p>
<gl-sprintf :message="$options.modalContent">
- <template #source>{{ member.source.name }}</template>
+ <template #source>{{ member.source.fullName }}</template>
</gl-sprintf>
</p>
diff --git a/app/assets/javascripts/members/components/table/member_source.vue b/app/assets/javascripts/members/components/table/member_source.vue
index 030d72c3420..30fcbfcd3f8 100644
--- a/app/assets/javascripts/members/components/table/member_source.vue
+++ b/app/assets/javascripts/members/components/table/member_source.vue
@@ -22,6 +22,6 @@ export default {
<template>
<span v-if="isDirectMember">{{ __('Direct member') }}</span>
<a v-else v-gl-tooltip.hover :title="__('Inherited')" :href="memberSource.webUrl">{{
- memberSource.name
+ memberSource.fullName
}}</a>
</template>
diff --git a/app/assets/javascripts/notes/components/multiline_comment_form.vue b/app/assets/javascripts/notes/components/multiline_comment_form.vue
index bb13eb87af7..9fbf2c9265c 100644
--- a/app/assets/javascripts/notes/components/multiline_comment_form.vue
+++ b/app/assets/javascripts/notes/components/multiline_comment_form.vue
@@ -1,5 +1,5 @@
<script>
-import { mapActions } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import { GlFormSelect, GlSprintf } from '@gitlab/ui';
import { getSymbol, getLineClasses } from './multiline_comment_utils';
@@ -27,12 +27,13 @@ export default {
};
},
computed: {
+ ...mapState({ selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition }),
lineNumber() {
return this.commentLineOptions[this.commentLineOptions.length - 1].text;
},
},
created() {
- const line = this.lineRange?.start || this.line;
+ const line = this.selectedCommentPosition?.start || this.lineRange?.start || this.line;
this.commentLineStart = {
line_code: line.line_code,
@@ -40,6 +41,8 @@ export default {
old_line: line.old_line,
new_line: line.new_line,
};
+
+ if (this.selectedCommentPosition) return;
this.highlightSelection();
},
destroyed() {
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 4421a84a6b1..144a3d7ba90 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -15,7 +15,7 @@ export default () => ({
batchSuggestionsInfo: [],
currentlyFetchingDiscussions: false,
/**
- * selectedCommentPosition & selectedCommentPosition structures are the same as `position.line_range`:
+ * selectedCommentPosition & selectedCommentPositionHover structures are the same as `position.line_range`:
* {
* start: { line_code: string, new_line: number, old_line:number, type: string },
* end: { line_code: string, new_line: number, old_line:number, type: string },
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
index 75f5edbfd2b..7bb62cf4a73 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
@@ -1,38 +1,18 @@
<script>
-import dateFormat from 'dateformat';
-import { GlColumnChart } from '@gitlab/ui/dist/charts';
-import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
-import { __, s__, sprintf } from '~/locale';
-import { getDateInPast } from '~/lib/utils/datetime_utility';
+import { GlAlert, GlTabs, GlTab } from '@gitlab/ui';
+import { s__ } from '~/locale';
import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql';
import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql';
-import StatisticsList from './statistics_list.vue';
-import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
+import PipelineCharts from './pipeline_charts.vue';
import {
- CHART_CONTAINER_HEIGHT,
- CHART_DATE_FORMAT,
DEFAULT,
- INNER_CHART_HEIGHT,
LOAD_ANALYTICS_FAILURE,
LOAD_PIPELINES_FAILURE,
- ONE_WEEK_AGO_DAYS,
- ONE_MONTH_AGO_DAYS,
PARSE_FAILURE,
UNSUPPORTED_DATA,
- X_AXIS_LABEL_ROTATION,
- X_AXIS_TITLE_OFFSET,
} from '../constants';
-const defaultCountValues = {
- totalPipelines: {
- count: 0,
- },
- successfulPipelines: {
- count: 0,
- },
-};
-
const defaultAnalyticsValues = {
weekPipelinesTotals: [],
weekPipelinesLabels: [],
@@ -47,36 +27,40 @@ const defaultAnalyticsValues = {
pipelineTimesValues: [],
};
+const defaultCountValues = {
+ totalPipelines: {
+ count: 0,
+ },
+ successfulPipelines: {
+ count: 0,
+ },
+};
+
export default {
components: {
GlAlert,
- GlColumnChart,
- GlSkeletonLoader,
- StatisticsList,
- CiCdAnalyticsAreaChart,
+ GlTabs,
+ GlTab,
+ PipelineCharts,
DeploymentFrequencyCharts: () =>
import('ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue'),
},
inject: {
- projectPath: {
- type: String,
- default: '',
- },
shouldRenderDeploymentFrequencyCharts: {
type: Boolean,
default: false,
},
+ projectPath: {
+ type: String,
+ default: '',
+ },
},
data() {
return {
- counts: {
- ...defaultCountValues,
- },
- analytics: {
- ...defaultAnalyticsValues,
- },
showFailureAlert: false,
failureType: null,
+ analytics: { ...defaultAnalyticsValues },
+ counts: { ...defaultCountValues },
};
},
apollo: {
@@ -134,41 +118,6 @@ export default {
};
}
},
- successRatio() {
- const { successfulPipelines, failedPipelines } = this.counts;
- const successfulCount = successfulPipelines?.count;
- const failedCount = failedPipelines?.count;
- const ratio = (successfulCount / (successfulCount + failedCount)) * 100;
-
- return failedCount === 0 ? 100 : ratio;
- },
- formattedCounts() {
- const { totalPipelines, successfulPipelines, failedPipelines } = this.counts;
-
- return {
- total: totalPipelines?.count,
- success: successfulPipelines?.count,
- failed: failedPipelines?.count,
- successRatio: this.successRatio,
- };
- },
- areaCharts() {
- const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles;
- let areaChartsData = [];
-
- try {
- areaChartsData = [
- this.buildAreaChartData(lastWeek, this.lastWeekChartData),
- this.buildAreaChartData(lastMonth, this.lastMonthChartData),
- this.buildAreaChartData(lastYear, this.lastYearChartData),
- ];
- } catch {
- areaChartsData = [];
- this.reportFailure(PARSE_FAILURE);
- }
-
- return areaChartsData;
- },
lastWeekChartData() {
return {
labels: this.analytics.weekPipelinesLabels,
@@ -190,39 +139,32 @@ export default {
success: this.analytics.yearPipelinesSuccessful,
};
},
- timesChartTransformedData() {
- return [
- {
- name: 'full',
- data: this.mergeLabelsAndValues(
- this.analytics.pipelineTimesLabels,
- this.analytics.pipelineTimesValues,
- ),
- },
- ];
+ timesChartData() {
+ return {
+ labels: this.analytics.pipelineTimesLabels,
+ values: this.analytics.pipelineTimesValues,
+ };
},
- },
- methods: {
- mergeLabelsAndValues(labels, values) {
- return labels.map((label, index) => [label, values[index]]);
+ successRatio() {
+ const { successfulPipelines, failedPipelines } = this.counts;
+ const successfulCount = successfulPipelines?.count;
+ const failedCount = failedPipelines?.count;
+ const ratio = (successfulCount / (successfulCount + failedCount)) * 100;
+
+ return failedCount === 0 ? 100 : ratio;
},
- buildAreaChartData(title, data) {
- const { labels, totals, success } = data;
+ formattedCounts() {
+ const { totalPipelines, successfulPipelines, failedPipelines } = this.counts;
return {
- title,
- data: [
- {
- name: 'all',
- data: this.mergeLabelsAndValues(labels, totals),
- },
- {
- name: 'success',
- data: this.mergeLabelsAndValues(labels, success),
- },
- ],
+ total: totalPipelines?.count,
+ success: successfulPipelines?.count,
+ failed: failedPipelines?.count,
+ successRatio: this.successRatio,
};
},
+ },
+ methods: {
hideAlert() {
this.showFailureAlert = false;
},
@@ -231,16 +173,6 @@ export default {
this.failureType = type;
},
},
- chartContainerHeight: CHART_CONTAINER_HEIGHT,
- timesChartOptions: {
- height: INNER_CHART_HEIGHT,
- xAxis: {
- axisLabel: {
- rotate: X_AXIS_LABEL_ROTATION,
- },
- nameGap: X_AXIS_TITLE_OFFSET,
- },
- },
errorTexts: {
[LOAD_ANALYTICS_FAILURE]: s__(
'PipelineCharts|An error has ocurred when retrieving the analytics data',
@@ -251,74 +183,38 @@ export default {
[PARSE_FAILURE]: s__('PipelineCharts|There was an error parsing the data for the charts.'),
[DEFAULT]: s__('PipelineCharts|An unknown error occurred while processing CI/CD analytics.'),
},
- get chartTitles() {
- const today = dateFormat(new Date(), CHART_DATE_FORMAT);
- const pastDate = (timeScale) =>
- dateFormat(getDateInPast(new Date(), timeScale), CHART_DATE_FORMAT);
- return {
- lastWeek: sprintf(__('Pipelines for last week (%{oneWeekAgo} - %{today})'), {
- oneWeekAgo: pastDate(ONE_WEEK_AGO_DAYS),
- today,
- }),
- lastMonth: sprintf(__('Pipelines for last month (%{oneMonthAgo} - %{today})'), {
- oneMonthAgo: pastDate(ONE_MONTH_AGO_DAYS),
- today,
- }),
- lastYear: __('Pipelines for last year'),
- };
- },
- areaChartOptions: {
- xAxis: {
- name: s__('Pipeline|Date'),
- type: 'category',
- },
- yAxis: {
- name: s__('Pipeline|Pipelines'),
- },
- },
};
</script>
<template>
<div>
- <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">
- {{ failure.text }}
- </gl-alert>
- <div class="gl-mb-3">
- <h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3>
- </div>
- <h4 class="gl-my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4>
- <div class="row">
- <div class="col-md-6">
- <gl-skeleton-loader v-if="$apollo.queries.counts.loading" :lines="5" />
- <statistics-list v-else :counts="formattedCounts" />
- </div>
- <div class="col-md-6">
- <strong>
- {{ __('Duration for the last 30 commits') }}
- </strong>
- <gl-column-chart
- :height="$options.chartContainerHeight"
- :option="$options.timesChartOptions"
- :bars="timesChartTransformedData"
- :y-axis-title="__('Minutes')"
- :x-axis-title="__('Commit')"
- x-axis-type="category"
+ <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">{{
+ failure.text
+ }}</gl-alert>
+ <gl-tabs v-if="shouldRenderDeploymentFrequencyCharts">
+ <gl-tab :title="__('Pipelines')">
+ <pipeline-charts
+ :counts="formattedCounts"
+ :last-week="lastWeekChartData"
+ :last-month="lastMonthChartData"
+ :last-year="lastYearChartData"
+ :times-chart="timesChartData"
+ :loading="$apollo.queries.counts.loading"
+ @report-failure="reportFailure"
/>
- </div>
- </div>
- <hr />
- <h4 class="gl-my-4">{{ __('Pipelines charts') }}</h4>
- <ci-cd-analytics-area-chart
- v-for="(chart, index) in areaCharts"
- :key="index"
- :chart-data="chart.data"
- :area-chart-options="$options.areaChartOptions"
- >
- {{ chart.title }}
- </ci-cd-analytics-area-chart>
- <template v-if="shouldRenderDeploymentFrequencyCharts">
- <hr />
- <deployment-frequency-charts />
- </template>
+ </gl-tab>
+ <gl-tab :title="__('Deployments')">
+ <deployment-frequency-charts />
+ </gl-tab>
+ </gl-tabs>
+ <pipeline-charts
+ v-else
+ :counts="formattedCounts"
+ :last-week="lastWeekChartData"
+ :last-month="lastMonthChartData"
+ :last-year="lastYearChartData"
+ :times-chart="timesChartData"
+ :loading="$apollo.queries.counts.loading"
+ @report-failure="reportFailure"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app_legacy.vue b/app/assets/javascripts/projects/pipelines/charts/components/app_legacy.vue
index 915d334f949..03e02659060 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app_legacy.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app_legacy.vue
@@ -1,26 +1,12 @@
<script>
-import dateFormat from 'dateformat';
-import { GlColumnChart } from '@gitlab/ui/dist/charts';
-import { __, s__, sprintf } from '~/locale';
-import { getDateInPast } from '~/lib/utils/datetime_utility';
-import StatisticsList from './statistics_list.vue';
-import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
-
-import {
- CHART_CONTAINER_HEIGHT,
- INNER_CHART_HEIGHT,
- X_AXIS_LABEL_ROTATION,
- X_AXIS_TITLE_OFFSET,
- CHART_DATE_FORMAT,
- ONE_WEEK_AGO_DAYS,
- ONE_MONTH_AGO_DAYS,
-} from '../constants';
+import { GlTabs, GlTab } from '@gitlab/ui';
+import PipelineCharts from './pipeline_charts.vue';
export default {
components: {
- StatisticsList,
- GlColumnChart,
- CiCdAnalyticsAreaChart,
+ GlTabs,
+ GlTab,
+ PipelineCharts,
DeploymentFrequencyCharts: () =>
import('ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue'),
},
@@ -54,121 +40,41 @@ export default {
},
data() {
return {
- timesChartTransformedData: [
- {
- name: 'full',
- data: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values),
- },
- ],
+ // this loading flag gives the echarts library just enough time
+ // to ensure all DOM nodes have been mounted.
+ //
+ // https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1131
+ loading: true,
};
},
- computed: {
- areaCharts() {
- const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles;
-
- return [
- this.buildAreaChartData(lastWeek, this.lastWeekChartData),
- this.buildAreaChartData(lastMonth, this.lastMonthChartData),
- this.buildAreaChartData(lastYear, this.lastYearChartData),
- ];
- },
- },
- methods: {
- mergeLabelsAndValues(labels, values) {
- return labels.map((label, index) => [label, values[index]]);
- },
- buildAreaChartData(title, data) {
- const { labels, totals, success } = data;
-
- return {
- title,
- data: [
- {
- name: 'all',
- data: this.mergeLabelsAndValues(labels, totals),
- },
- {
- name: 'success',
- data: this.mergeLabelsAndValues(labels, success),
- },
- ],
- };
- },
- },
- chartContainerHeight: CHART_CONTAINER_HEIGHT,
- timesChartOptions: {
- height: INNER_CHART_HEIGHT,
- xAxis: {
- axisLabel: {
- rotate: X_AXIS_LABEL_ROTATION,
- },
- nameGap: X_AXIS_TITLE_OFFSET,
- },
- },
- get chartTitles() {
- const today = dateFormat(new Date(), CHART_DATE_FORMAT);
- const pastDate = (timeScale) =>
- dateFormat(getDateInPast(new Date(), timeScale), CHART_DATE_FORMAT);
- return {
- lastWeek: sprintf(__('Pipelines for last week (%{oneWeekAgo} - %{today})'), {
- oneWeekAgo: pastDate(ONE_WEEK_AGO_DAYS),
- today,
- }),
- lastMonth: sprintf(__('Pipelines for last month (%{oneMonthAgo} - %{today})'), {
- oneMonthAgo: pastDate(ONE_MONTH_AGO_DAYS),
- today,
- }),
- lastYear: __('Pipelines for last year'),
- };
- },
- areaChartOptions: {
- xAxis: {
- name: s__('Pipeline|Date'),
- type: 'category',
- },
- yAxis: {
- name: s__('Pipeline|Pipelines'),
- },
+ async mounted() {
+ await this.$nextTick();
+ this.loading = false;
},
};
</script>
<template>
- <div>
- <div class="mb-3">
- <h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3>
- </div>
- <h4 class="my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4>
- <div class="row">
- <div class="col-md-6">
- <statistics-list :counts="counts" />
- </div>
- <div class="col-md-6">
- <strong>
- {{ __('Duration for the last 30 commits') }}
- </strong>
- <gl-column-chart
- :height="$options.chartContainerHeight"
- :option="$options.timesChartOptions"
- :bars="timesChartTransformedData"
- :y-axis-title="__('Minutes')"
- :x-axis-title="__('Commit')"
- x-axis-type="category"
- />
- </div>
- </div>
- <hr />
- <h4 class="my-4">{{ __('Pipelines charts') }}</h4>
- <ci-cd-analytics-area-chart
- v-for="(chart, index) in areaCharts"
- :key="index"
- :chart-data="chart.data"
- :area-chart-options="$options.areaChartOptions"
- >
- {{ chart.title }}
- </ci-cd-analytics-area-chart>
- <template v-if="shouldRenderDeploymentFrequencyCharts">
- <hr />
+ <gl-tabs v-if="shouldRenderDeploymentFrequencyCharts">
+ <gl-tab :title="__('Pipelines')">
+ <pipeline-charts
+ :counts="counts"
+ :last-week="lastWeekChartData"
+ :last-month="lastMonthChartData"
+ :last-year="lastYearChartData"
+ :times-chart="timesChartData"
+ :loading="loading"
+ />
+ </gl-tab>
+ <gl-tab :title="__('Deployments')">
<deployment-frequency-charts />
- </template>
- </div>
+ </gl-tab>
+ </gl-tabs>
+ <pipeline-charts
+ v-else
+ :counts="counts"
+ :last-week="lastWeekChartData"
+ :last-month="lastMonthChartData"
+ :last-year="lastYearChartData"
+ :times-chart="timesChartData"
+ />
</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
new file mode 100644
index 00000000000..bec4ab407f0
--- /dev/null
+++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
@@ -0,0 +1,176 @@
+<script>
+import dateFormat from 'dateformat';
+import { GlColumnChart } from '@gitlab/ui/dist/charts';
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { __, s__, sprintf } from '~/locale';
+import { getDateInPast } from '~/lib/utils/datetime_utility';
+import {
+ CHART_CONTAINER_HEIGHT,
+ CHART_DATE_FORMAT,
+ INNER_CHART_HEIGHT,
+ ONE_WEEK_AGO_DAYS,
+ ONE_MONTH_AGO_DAYS,
+ X_AXIS_LABEL_ROTATION,
+ X_AXIS_TITLE_OFFSET,
+ PARSE_FAILURE,
+} from '../constants';
+import StatisticsList from './statistics_list.vue';
+import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
+
+export default {
+ components: {
+ GlColumnChart,
+ GlSkeletonLoader,
+ StatisticsList,
+ CiCdAnalyticsAreaChart,
+ },
+ props: {
+ counts: {
+ required: true,
+ type: Object,
+ },
+ loading: {
+ required: false,
+ default: false,
+ type: Boolean,
+ },
+ lastWeek: {
+ required: true,
+ type: Object,
+ },
+ lastMonth: {
+ required: true,
+ type: Object,
+ },
+ lastYear: {
+ required: true,
+ type: Object,
+ },
+ timesChart: {
+ required: true,
+ type: Object,
+ },
+ },
+ computed: {
+ areaCharts() {
+ const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles;
+ const charts = [
+ { title: lastWeek, data: this.lastWeek },
+ { title: lastMonth, data: this.lastMonth },
+ { title: lastYear, data: this.lastYear },
+ ];
+ let areaChartsData = [];
+
+ try {
+ areaChartsData = charts.map(this.buildAreaChartData);
+ } catch {
+ areaChartsData = [];
+ this.vm.$emit('report-failure', PARSE_FAILURE);
+ }
+
+ return areaChartsData;
+ },
+ timesChartTransformedData() {
+ return [
+ {
+ name: 'full',
+ data: this.mergeLabelsAndValues(this.timesChart.labels, this.timesChart.values),
+ },
+ ];
+ },
+ },
+ methods: {
+ mergeLabelsAndValues(labels, values) {
+ return labels.map((label, index) => [label, values[index]]);
+ },
+ buildAreaChartData({ title, data }) {
+ const { labels, totals, success } = data;
+
+ return {
+ title,
+ data: [
+ {
+ name: 'all',
+ data: this.mergeLabelsAndValues(labels, totals),
+ },
+ {
+ name: 'success',
+ data: this.mergeLabelsAndValues(labels, success),
+ },
+ ],
+ };
+ },
+ },
+ chartContainerHeight: CHART_CONTAINER_HEIGHT,
+ timesChartOptions: {
+ height: INNER_CHART_HEIGHT,
+ xAxis: {
+ axisLabel: {
+ rotate: X_AXIS_LABEL_ROTATION,
+ },
+ nameGap: X_AXIS_TITLE_OFFSET,
+ },
+ },
+ areaChartOptions: {
+ xAxis: {
+ name: s__('Pipeline|Date'),
+ type: 'category',
+ },
+ yAxis: {
+ name: s__('Pipeline|Pipelines'),
+ },
+ },
+ get chartTitles() {
+ const today = dateFormat(new Date(), CHART_DATE_FORMAT);
+ const pastDate = (timeScale) =>
+ dateFormat(getDateInPast(new Date(), timeScale), CHART_DATE_FORMAT);
+ return {
+ lastWeek: sprintf(__('Pipelines for last week (%{oneWeekAgo} - %{today})'), {
+ oneWeekAgo: pastDate(ONE_WEEK_AGO_DAYS),
+ today,
+ }),
+ lastMonth: sprintf(__('Pipelines for last month (%{oneMonthAgo} - %{today})'), {
+ oneMonthAgo: pastDate(ONE_MONTH_AGO_DAYS),
+ today,
+ }),
+ lastYear: __('Pipelines for last year'),
+ };
+ },
+};
+</script>
+<template>
+ <div>
+ <div class="gl-mb-3">
+ <h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3>
+ </div>
+ <h4 class="gl-my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4>
+ <div class="row">
+ <div class="col-md-6">
+ <gl-skeleton-loader v-if="loading" :lines="5" />
+ <statistics-list v-else :counts="counts" />
+ </div>
+ <div v-if="!loading" class="col-md-6">
+ <strong>{{ __('Duration for the last 30 commits') }}</strong>
+ <gl-column-chart
+ :height="$options.chartContainerHeight"
+ :option="$options.timesChartOptions"
+ :bars="timesChartTransformedData"
+ :y-axis-title="__('Minutes')"
+ :x-axis-title="__('Commit')"
+ x-axis-type="category"
+ />
+ </div>
+ </div>
+ <template v-if="!loading">
+ <hr />
+ <h4 class="gl-my-4">{{ __('Pipelines charts') }}</h4>
+ <ci-cd-analytics-area-chart
+ v-for="(chart, index) in areaCharts"
+ :key="index"
+ :chart-data="chart.data"
+ :area-chart-options="$options.areaChartOptions"
+ >{{ chart.title }}</ci-cd-analytics-area-chart
+ >
+ </template>
+ </div>
+</template>
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 00230d61a46..41fc4d3dd4e 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -986,6 +986,8 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
.labels-select-wrapper {
&.is-standalone {
+ min-width: $input-md-width;
+
.labels-select-dropdown-contents {
max-height: 350px;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index d5a0371e386..674ba1a307b 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -516,9 +516,12 @@ $line-removed-dark-transparent: rgba(246, 53, 85, 0.2);
$line-number-old: #f9d7dc;
$line-number-new: #ddfbe6;
$line-number-select: #fbf2da;
+$line-number-commented: #dae5fb;
$line-target-blue: $blue-50;
$line-select-yellow: #fcf8e7;
$line-select-yellow-dark: #f0e2bd;
+$line-commented-blue: #e8effc;
+$line-commented-blue-dark: #bccef0;
$dark-diff-match-bg: rgba($white, 0.3);
$dark-diff-match-color: rgba($white, 0.1);
$diff-image-info-color: #808080;
diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb
index cc391868df0..ef9025ae52f 100644
--- a/app/controllers/projects/ci/pipeline_editor_controller.rb
+++ b/app/controllers/projects/ci/pipeline_editor_controller.rb
@@ -3,7 +3,7 @@
class Projects::Ci::PipelineEditorController < Projects::ApplicationController
before_action :check_can_collaborate!
before_action do
- push_frontend_feature_flag(:ci_config_visualization_tab, @project, default_enabled: false)
+ push_frontend_feature_flag(:ci_config_visualization_tab, @project, default_enabled: :yaml)
end
feature_category :pipeline_authoring
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 8ded9c2313f..2ecbf8db576 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -33,6 +33,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:batch_suggestions, @project, default_enabled: true)
push_frontend_feature_flag(:approvals_commented_by, @project, default_enabled: true)
push_frontend_feature_flag(:merge_request_widget_graphql, @project)
+ push_frontend_feature_flag(:drag_comment_selection, @project, default_enabled: true)
push_frontend_feature_flag(:unified_diff_components, @project, default_enabled: true)
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true)
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 017981c8c8e..b58ff21b257 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -214,6 +214,29 @@ module EmailsHelper
end
end
+ def group_membership_expiration_changed_text(member, group)
+ if member.expires?
+ days = (member.expires_at - Date.today).to_i
+ days_formatted = pluralize(days, 'day')
+
+ _('Your %{group} membership will now expire in %{days}.') % { group: group.human_name, days: days_formatted }
+ else
+ _('Your membership in %{group} no longer expires.') % { group: group.human_name }
+ end
+ end
+
+ def group_membership_expiration_changed_link(member, group, format: nil)
+ url = group_group_members_url(group, search: member.user.username)
+
+ case format
+ when :html
+ link_to = generate_link('group membership', url).html_safe
+ _('For additional information, review your %{link_to} or contact your group owner.').html_safe % { link_to: link_to }
+ else
+ _('For additional information, review your group membership: %{link_to} or contact your group owner.') % { link_to: url }
+ end
+ end
+
def instance_access_request_text(user, format: nil)
gitlab_host = Gitlab.config.gitlab.host
diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb
index adc9d85a384..a4159ed6b19 100644
--- a/app/helpers/groups/group_members_helper.rb
+++ b/app/helpers/groups/group_members_helper.rb
@@ -18,7 +18,7 @@ module Groups::GroupMembersHelper
end
def members_data_json(group, members)
- members_data(group, members).to_json
+ MemberSerializer.new.represent(members, { current_user: current_user, group: group }).to_json
end
# Overridden in `ee/app/helpers/ee/groups/group_members_helper.rb`
@@ -38,84 +38,6 @@ module Groups::GroupMembersHelper
group_id: group.id
}
end
-
- private
-
- def members_data(group, members)
- members.map do |member|
- user = member.user
- source = member.source
-
- data = {
- id: member.id,
- created_at: member.created_at,
- expires_at: member.expires_at&.to_time,
- requested_at: member.requested_at,
- can_update: member.can_update?,
- can_remove: member.can_remove?,
- access_level: {
- string_value: member.human_access,
- integer_value: member.access_level
- },
- source: {
- id: source.id,
- name: source.full_name,
- web_url: Gitlab::UrlBuilder.build(source)
- },
- valid_roles: member.valid_level_roles
- }.merge(member_created_by_data(member.created_by))
-
- if member.invite?
- data[:invite] = member_invite_data(member)
- elsif user.present?
- data[:user] = member_user_data(user)
- end
-
- data
- end
- end
-
- def member_created_by_data(created_by)
- return {} unless created_by.present?
-
- {
- created_by: {
- name: created_by.name,
- web_url: Gitlab::UrlBuilder.build(created_by)
- }
- }
- end
-
- def member_user_data(user)
- {
- id: user.id,
- name: user.name,
- username: user.username,
- web_url: Gitlab::UrlBuilder.build(user),
- avatar_url: avatar_icon_for_user(user, AVATAR_SIZE),
- blocked: user.blocked?,
- two_factor_enabled: user.two_factor_enabled?
- }.merge(member_user_status_data(user.status))
- end
-
- def member_user_status_data(status)
- return {} unless status.present?
-
- {
- status: {
- emoji: status.emoji,
- message_html: status.message_html
- }
- }
- end
-
- def member_invite_data(member)
- {
- email: member.invite_email,
- avatar_url: avatar_icon_for_email(member.invite_email, AVATAR_SIZE),
- can_resend: member.can_resend_invite?
- }
- end
end
Groups::GroupMembersHelper.prepend_if_ee('EE::Groups::GroupMembersHelper')
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index 02726c4354e..69f5fe1430a 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -114,6 +114,23 @@ module Emails
subject: subject('Invitation declined'))
end
+ def member_expiration_date_updated_email(member_source_type, member_id)
+ @member_source_type = member_source_type
+ @member_id = member_id
+
+ return unless member_exists?
+
+ subject = if member.expires?
+ _('Group membership expiration date changed')
+ else
+ _('Group membership expiration date removed')
+ end
+
+ member_email_with_layout(
+ to: member.user.notification_email_for(notification_group),
+ subject: subject(subject))
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def member
@member ||= Member.find_by(id: @member_id)
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index f1f0fc3b11c..a399ffc32de 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -209,7 +209,7 @@ class CommitStatus < ApplicationRecord
def group_name
# 'rspec:linux: 1/10' => 'rspec:linux'
- common_name = name.to_s.gsub(%r{\d+[\s:\/\\]+\d+\s*}, '')
+ common_name = name.to_s.gsub(%r{\b\d+[\s:\/\\]+\d+\s*}, '')
# 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux'
common_name.gsub!(%r{: \[.*\]\s*\z}, '')
diff --git a/app/models/member.rb b/app/models/member.rb
index 687830f5267..2e79b50d6b7 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -13,6 +13,8 @@ class Member < ApplicationRecord
include FromUnion
include UpdateHighestRole
+ AVATAR_SIZE = 40
+
attr_accessor :raw_invite_token
belongs_to :created_by, class_name: "User"
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index dc300e8f540..c30f6dc81ee 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -74,6 +74,10 @@ class GroupMember < Member
run_after_commit { notification_service.update_group_member(self) }
end
+ if saved_change_to_expires_at?
+ run_after_commit { notification_service.updated_group_member_expiration(self) }
+ end
+
super
end
diff --git a/app/presenters/packages/nuget/service_index_presenter.rb b/app/presenters/packages/nuget/service_index_presenter.rb
index 67476097714..b3cc912b811 100644
--- a/app/presenters/packages/nuget/service_index_presenter.rb
+++ b/app/presenters/packages/nuget/service_index_presenter.rb
@@ -100,7 +100,7 @@ module Packages
case scope
when :group
- api_v4_groups_packages_nuget_metadata_package_name_package_version_path(
+ api_v4_groups___packages_nuget_metadata_package_name_package_version_path(
params,
true
)
@@ -115,7 +115,7 @@ module Packages
def search_service_url
case scope
when :group
- api_v4_groups_packages_nuget_query_path(id: @project_or_group.id)
+ api_v4_groups___packages_nuget_query_path(id: @project_or_group.id)
when :project
api_v4_projects_packages_nuget_query_path(id: @project_or_group.id)
end
diff --git a/app/serializers/diffs_metadata_entity.rb b/app/serializers/diffs_metadata_entity.rb
index 7b0de3bce4e..681e629244f 100644
--- a/app/serializers/diffs_metadata_entity.rb
+++ b/app/serializers/diffs_metadata_entity.rb
@@ -18,8 +18,30 @@ class DiffsMetadataEntity < DiffsEntity
options[:merge_request].can_be_merged_by?(request.current_user)
end
+ expose :project_path
+ expose :project_name
+
+ expose :username
+ expose :user_full_name
+
private
+ def project_path
+ request.project&.full_path
+ end
+
+ def project_name
+ request.project&.name
+ end
+
+ def username
+ request.current_user&.username
+ end
+
+ def user_full_name
+ request.current_user&.name
+ end
+
def presenter(merge_request)
@presenters ||= {}
@presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: request.current_user) # rubocop: disable CodeReuse/Presenter
diff --git a/app/serializers/member_entity.rb b/app/serializers/member_entity.rb
new file mode 100644
index 00000000000..584ba4c62de
--- /dev/null
+++ b/app/serializers/member_entity.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+class MemberEntity < Grape::Entity
+ include RequestAwareEntity
+ include AvatarsHelper
+
+ expose :id
+ expose :created_at
+ expose :expires_at do |member|
+ member.expires_at&.to_time
+ end
+ expose :requested_at
+
+ expose :created_by, if: -> (member) { member.created_by.present? } do |member|
+ UserEntity.represent(member.created_by, only: [:name, :web_url])
+ end
+
+ expose :can_update do |member|
+ member.can_update?
+ end
+
+ expose :can_remove do |member|
+ member.can_remove?
+ end
+
+ expose :access_level do
+ expose :human_access, as: :string_value
+ expose :access_level, as: :integer_value
+ end
+
+ expose :source do |member|
+ GroupEntity.represent(member.source, only: [:id, :full_name, :web_url])
+ end
+
+ expose :valid_level_roles, as: :valid_roles
+
+ expose :user, if: -> (member) { member.user.present? }, using: MemberUserEntity
+
+ expose :invite, if: -> (member) { member.invite? } do
+ expose :email do |member|
+ member.invite_email
+ end
+
+ expose :avatar_url do |member|
+ avatar_icon_for_email(member.invite_email, Member::AVATAR_SIZE)
+ end
+
+ expose :can_resend do |member|
+ member.can_resend_invite?
+ end
+ end
+end
+
+MemberEntity.prepend_if_ee('EE::MemberEntity')
diff --git a/app/serializers/member_serializer.rb b/app/serializers/member_serializer.rb
new file mode 100644
index 00000000000..b34d7f30a58
--- /dev/null
+++ b/app/serializers/member_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class MemberSerializer < BaseSerializer
+ entity MemberEntity
+end
diff --git a/app/serializers/member_user_entity.rb b/app/serializers/member_user_entity.rb
new file mode 100644
index 00000000000..a022966c041
--- /dev/null
+++ b/app/serializers/member_user_entity.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class MemberUserEntity < UserEntity
+ unexpose :show_status
+ unexpose :path
+ unexpose :state
+ unexpose :status_tooltip_html
+
+ expose :avatar_url do |user|
+ user.avatar_url(size: Member::AVATAR_SIZE, only_path: false)
+ end
+
+ expose :blocked do |user|
+ user.blocked?
+ end
+
+ expose :two_factor_enabled do |user|
+ user.two_factor_enabled?
+ end
+
+ expose :status, if: -> (user) { user.status.present? } do
+ expose :emoji do |user|
+ user.status.emoji
+ end
+ end
+end
+
+MemberUserEntity.prepend_if_ee('EE::MemberUserEntity')
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index d0bd3206556..5a71e0eac7c 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -481,6 +481,12 @@ class NotificationService
mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
end
+ def updated_group_member_expiration(group_member)
+ return true unless group_member.notifiable?(:mention)
+
+ mailer.member_expiration_date_updated_email(group_member.real_source_type, group_member.id).deliver_later
+ end
+
def project_was_moved(project, old_path_with_namespace)
recipients = project_moved_recipients(project)
recipients = notifiable_users(recipients, :custom, custom_action: :moved_project, project: project)
diff --git a/app/views/admin/dev_ops_report/show.html.haml b/app/views/admin/dev_ops_report/show.html.haml
index 733c0de9fe9..7659f02bdce 100644
--- a/app/views/admin/dev_ops_report/show.html.haml
+++ b/app/views/admin/dev_ops_report/show.html.haml
@@ -3,7 +3,8 @@
.container
.gl-mt-3
- - if Gitlab.ee? && Feature.enabled?(:devops_adoption_feature, default_enabled: false) && License.feature_available?(:devops_adoption)
+ - feature_already_in_use = Analytics::DevopsAdoption::Segment.any?
+ - if Gitlab.ee? && Feature.enabled?(:devops_adoption_feature, default_enabled: feature_already_in_use) && License.feature_available?(:devops_adoption)
= render_if_exists 'admin/dev_ops_report/devops_tabs'
- else
= render 'report'
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 5cadabd5f90..e02b8333c60 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -300,8 +300,8 @@
= link_to _('Auto DevOps'), help_page_path('topics/autodevops/index.md')
%span= _('uses Kubernetes clusters to deploy your code!')
%hr
- %button.btn.btn-success.btn-sm.dismiss-feature-highlight{ type: 'button' }
- %span= _("Got it!")
+ %button.gl-button.btn.btn-success.btn-sm.dismiss-feature-highlight{ type: 'button' }
+ %span.gl-mr-2= _("Got it!")
= sprite_icon('thumb-up')
- if project_nav_tab? :environments
diff --git a/app/views/notify/member_expiration_date_updated_email.html.haml b/app/views/notify/member_expiration_date_updated_email.html.haml
new file mode 100644
index 00000000000..6c4db22eeaa
--- /dev/null
+++ b/app/views/notify/member_expiration_date_updated_email.html.haml
@@ -0,0 +1,6 @@
+= email_default_heading(say_hi(@member.user))
+
+%p
+ = group_membership_expiration_changed_text(@member, @member_source)
+%p
+ = group_membership_expiration_changed_link(@member, @member_source, format: :html)
diff --git a/app/views/notify/member_expiration_date_updated_email.text.erb b/app/views/notify/member_expiration_date_updated_email.text.erb
new file mode 100644
index 00000000000..8b3a5a55e77
--- /dev/null
+++ b/app/views/notify/member_expiration_date_updated_email.text.erb
@@ -0,0 +1,5 @@
+<%= say_hi(@member.user) %>
+
+<%= group_membership_expiration_changed_text(@member, @member_source) %>
+
+<%= group_membership_expiration_changed_link(@member, @member_source) %>
diff --git a/app/views/projects/issues/_design_management.html.haml b/app/views/projects/issues/_design_management.html.haml
index ad0605b10a8..a2ff9620c0c 100644
--- a/app/views/projects/issues/_design_management.html.haml
+++ b/app/views/projects/issues/_design_management.html.haml
@@ -3,7 +3,7 @@
- requirements_link_url = help_page_path('user/project/issues/design_management', anchor: 'requirements')
- requirements_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: requirements_link_url }
- link_end = '</a>'.html_safe
-- enable_lfs_message = s_("DesignManagement|To upload designs, you'll need to enable LFS and have admin enable hashed storage. %{requirements_link_start}More information%{requirements_link_end}").html_safe % { requirements_link_start: requirements_link_start, requirements_link_end: link_end }
+- enable_lfs_message = s_("DesignManagement|To upload designs, you'll need to enable LFS and have an admin enable hashed storage. %{requirements_link_start}More information%{requirements_link_end}").html_safe % { requirements_link_start: requirements_link_start, requirements_link_end: link_end }
- if @project.design_management_enabled?
- add_page_startup_graphql_call('design_management/get_design_list', { fullPath: @project.full_path, iid: @issue.iid.to_s, atVersion: nil })
diff --git a/changelogs/unreleased/291027-extend-diffs_metadata-with-project-and-user-names.yml b/changelogs/unreleased/291027-extend-diffs_metadata-with-project-and-user-names.yml
new file mode 100644
index 00000000000..8d47ba92593
--- /dev/null
+++ b/changelogs/unreleased/291027-extend-diffs_metadata-with-project-and-user-names.yml
@@ -0,0 +1,5 @@
+---
+title: Add additional fields to diff_metadata.json endpoint
+merge_request: 50666
+author:
+type: changed
diff --git a/changelogs/unreleased/294422-follow-up-from-fire-webhook-when-updating-or-removing-a-group-memb.yml b/changelogs/unreleased/294422-follow-up-from-fire-webhook-when-updating-or-removing-a-group-memb.yml
new file mode 100644
index 00000000000..f1659f03ae2
--- /dev/null
+++ b/changelogs/unreleased/294422-follow-up-from-fire-webhook-when-updating-or-removing-a-group-memb.yml
@@ -0,0 +1,5 @@
+---
+title: Send email when group member expiry is updated
+merge_request: 50310
+author:
+type: added
diff --git a/changelogs/unreleased/astoicescu-remove-limit-of-features-on-billing-page.yml b/changelogs/unreleased/astoicescu-remove-limit-of-features-on-billing-page.yml
new file mode 100644
index 00000000000..e965c8453c3
--- /dev/null
+++ b/changelogs/unreleased/astoicescu-remove-limit-of-features-on-billing-page.yml
@@ -0,0 +1,5 @@
+---
+title: Remove limit of four features per plan
+merge_request: 51264
+author:
+type: changed
diff --git a/changelogs/unreleased/ff-enable-ci-vis-by_-default.yml b/changelogs/unreleased/ff-enable-ci-vis-by_-default.yml
new file mode 100644
index 00000000000..8ef535f3e4f
--- /dev/null
+++ b/changelogs/unreleased/ff-enable-ci-vis-by_-default.yml
@@ -0,0 +1,5 @@
+---
+title: Enable CI visualization by default
+merge_request: 51701
+author:
+type: added
diff --git a/changelogs/unreleased/ld-fix-typo-on-designs-lfs-notice.yml b/changelogs/unreleased/ld-fix-typo-on-designs-lfs-notice.yml
new file mode 100644
index 00000000000..1c63971a3cf
--- /dev/null
+++ b/changelogs/unreleased/ld-fix-typo-on-designs-lfs-notice.yml
@@ -0,0 +1,5 @@
+---
+title: Fix typo in notice displayed when Design Management requires LFS to be enabled
+merge_request: 51644
+author:
+type: fixed
diff --git a/changelogs/unreleased/lm-update-regex-group-ame.yml b/changelogs/unreleased/lm-update-regex-group-ame.yml
new file mode 100644
index 00000000000..34d0d7acbe0
--- /dev/null
+++ b/changelogs/unreleased/lm-update-regex-group-ame.yml
@@ -0,0 +1,5 @@
+---
+title: Updates regex for group_name to support numbers in job name
+merge_request: 51157
+author:
+type: changed
diff --git a/changelogs/unreleased/yo-gl-button-feature-highlight.yml b/changelogs/unreleased/yo-gl-button-feature-highlight.yml
new file mode 100644
index 00000000000..8e0b9ad6fe2
--- /dev/null
+++ b/changelogs/unreleased/yo-gl-button-feature-highlight.yml
@@ -0,0 +1,5 @@
+---
+title: Add gl-button to dismiss feature highlight button
+merge_request: 51555
+author: Yogi (@yo)
+type: other
diff --git a/config/feature_flags/development/ci_config_visualization_tab.yml b/config/feature_flags/development/ci_config_visualization_tab.yml
index 70e395d83e9..cdc7322b7fd 100644
--- a/config/feature_flags/development/ci_config_visualization_tab.yml
+++ b/config/feature_flags/development/ci_config_visualization_tab.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/290117
milestone: '13.7'
type: development
group: group::pipeline authoring
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/drag_comment_selection.yml b/config/feature_flags/development/drag_comment_selection.yml
new file mode 100644
index 00000000000..a34d2a6231b
--- /dev/null
+++ b/config/feature_flags/development/drag_comment_selection.yml
@@ -0,0 +1,8 @@
+---
+name: drag_comment_selection
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49875
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/293945
+milestone: '13.7'
+type: development
+group: group::source code
+default_enabled: true
diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md
index 544dacc707a..2b0a3e2f114 100644
--- a/doc/administration/geo/disaster_recovery/index.md
+++ b/doc/administration/geo/disaster_recovery/index.md
@@ -181,7 +181,7 @@ secondary. If the node is paused, be sure to resume before promoting. This
issue has been fixed in GitLab 13.4 and later.
WARNING:
- If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
+If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
a point-in-time recovery to the last known state.
Data that was created on the primary while the secondary was paused will be lost.
@@ -220,6 +220,75 @@ Data that was created on the primary while the secondary was paused will be lost
previously for the **secondary**.
1. Success! The **secondary** has now been promoted to **primary**.
+#### Promoting a **secondary** node with a Patroni standby cluster
+
+The `gitlab-ctl promote-to-primary-node` command cannot be used yet in
+conjunction with a Patroni standby cluster, as it can only
+perform changes on a **secondary** with only a single machine. Instead, you must
+do this manually.
+
+WARNING:
+In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the
+secondary is paused fails. Do not pause replication before promoting a
+secondary. If the node is paused, be sure to resume before promoting. This
+issue has been fixed in GitLab 13.4 and later.
+
+WARNING:
+If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
+a point-in-time recovery to the last known state.
+Data that was created on the primary while the secondary was paused will be lost.
+
+1. SSH in to the Standby Leader database node in the **secondary** and trigger PostgreSQL to
+ promote to read-write:
+
+ ```shell
+ sudo gitlab-ctl promote-db
+ ```
+
+1. Disable Patroni auto-failover:
+
+ ```shell
+ sudo gitlab-ctl patroni pause
+ ```
+
+1. Edit `/etc/gitlab/gitlab.rb` on every application and Sidekiq nodes in the secondary to reflect its new status as primary by removing any lines that enabled the `geo_secondary_role`:
+
+ ```ruby
+ ## In pre-11.5 documentation, the role was enabled as follows. Remove this line.
+ geo_secondary_role['enable'] = true
+
+ ## In 11.5+ documentation, the role was enabled as follows. Remove this line.
+ roles ['geo_secondary_role']
+ ```
+
+1. Edit `/etc/gitlab/gitlab.rb` on every Patroni node in the secondary to disable the standby cluster:
+
+ ```ruby
+ patroni['standby_cluster']['enable'] = false
+ ```
+
+1. Reconfigure GitLab on each machine for the changes to take effect:
+
+ ```shell
+ sudo gitlab-ctl reconfigure
+ ```
+
+1. Resume Patroni auto-failover:
+
+ ```shell
+ sudo gitlab-ctl patroni resume
+ ```
+
+1. Promote the **secondary** to **primary**. SSH into a single application server and execute:
+
+ ```shell
+ sudo gitlab-rake geo:set_secondary_as_primary
+ ```
+
+1. Verify you can connect to the newly promoted **primary** using the URL used previously for the **secondary**.
+
+1. Success! The **secondary** has now been promoted to **primary**.
+
#### Promoting a **secondary** node with an external PostgreSQL database
The `gitlab-ctl promote-to-primary-node` command cannot be used in conjunction with
@@ -278,7 +347,7 @@ required:
1. Verify you can connect to the newly promoted **primary** site using the URL used
previously for the **secondary** site.
-Success! The **secondary** site has now been promoted to **primary**.
+1. Success! The **secondary** site has now been promoted to **primary**.
### Step 4. (Optional) Updating the primary domain DNS record
diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md
index 90d2f0d916d..eedd0fb8293 100644
--- a/doc/administration/housekeeping.md
+++ b/doc/administration/housekeeping.md
@@ -40,3 +40,20 @@ from your project on the same schedule as the `git gc` operation, freeing up sto
You can find this option under your project's **Settings > General > Advanced**.
![Housekeeping settings](img/housekeeping_settings.png)
+
+## How housekeeping handles pool repositories
+
+Housekeeping for pool repositories is handled differently from standard repositories.
+It is ultimately performed by the Gitaly RPC `FetchIntoObjectPool`.
+
+This is the current call stack by which it is invoked:
+
+1. `Projects::HousekeepingService#execute_gitlab_shell_gc`
+1. `GitGarbageCollectWorker#perform`
+1. `Projects::GitDeduplicationService#fetch_from_source`
+1. `ObjectPool#fetch`
+1. `ObjectPoolService#fetch`
+1. `Gitaly::FetchIntoObjectPoolRequest`
+
+To manually invoke it from a Rails console, if needed, you can call `project.pool_repository.object_pool.fetch`.
+This is a potentially long-running task, though Gitaly will timeout in about 8 hours.
diff --git a/doc/api/epic_links.md b/doc/api/epic_links.md
index 319d1a1ee9b..cabab18ed71 100644
--- a/doc/api/epic_links.md
+++ b/doc/api/epic_links.md
@@ -76,7 +76,7 @@ Example response:
Creates an association between two epics, designating one as the parent epic and the other as the child epic. A parent epic can have multiple child epics. If the new child epic already belonged to another epic, it is unassigned from that previous parent.
```plaintext
-POST /groups/:id/epics/:epic_iid/epics
+POST /groups/:id/epics/:epic_iid/epics/:child_epic_id
```
| Attribute | Type | Required | Description |
diff --git a/doc/api/groups.md b/doc/api/groups.md
index abd4a7ecdea..eb255f8de00 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -766,7 +766,6 @@ Parameters:
| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). Default to the global level default branch protection setting. |
| `shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` |
| `extra_shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). |
-| `shared_runners_setting` | string | no | See [Options for `shared_runners_setting`](#options-for-shared_runners_setting). Enable or disable shared runners for a group's subgroups and projects. |
### Options for `default_branch_protection`
@@ -778,16 +777,6 @@ The `default_branch_protection` attribute determines whether developers and main
| `1` | Partial protection. Developers and maintainers can: <br>- Push new commits |
| `2` | Full protection. Only maintainers can: <br>- Push new commits |
-### Options for `shared_runners_setting`
-
-The `shared_runners_setting` attribute determines whether shared runners are enabled for a group's subgroups and projects.
-
-| Value | Description |
-|-------|-------------------------------------------------------------------------------------------------------------|
-| `enabled` | Enables shared runners for all projects and subgroups in this group. |
-| `disabled_with_override` | Disables shared runners for all projects and subgroups in this group, but allows subgroups to override this setting. |
-| `disabled_and_unoverridable` | Disables shared runners for all projects and subgroups in this group, and prevents subgroups from overriding this setting. |
-
## New Subgroup
This is similar to creating a [New group](#new-group). You need the `parent_id` from the [List groups](#list-groups) call. You can then enter the desired:
@@ -852,6 +841,7 @@ PUT /groups/:id
| `shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` |
| `extra_shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). |
| `prevent_forking_outside_group` | boolean | no | **(PREMIUM)** When enabled, users can **not** fork projects from this group to external namespaces
+| `shared_runners_setting` | string | no | See [Options for `shared_runners_setting`](#options-for-shared_runners_setting). Enable or disable shared runners for a group's subgroups and projects. |
NOTE:
The `projects` and `shared_projects` attributes in the response are deprecated and [scheduled for removal in API v5](https://gitlab.com/gitlab-org/gitlab/-/issues/213797).
@@ -941,6 +931,16 @@ with Rails console access to run the following command:
Feature.disable(:limit_projects_in_groups_api)
```
+### Options for `shared_runners_setting`
+
+The `shared_runners_setting` attribute determines whether shared runners are enabled for a group's subgroups and projects.
+
+| Value | Description |
+|-------|-------------------------------------------------------------------------------------------------------------|
+| `enabled` | Enables shared runners for all projects and subgroups in this group. |
+| `disabled_with_override` | Disables shared runners for all projects and subgroups in this group, but allows subgroups to override this setting. |
+| `disabled_and_unoverridable` | Disables shared runners for all projects and subgroups in this group, and prevents subgroups from overriding this setting. |
+
## Remove group
Only available to group owners and administrators.
diff --git a/doc/api/personal_access_tokens.md b/doc/api/personal_access_tokens.md
index 535f33ece4a..ca0ac3522c3 100644
--- a/doc/api/personal_access_tokens.md
+++ b/doc/api/personal_access_tokens.md
@@ -70,7 +70,8 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
## Revoke a personal access token
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216004) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216004) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/270200) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.6.
Revoke a personal access token.
diff --git a/doc/ci/environments/deployment_safety.md b/doc/ci/environments/deployment_safety.md
index 97db7c90c8b..4dac076ffb7 100644
--- a/doc/ci/environments/deployment_safety.md
+++ b/doc/ci/environments/deployment_safety.md
@@ -14,6 +14,9 @@ You can:
- [Restrict write-access to a critical environment](#restrict-write-access-to-a-critical-environment)
- [Prevent deployments during deploy freeze windows](#prevent-deployments-during-deploy-freeze-windows)
+- [Set appropriate roles to your project](#setting-appropriate-roles-to-your-project)
+- [Protect production secrets](#protect-production-secrets)
+- [Separate project for deployments](#separate-project-for-deployments)
If you are using a continuous deployment workflow and want to ensure that concurrent deployments to the same environment do not happen, you should enable the following options:
@@ -38,8 +41,8 @@ For example:
```yaml
deploy:
- script: deploy-to-prod
- resource_group: prod
+ script: deploy-to-prod
+ resource_group: prod
```
Example of a problematic pipeline flow **before** using the resource group:
@@ -89,6 +92,56 @@ If you want to prevent deployments for a particular period, for example during a
vacation period when most employees are out, you can set up a [Deploy Freeze](../../user/project/releases/index.md#prevent-unintentional-releases-by-setting-a-deploy-freeze).
During a deploy freeze period, no deployment can be executed. This is helpful to
ensure that deployments do not happen unexpectedly.
+
+## Setting appropriate roles to your project
+
+GitLab supports several different roles that can be assigned to your project members. See
+[Project members permissions](../../user/permissions.md#project-members-permissions)
+for an explanation of these roles and the permissions of each.
+
+<div class="video-fallback">
+ See the video: <a href="https://www.youtube.com/watch?v=Mq3C1KveDc0">How to secure your CD pipelines</a>.
+</div>
+<figure class="video-container">
+ <iframe src="https://www.youtube.com/embed/Mq3C1KveDc0" frameborder="0" allowfullscreen="true"> </iframe>
+</figure>
+
+## Protect production secrets
+
+Production secrets are needed to deploy successfully. For example, when deploying to the cloud,
+cloud providers require these secrets to connect to their services. In the project settings, you can
+define and protect environment variables for these secrets. [Protected variables](../variables/README.md#protect-a-custom-variable)
+are only passed to pipelines running on [protected branches](../../user/project/protected_branches.md)
+or [protected tags](../../user/project/protected_tags.md).
+The other pipelines don't get the protected variable. You can also
+[scope variables to specific environments](../variables/where_variables_can_be_used.md#variables-with-an-environment-scope).
+We recommend that you use protected variables on protected environments to make sure that the
+secrets aren't exposed unintentionally. You can also define production secrets on the
+[runner side](../runners/README.md#prevent-runners-from-revealing-sensitive-information).
+This prevents other maintainers from reading the secrets and makes sure that the runner only runs on
+protected branches.
+
+For more information, see [pipeline security](../pipelines/index.md#pipeline-security-on-protected-branches).
+
+## Separate project for deployments
+
+All project maintainers have access to production secrets. If you need to limit the number of users
+that can deploy to a production environment, you can create a separate project and configure a new
+permission model that isolates the CD permissions from the original project and prevents the
+original project's maintainers from accessing the production secret and CD configuration. You can
+connect the CD project to your development projects by using [multi-project pipelines](../multi_project_pipelines.md).
+
+## Protect `gitlab-ci.yml` from change
+
+A `.gitlab-ci.yml` may contain rules to deploy an application to the production server. This
+deployment usually runs automatically after pushing a merge request. To prevent developers from
+changing the `gitlab-ci.yml`, you can define it in a different repository. The configuration can
+reference a file in another project with a completely different set of permissions (similar to
+[separating a project for deployments](#separate-project-for-deployments)).
+In this scenario, the `gitlab-ci.yml` is publicly accessible, but can only be edited by users with
+appropriate permissions in the other project.
+
+For more information, see [Custom CI configuration path](../pipelines/settings.md#custom-ci-configuration-path).
## Troubleshooting
@@ -99,15 +152,13 @@ If you have multiple jobs for the same environment (including non-deployment job
```yaml
build:service-a:
- environment:
- name: production
-
+ environment:
+ name: production
+
build:service-b:
- environment:
- name: production
+ environment:
+ name: production
```
The [Skip outdated deployment jobs](../pipelines/settings.md#skip-outdated-deployment-jobs) might
not work well with this configuration, and must be disabled.
-
-There is a [plan to introduce a new annotation for environments](https://gitlab.com/gitlab-org/gitlab/-/issues/208655) to address this issue.
diff --git a/doc/ci/examples/semantic-release.md b/doc/ci/examples/semantic-release.md
index 70d29b739b1..037faaf66a2 100644
--- a/doc/ci/examples/semantic-release.md
+++ b/doc/ci/examples/semantic-release.md
@@ -35,7 +35,7 @@ You can also view or fork the complete [example source](https://gitlab.com/gitla
}
```
-1. Update the `files` key with glob pattern(s) that selects all files that should be included in the published module. More information about `files` can be found [in NPM's documentation](https://docs.npmjs.com/cli/v6/configuring-npm/package-json#files).
+1. Update the `files` key with glob pattern(s) that selects all files that should be included in the published module. More information about `files` can be found [in NPM's documentation](https://docs.npmjs.com/cli/v6/configuring-npm/package-json/#files).
1. Add a `.gitignore` file to the project to avoid committing `node_modules`:
diff --git a/doc/ci/pipeline_editor/index.md b/doc/ci/pipeline_editor/index.md
index 2d03ceb264f..61b8e289509 100644
--- a/doc/ci/pipeline_editor/index.md
+++ b/doc/ci/pipeline_editor/index.md
@@ -59,8 +59,9 @@ reflected in the CI lint. It displays the same results as the existing [CI Lint
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241722) in GitLab 13.5.
> - [Moved to **CI/CD > Editor**](https://gitlab.com/gitlab-org/gitlab/-/issues/263141) in GitLab 13.7.
-> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
-> - It's disabled on GitLab.com.
+> - It was [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
+> - [Became enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/290117) in GitLab 13.8.
+> - It's enabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-cicd-configuration-visualization). **(CORE ONLY)**
@@ -82,21 +83,21 @@ each job depends only on the previous stage being completed successfully.
### Enable or disable CI/CD configuration visualization **(CORE ONLY)**
-CI/CD configuration visualization is under development and not ready for production use. It is
-deployed behind a feature flag that is **disabled by default**.
+CI/CD configuration visualization is under development but ready for production use.
+It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
-can enable it.
+can opt to disable it.
-To enable it:
+To disable it:
```ruby
-Feature.enable(:ci_config_visualization_tab)
+Feature.disable(:ci_config_visualization_tab)
```
-To disable it:
+To enable it:
```ruby
-Feature.disable(:ci_config_visualization_tab)
+Feature.enable(:ci_config_visualization_tab)
```
## Commit changes to CI configuration
diff --git a/doc/development/agent/identity.md b/doc/development/agent/identity.md
index 65de1a6f0c8..884ce015a02 100644
--- a/doc/development/agent/identity.md
+++ b/doc/development/agent/identity.md
@@ -37,9 +37,9 @@ has a different configuration. Some may enable features A and B, and some may
enable features B and C. This flexibility enables different groups of people to
use different features of the agent in the same cluster.
-For example, [Priyanka (Platform Engineer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#priyanka-platform-engineer)
+For example, [Priyanka (Platform Engineer)](https://about.gitlab.com/handbook/marketing/strategic-marketing/roles-personas/#priyanka-platform-engineer)
may want to use cluster-wide features of the agent, while
-[Sasha (Software Developer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#sasha-software-developer)
+[Sasha (Software Developer)](https://about.gitlab.com/handbook/marketing/strategic-marketing/roles-personas/#sasha-software-developer)
uses the agent that only has access to a particular namespace.
Each agent is likely running using a
diff --git a/doc/development/packages.md b/doc/development/packages.md
index 689dc6b4141..aadd71c9ffa 100644
--- a/doc/development/packages.md
+++ b/doc/development/packages.md
@@ -242,6 +242,24 @@ create the package record. Workhorse provides a variety of file metadata such as
For testing purposes, you may want to [enable object storage](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/object_storage.md)
in your local development environment.
+#### Rate Limits on GitLab.com
+
+Package manager clients can make rapid requests that exceed the
+[GitLab.com standard API rate limits](../user/gitlab_com/index.md#gitlabcom-specific-rate-limits).
+This results in a `429 Too Many Requests` error.
+
+We have opened a set of paths to allow higher rate limits. Unless it is not possible,
+new package managers should follow these conventions so they can take advantage of the
+expanded package rate limit.
+
+These route prefixes guarantee a higher rate limit:
+
+```plaintext
+/api/v4/packages/
+/api/v4/projects/:project_id/packages/
+/api/v4/groups/:group_id/-/packages/
+```
+
### Future Work
While working on the MVC, contributors might find features that are not mandatory for the MVC but can provide a better user experience. It's generally a good idea to keep an eye on those and open issues.
diff --git a/doc/development/usage_ping.md b/doc/development/usage_ping.md
index cb38fb9802e..10c3de2f0a1 100644
--- a/doc/development/usage_ping.md
+++ b/doc/development/usage_ping.md
@@ -132,8 +132,8 @@ general guidelines around how to collect those, due to the individual nature of
There are several types of counters which are all found in `usage_data.rb`:
- **Ordinary Batch Counters:** Simple count of a given ActiveRecord_Relation
-- **Distinct Batch Counters:** Distinct count of a given ActiveRecord_Relation on given column
-- **Sum Batch Counters:** Sum the values of a given ActiveRecord_Relation on given column
+- **Distinct Batch Counters:** Distinct count of a given ActiveRecord_Relation in a given column
+- **Sum Batch Counters:** Sum the values of a given ActiveRecord_Relation in a given column
- **Alternative Counters:** Used for settings and configurations
- **Redis Counters:** Used for in-memory counts.
@@ -153,7 +153,15 @@ For GitLab.com, there are extremely large tables with 15 second query timeouts,
| `merge_request_diff_files` | 1082 |
| `events` | 514 |
-There are two batch counting methods provided, `Ordinary Batch Counters` and `Distinct Batch Counters`. Batch counting requires indexes on columns to calculate max, min, and range queries. In some cases, a specialized index may need to be added on the columns involved in a counter.
+We have several batch counting methods available:
+
+- `Ordinary Batch Counters`
+- `Distinct Batch Counters`
+- `Sum Batch Counters`
+- `Estimated Batch Counters`
+
+Batch counting requires indexes on columns to calculate max, min, and range queries. In some cases,
+you may need to add a specialized index on the columns involved in a counter.
### Ordinary Batch Counters
@@ -248,6 +256,82 @@ sum(Issue.group(:state_id), :weight))
# returns => {1=>3542, 2=>6820}
```
+### Estimated Batch Counters
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48233) in GitLab 13.7.
+
+Estimated batch counter functionality handles `ActiveRecord::StatementInvalid` errors
+when used through the provided `estimate_batch_distinct_count` method.
+Errors return a value of `-1`.
+
+WARNING:
+This functionality estimates a distinct count of a specific ActiveRecord_Relation in a given column,
+which uses the [HyperLogLog](http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf) algorithm.
+As the HyperLogLog algorithm is probabilistic, the **results always include error**.
+The highest encountered error rate is 4.9%.
+
+When correctly used, the `estimate_batch_distinct_count` method enables efficient counting over
+columns that contain non-unique values, which can not be assured by other counters.
+
+Method: [`estimate_batch_distinct_count(relation, column = nil, batch_size: nil, start: nil, finish: nil)`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/utils/usage_data.rb#L63)
+
+The method includes the following arguments:
+
+- `relation`: The ActiveRecord_Relation to perform the count.
+- `column`: The column to perform the distinct count. The default is the primary key.
+- `batch_size`: The default is 10,000, from `Gitlab::Database::PostgresHll::BatchDistinctCounter::DEFAULT_BATCH_SIZE`.
+- `start`: The custom start of the batch count, to avoid complex minimum calculations.
+- `finish`: The custom end of the batch count in order to avoid complex maximum calculations.
+
+The method includes the following prerequisites:
+
+1. The supplied `relation` must include the primary key defined as the numeric column.
+ For example: `id bigint NOT NULL`.
+1. The `estimate_batch_distinct_count` can handle a joined relation. To use its ability to
+ count non-unique columns, the joined relation **must NOT** have a one-to-many relationship,
+ such as `has_many :boards`.
+1. Both `start` and `finish` arguments should always represent primary key relationship values,
+ even if the estimated count refers to another column, for example:
+
+ ```ruby
+ estimate_batch_distinct_count(::Note, :author_id, start: ::Note.minimum(:id), finish: ::Note.maximum(:id))
+ ```
+
+Examples:
+
+1. Simple execution of estimated batch counter, with only relation provided,
+ returned value represents estimated number of unique values in `id` column
+ (which is the primary key) of `Project` relation:
+
+ ```ruby
+ estimate_batch_distinct_count(::Project)
+ ```
+
+1. Execution of estimated batch counter, where provided relation has applied
+ additional filter (`.where(time_period)`), number of unique values estimated
+ in custom column (`:author_id`), and parameters: `start` and `finish` together
+ apply boundaries that defines range of provided relation to analyze:
+
+ ```ruby
+ estimate_batch_distinct_count(::Note.with_suggestions.where(time_period), :author_id, start: ::Note.minimum(:id), finish: ::Note.maximum(:id))
+ ```
+
+1. Execution of estimated batch counter with joined relation (`joins(:cluster)`),
+ for a custom column (`'clusters.user_id'`):
+
+ ```ruby
+ estimate_batch_distinct_count(::Clusters::Applications::CertManager.where(time_period).available.joins(:cluster), 'clusters.user_id')
+ ```
+
+When instrumenting metric with usage of estimated batch counter please add
+`_estimated` suffix to its name, for example:
+
+```ruby
+ "counts": {
+ "ci_builds_estimated": estimate_batch_distinct_count(Ci::Build),
+ ...
+```
+
### Redis Counters
Handles `::Redis::CommandError` and `Gitlab::UsageDataCounters::BaseCounter::UnknownEvent`
@@ -309,6 +393,10 @@ Examples of implementation:
#### Redis HLL Counters
+WARNING:
+HyperLogLog (HLL) is a probabilistic algorithm and its **results always includes some small error**. According to [Redis documentation](https://redis.io/commands/pfcount), data from
+used HLL implementation is "approximated with a standard error of 0.81%".
+
With `Gitlab::UsageDataCounters::HLLRedisCounter` we have available data structures used to count unique values.
Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PFCOUNT](https://redis.io/commands/pfcount).
@@ -414,7 +502,7 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
end
```
-1. Track event using `track_usage_event(event_name, values) in services and graphql
+1. Track event using `track_usage_event(event_name, values) in services and GraphQL
Increment unique values count using Redis HLL, for given event name.
@@ -422,7 +510,7 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
[Track usage event for incident created in service](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/issues/update_service.rb)
- [Track usage event for incident created in graphql](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/graphql/mutations/alert_management/update_alert_status.rb)
+ [Track usage event for incident created in GraphQL](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/graphql/mutations/alert_management/update_alert_status.rb)
```ruby
track_usage_event(:incident_management_incident_created, current_user.id)
@@ -549,7 +637,7 @@ For each event we add metrics for the weekly and monthly time frames, and totals
- `#{event_name}_weekly`: Data for 7 days for daily [aggregation](#adding-new-events) events and data for the last complete week for weekly [aggregation](#adding-new-events) events.
- `#{event_name}_monthly`: Data for 28 days for daily [aggregation](#adding-new-events) events and data for the last 4 complete weeks for weekly [aggregation](#adding-new-events) events.
-Redis HLL implementation calculates automatic total metrics, if there are more than one metric for the same category, aggregation and Redis slot.
+Redis HLL implementation calculates automatic total metrics, if there are more than one metric for the same category, aggregation and Redis slot.
- `#{category}_total_unique_counts_weekly`: Total unique counts for events in the same category for the last 7 days or the last complete week, if events are in the same Redis slot and we have more than one metric.
- `#{category}_total_unique_counts_monthly`: Total unique counts for events in same category for the last 28 days or the last 4 complete weeks, if events are in the same Redis slot and we have more than one metric.
@@ -732,7 +820,7 @@ On GitLab.com, we have DangerBot setup to monitor Product Intelligence related f
### 10. Verify your metric
-On GitLab.com, the Product Intelligence team regularly monitors Usage Ping. They may alert you that your metrics need further optimization to run quicker and with greater success. You may also use the [Usage Ping QA dashboard](https://app.periscopedata.com/app/gitlab/632033/Usage-Ping-QA) to check how well your metric performs. The dashboard allows filtering by GitLab version, by "Self-managed" & "Saas" and shows you how many failures have occurred for each metric. Whenever you notice a high failure rate, you may re-optimize your metric.
+On GitLab.com, the Product Intelligence team regularly monitors Usage Ping. They may alert you that your metrics need further optimization to run quicker and with greater success. You may also use the [Usage Ping QA dashboard](https://app.periscopedata.com/app/gitlab/632033/Usage-Ping-QA) to check how well your metric performs. The dashboard allows filtering by GitLab version, by "Self-managed" & "SaaS" and shows you how many failures have occurred for each metric. Whenever you notice a high failure rate, you may re-optimize your metric.
### Optional: Test Prometheus based Usage Ping
@@ -783,8 +871,6 @@ appear to be associated to any of the services running, since they all appear to
## Aggregated metrics
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45979) in GitLab 13.6.
-> - It's [deployed behind a feature flag](../user/feature_flags.md), disabled by default.
-> - It's enabled on GitLab.com.
WARNING:
This feature is intended solely for internal GitLab use.
diff --git a/doc/user/group/iterations/index.md b/doc/user/group/iterations/index.md
index a06c7a8f325..65d3129a825 100644
--- a/doc/user/group/iterations/index.md
+++ b/doc/user/group/iterations/index.md
@@ -88,6 +88,22 @@ similar to how they appear when viewing a [milestone](../../project/milestones/i
Burndown charts help track completion progress of total scope, and burnup charts track the daily
total count and weight of issues added to and completed in a given timebox.
+### Group issues by label
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/225500) in GitLab 13.8.
+
+You can group the list of issues by label.
+This can help you view issues that have your team's label,
+and get a more accurate understanding of scope attributable to each label.
+
+To group issues by label:
+
+1. In the **Group by** dropdown, select **Label**.
+1. Select the **Filter by label** dropdown.
+1. Select the labels you want to group by in the labels dropdown.
+ You can also search for labels by typing in the search input.
+1. Click or tap outside of the label dropdown. The page is now grouped by the selected labels.
+
## Disable iterations **(STARTER ONLY)**
GitLab Iterations feature is deployed with a feature flag that is **enabled by default**.
diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md
index fbede6d13b7..5e5aadfae2b 100644
--- a/doc/user/packages/dependency_proxy/index.md
+++ b/doc/user/packages/dependency_proxy/index.md
@@ -141,11 +141,10 @@ You can use the Dependency Proxy to pull your base image.
bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ=
```
- This can also be other credentials such as:
+ This can also be a [personal access token](../../../user/profile/personal_access_tokens.md) such as:
```shell
echo -n "my_username:personal_access_token" | base64
- echo -n "deploy_token_username:deploy_token" | base64
```
1. Create a [custom environment variables](../../../ci/variables/README.md#custom-environment-variables)
diff --git a/doc/user/packages/nuget_repository/index.md b/doc/user/packages/nuget_repository/index.md
index 50837e69e74..35172663cc1 100644
--- a/doc/user/packages/nuget_repository/index.md
+++ b/doc/user/packages/nuget_repository/index.md
@@ -123,7 +123,7 @@ nuget source Add -Name "GitLab" -Source "https://gitlab.example.com/api/v4/proje
To use the [group-level](#use-the-gitlab-endpoint-for-nuget-packages) NuGet endpoint, add the Package Registry as a source with `nuget`:
```shell
-nuget source Add -Name <source_name> -Source "https://gitlab.example.com/api/v4/groups/<your_group_id>/packages/nuget/index.json" -UserName <gitlab_username or deploy_token_username> -Password <gitlab_personal_access_token or deploy_token>
+nuget source Add -Name <source_name> -Source "https://gitlab.example.com/api/v4/groups/<your_group_id>/-/packages/nuget/index.json" -UserName <gitlab_username or deploy_token_username> -Password <gitlab_personal_access_token or deploy_token>
```
- `<source_name>` is the desired source name.
@@ -131,7 +131,7 @@ nuget source Add -Name <source_name> -Source "https://gitlab.example.com/api/v4/
For example:
```shell
-nuget source Add -Name "GitLab" -Source "https://gitlab.example.com/api/v4/groups/23/packages/nuget/index.json" -UserName carol -Password 12345678asdf
+nuget source Add -Name "GitLab" -Source "https://gitlab.example.com/api/v4/groups/23/-/packages/nuget/index.json" -UserName carol -Password 12345678asdf
```
### Add a source with Visual Studio
@@ -173,7 +173,7 @@ To use the [group-level](#use-the-gitlab-endpoint-for-nuget-packages) NuGet endp
1. Select **Add**.
1. Complete the following fields:
- **Name**: Name for the source.
- - **Location**: `https://gitlab.example.com/api/v4/group/<your_group_id>/packages/nuget/index.json`,
+ - **Location**: `https://gitlab.example.com/api/v4/groups/<your_group_id>/-/packages/nuget/index.json`,
where `<your_group_id>` is your group ID, and `gitlab.example.com` is
your domain name.
- **Username**: Your GitLab username or deploy token username.
@@ -227,7 +227,7 @@ To use the [group-level](#use-the-gitlab-endpoint-for-nuget-packages) Package Re
<configuration>
<packageSources>
<clear />
- <add key="gitlab" value="https://gitlab.example.com/api/v4/group/<your_group_id>/packages/nuget/index.json" />
+ <add key="gitlab" value="https://gitlab.example.com/api/v4/groups/<your_group_id>/-/packages/nuget/index.json" />
</packageSources>
<packageSourceCredentials>
<gitlab>
diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md
index ad18e5e78fd..38ef01b7537 100644
--- a/doc/user/profile/notifications.md
+++ b/doc/user/profile/notifications.md
@@ -153,8 +153,7 @@ Users are notified of the following events:
| Project access level changed | User | Sent when user project access level is changed |
| User added to group | User | Sent when user is added to group |
| Group access level changed | User | Sent when user group access level is changed |
-| Personal Access Tokens expiring soon | User | Security email, always sent. |
-<!-- Do not delete or lint this instance of future tense -->
+| Personal Access Tokens expiring soon <!-- Do not delete or lint this instance of future tense --> | User | Security email, always sent. |
| Personal Access Tokens have expired | User | Security email, always sent. |
| Project moved | Project members (1) | (1) not disabled |
| New release | Project members | Custom notification |
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index c01efd9c813..64be3182dab 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -13,6 +13,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> quick actions when updating the description of issues, epics, and merge requests.
> - Introduced in [GitLab 13.8](https://gitlab.com/gitlab-org/gitlab/-/issues/292393): when you enter
> `/` into a description or comment field, all available quick actions are displayed in a scrollable list.
+> - The rebase quick action was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49800) in GitLab 13.8.
Quick actions are textual shortcuts for common actions on issues, epics, merge requests,
and commits that are usually done by clicking buttons or dropdowns in the GitLab UI.
@@ -61,7 +62,7 @@ The following quick actions are applicable to descriptions, discussions and thre
| `/promote` | ✓ | | | Promote issue to epic. **(PREMIUM)** |
| `/publish` | ✓ | | | Publish issue to an associated [Status Page](../../operations/incident_management/status_page.md) ([Introduced in GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30906)) **(ULTIMATE)** |
| `/reassign @user1 @user2` | ✓ | ✓ | | Replace current assignees with those specified. **(STARTER)** |
-| `/rebase` | | ✓ | | Rebase source branch. This will schedule a background task that attempt to rebase the changes in the source branch on the latest commit of the target branch. If `/rebase` is used, `/merge` will be ignored to avoid a race condition where the source branch is merged or deleted before it is rebased. |
+| `/rebase` | | ✓ | | Rebase source branch. This will schedule a background task that attempt to rebase the changes in the source branch on the latest commit of the target branch. If `/rebase` is used, `/merge` will be ignored to avoid a race condition where the source branch is merged or deleted before it is rebased. If there are merge conflicts, GitLab will display a message that a rebase cannot be scheduled. Rebase failures will be displayed with the merge request status. |
| `/reassign_reviewer @user1 @user2` | | ✓ | | Replace current reviewers with those specified. **(STARTER)** |
| `/relabel ~label1 ~label2` | ✓ | ✓ | ✓ | Replace current labels with those specified. |
| `/relate #issue1 #issue2` | ✓ | | | Mark issues as related. **(STARTER)** |
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
index 29b24028e48..af8e78afb28 100644
--- a/doc/user/project/web_ide/index.md
+++ b/doc/user/project/web_ide/index.md
@@ -33,8 +33,8 @@ start seeing results.
## Command palette
You can see all available commands for manipulating editor content by pressing
-the <kbd>F1</kbd> key when the editor is in focus. After that,
-you'll see a complete list of available commands for
+the <kbd>F1</kbd> key when the editor is in focus. After that, the editor displays
+a complete list of available commands for
manipulating editor content. The editor supports commands for multi-cursor
editing, code block folding, commenting, searching and replacing, navigating
editor warnings and suggestions, and more.
@@ -47,7 +47,7 @@ the command without having to select it in the command palette.
## Syntax highlighting
-As expected from an IDE, syntax highlighting for many languages within
+As expected from an IDE, syntax highlighting for many languages in
the Web IDE makes your direct editing even easier.
The Web IDE currently provides:
@@ -78,7 +78,7 @@ All the themes GitLab supports for syntax highlighting are added to the Web IDE'
You can pick a theme from your [profile preferences](../../profile/preferences.md).
The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808) and
-the [solarized dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/219228),
+the [Solarized dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/219228),
which apply to the entire Web IDE screen.
| Solarized Light Theme | Solarized Dark Theme | Dark Theme |
@@ -144,12 +144,13 @@ schemas:
Each schema entry supports two properties:
-- `uri`: please provide an absolute URL for the schema definition file here. The schema from this URL
-is loaded when a matching file is open.
-- `match`: a list of matching paths or glob expressions. If a schema matches a particular path pattern,
-it will be applied to that file. Please enclose the pattern in quotes if it begins with an asterisk (`*`),
-it's be applied to that file. If a pattern begins with an asterisk (`*`), enclose it in quotation
-marks. Otherwise, the configuration file is not valid YAML.
+- `uri`: please provide an absolute URL for the schema definition file here.
+ The schema from this URL is loaded when a matching file is open.
+- `match`: a list of matching paths or glob expressions. If a schema matches a
+ particular path pattern, it is applied to that file. Please enclose the pattern
+ in quotes if it begins with an asterisk (`*`), it's be applied to that file.
+ If a pattern begins with an asterisk (`*`), enclose it in quotation marks.
+ Otherwise, the configuration file is not valid YAML.
## Configure the Web IDE
@@ -180,7 +181,7 @@ The Web IDE currently supports the following `.editorconfig` settings:
After making your changes, click the **Commit** button on the bottom-left to
review the list of changed files.
-Once you have finalized your changes, you can add a commit message, commit the
+After you have finalized your changes, you can add a commit message, commit the
changes and directly create a merge request. In case you don't have write
access to the selected branch, you see a warning, but can still create
a new branch and start a merge request.
@@ -268,7 +269,7 @@ GitLab.com
![Administrator Live Preview setting](img/admin_live_preview_v13_0.png)
-Once you have done that, you can preview projects with a `package.json` file and
+After you have done that, you can preview projects with a `package.json` file and
a `main` entry point inside the Web IDE. An example `package.json` is shown
below.
@@ -325,7 +326,7 @@ In order to enable the Web IDE terminals you need to create the file
file is fairly similar to the [CI configuration file](../../../ci/yaml/README.md)
syntax but with some restrictions:
-- No global blocks can be defined (i.e., `before_script` or `after_script`)
+- No global blocks (such as `before_script` or `after_script`) can be defined.
- Only one job named `terminal` can be added to this file.
- Only the keywords `image`, `services`, `tags`, `before_script`, `script`, and
`variables` are allowed to be used to configure the job.
@@ -350,7 +351,7 @@ terminal:
NODE_ENV: "test"
```
-Once the terminal has started, the console is displayed and we could access
+After the terminal has started, the console is displayed and we could access
the project repository files.
**Important**. The terminal job is branch dependent. This means that the
@@ -364,7 +365,7 @@ If there is no configuration file in a branch, an error message is shown.
If Interactive Terminals are available for the current user, the **Terminal** button is visible in the right sidebar of the Web IDE. Click this button to open
or close the terminal tab.
-Once open, the tab shows the **Start Web Terminal** button. This button may
+After opening, the tab shows the **Start Web Terminal** button. This button may
be disabled if the environment is not configured correctly. If so, a status
message describes the issue. Here are some reasons why **Start Web Terminal**
may be disabled:
@@ -378,7 +379,7 @@ can be closed and reopened and the state of the terminal is not affected.
When the terminal is started and is successfully connected to the runner, then the
runner's shell prompt appears in the terminal. From here, you can enter
-commands executed within the runner's environment. This is similar
+commands executed in the runner's environment. This is similar
to running commands in a local terminal or through SSH.
While the terminal is running, it can be stopped by clicking **Stop Terminal**.
@@ -426,7 +427,7 @@ terminal:
[predefined environment variable](../../../ci/variables/predefined_variables.md)
for GitLab Runners. This is where your project's repository resides.
-Once you have configured the web terminal for file syncing, then when the web
+After you have configured the web terminal for file syncing, then when the web
terminal is started, a **Terminal** status is visible in the status bar.
![Web IDE Client Side Evaluation](img/terminal_status.png)
@@ -434,7 +435,7 @@ terminal is started, a **Terminal** status is visible in the status bar.
Changes made to your files via the Web IDE sync to the running terminal
when:
-- <kbd>Ctrl</kbd> + <kbd>S</kbd> (or <kbd>Cmd</kbd> + <kbd>S</kbd> on Mac)
+- <kbd>Control</kbd> + <kbd>S</kbd> (or <kbd>Command</kbd> + <kbd>S</kbd> on Mac)
is pressed while editing a file.
- Anything outside the file editor is clicked after editing a file.
- A file or folder is created, deleted, or renamed.
@@ -446,7 +447,7 @@ The Web IDE has a few limitations:
- Interactive Terminals is in a beta phase and continues to be improved in upcoming releases. In the meantime, please note that the user is limited to having only one
active terminal at a time.
-- LFS files can be rendered and displayed but they cannot be updated and committed using the Web IDE. If an LFS file is modified and pushed to the repository, the LFS pointer in the repository will be overwritten with the modified LFS file content.
+- LFS files can be rendered and displayed but they cannot be updated and committed using the Web IDE. If an LFS file is modified and pushed to the repository, the LFS pointer in the repository is overwritten with the modified LFS file content.
### Troubleshooting
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index b6be21e9653..d229c27b608 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -18,7 +18,7 @@ The number displayed on their right represents the number of issues and merge re
![issues and MRs dashboard links](img/dashboard_links.png)
-When you click **Issues**, you'll see the opened issues assigned to you straight away:
+When you click **Issues**, the opened issues assigned to you are shown straight away:
![Issues assigned to you](img/issues_assigned_to_you.png)
@@ -29,7 +29,7 @@ You can also filter the results using the search and filter field, as described
### Issues and MRs assigned to you or created by you
-You'll also find shortcuts to issues and merge requests created by you or assigned to you
+GitLab shows shortcuts to issues and merge requests created by you or assigned to you
on the search field on the top-right of your screen:
![shortcut to your issues and merge requests](img/issues_mrs_shortcut.png)
@@ -40,7 +40,7 @@ on the search field on the top-right of your screen:
> - Filtering by child Epics was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9029) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0.
> - Filtering by Iterations was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118742) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.6.
-Follow these steps to filter the **Issues** and **Merge Requests** list pages within projects and
+Follow these steps to filter the **Issues** and **Merge Requests** list pages in projects and
groups:
1. Click in the field **Search or filter results...**.
@@ -74,7 +74,7 @@ Some filter fields like milestone and assignee, allow you to filter by **None**
![filter by none any](img/issues_filter_none_any.png)
-Selecting **None** returns results that have an empty value for that field. E.g.: no milestone, no assignee.
+Selecting **None** returns results that have an empty value for that field. For example: no milestone, no assignee.
Selecting **Any** does the opposite. It returns results that have a non-empty value for that field.
@@ -83,11 +83,11 @@ Selecting **Any** does the opposite. It returns results that have a non-empty va
You can filter issues and merge requests by specific terms included in titles or descriptions.
- Syntax
- - Searches look for all the words in a query, in any order. E.g.: searching
- issues for `display bug` will return all issues matching both those words, in any order.
+ - Searches look for all the words in a query, in any order. For example: searching
+ issues for `display bug` returns all issues matching both those words, in any order.
- To find the exact term, use double quotes: `"display bug"`
- Limitation
- - For performance reasons, terms shorter than 3 chars are ignored. E.g.: searching
+ - For performance reasons, terms shorter than 3 chars are ignored. For example: searching
issues for `included in titles` is same as `included titles`
- Search is limited to 4096 characters and 64 terms per query.
@@ -157,7 +157,7 @@ relevant users or other attributes.
For performance optimization, there is a requirement of a minimum of three
characters to begin your search. For example, if you want to search for
-issues that have the assignee "Simone Presley", you'll need to type at
+issues that have the assignee "Simone Presley", you must type at
least "Sim" before autocomplete gives any relevant results.
## Search history
@@ -170,11 +170,11 @@ You can view recent searches by clicking on the little arrow-clock icon, which i
Individual filters can be removed by clicking on the filter's (x) button or backspacing. The entire search filter can be cleared by clicking on the search box's (x) button or via <kbd>⌘</kbd> (Mac) + <kbd>⌫</kbd>.
-To delete filter tokens one at a time, the <kbd>⌥</kbd> (Mac) / <kbd>Ctrl</kbd> + <kbd>⌫</kbd> keyboard combination can be used.
+To delete filter tokens one at a time, the <kbd>⌥</kbd> (Mac) / <kbd>Control</kbd> + <kbd>⌫</kbd> keyboard combination can be used.
## Filtering with multiple filters of the same type
-Some filters can be added multiple times. These include but are not limited to assignees and labels. When you filter with these multiple filters of the same type, the AND logic is applied. For example, if you were filtering `assignee:@sam assignee:@sarah`, your results will only include entries whereby the assignees are assigned to both Sam and Sarah are returned.
+Some filters can be added multiple times. These include but are not limited to assignees and labels. When you filter with these multiple filters of the same type, the AND logic is applied. For example, if you were filtering `assignee:@sam assignee:@sarah`, your results include only entries whereby the assignees are assigned to both Sam and Sarah are returned.
![multiple assignees filtering](img/multiple_assignees.png)
@@ -190,7 +190,7 @@ author, type, and action. Also, you can sort them by
You can search through your projects from the left menu, by clicking the menu bar, then **Projects**.
On the field **Filter by name**, type the project or group name you want to find, and GitLab
-will filter them for you as you type.
+filters them for you as you type.
You can also look for the projects you [starred](../project/index.md#star-a-project) (**Starred projects**), and **Explore** all
public and internal projects available in GitLab.com, from which you can filter by visibility,
@@ -207,7 +207,7 @@ Similarly to [projects search](#projects), you can search through your groups fr
the left menu, by clicking the menu bar, then **Groups**.
On the field **Filter by name**, type the group name you want to find, and GitLab
-will filter them for you as you type.
+filters them for you as you type.
You can also **Explore** all public and internal groups available in GitLab.com,
and sort them by **Last created**, **Oldest created**, **Last updated**, or **Oldest updated**.
@@ -219,15 +219,15 @@ You can also filter them by name (issue title), from the field **Filter by name*
When you want to search for issues to add to lists present in your Issue Board, click
the button **Add issues** on the top-right of your screen, opening a modal window from which
-you'll be able to, besides filtering them by **Name**, **Author**, **Assignee**, **Milestone**,
+you can, besides filtering them by **Name**, **Author**, **Assignee**, **Milestone**,
and **Labels**, select multiple issues to add to a list of your choice:
![search and select issues to add to board](img/search_issues_board.png)
## Shortcut
-You'll find a shortcut on the search field on the top-right of the project's dashboard to
-quickly access issues and merge requests created or assigned to you within that project:
+GitLab shows a shortcut on the search field on the top-right of the project's dashboard to
+quickly access issues and merge requests created or assigned to you in that project:
![search per project - shortcut](img/project_search.png)
@@ -242,12 +242,12 @@ You can also type in this search bar to see autocomplete suggestions for:
- Recently viewed issues (try and type some word from the title of a recently viewed issue)
- Recently viewed merge requests (try and type some word from the title of a recently viewed merge request)
- Recently viewed epics (try and type some word from the title of a recently viewed epic)
-- [GitLab Flavored Markdown](../markdown.md#special-gitlab-references) (GFM) for issues within a project (try and type a GFM reference for an issue)
+- [GitLab Flavored Markdown](../markdown.md#special-gitlab-references) (GFM) for issues in a project (try and type a GFM reference for an issue)
## Basic search
The Basic search in GitLab is a global search service that allows you to search
-across the entire GitLab instance, within a group, or a single project. Basic search is
+across the entire GitLab instance, in a group, or in a single project. Basic search is
backed by the database and allows searching in:
- Projects
@@ -262,12 +262,12 @@ backed by the database and allows searching in:
- Wiki (Project only)
To start a search, type into the search bar on the top-right of the screen. You can always search
-in all GitLab and may also see the options to search within a group or project if you are in the
+in all GitLab and may also see the options to search in a group or project if you are in the
group or project dashboard.
![basic search](img/basic_search.png)
-Once the results are returned, you can modify the search, select a different type of data to
+After the results are returned, you can modify the search, select a different type of data to
search, or choose a specific group or project.
![basic_search_results](img/basic_search_results.png)
@@ -282,11 +282,11 @@ the search field on the top-right of your screen while the project page is open.
### SHA search
-You can quickly access a commit from within the project dashboard by entering the SHA
-into the search field on the top right of the screen. If a single result is found, you will be
+You can quickly access a commit from the project dashboard by entering the SHA
+into the search field on the top right of the screen. If a single result is found, you are
redirected to the commit result and given the option to return to the search results page.
-![project sha search redirect](img/project_search_sha_redirect.png)
+![project SHA search redirect](img/project_search_sha_redirect.png)
## Advanced Search **(STARTER)**
@@ -314,7 +314,7 @@ This feature might not be available to you. Check the **version history** note a
You can search inside the project’s settings sections by entering a search
term in the search box located at the top of the page. The search results
-will appear highlighted in the sections that match the search term.
+appear highlighted in the sections that match the search term.
![Search project settings](img/project_search_general_settings_v13_8.png)
diff --git a/doc/user/snippets.md b/doc/user/snippets.md
index 5dd93f9e9db..af499221da6 100644
--- a/doc/user/snippets.md
+++ b/doc/user/snippets.md
@@ -69,8 +69,8 @@ new commit to the master branch is recorded. Commit messages are automatically
generated. The snippet's repository has only one branch (master) by default, deleting
it or creating other branches is not supported.
-Existing snippets will be automatically migrated in 13.0. Their current
-content will be saved as the initial commit to the snippets' repository.
+Existing snippets are automatically migrated in 13.0. Their current
+content is saved as the initial commit to the snippets' repository.
### Filenames
@@ -86,10 +86,10 @@ number increases incrementally when more snippets without an attributed
filename are added.
When upgrading from an earlier version of GitLab to 13.0, existing snippets
-without a supported filename will be renamed to a compatible format. For
-example, if the snippet's filename is `http://a-weird-filename.me` it will
-be changed to `http-a-weird-filename-me` to be included in the snippet's
-repository. As snippets are stored by ID, changing their filenames will not break
+without a supported filename are renamed to a compatible format. For
+example, if the snippet's filename is `http://a-weird-filename.me` it is
+changed to `http-a-weird-filename-me` to be included in the snippet's
+repository. As snippets are stored by ID, changing their filenames breaks
direct or embedded links to the snippet.
### Multiple files by Snippet
@@ -105,8 +105,8 @@ to a certain context. For example:
- A snippet with a `docker-compose.yml` file and its associated `.env` file.
- A `gulpfile.js` file coupled with a `package.json` file, which together can be used to bootstrap a project and manage its dependencies.
-Snippets support between 1 and 10 files. They can be managed via Git (since they're [versioned](#versioned-snippets)
-by a Git repository), through the [Snippets API](../api/snippets.md), or within the GitLab UI.
+Snippets support between 1 and 10 files. They can be managed via Git (because they're [versioned](#versioned-snippets)
+by a Git repository), through the [Snippets API](../api/snippets.md), or in the GitLab UI.
![Multi-file Snippet](img/gitlab_snippet_v13_5.png)
@@ -139,7 +139,7 @@ master branch.
### Reduce snippets repository size
-Since versioned Snippets are considered as part of the [namespace storage size](../user/admin_area/settings/account_and_limit_settings.md),
+Because versioned Snippets are considered as part of the [namespace storage size](../user/admin_area/settings/account_and_limit_settings.md),
it's recommended to keep snippets' repositories as compact as possible.
For more information about tools to compact repositories,
@@ -151,7 +151,7 @@ see the documentation on [reducing repository size](../user/project/repository/r
- Creating or deleting branches is not supported. Only a default *master* branch is used.
- Git tags are not supported in snippet repositories.
- Snippets' repositories are limited to 10 files. Attempting to push more
-than 10 files will result in an error.
+than 10 files results in an error.
- Revisions are not *yet* visible to the user on the GitLab UI, but
it's planned to be added in future iterations. See the [revisions tab issue](https://gitlab.com/gitlab-org/gitlab/-/issues/39271)
for updates.
@@ -187,9 +187,9 @@ facilitating the collaboration among users.
You can download the raw content of a snippet.
-By default snippets will be downloaded with Linux-style line endings (`LF`). If
+By default snippets are downloaded with Linux-style line endings (`LF`). If
you want to preserve the original line endings you need to add a parameter `line_ending=raw`
-(e.g., `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a
+(For example: `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a
snippet was created using the GitLab web interface the original line ending is Windows-like (`CRLF`).
## Embedded snippets
@@ -207,7 +207,7 @@ To embed a snippet, first make sure that:
- In **Project > Settings > Permissions**, the snippets permissions are
set to **Everyone with access**
-Once the above conditions are met, the "Embed" section will appear in your
+After the above conditions are met, the "Embed" section appears in your
snippet where you can simply click on the "Copy" button. This copies a one-line
script that you can add to any website or blog post.
diff --git a/lib/api/nuget_group_packages.rb b/lib/api/nuget_group_packages.rb
index 76bfc54f37a..e373f051b24 100644
--- a/lib/api/nuget_group_packages.rb
+++ b/lib/api/nuget_group_packages.rb
@@ -45,7 +45,7 @@ module API
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- namespace ':id/packages/nuget' do
+ namespace ':id/-/packages/nuget' do
after_validation do
# This API can't be accessed anonymously
require_authenticated!
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e8e66359044..0e177b97ddb 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -724,6 +724,9 @@ msgstr ""
msgid "%{reportType} detected %{totalStart}no%{totalEnd} vulnerabilities."
msgstr ""
+msgid "%{requirementCount} requirements have been selected for export. These will be sent to %{email} as an attachment once finished."
+msgstr ""
+
msgid "%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}."
msgstr ""
@@ -1627,6 +1630,9 @@ msgstr ""
msgid "Add a comment to this line"
msgstr ""
+msgid "Add a comment to this line or drag for multiple lines"
+msgstr ""
+
msgid "Add a general comment to this %{noteableDisplayName}."
msgstr ""
@@ -9694,6 +9700,9 @@ msgstr ""
msgid "DeploymentFrequencyCharts|Something went wrong while getting deployment frequency data"
msgstr ""
+msgid "Deployments"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -9877,7 +9886,7 @@ msgstr ""
msgid "DesignManagement|There was an error moving your designs. Please upload your designs below."
msgstr ""
-msgid "DesignManagement|To upload designs, you'll need to enable LFS and have admin enable hashed storage. %{requirements_link_start}More information%{requirements_link_end}"
+msgid "DesignManagement|To upload designs, you'll need to enable LFS and have an admin enable hashed storage. %{requirements_link_start}More information%{requirements_link_end}"
msgstr ""
msgid "DesignManagement|Unresolve thread"
@@ -9928,25 +9937,25 @@ msgstr ""
msgid "DevopsAdoptionSegment|The maximum number of selections has been reached"
msgstr ""
-msgid "DevopsAdoption|%{selectedCount} group selected (20 max)"
+msgid "DevopsAdoption|%{selectedCount} group selected"
msgstr ""
-msgid "DevopsAdoption|%{selectedCount} groups selected (20 max)"
+msgid "DevopsAdoption|%{selectedCount} groups selected"
msgstr ""
-msgid "DevopsAdoption|Add a segment to get started"
+msgid "DevopsAdoption|Add Group"
msgstr ""
-msgid "DevopsAdoption|Add new segment"
+msgid "DevopsAdoption|Add a group to get started"
msgstr ""
msgid "DevopsAdoption|Adopted"
msgstr ""
-msgid "DevopsAdoption|An error occured while deleting the segment. Please try again."
+msgid "DevopsAdoption|An error occured while deleting the group. Please try again."
msgstr ""
-msgid "DevopsAdoption|An error occured while saving the segment. Please try again."
+msgid "DevopsAdoption|An error occured while saving the group. Please try again."
msgstr ""
msgid "DevopsAdoption|Approvals"
@@ -9973,22 +9982,19 @@ msgstr ""
msgid "DevopsAdoption|At least 1 security scan of any type run in pipeline"
msgstr ""
-msgid "DevopsAdoption|Confirm delete segment"
-msgstr ""
-
-msgid "DevopsAdoption|Create new segment"
+msgid "DevopsAdoption|Confirm delete Group"
msgstr ""
-msgid "DevopsAdoption|Delete segment"
+msgid "DevopsAdoption|Delete Group"
msgstr ""
msgid "DevopsAdoption|Deploys"
msgstr ""
-msgid "DevopsAdoption|DevOps adoption uses segments to track adoption across key features. Segments are a way to track multiple related projects and groups at once. For example, you could create a segment for the engineering department or a particular product team."
+msgid "DevopsAdoption|DevOps adoption tracks the use of key features across your favorite groups. Add a group to the table to begin."
msgstr ""
-msgid "DevopsAdoption|Edit segment"
+msgid "DevopsAdoption|Edit Group"
msgstr ""
msgid "DevopsAdoption|Feature adoption is based on usage in the last calendar month. Last updated: %{timestamp}."
@@ -9997,24 +10003,24 @@ msgstr ""
msgid "DevopsAdoption|Filter by name"
msgstr ""
+msgid "DevopsAdoption|Group data pending until the start of next month"
+msgstr ""
+
msgid "DevopsAdoption|Issues"
msgstr ""
msgid "DevopsAdoption|MRs"
msgstr ""
-msgid "DevopsAdoption|Maximum %{maxSegments} segments allowed"
+msgid "DevopsAdoption|Maximum %{maxSegments} groups allowed"
msgstr ""
-msgid "DevopsAdoption|My segment"
+msgid "DevopsAdoption|My group"
msgstr ""
msgid "DevopsAdoption|Name"
msgstr ""
-msgid "DevopsAdoption|New segment"
-msgstr ""
-
msgid "DevopsAdoption|No filter results."
msgstr ""
@@ -10036,18 +10042,12 @@ msgstr ""
msgid "DevopsAdoption|Scanning"
msgstr ""
-msgid "DevopsAdoption|Segment"
-msgstr ""
-
-msgid "DevopsAdoption|Segment data pending until the start of next month"
+msgid "DevopsAdoption|There was an error fetching Group adoption data. Please refresh the page to try again."
msgstr ""
msgid "DevopsAdoption|There was an error fetching Groups. Please refresh the page to try again."
msgstr ""
-msgid "DevopsAdoption|There was an error fetching Segments. Please refresh the page to try again."
-msgstr ""
-
msgid "DevopsReport|Adoption"
msgstr ""
@@ -11799,6 +11799,9 @@ msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export requirements"
+msgstr ""
+
msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
msgstr ""
@@ -12638,6 +12641,12 @@ msgstr ""
msgid "For a faster browsing experience, some files are collapsed by default."
msgstr ""
+msgid "For additional information, review your %{link_to} or contact your group owner."
+msgstr ""
+
+msgid "For additional information, review your group membership: %{link_to} or contact your group owner."
+msgstr ""
+
msgid "For help setting up the Service Desk for your instance, please contact an administrator."
msgstr ""
@@ -13733,6 +13742,12 @@ msgstr ""
msgid "Group members"
msgstr ""
+msgid "Group membership expiration date changed"
+msgstr ""
+
+msgid "Group membership expiration date removed"
+msgstr ""
+
msgid "Group milestone"
msgstr ""
@@ -26356,6 +26371,9 @@ msgstr ""
msgid "Something went wrong while editing your comment. Please try again."
msgstr ""
+msgid "Something went wrong while exporting requirements"
+msgstr ""
+
msgid "Something went wrong while fetching %{listType} list"
msgstr ""
@@ -32639,6 +32657,9 @@ msgstr ""
msgid "YouTube URL or ID"
msgstr ""
+msgid "Your %{group} membership will now expire in %{days}."
+msgstr ""
+
msgid "Your %{host} account was signed in to from a new location"
msgstr ""
@@ -32837,6 +32858,9 @@ msgstr ""
msgid "Your license will be included in your GitLab backup and will survive upgrades, so in normal usage you should never need to re-upload your %{code_open}.gitlab-license%{code_close} file."
msgstr ""
+msgid "Your membership in %{group} no longer expires."
+msgstr ""
+
msgid "Your message here"
msgstr ""
diff --git a/package.json b/package.json
index 230625fc09d..d827e12ae77 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "1.178.0",
"@gitlab/tributejs": "1.0.0",
- "@gitlab/ui": "25.8.0",
+ "@gitlab/ui": "25.11.1",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-4",
"@rails/ujs": "^6.0.3-4",
diff --git a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
index b5d5527bbfe..bfa7be5bb5c 100644
--- a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
+++ b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe 'User uploads new design', :js do
let(:feature_enabled) { false }
it 'shows the message about requirements' do
- expect(page).to have_content("To upload designs, you'll need to enable LFS and have admin enable hashed storage.")
+ expect(page).to have_content("To upload designs, you'll need to enable LFS and have an admin enable hashed storage.")
end
end
diff --git a/spec/fixtures/api/schemas/group_member.json b/spec/fixtures/api/schemas/entities/member.json
index 3425108e46e..e8b40745803 100644
--- a/spec/fixtures/api/schemas/group_member.json
+++ b/spec/fixtures/api/schemas/entities/member.json
@@ -24,16 +24,18 @@
"properties": {
"integer_value": { "type": "integer" },
"string_value": { "type": "string" }
- }
+ },
+ "additionalProperties": false
},
"source": {
"type": "object",
- "required": ["id", "name", "web_url"],
+ "required": ["id", "full_name", "web_url"],
"properties": {
"id": { "type": "integer" },
- "name": { "type": "string" },
+ "full_name": { "type": "string" },
"web_url": { "type": "string" }
- }
+ },
+ "additionalProperties": false
},
"valid_roles": { "type": "object" },
"created_by": {
@@ -42,39 +44,13 @@
"properties": {
"name": { "type": "string" },
"web_url": { "type": "string" }
- }
+ },
+ "additionalProperties": false
},
"user": {
- "type": "object",
- "required": [
- "id",
- "name",
- "username",
- "avatar_url",
- "web_url",
- "blocked",
- "two_factor_enabled"
- ],
- "properties": {
- "id": { "type": "integer" },
- "name": { "type": "string" },
- "username": { "type": "string" },
- "avatar_url": { "type": ["string", "null"] },
- "web_url": { "type": "string" },
- "blocked": { "type": "boolean" },
- "two_factor_enabled": { "type": "boolean" },
- "status": {
- "type": "object",
- "required": [
- "emoji",
- "message_html"
- ],
- "properties": {
- "emoji": { "type": "string" },
- "message_html": { "type": "string" }
- }
- }
- }
+ "allOf": [
+ { "$ref": "member_user.json" }
+ ]
},
"invite": {
"type": "object",
@@ -83,7 +59,8 @@
"email": { "type": "string" },
"avatar_url": { "type": "string" },
"can_resend": { "type": "boolean" }
- }
+ },
+ "additionalProperties": false
}
}
}
diff --git a/spec/fixtures/api/schemas/entities/member_user.json b/spec/fixtures/api/schemas/entities/member_user.json
new file mode 100644
index 00000000000..983cdb7b9d9
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/member_user.json
@@ -0,0 +1,22 @@
+{
+ "type": "object",
+ "required": ["id", "name", "username", "avatar_url", "web_url", "blocked", "two_factor_enabled"],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "avatar_url": { "type": ["string", "null"] },
+ "web_url": { "type": "string" },
+ "blocked": { "type": "boolean" },
+ "two_factor_enabled": { "type": "boolean" },
+ "status": {
+ "type": "object",
+ "required": ["emoji"],
+ "properties": {
+ "emoji": { "type": "string" }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/group_members.json b/spec/fixtures/api/schemas/members.json
index 6268c7ef4d8..0b0b56da9f8 100644
--- a/spec/fixtures/api/schemas/group_members.json
+++ b/spec/fixtures/api/schemas/members.json
@@ -1,6 +1,6 @@
{
"type": "array",
"items": {
- "$ref": "group_member.json"
+ "$ref": "entities/member.json"
}
}
diff --git a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
index 6f28573c808..ee4ec4636ea 100644
--- a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
+++ b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
@@ -41,109 +41,119 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
role="menu"
tabindex="-1"
>
- <!---->
-
- <li
- class="gl-new-dropdown-item"
- role="presentation"
+ <div
+ class="gl-new-dropdown-inner"
>
- <button
- class="dropdown-item"
- role="menuitem"
- type="button"
+ <!---->
+
+ <div
+ class="gl-new-dropdown-contents"
>
- <svg
- aria-hidden="true"
- class="gl-icon s16 gl-new-dropdown-item-check-icon"
- data-testid="mobile-issue-close-icon"
- >
- <use
- href="#mobile-issue-close"
- />
- </svg>
-
- <!---->
-
- <!---->
-
- <div
- class="gl-new-dropdown-item-text-wrapper"
+ <li
+ class="gl-new-dropdown-item"
+ role="presentation"
>
- <p
- class="gl-new-dropdown-item-text-primary"
+ <button
+ class="dropdown-item"
+ role="menuitem"
+ type="button"
>
- <strong>
- Remove integration and resources
- </strong>
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16 gl-new-dropdown-item-check-icon"
+ data-testid="mobile-issue-close-icon"
+ >
+ <use
+ href="#mobile-issue-close"
+ />
+ </svg>
+
+ <!---->
+
+ <!---->
- <div>
- Deletes all GitLab resources attached to this cluster during removal
+ <div
+ class="gl-new-dropdown-item-text-wrapper"
+ >
+ <p
+ class="gl-new-dropdown-item-text-primary"
+ >
+ <strong>
+ Remove integration and resources
+ </strong>
+
+ <div>
+ Deletes all GitLab resources attached to this cluster during removal
+ </div>
+ </p>
+
+ <!---->
</div>
- </p>
-
- <!---->
- </div>
+
+ <!---->
+ </button>
+ </li>
- <!---->
- </button>
- </li>
-
- <li
- class="gl-new-dropdown-divider"
- role="presentation"
- >
- <hr
- aria-orientation="horizontal"
- class="dropdown-divider"
- role="separator"
- />
- </li>
- <li
- class="gl-new-dropdown-item"
- role="presentation"
- >
- <button
- class="dropdown-item"
- role="menuitem"
- type="button"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16 gl-new-dropdown-item-check-icon gl-visibility-hidden"
- data-testid="mobile-issue-close-icon"
+ <li
+ class="gl-new-dropdown-divider"
+ role="presentation"
>
- <use
- href="#mobile-issue-close"
+ <hr
+ aria-orientation="horizontal"
+ class="dropdown-divider"
+ role="separator"
/>
- </svg>
-
- <!---->
-
- <!---->
-
- <div
- class="gl-new-dropdown-item-text-wrapper"
+ </li>
+ <li
+ class="gl-new-dropdown-item"
+ role="presentation"
>
- <p
- class="gl-new-dropdown-item-text-primary"
+ <button
+ class="dropdown-item"
+ role="menuitem"
+ type="button"
>
- <strong>
- Remove integration
- </strong>
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16 gl-new-dropdown-item-check-icon gl-visibility-hidden"
+ data-testid="mobile-issue-close-icon"
+ >
+ <use
+ href="#mobile-issue-close"
+ />
+ </svg>
+
+ <!---->
+
+ <!---->
- <div>
- Removes cluster from project but keeps associated resources
+ <div
+ class="gl-new-dropdown-item-text-wrapper"
+ >
+ <p
+ class="gl-new-dropdown-item-text-primary"
+ >
+ <strong>
+ Remove integration
+ </strong>
+
+ <div>
+ Removes cluster from project but keeps associated resources
+ </div>
+ </p>
+
+ <!---->
</div>
- </p>
-
- <!---->
- </div>
+
+ <!---->
+ </button>
+ </li>
<!---->
- </button>
- </li>
-
- <!---->
+ </div>
+
+ <!---->
+ </div>
</ul>
</div>
diff --git a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
index 8c6b446794f..63afc3f000d 100644
--- a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
+++ b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
@@ -4,6 +4,7 @@ exports[`Design management design version dropdown component renders design vers
<gl-dropdown-stub
category="primary"
headertext=""
+ hideheaderborder="true"
issueiid=""
projectpath=""
size="small"
@@ -44,6 +45,7 @@ exports[`Design management design version dropdown component renders design vers
<gl-dropdown-stub
category="primary"
headertext=""
+ hideheaderborder="true"
issueiid=""
projectpath=""
size="small"
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 0d9d1ad1293..7fbeb33dd93 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -282,14 +282,10 @@ describe('diffs/components/app', () => {
let moveSpy;
let jumpSpy;
- function setup(componentProps, featureFlags) {
- createComponent(
- componentProps,
- ({ state }) => {
- state.diffs.commit = { id: 'SHA123' };
- },
- { glFeatures: featureFlags },
- );
+ function setup(componentProps) {
+ createComponent(componentProps, ({ state }) => {
+ state.diffs.commit = { id: 'SHA123' };
+ });
moveSpy = jest.spyOn(wrapper.vm, 'moveToNeighboringCommit').mockImplementation(() => {});
jumpSpy = jest.spyOn(wrapper.vm, 'jumpToFile').mockImplementation(() => {});
@@ -298,17 +294,17 @@ describe('diffs/components/app', () => {
describe('visible app', () => {
it.each`
- key | name | spy | args | featureFlags
- ${'['} | ${'jumpToFile'} | ${0} | ${[-1]} | ${{}}
- ${'k'} | ${'jumpToFile'} | ${0} | ${[-1]} | ${{}}
- ${']'} | ${'jumpToFile'} | ${0} | ${[+1]} | ${{}}
- ${'j'} | ${'jumpToFile'} | ${0} | ${[+1]} | ${{}}
- ${'x'} | ${'moveToNeighboringCommit'} | ${1} | ${[{ direction: 'previous' }]} | ${{}}
- ${'c'} | ${'moveToNeighboringCommit'} | ${1} | ${[{ direction: 'next' }]} | ${{}}
+ key | name | spy | args
+ ${'['} | ${'jumpToFile'} | ${0} | ${[-1]}
+ ${'k'} | ${'jumpToFile'} | ${0} | ${[-1]}
+ ${']'} | ${'jumpToFile'} | ${0} | ${[+1]}
+ ${'j'} | ${'jumpToFile'} | ${0} | ${[+1]}
+ ${'x'} | ${'moveToNeighboringCommit'} | ${1} | ${[{ direction: 'previous' }]}
+ ${'c'} | ${'moveToNeighboringCommit'} | ${1} | ${[{ direction: 'next' }]}
`(
'calls `$name()` with correct parameters whenever the "$key" key is pressed',
- async ({ key, spy, args, featureFlags }) => {
- setup({ shouldShow: true }, featureFlags);
+ async ({ key, spy, args }) => {
+ setup({ shouldShow: true });
await nextTick();
expect(spies[spy]).not.toHaveBeenCalled();
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index ad515c17e61..f588f65dafd 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -37,15 +37,12 @@ describe('diffs/components/commit_item', () => {
const getPrevCommitNavElement = () =>
getCommitNavButtonsElement().find('.btn-group > *:first-child');
- const mountComponent = (propsData, featureFlags = {}) => {
+ const mountComponent = (propsData) => {
wrapper = mount(Component, {
propsData: {
commit,
...propsData,
},
- provide: {
- glFeatures: featureFlags,
- },
stubs: {
CommitPipelineStatus: true,
},
diff --git a/spec/frontend/diffs/components/diff_line_note_form_spec.js b/spec/frontend/diffs/components/diff_line_note_form_spec.js
index 42815e63ef7..faa68159c58 100644
--- a/spec/frontend/diffs/components/diff_line_note_form_spec.js
+++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js
@@ -11,14 +11,16 @@ describe('DiffLineNoteForm', () => {
let diffLines;
const getDiffFileMock = () => ({ ...diffFileMockData });
- beforeEach(() => {
+ const createComponent = (args = {}) => {
diffFile = getDiffFileMock();
diffLines = diffFile.highlighted_diff_lines;
const store = createStore();
store.state.notes.userData.id = 1;
store.state.notes.noteableData = noteableDataMock;
- wrapper = shallowMount(DiffLineNoteForm, {
+ store.replaceState({ ...store.state, ...args.state });
+
+ return shallowMount(DiffLineNoteForm, {
store,
propsData: {
diffFileHash: diffFile.file_hash,
@@ -27,9 +29,13 @@ describe('DiffLineNoteForm', () => {
noteTargetLine: diffLines[0],
},
});
- });
+ };
describe('methods', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
describe('handleCancelCommentForm', () => {
it('should ask for confirmation when shouldConfirm and isDirty passed as truthy', () => {
jest.spyOn(window, 'confirm').mockReturnValue(false);
@@ -114,14 +120,39 @@ describe('DiffLineNoteForm', () => {
describe('mounted', () => {
it('should init autosave', () => {
const key = 'autosave/Note/Issue/98//DiffNote//1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1';
+ wrapper = createComponent();
expect(wrapper.vm.autosave).toBeDefined();
expect(wrapper.vm.autosave.key).toEqual(key);
});
+
+ it('should set selectedCommentPosition', () => {
+ wrapper = createComponent();
+ let startLineCode = wrapper.vm.commentLineStart.line_code;
+ let lineCode = wrapper.vm.line.line_code;
+
+ expect(startLineCode).toEqual(lineCode);
+ wrapper.destroy();
+
+ const state = {
+ notes: {
+ selectedCommentPosition: {
+ start: {
+ line_code: 'test',
+ },
+ },
+ },
+ };
+ wrapper = createComponent({ state });
+ startLineCode = wrapper.vm.commentLineStart.line_code;
+ lineCode = state.notes.selectedCommentPosition.start.line_code;
+ expect(startLineCode).toEqual(lineCode);
+ });
});
describe('template', () => {
it('should have note form', () => {
+ wrapper = createComponent();
expect(wrapper.find(NoteForm).exists()).toBe(true);
});
});
diff --git a/spec/frontend/diffs/components/diff_row_spec.js b/spec/frontend/diffs/components/diff_row_spec.js
index 0ec075c8ad8..f0d335b88e5 100644
--- a/spec/frontend/diffs/components/diff_row_spec.js
+++ b/spec/frontend/diffs/components/diff_row_spec.js
@@ -1,4 +1,5 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { getByTestId, fireEvent } from '@testing-library/dom';
import Vuex from 'vuex';
import diffsModule from '~/diffs/store/modules';
import DiffRow from '~/diffs/components/diff_row.vue';
@@ -42,16 +43,16 @@ describe('DiffRow', () => {
fileHash: 'abc',
filePath: 'abc',
line: {},
+ index: 0,
...props,
};
- return shallowMount(DiffRow, { propsData, localVue, store });
- };
- it('isHighlighted returns true if isCommented is true', () => {
- const props = { isCommented: true };
- const wrapper = createWrapper({ props });
- expect(wrapper.vm.isHighlighted).toBe(true);
- });
+ const provide = {
+ glFeatures: { dragCommentSelection: true },
+ };
+
+ return shallowMount(DiffRow, { propsData, localVue, store, provide });
+ };
it('isHighlighted returns true given line.left', () => {
const props = {
@@ -124,4 +125,36 @@ describe('DiffRow', () => {
const lineNumber = testLines[0].right.new_line;
expect(wrapper.find(`[data-linenumber="${lineNumber}"]`).exists()).toBe(true);
});
+
+ describe('drag operations', () => {
+ let line;
+
+ beforeEach(() => {
+ line = { ...testLines[0] };
+ });
+
+ it.each`
+ side
+ ${'left'}
+ ${'right'}
+ `('emits `enterdragging` onDragEnter $side side', ({ side }) => {
+ const expectation = { ...line[side], index: 0 };
+ const wrapper = createWrapper({ props: { line } });
+ fireEvent.dragEnter(getByTestId(wrapper.element, `${side}-side`));
+
+ expect(wrapper.emitted().enterdragging).toBeTruthy();
+ expect(wrapper.emitted().enterdragging[0]).toEqual([expectation]);
+ });
+
+ it.each`
+ side
+ ${'left'}
+ ${'right'}
+ `('emits `stopdragging` onDrop $side side', ({ side }) => {
+ const wrapper = createWrapper({ props: { line } });
+ fireEvent.dragEnd(getByTestId(wrapper.element, `${side}-side`));
+
+ expect(wrapper.emitted().stopdragging).toBeTruthy();
+ });
+ });
});
diff --git a/spec/frontend/diffs/components/diff_row_utils_spec.js b/spec/frontend/diffs/components/diff_row_utils_spec.js
index f397c3dc012..d70d6b609ac 100644
--- a/spec/frontend/diffs/components/diff_row_utils_spec.js
+++ b/spec/frontend/diffs/components/diff_row_utils_spec.js
@@ -126,14 +126,14 @@ describe('lineCode', () => {
describe('classNameMapCell', () => {
it.each`
- line | hll | loggedIn | hovered | expectation
- ${undefined} | ${true} | ${true} | ${true} | ${[]}
- ${{ type: 'new' }} | ${false} | ${false} | ${false} | ${['new', { hll: false, 'is-over': false, new_line: true, old_line: false }]}
- ${{ type: 'new' }} | ${true} | ${true} | ${false} | ${['new', { hll: true, 'is-over': false, new_line: true, old_line: false }]}
- ${{ type: 'new' }} | ${true} | ${false} | ${true} | ${['new', { hll: true, 'is-over': false, new_line: true, old_line: false }]}
- ${{ type: 'new' }} | ${true} | ${true} | ${true} | ${['new', { hll: true, 'is-over': true, new_line: true, old_line: false }]}
- `('should return $expectation', ({ line, hll, loggedIn, hovered, expectation }) => {
- const classes = utils.classNameMapCell(line, hll, loggedIn, hovered);
+ line | hll | isLoggedIn | isHover | expectation
+ ${undefined} | ${true} | ${true} | ${true} | ${[]}
+ ${{ type: 'new' }} | ${false} | ${false} | ${false} | ${['new', { hll: false, 'is-over': false, new_line: true, old_line: false }]}
+ ${{ type: 'new' }} | ${true} | ${true} | ${false} | ${['new', { hll: true, 'is-over': false, new_line: true, old_line: false }]}
+ ${{ type: 'new' }} | ${true} | ${false} | ${true} | ${['new', { hll: true, 'is-over': false, new_line: true, old_line: false }]}
+ ${{ type: 'new' }} | ${true} | ${true} | ${true} | ${['new', { hll: true, 'is-over': true, new_line: true, old_line: false }]}
+ `('should return $expectation', ({ line, hll, isLoggedIn, isHover, expectation }) => {
+ const classes = utils.classNameMapCell({ line, hll, isLoggedIn, isHover });
expect(classes).toEqual(expectation);
});
});
diff --git a/spec/frontend/diffs/components/diff_view_spec.js b/spec/frontend/diffs/components/diff_view_spec.js
index 9d1242e4b77..3d36ebf14a3 100644
--- a/spec/frontend/diffs/components/diff_view_spec.js
+++ b/spec/frontend/diffs/components/diff_view_spec.js
@@ -1,19 +1,19 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vue from 'vue';
+import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import DiffView from '~/diffs/components/diff_view.vue';
-// import DraftNote from '~/batch_comments/components/draft_note.vue';
-// import DiffRow from '~/diffs/components/diff_row.vue';
-// import DiffCommentCell from '~/diffs/components/diff_comment_cell.vue';
-// import DiffExpansionCell from '~/diffs/components/diff_expansion_cell.vue';
describe('DiffView', () => {
const DiffExpansionCell = { template: `<div/>` };
const DiffRow = { template: `<div/>` };
const DiffCommentCell = { template: `<div/>` };
const DraftNote = { template: `<div/>` };
+ const showCommentForm = jest.fn();
+ const setSelectedCommentPosition = jest.fn();
+ const getDiffRow = (wrapper) => wrapper.findComponent(DiffRow).vm;
+
const createWrapper = (props) => {
- const localVue = createLocalVue();
- localVue.use(Vuex);
+ Vue.use(Vuex);
const batchComments = {
getters: {
@@ -26,8 +26,13 @@ describe('DiffView', () => {
},
namespaced: true,
};
- const diffs = { getters: { commitId: () => 'abc123' }, namespaced: true };
+ const diffs = {
+ actions: { showCommentForm },
+ getters: { commitId: () => 'abc123' },
+ namespaced: true,
+ };
const notes = {
+ actions: { setSelectedCommentPosition },
state: { selectedCommentPosition: null, selectedCommentPositionHover: null },
};
@@ -41,7 +46,7 @@ describe('DiffView', () => {
...props,
};
const stubs = { DiffExpansionCell, DiffRow, DiffCommentCell, DraftNote };
- return shallowMount(DiffView, { propsData, store, localVue, stubs });
+ return shallowMount(DiffView, { propsData, store, stubs });
};
it('renders a match line', () => {
@@ -74,4 +79,55 @@ describe('DiffView', () => {
});
expect(wrapper.find(DraftNote).exists()).toBe(true);
});
+
+ describe('drag operations', () => {
+ it('sets `dragStart` onStartDragging', () => {
+ const wrapper = createWrapper({ diffLines: [{}] });
+
+ wrapper.findComponent(DiffRow).vm.$emit('startdragging', { test: true });
+ expect(wrapper.vm.dragStart).toEqual({ test: true });
+ });
+
+ it('does not call `setSelectedCommentPosition` on different chunks onDragOver', () => {
+ const wrapper = createWrapper({ diffLines: [{}] });
+ const diffRow = getDiffRow(wrapper);
+
+ diffRow.$emit('startdragging', { chunk: 0 });
+ diffRow.$emit('enterdragging', { chunk: 1 });
+
+ expect(setSelectedCommentPosition).not.toHaveBeenCalled();
+ });
+
+ it.each`
+ start | end | expectation
+ ${1} | ${2} | ${{ start: { index: 1 }, end: { index: 2 } }}
+ ${2} | ${1} | ${{ start: { index: 1 }, end: { index: 2 } }}
+ ${1} | ${1} | ${{ start: { index: 1 }, end: { index: 1 } }}
+ `(
+ 'calls `setSelectedCommentPosition` with correct `updatedLineRange`',
+ ({ start, end, expectation }) => {
+ const wrapper = createWrapper({ diffLines: [{}] });
+ const diffRow = getDiffRow(wrapper);
+
+ diffRow.$emit('startdragging', { chunk: 1, index: start });
+ diffRow.$emit('enterdragging', { chunk: 1, index: end });
+
+ const arg = setSelectedCommentPosition.mock.calls[0][1];
+
+ expect(arg).toMatchObject(expectation);
+ },
+ );
+
+ it('sets `dragStart` to null onStopDragging', () => {
+ const wrapper = createWrapper({ diffLines: [{}] });
+ const diffRow = getDiffRow(wrapper);
+
+ diffRow.$emit('startdragging', { test: true });
+ expect(wrapper.vm.dragStart).toEqual({ test: true });
+
+ diffRow.$emit('stopdragging');
+ expect(wrapper.vm.dragStart).toBeNull();
+ expect(showCommentForm).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 9fef48ccf42..a19e5e91677 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -1159,7 +1159,7 @@ describe('DiffsStoreUtils', () => {
it('converts inline diff lines to parallel diff lines', () => {
const file = getDiffFileMock();
- expect(utils.parallelizeDiffLines(file[INLINE_DIFF_LINES_KEY])).toEqual(
+ expect(utils.parallelizeDiffLines(file[INLINE_DIFF_LINES_KEY])).toMatchObject(
file.parallel_diff_lines,
);
});
@@ -1178,16 +1178,17 @@ describe('DiffsStoreUtils', () => {
{
left: null,
right: {
+ chunk: 0,
type: 'new',
},
},
{
- left: { type: 'conflict_marker_our' },
- right: { type: 'conflict_marker_their' },
+ left: { chunk: 0, type: 'conflict_marker_our' },
+ right: { chunk: 0, type: 'conflict_marker_their' },
},
{
- left: { type: 'conflict_our' },
- right: { type: 'conflict_their' },
+ left: { chunk: 0, type: 'conflict_our' },
+ right: { chunk: 0, type: 'conflict_their' },
},
]);
});
@@ -1196,9 +1197,9 @@ describe('DiffsStoreUtils', () => {
const file = getDiffFileMock();
const files = utils.parallelizeDiffLines(file.highlighted_diff_lines, true);
- expect(files[5].left).toEqual(file.parallel_diff_lines[5].left);
+ expect(files[5].left).toMatchObject(file.parallel_diff_lines[5].left);
expect(files[5].right).toBeNull();
- expect(files[6].left).toEqual(file.parallel_diff_lines[5].right);
+ expect(files[6].left).toMatchObject(file.parallel_diff_lines[5].right);
expect(files[6].right).toBeNull();
});
});
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap
index c3fd4a9bab2..82d7f691efd 100644
--- a/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap
+++ b/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap
@@ -50,6 +50,7 @@ exports[`Alert integration settings form default state should match the default
category="primary"
data-qa-selector="incident_templates_dropdown"
headertext=""
+ hideheaderborder="true"
id="alert-integration-settings-issue-template"
size="medium"
text="selecte_tmpl"
diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
index c40b7c90c72..bea27c8877d 100644
--- a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
+++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
@@ -114,49 +114,59 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
role="menu"
tabindex="-1"
>
- <!---->
-
<div
- class="gl-search-box-by-type"
+ class="gl-new-dropdown-inner"
>
- <svg
- aria-hidden="true"
- class="gl-search-box-by-type-search-icon gl-icon s16"
- data-testid="search-icon"
- >
- <use
- href="#search"
- />
- </svg>
-
- <input
- aria-label="Search"
- class="gl-form-input gl-search-box-by-type-input form-control"
- placeholder="Search"
- type="text"
- />
+ <!---->
<div
- class="gl-search-box-by-type-right-icons"
- >
- <!---->
-
- <!---->
- </div>
- </div>
-
- <li
- class="gl-new-dropdown-text text-secondary"
- role="presentation"
- >
- <p
- class="b-dropdown-text"
+ class="gl-new-dropdown-contents"
>
-
+ <div
+ class="gl-search-box-by-type"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-search-box-by-type-search-icon gl-icon s16"
+ data-testid="search-icon"
+ >
+ <use
+ href="#search"
+ />
+ </svg>
+
+ <input
+ aria-label="Search"
+ class="gl-form-input gl-search-box-by-type-input form-control"
+ placeholder="Search"
+ type="text"
+ />
+
+ <div
+ class="gl-search-box-by-type-right-icons"
+ >
+ <!---->
+
+ <!---->
+ </div>
+ </div>
+
+ <li
+ class="gl-new-dropdown-text text-secondary"
+ role="presentation"
+ >
+ <p
+ class="b-dropdown-text"
+ >
+
No matches found
- </p>
- </li>
+ </p>
+ </li>
+ </div>
+
+ <!---->
+ </div>
</ul>
</div>
@@ -229,49 +239,59 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
role="menu"
tabindex="-1"
>
- <!---->
-
<div
- class="gl-search-box-by-type"
+ class="gl-new-dropdown-inner"
>
- <svg
- aria-hidden="true"
- class="gl-search-box-by-type-search-icon gl-icon s16"
- data-testid="search-icon"
- >
- <use
- href="#search"
- />
- </svg>
-
- <input
- aria-label="Search"
- class="gl-form-input gl-search-box-by-type-input form-control"
- placeholder="Search"
- type="text"
- />
+ <!---->
<div
- class="gl-search-box-by-type-right-icons"
- >
- <!---->
-
- <!---->
- </div>
- </div>
-
- <li
- class="gl-new-dropdown-text text-secondary"
- role="presentation"
- >
- <p
- class="b-dropdown-text"
+ class="gl-new-dropdown-contents"
>
-
+ <div
+ class="gl-search-box-by-type"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-search-box-by-type-search-icon gl-icon s16"
+ data-testid="search-icon"
+ >
+ <use
+ href="#search"
+ />
+ </svg>
+
+ <input
+ aria-label="Search"
+ class="gl-form-input gl-search-box-by-type-input form-control"
+ placeholder="Search"
+ type="text"
+ />
+
+ <div
+ class="gl-search-box-by-type-right-icons"
+ >
+ <!---->
+
+ <!---->
+ </div>
+ </div>
+
+ <li
+ class="gl-new-dropdown-text text-secondary"
+ role="presentation"
+ >
+ <p
+ class="b-dropdown-text"
+ >
+
No matches found
- </p>
- </li>
+ </p>
+ </li>
+ </div>
+
+ <!---->
+ </div>
</ul>
</div>
diff --git a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
index 9a8434a1222..30166e2d5ae 100644
--- a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
@@ -49,7 +49,7 @@ describe('AccessRequestActionButtons', () => {
describe('when member is the current user', () => {
it('sets `message` prop correctly', () => {
expect(findRemoveMemberButton().props('message')).toBe(
- `Are you sure you want to withdraw your access request for "${member.source.name}"`,
+ `Are you sure you want to withdraw your access request for "${member.source.fullName}"`,
);
});
});
@@ -64,7 +64,7 @@ describe('AccessRequestActionButtons', () => {
});
expect(findRemoveMemberButton().props('message')).toBe(
- `Are you sure you want to deny ${member.user.name}'s request to join "${member.source.name}"`,
+ `Are you sure you want to deny ${member.user.name}'s request to join "${member.source.fullName}"`,
);
});
});
diff --git a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
index 887b21dc1d0..fe63f9bfaa7 100644
--- a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
@@ -39,7 +39,7 @@ describe('InviteActionButtons', () => {
it('sets props correctly', () => {
expect(findRemoveMemberButton().props()).toEqual({
memberId: member.id,
- message: `Are you sure you want to revoke the invitation for ${member.invite.email} to join "${member.source.name}"`,
+ message: `Are you sure you want to revoke the invitation for ${member.invite.email} to join "${member.source.fullName}"`,
title: 'Revoke invite',
isAccessRequest: false,
icon: 'remove',
diff --git a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
index b03e80a537d..f28e5040006 100644
--- a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
@@ -39,7 +39,7 @@ describe('UserActionButtons', () => {
it('sets props correctly', () => {
expect(findRemoveMemberButton().props()).toEqual({
memberId: member.id,
- message: `Are you sure you want to remove ${member.user.name} from "${member.source.name}"`,
+ message: `Are you sure you want to remove ${member.user.name} from "${member.source.fullName}"`,
title: 'Remove member',
isAccessRequest: false,
icon: 'remove',
@@ -56,7 +56,7 @@ describe('UserActionButtons', () => {
});
expect(findRemoveMemberButton().props('message')).toBe(
- `Are you sure you want to remove this orphaned member from "${orphanedMember.source.name}"`,
+ `Are you sure you want to remove this orphaned member from "${orphanedMember.source.fullName}"`,
);
});
});
diff --git a/spec/frontend/members/components/modals/leave_modal_spec.js b/spec/frontend/members/components/modals/leave_modal_spec.js
index d7acf12212c..dca47d1f6af 100644
--- a/spec/frontend/members/components/modals/leave_modal_spec.js
+++ b/spec/frontend/members/components/modals/leave_modal_spec.js
@@ -60,11 +60,11 @@ describe('LeaveModal', () => {
});
it('displays modal title', () => {
- expect(getByText(`Leave "${member.source.name}"`).exists()).toBe(true);
+ expect(getByText(`Leave "${member.source.fullName}"`).exists()).toBe(true);
});
it('displays modal body', () => {
- expect(getByText(`Are you sure you want to leave "${member.source.name}"?`).exists()).toBe(
+ expect(getByText(`Are you sure you want to leave "${member.source.fullName}"?`).exists()).toBe(
true,
);
});
diff --git a/spec/frontend/members/components/table/member_source_spec.js b/spec/frontend/members/components/table/member_source_spec.js
index 2580f36cd64..95547090aed 100644
--- a/spec/frontend/members/components/table/member_source_spec.js
+++ b/spec/frontend/members/components/table/member_source_spec.js
@@ -11,7 +11,7 @@ describe('MemberSource', () => {
propsData: {
memberSource: {
id: 102,
- name: 'Foo bar',
+ fullName: 'Foo bar',
webUrl: 'https://gitlab.com/groups/foo-bar',
},
...propsData,
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index 5674929716d..e668f2a1998 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -7,7 +7,7 @@ export const member = {
accessLevel: { integerValue: 50, stringValue: 'Owner' },
source: {
id: 178,
- name: 'Foo Bar',
+ fullName: 'Foo Bar',
webUrl: 'https://gitlab.com/groups/foo-bar',
},
user: {
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index 17720aeb702..e873edaad3b 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -37,6 +37,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
class="flex-grow-1"
data-qa-selector="environments_dropdown"
headertext=""
+ hideheaderborder="true"
id="monitor-environments-dropdown"
menu-class="monitor-environment-dropdown-menu"
size="medium"
diff --git a/spec/frontend/notes/components/multiline_comment_form_spec.js b/spec/frontend/notes/components/multiline_comment_form_spec.js
new file mode 100644
index 00000000000..081fd6e10ef
--- /dev/null
+++ b/spec/frontend/notes/components/multiline_comment_form_spec.js
@@ -0,0 +1,89 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { mount } from '@vue/test-utils';
+import { GlFormSelect } from '@gitlab/ui';
+import MultilineCommentForm from '~/notes/components/multiline_comment_form.vue';
+import notesModule from '~/notes/stores/modules';
+
+describe('MultilineCommentForm', () => {
+ Vue.use(Vuex);
+ const setSelectedCommentPosition = jest.fn();
+ const testLine = {
+ line_code: 'test',
+ type: 'test',
+ old_line: 'test',
+ new_line: 'test',
+ };
+
+ const createWrapper = (props = {}, state) => {
+ setSelectedCommentPosition.mockReset();
+
+ const store = new Vuex.Store({
+ modules: { notes: notesModule() },
+ actions: { setSelectedCommentPosition },
+ });
+ if (state) store.replaceState({ ...store.state, ...state });
+
+ const propsData = {
+ line: { ...testLine },
+ commentLineOptions: [{ text: '1' }],
+ ...props,
+ };
+ return mount(MultilineCommentForm, { propsData, store });
+ };
+
+ describe('created', () => {
+ it('sets commentLineStart to line', () => {
+ const line = { ...testLine };
+ const wrapper = createWrapper({ line });
+
+ expect(wrapper.vm.commentLineStart).toEqual(line);
+ expect(setSelectedCommentPosition).toHaveBeenCalled();
+ });
+
+ it('sets commentLineStart to lineRange', () => {
+ const lineRange = {
+ start: { ...testLine },
+ };
+ const wrapper = createWrapper({ lineRange });
+
+ expect(wrapper.vm.commentLineStart).toEqual(lineRange.start);
+ expect(setSelectedCommentPosition).toHaveBeenCalled();
+ });
+
+ it('sets commentLineStart to selectedCommentPosition', () => {
+ const notes = {
+ selectedCommentPosition: {
+ start: { ...testLine },
+ },
+ };
+ const wrapper = createWrapper({}, { notes });
+
+ expect(wrapper.vm.commentLineStart).toEqual(wrapper.vm.selectedCommentPosition.start);
+ expect(setSelectedCommentPosition).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('destroyed', () => {
+ it('calls setSelectedCommentPosition', () => {
+ const wrapper = createWrapper();
+ wrapper.destroy();
+
+ // Once during created, once during destroyed
+ expect(setSelectedCommentPosition).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ it('handles changing the start line', () => {
+ const line = { ...testLine };
+ const wrapper = createWrapper({ line });
+ const glSelect = wrapper.findComponent(GlFormSelect);
+
+ glSelect.vm.$emit('change', { ...testLine });
+
+ expect(wrapper.vm.commentLineStart).toEqual(line);
+ expect(wrapper.emitted('input')).toBeTruthy();
+ // Once during created, once during updateCommentLineStart
+ expect(setSelectedCommentPosition).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
index 324c9788309..c4c48ea7517 100644
--- a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
+++ b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
@@ -12,6 +12,7 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`]
<gl-dropdown-stub
category="primary"
headertext=""
+ hideheaderborder="true"
size="medium"
text="rspec"
variant="default"
diff --git a/spec/frontend/projects/pipelines/charts/components/app_legacy_spec.js b/spec/frontend/projects/pipelines/charts/components/app_legacy_spec.js
index 99d0e910485..23f37073a0f 100644
--- a/spec/frontend/projects/pipelines/charts/components/app_legacy_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/app_legacy_spec.js
@@ -1,8 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import { GlColumnChart } from '@gitlab/ui/dist/charts';
import Component from '~/projects/pipelines/charts/components/app_legacy.vue';
-import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
-import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
+import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
import {
counts,
timesChartData,
@@ -38,41 +36,17 @@ describe('ProjectsPipelinesChartsApp', () => {
wrapper = null;
});
- describe('overall statistics', () => {
- it('displays the statistics list', () => {
- const list = wrapper.find(StatisticsList);
-
- expect(list.exists()).toBeTruthy();
- expect(list.props('counts')).toBe(counts);
- });
-
- it('displays the commit duration chart', () => {
- const chart = wrapper.find(GlColumnChart);
-
- expect(chart.exists()).toBeTruthy();
- expect(chart.props('yAxisTitle')).toBe('Minutes');
- expect(chart.props('xAxisTitle')).toBe('Commit');
- expect(chart.props('bars')).toBe(wrapper.vm.timesChartTransformedData);
- expect(chart.props('option')).toBe(wrapper.vm.$options.timesChartOptions);
- });
- });
-
describe('pipelines charts', () => {
- it('displays 3 area charts', () => {
- expect(wrapper.findAll(CiCdAnalyticsAreaChart).length).toBe(3);
- });
-
- describe('displays individual correctly', () => {
- it('renders with the correct data', () => {
- const charts = wrapper.findAll(CiCdAnalyticsAreaChart);
+ it('displays the pipeline charts', () => {
+ const chart = wrapper.find(PipelineCharts);
- for (let i = 0; i < charts.length; i += 1) {
- const chart = charts.at(i);
-
- expect(chart.exists()).toBeTruthy();
- expect(chart.props('chartData')).toBe(wrapper.vm.areaCharts[i].data);
- expect(chart.text()).toBe(wrapper.vm.areaCharts[i].title);
- }
+ expect(chart.exists()).toBe(true);
+ expect(chart.props()).toMatchObject({
+ counts,
+ lastWeek: lastWeekChartData,
+ lastMonth: lastMonthChartData,
+ lastYear: lastYearChartData,
+ timesChart: timesChartData,
});
});
});
diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js
index 1313a549f02..e1f84d7bf01 100644
--- a/spec/frontend/projects/pipelines/charts/components/app_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js
@@ -1,11 +1,10 @@
import { merge } from 'lodash';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
+import { GlTabs, GlTab } from '@gitlab/ui';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
-import { GlColumnChart } from '@gitlab/ui/dist/charts';
import Component from '~/projects/pipelines/charts/components/app.vue';
-import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
-import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
+import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
import getPipelineCountByStatus from '~/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql';
import getProjectPipelineStatistics from '~/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql';
import { mockPipelineCount, mockPipelineStatistics } from '../mock_data';
@@ -58,60 +57,62 @@ describe('ProjectsPipelinesChartsApp', () => {
wrapper = null;
});
- describe('overall statistics', () => {
- it('displays the statistics list', () => {
- const list = wrapper.find(StatisticsList);
-
- expect(list.exists()).toBe(true);
- expect(list.props('counts')).toMatchObject({
- failed: 1,
- success: 23,
- total: 34,
- successRatio: 95.83333333333334,
- });
- });
-
- it('displays the commit duration chart', () => {
- const chart = wrapper.find(GlColumnChart);
-
- expect(chart.exists()).toBe(true);
- expect(chart.props('yAxisTitle')).toBe('Minutes');
- expect(chart.props('xAxisTitle')).toBe('Commit');
- expect(chart.props('bars')).toBe(wrapper.vm.timesChartTransformedData);
- expect(chart.props('option')).toBe(wrapper.vm.$options.timesChartOptions);
- });
- });
-
describe('pipelines charts', () => {
- it('displays 3 area charts', () => {
- expect(wrapper.findAll(CiCdAnalyticsAreaChart)).toHaveLength(3);
- });
-
- describe('displays individual correctly', () => {
- it('renders with the correct data', () => {
- const charts = wrapper.findAll(CiCdAnalyticsAreaChart);
+ it('displays the pipeline charts', () => {
+ const chart = wrapper.find(PipelineCharts);
+ const analytics = mockPipelineStatistics.data.project.pipelineAnalytics;
- for (let i = 0; i < charts.length; i += 1) {
- const chart = charts.at(i);
+ const {
+ totalPipelines: total,
+ successfulPipelines: success,
+ failedPipelines: failed,
+ } = mockPipelineCount.data.project;
- expect(chart.exists()).toBe(true);
- // TODO: Refactor this to use the mocked data instead of the vm data
- // https://gitlab.com/gitlab-org/gitlab/-/issues/292085
- expect(chart.props('chartData')).toBe(wrapper.vm.areaCharts[i].data);
- expect(chart.text()).toBe(wrapper.vm.areaCharts[i].title);
- }
+ expect(chart.exists()).toBe(true);
+ expect(chart.props()).toMatchObject({
+ counts: {
+ failed: failed.count,
+ success: success.count,
+ total: total.count,
+ successRatio: (success.count / (success.count + failed.count)) * 100,
+ },
+ lastWeek: {
+ labels: analytics.weekPipelinesLabels,
+ totals: analytics.weekPipelinesTotals,
+ success: analytics.weekPipelinesSuccessful,
+ },
+ lastMonth: {
+ labels: analytics.monthPipelinesLabels,
+ totals: analytics.monthPipelinesTotals,
+ success: analytics.monthPipelinesSuccessful,
+ },
+ lastYear: {
+ labels: analytics.yearPipelinesLabels,
+ totals: analytics.yearPipelinesTotals,
+ success: analytics.yearPipelinesSuccessful,
+ },
+ timesChart: {
+ labels: analytics.pipelineTimesLabels,
+ values: analytics.pipelineTimesValues,
+ },
});
});
});
const findDeploymentFrequencyCharts = () => wrapper.find(DeploymentFrequencyChartsStub);
+ const findGlTabs = () => wrapper.find(GlTabs);
+ const findAllGlTab = () => wrapper.findAll(GlTab);
+ const findGlTabAt = (i) => findAllGlTab().at(i);
describe('when shouldRenderDeploymentFrequencyCharts is true', () => {
beforeEach(() => {
createComponent({ provide: { shouldRenderDeploymentFrequencyCharts: true } });
});
- it('renders the deployment frequency charts', () => {
+ it('renders the deployment frequency charts in a tab', () => {
+ expect(findGlTabs().exists()).toBe(true);
+ expect(findGlTabAt(0).attributes('title')).toBe('Pipelines');
+ expect(findGlTabAt(1).attributes('title')).toBe('Deployments');
expect(findDeploymentFrequencyCharts().exists()).toBe(true);
});
});
@@ -121,7 +122,8 @@ describe('ProjectsPipelinesChartsApp', () => {
createComponent({ provide: { shouldRenderDeploymentFrequencyCharts: false } });
});
- it('does not render the deployment frequency charts', () => {
+ it('does not render the deployment frequency charts in a tab', () => {
+ expect(findGlTabs().exists()).toBe(false);
expect(findDeploymentFrequencyCharts().exists()).toBe(false);
});
});
diff --git a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
new file mode 100644
index 00000000000..598055d5828
--- /dev/null
+++ b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
@@ -0,0 +1,78 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlColumnChart } from '@gitlab/ui/dist/charts';
+import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
+import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
+import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
+import {
+ counts,
+ timesChartData as timesChart,
+ areaChartData as lastWeek,
+ areaChartData as lastMonth,
+ lastYearChartData as lastYear,
+} from '../mock_data';
+
+describe('ProjectsPipelinesChartsApp', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallowMount(PipelineCharts, {
+ propsData: {
+ counts,
+ timesChart,
+ lastWeek,
+ lastMonth,
+ lastYear,
+ },
+ provide: {
+ projectPath: 'test/project',
+ shouldRenderDeploymentFrequencyCharts: true,
+ },
+ stubs: {
+ DeploymentFrequencyCharts: true,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('overall statistics', () => {
+ it('displays the statistics list', () => {
+ const list = wrapper.find(StatisticsList);
+
+ expect(list.exists()).toBe(true);
+ expect(list.props('counts')).toBe(counts);
+ });
+
+ it('displays the commit duration chart', () => {
+ const chart = wrapper.find(GlColumnChart);
+
+ expect(chart.exists()).toBeTruthy();
+ expect(chart.props('yAxisTitle')).toBe('Minutes');
+ expect(chart.props('xAxisTitle')).toBe('Commit');
+ expect(chart.props('bars')).toBe(wrapper.vm.timesChartTransformedData);
+ expect(chart.props('option')).toBe(wrapper.vm.$options.timesChartOptions);
+ });
+ });
+
+ describe('pipelines charts', () => {
+ it('displays 3 area charts', () => {
+ expect(wrapper.findAll(CiCdAnalyticsAreaChart)).toHaveLength(3);
+ });
+
+ describe('displays individual correctly', () => {
+ it('renders with the correct data', () => {
+ const charts = wrapper.findAll(CiCdAnalyticsAreaChart);
+ for (let i = 0; i < charts.length; i += 1) {
+ const chart = charts.at(i);
+
+ expect(chart.exists()).toBeTruthy();
+ expect(chart.props('chartData')).toBe(wrapper.vm.areaCharts[i].data);
+ expect(chart.text()).toBe(wrapper.vm.areaCharts[i].title);
+ }
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
index 63d38e7587a..1bf757ea312 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
@@ -4,6 +4,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<gl-dropdown-stub
category="primary"
headertext=""
+ hideheaderborder="true"
right="true"
size="medium"
text="Clone"
diff --git a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
index dd88ba9a6fb..c4f351eb58d 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
@@ -4,6 +4,7 @@ exports[`SplitButton renders actionItems 1`] = `
<gl-dropdown-stub
category="primary"
headertext=""
+ hideheaderborder="true"
menu-class=""
size="medium"
split="true"
diff --git a/spec/frontend_integration/ide/ide_integration_spec.js b/spec/frontend_integration/ide/ide_integration_spec.js
index 3dcf3a2cc15..00a73661d14 100644
--- a/spec/frontend_integration/ide/ide_integration_spec.js
+++ b/spec/frontend_integration/ide/ide_integration_spec.js
@@ -1,5 +1,6 @@
import { waitForText } from 'helpers/wait_for_text';
import waitForPromises from 'helpers/wait_for_promises';
+import { setTestTimeout } from 'helpers/timeout';
import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
import { createCommitId } from 'test_helpers/factories/commit_id';
import * as ideHelper from './helpers/ide_helper';
@@ -12,6 +13,9 @@ describe('WebIDE', () => {
let container;
beforeEach(() => {
+ // For some reason these tests were timing out in CI.
+ // We will investigate in https://gitlab.com/gitlab-org/gitlab/-/issues/298714
+ setTestTimeout(20000);
setFixtures('<div class="webide-container"></div>');
container = document.querySelector('.webide-container');
});
diff --git a/spec/helpers/groups/group_members_helper_spec.rb b/spec/helpers/groups/group_members_helper_spec.rb
index 222cca43860..d75124b6da7 100644
--- a/spec/helpers/groups/group_members_helper_spec.rb
+++ b/spec/helpers/groups/group_members_helper_spec.rb
@@ -34,38 +34,38 @@ RSpec.describe Groups::GroupMembersHelper do
end
describe '#members_data_json' do
- shared_examples 'group_members.json' do
+ shared_examples 'members.json' do
it 'matches json schema' do
json = helper.members_data_json(group, present_members([group_member]))
- expect(json).to match_schema('group_members')
+ expect(json).to match_schema('members')
end
end
context 'for a group member' do
let(:group_member) { create(:group_member, group: group, created_by: current_user) }
- it_behaves_like 'group_members.json'
+ it_behaves_like 'members.json'
context 'with user status set' do
let(:user) { create(:user) }
let!(:status) { create(:user_status, user: user) }
let(:group_member) { create(:group_member, group: group, user: user, created_by: current_user) }
- it_behaves_like 'group_members.json'
+ it_behaves_like 'members.json'
end
end
context 'for an invited group member' do
let(:group_member) { create(:group_member, :invited, group: group, created_by: current_user) }
- it_behaves_like 'group_members.json'
+ it_behaves_like 'members.json'
end
context 'for an access request' do
let(:group_member) { create(:group_member, :access_request, group: group, created_by: current_user) }
- it_behaves_like 'group_members.json'
+ it_behaves_like 'members.json'
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index ab45f9472a3..53ce200eed5 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -1615,6 +1615,88 @@ RSpec.describe Notify do
is_expected.to have_body_text group_member.invite_email
end
end
+
+ describe 'group expiration date updated' do
+ let_it_be(:group_member) { create(:group_member, group: group, expires_at: 1.day.from_now) }
+
+ context 'when expiration date is changed' do
+ subject { described_class.member_expiration_date_updated_email('group', group_member.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+
+ context 'when expiration date is one day away' do
+ it 'contains all the useful information' do
+ is_expected.to have_subject 'Group membership expiration date changed'
+ is_expected.to have_body_text group_member.user.name
+ is_expected.to have_body_text group.name
+ is_expected.to have_body_text group.web_url
+ is_expected.to have_body_text group_group_members_url(group, search: group_member.user.username)
+ is_expected.to have_body_text 'day.'
+ is_expected.not_to have_body_text 'days.'
+ end
+ end
+
+ context 'when expiration date is more than one day away' do
+ before do
+ group_member.update!(expires_at: 20.days.from_now)
+ end
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject 'Group membership expiration date changed'
+ is_expected.to have_body_text group_member.user.name
+ is_expected.to have_body_text group.name
+ is_expected.to have_body_text group.web_url
+ is_expected.to have_body_text group_group_members_url(group, search: group_member.user.username)
+ is_expected.to have_body_text 'days.'
+ is_expected.not_to have_body_text 'day.'
+ end
+ end
+
+ context 'when a group member is newly given an expiration date' do
+ let_it_be(:group_member) { create(:group_member, group: group) }
+
+ before do
+ group_member.update!(expires_at: 5.days.from_now)
+ end
+
+ subject { described_class.member_expiration_date_updated_email('group', group_member.id) }
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject 'Group membership expiration date changed'
+ is_expected.to have_body_text group_member.user.name
+ is_expected.to have_body_text group.name
+ is_expected.to have_body_text group.web_url
+ is_expected.to have_body_text group_group_members_url(group, search: group_member.user.username)
+ is_expected.to have_body_text 'days.'
+ is_expected.not_to have_body_text 'day.'
+ end
+ end
+ end
+
+ context 'when expiration date is removed' do
+ before do
+ group_member.update!(expires_at: nil)
+ end
+
+ subject { described_class.member_expiration_date_updated_email('group', group_member.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject 'Group membership expiration date removed'
+ is_expected.to have_body_text group_member.user.name
+ is_expected.to have_body_text group.name
+ end
+ end
+ end
end
describe 'confirmation if email changed' do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index e17ea1f5e59..532f68c2f18 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -519,6 +519,9 @@ RSpec.describe CommitStatus do
subject { commit_status.group_name }
where(:name, :group_name) do
+ 'rspec1' | 'rspec1'
+ 'rspec1 0 1' | 'rspec1'
+ 'rspec1 0/2' | 'rspec1'
'rspec:windows' | 'rspec:windows'
'rspec:windows 0' | 'rspec:windows 0'
'rspec:windows 0 test' | 'rspec:windows 0 test'
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 2b24e2d6455..3d3ed6fc54a 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -123,4 +123,16 @@ RSpec.describe GroupMember do
end
end
end
+
+ context 'when group member expiration date is updated' do
+ let_it_be(:group_member) { create(:group_member) }
+
+ it 'emails the user that their group membership expiry has changed' do
+ expect_next_instance_of(NotificationService) do |notification|
+ allow(notification).to receive(:updated_group_member_expiration).with(group_member)
+ end
+
+ group_member.update!(expires_at: 5.days.from_now)
+ end
+ end
end
diff --git a/spec/requests/api/nuget_group_packages_spec.rb b/spec/requests/api/nuget_group_packages_spec.rb
index aeda51ad315..f7e81494660 100644
--- a/spec/requests/api/nuget_group_packages_spec.rb
+++ b/spec/requests/api/nuget_group_packages_spec.rb
@@ -15,27 +15,27 @@ RSpec.describe API::NugetGroupPackages do
let(:target_type) { 'groups' }
shared_examples 'handling all endpoints' do
- describe 'GET /api/v4/groups/:id/packages/nuget' do
+ describe 'GET /api/v4/groups/:id/-/packages/nuget' do
it_behaves_like 'handling nuget service requests', anonymous_requests_example_name: 'rejects nuget packages access', anonymous_requests_status: :unauthorized do
- let(:url) { "/groups/#{target.id}/packages/nuget/index.json" }
+ let(:url) { "/groups/#{target.id}/-/packages/nuget/index.json" }
end
end
- describe 'GET /api/v4/groups/:id/packages/nuget/metadata/*package_name/index' do
+ describe 'GET /api/v4/groups/:id/-/packages/nuget/metadata/*package_name/index' do
it_behaves_like 'handling nuget metadata requests with package name', anonymous_requests_example_name: 'rejects nuget packages access', anonymous_requests_status: :unauthorized do
- let(:url) { "/groups/#{target.id}/packages/nuget/metadata/#{package_name}/index.json" }
+ let(:url) { "/groups/#{target.id}/-/packages/nuget/metadata/#{package_name}/index.json" }
end
end
- describe 'GET /api/v4/groups/:id/packages/nuget/metadata/*package_name/*package_version' do
+ describe 'GET /api/v4/groups/:id/-/packages/nuget/metadata/*package_name/*package_version' do
it_behaves_like 'handling nuget metadata requests with package name and package version', anonymous_requests_example_name: 'rejects nuget packages access', anonymous_requests_status: :unauthorized do
- let(:url) { "/groups/#{target.id}/packages/nuget/metadata/#{package_name}/#{package.version}.json" }
+ let(:url) { "/groups/#{target.id}/-/packages/nuget/metadata/#{package_name}/#{package.version}.json" }
end
end
- describe 'GET /api/v4/groups/:id/packages/nuget/query' do
+ describe 'GET /api/v4/groups/:id/-/packages/nuget/query' do
it_behaves_like 'handling nuget search requests', anonymous_requests_example_name: 'rejects nuget packages access', anonymous_requests_status: :unauthorized do
- let(:url) { "/groups/#{target.id}/packages/nuget/query?#{query_parameters.to_query}" }
+ let(:url) { "/groups/#{target.id}/-/packages/nuget/query?#{query_parameters.to_query}" }
end
end
end
@@ -94,21 +94,21 @@ RSpec.describe API::NugetGroupPackages do
end
end
- describe 'GET /api/v4/groups/:id/packages/nuget/metadata/*package_name/index' do
+ describe 'GET /api/v4/groups/:id/-/packages/nuget/metadata/*package_name/index' do
it_behaves_like 'handling mixed visibilities' do
- let(:url) { "/groups/#{target.id}/packages/nuget/metadata/#{package_name}/index.json" }
+ let(:url) { "/groups/#{target.id}/-/packages/nuget/metadata/#{package_name}/index.json" }
end
end
- describe 'GET /api/v4/groups/:id/packages/nuget/metadata/*package_name/*package_version' do
+ describe 'GET /api/v4/groups/:id/-/packages/nuget/metadata/*package_name/*package_version' do
it_behaves_like 'handling mixed visibilities' do
- let(:url) { "/groups/#{target.id}/packages/nuget/metadata/#{package_name}/#{packages.first.version}.json" }
+ let(:url) { "/groups/#{target.id}/-/packages/nuget/metadata/#{package_name}/#{packages.first.version}.json" }
end
end
- describe 'GET /api/v4/groups/:id/packages/nuget/query' do
+ describe 'GET /api/v4/groups/:id/-/packages/nuget/query' do
it_behaves_like 'handling mixed visibilities' do
- let(:url) { "/groups/#{target.id}/packages/nuget/query?#{query_parameters.to_query}" }
+ let(:url) { "/groups/#{target.id}/-/packages/nuget/query?#{query_parameters.to_query}" }
end
end
end
diff --git a/spec/serializers/diffs_metadata_entity_spec.rb b/spec/serializers/diffs_metadata_entity_spec.rb
index e8cbc2076d7..f6993d4652e 100644
--- a/spec/serializers/diffs_metadata_entity_spec.rb
+++ b/spec/serializers/diffs_metadata_entity_spec.rb
@@ -31,6 +31,7 @@ RSpec.describe DiffsMetadataEntity do
:merge_request_diffs, :context_commits,
:definition_path_prefix, :source_branch_exists,
:can_merge, :conflict_resolution_path, :has_conflicts,
+ :project_name, :project_path, :user_full_name, :username,
# Attributes
:diff_files
)
diff --git a/spec/serializers/member_entity_spec.rb b/spec/serializers/member_entity_spec.rb
new file mode 100644
index 00000000000..f34434188c1
--- /dev/null
+++ b/spec/serializers/member_entity_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MemberEntity do
+ let_it_be(:current_user) { create(:user) }
+ let(:entity) { described_class.new(member, { current_user: current_user, group: group }) }
+ let(:entity_hash) { entity.as_json }
+
+ shared_examples 'member.json' do
+ it 'matches json schema' do
+ expect(entity.to_json).to match_schema('entities/member')
+ end
+
+ it 'correctly exposes `can_update`' do
+ allow(member).to receive(:can_update?).and_return(true)
+
+ expect(entity_hash[:can_update]).to be(true)
+ end
+
+ it 'correctly exposes `can_remove`' do
+ allow(member).to receive(:can_remove?).and_return(true)
+
+ expect(entity_hash[:can_remove]).to be(true)
+ end
+ end
+
+ shared_examples 'invite' do
+ it 'correctly exposes `invite.avatar_url`' do
+ avatar_url = 'https://www.gravatar.com/avatar/c4637cb869d5f94c3193bde4f23d4cdc?s=80&d=identicon'
+ allow(entity).to receive(:avatar_icon_for_email).with(member.invite_email, Member::AVATAR_SIZE).and_return(avatar_url)
+
+ expect(entity_hash[:invite][:avatar_url]).to match(avatar_url)
+ end
+
+ it 'correctly exposes `invite.can_resend`' do
+ allow(member).to receive(:can_resend_invite?).and_return(true)
+
+ expect(entity_hash[:invite][:can_resend]).to be(true)
+ end
+ end
+
+ context 'group member' do
+ let(:group) { create(:group) }
+ let(:member) { GroupMemberPresenter.new(create(:group_member, group: group), current_user: current_user) }
+
+ it_behaves_like 'member.json'
+
+ context 'invite' do
+ let(:member) { GroupMemberPresenter.new(create(:group_member, :invited, group: group), current_user: current_user) }
+
+ it_behaves_like 'member.json'
+ it_behaves_like 'invite'
+ end
+ end
+
+ context 'project member' do
+ let(:project) { create(:project) }
+ let(:group) { project.group }
+ let(:member) { ProjectMemberPresenter.new(create(:project_member, project: project), current_user: current_user) }
+
+ it_behaves_like 'member.json'
+
+ context 'invite' do
+ let(:member) { ProjectMemberPresenter.new(create(:project_member, :invited, project: project), current_user: current_user) }
+
+ it_behaves_like 'member.json'
+ it_behaves_like 'invite'
+ end
+ end
+end
diff --git a/spec/serializers/member_serializer_spec.rb b/spec/serializers/member_serializer_spec.rb
new file mode 100644
index 00000000000..d3ec45fe9c4
--- /dev/null
+++ b/spec/serializers/member_serializer_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MemberSerializer do
+ include MembersPresentation
+
+ let_it_be(:current_user) { create(:user) }
+
+ subject { described_class.new.represent(members, { current_user: current_user, group: group }) }
+
+ shared_examples 'members.json' do
+ it 'matches json schema' do
+ expect(subject.to_json).to match_schema('members')
+ end
+ end
+
+ context 'group member' do
+ let(:group) { create(:group) }
+ let(:members) { present_members(create_list(:group_member, 1, group: group)) }
+
+ it_behaves_like 'members.json'
+ end
+
+ context 'project member' do
+ let(:project) { create(:project) }
+ let(:group) { project.group }
+ let(:members) { present_members(create_list(:project_member, 1, project: project)) }
+
+ it_behaves_like 'members.json'
+ end
+end
diff --git a/spec/serializers/member_user_entity_spec.rb b/spec/serializers/member_user_entity_spec.rb
new file mode 100644
index 00000000000..1c000c06bb6
--- /dev/null
+++ b/spec/serializers/member_user_entity_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MemberUserEntity do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:emoji) { 'slight_smile' }
+ let_it_be(:user_status) { create(:user_status, user: user, emoji: emoji) }
+ let(:entity) { described_class.new(user) }
+ let(:entity_hash) { entity.as_json }
+
+ it 'matches json schema' do
+ expect(entity.to_json).to match_schema('entities/member_user')
+ end
+
+ it 'correctly exposes `avatar_url`' do
+ avatar_url = 'https://www.gravatar.com/avatar/c4637cb869d5f94c3193bde4f23d4cdc?s=80&d=identicon'
+ allow(user).to receive(:avatar_url).and_return(avatar_url)
+
+ expect(entity_hash[:avatar_url]).to match(avatar_url)
+ end
+
+ it 'correctly exposes `blocked`' do
+ allow(user).to receive(:blocked?).and_return(true)
+
+ expect(entity_hash[:blocked]).to be(true)
+ end
+
+ it 'correctly exposes `two_factor_enabled`' do
+ allow(user).to receive(:two_factor_enabled?).and_return(true)
+
+ expect(entity_hash[:two_factor_enabled]).to be(true)
+ end
+
+ it 'correctly exposes `status.emoji`' do
+ expect(entity_hash[:status][:emoji]).to match(emoji)
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 9431c023850..85234077b1f 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -2455,6 +2455,18 @@ RSpec.describe NotificationService, :mailer do
let(:notification_trigger) { group.add_guest(added_user) }
end
end
+
+ describe '#updated_group_member_expiration' do
+ let_it_be(:group_member) { create(:group_member) }
+
+ it 'emails the user that their group membership expiry has changed' do
+ expect_next_instance_of(NotificationService) do |notification|
+ allow(notification).to receive(:updated_group_member_expiration).with(group_member)
+ end
+
+ group_member.update!(expires_at: 5.days.from_now)
+ end
+ end
end
describe 'ProjectMember', :deliver_mails_inline do
diff --git a/yarn.lock b/yarn.lock
index 4a4a536383e..af3b39f4d67 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -876,10 +876,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
-"@gitlab/ui@25.8.0":
- version "25.8.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-25.8.0.tgz#038090bc56215d2b0e5526097e1a16b0089ba5f4"
- integrity sha512-h84StVkrviIm1cMDmGb2+Q8R+U6wCjddz7IXKpgkTNitxYzAcwPSIp7cS1FkZ6eWEG9dVeB6uj7JpUhGqAzvfw==
+"@gitlab/ui@25.11.1":
+ version "25.11.1"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-25.11.1.tgz#67133b99583e15497166db75781bc1103e7904ac"
+ integrity sha512-5Ig9QjSH8iK6XqFafPUT051M8/aUvvASJ3Bv7UFnmLZdZxiKjY6QKiD/lwleQJKBf2XAoGB13c0gfQInZI4+IQ==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"