diff --git a/.rubocop_todo/gitlab/feature_flag_without_actor.yml b/.rubocop_todo/gitlab/feature_flag_without_actor.yml
index b2928ce27a0..922e737b9e6 100644
--- a/.rubocop_todo/gitlab/feature_flag_without_actor.yml
+++ b/.rubocop_todo/gitlab/feature_flag_without_actor.yml
@@ -40,7 +40,6 @@ Gitlab/FeatureFlagWithoutActor:
- 'app/models/ci/runner.rb'
- 'app/models/ci/secure_file.rb'
- 'app/models/clusters/instance.rb'
- - 'app/models/concerns/ci/partitionable/organizer.rb'
- 'app/models/concerns/counter_attribute.rb'
- 'app/models/concerns/protected_ref_access.rb'
- 'app/models/concerns/reset_on_column_errors.rb'
diff --git a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
index 974f712b4e3..f98a15cab7b 100644
--- a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
+++ b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
@@ -251,11 +251,7 @@ Layout/LineEndStringConcatenationIndentation:
- 'ee/app/workers/requirements_management/process_requirements_reports_worker.rb'
- 'ee/bin/custom-ability'
- 'ee/db/fixtures/development/90_productivity_analytics.rb'
- - 'ee/elastic/migrate/20230405500000_backfill_wiki_permissions_in_main_index.rb'
- - 'ee/elastic/migrate/20230415500000_migrate_wikis_to_separate_index.rb'
- - 'ee/elastic/migrate/20230503064300_backfill_project_permissions_in_blobs_using_permutations.rb'
- 'ee/elastic/migrate/20230518064300_backfill_project_permissions_in_blobs.rb'
- - 'ee/elastic/migrate/20230530500000_migrate_projects_to_separate_index.rb'
- 'ee/lib/api/geo_sites.rb'
- 'ee/lib/api/iterations.rb'
- 'ee/lib/api/protected_environments.rb'
diff --git a/.rubocop_todo/style/inline_disable_annotation.yml b/.rubocop_todo/style/inline_disable_annotation.yml
index 83e92294fbe..439f3e150db 100644
--- a/.rubocop_todo/style/inline_disable_annotation.yml
+++ b/.rubocop_todo/style/inline_disable_annotation.yml
@@ -1680,10 +1680,7 @@ Style/InlineDisableAnnotation:
- 'ee/db/geo/post_migrate/20231023230850_drop_project_registry.rb'
- 'ee/db/seeds/data_seeder/data_seeder.rb'
- 'ee/db/seeds/shared/dora_metrics.rb'
- - 'ee/elastic/migrate/20230325200700_backfill_hashed_root_namespace_id_to_commits.rb'
- - 'ee/elastic/migrate/20230405500000_backfill_wiki_permissions_in_main_index.rb'
- 'ee/elastic/migrate/20230518064300_backfill_project_permissions_in_blobs.rb'
- - 'ee/elastic/migrate/20230519500012_reindex_wikis_to_fix_permissions_and_traversal_ids.rb'
- 'ee/elastic/migrate/20230702000000_backfill_existing_group_wiki.rb'
- 'ee/elastic/migrate/20230703112233_reindex_commits_to_fix_permissions.rb'
- 'ee/elastic/migrate/20230720000000_reindex_wikis_to_fix_routing.rb'
diff --git a/app/assets/javascripts/analytics/cycle_analytics/components/base.vue b/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
index c1640343bb2..2312aef1b21 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
@@ -109,7 +109,9 @@ export default {
return Boolean(this.features?.groupLevelAnalyticsDashboard && this.groupPath);
},
dashboardsPath() {
- return this.showLinkToDashboard ? generateValueStreamsDashboardLink(this.groupPath) : null;
+ return this.showLinkToDashboard
+ ? generateValueStreamsDashboardLink(this.namespace.fullPath)
+ : null;
},
query() {
return {
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index 2b8c9bb9917..196149c493d 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -27,17 +27,17 @@ const boardDefaults = {
export default {
i18n: {
- [formType.new]: { title: s__('Board|Create new board'), btnText: s__('Board|Create board') },
- [formType.delete]: { title: s__('Board|Delete board'), btnText: __('Delete') },
- [formType.edit]: { title: s__('Board|Configure board'), btnText: __('Save changes') },
- scopeModalTitle: s__('Board|Board configuration'),
+ [formType.new]: { title: s__('Boards|Create new board'), btnText: s__('Boards|Create board') },
+ [formType.delete]: { title: s__('Boards|Delete board'), btnText: __('Delete') },
+ [formType.edit]: { title: s__('Boards|Configure board'), btnText: __('Save changes') },
+ scopeModalTitle: s__('Boards|Board configuration'),
cancelButtonText: __('Cancel'),
- deleteButtonText: s__('Board|Delete board'),
- deleteErrorMessage: s__('Board|Failed to delete board. Please try again.'),
+ deleteButtonText: s__('Boards|Delete board'),
+ deleteErrorMessage: s__('Boards|Failed to delete board. Please try again.'),
saveErrorMessage: __('Unable to save your changes. Please try again.'),
- deleteConfirmationMessage: s__('Board|Are you sure you want to delete this board?'),
+ deleteConfirmationMessage: s__('Boards|Are you sure you want to delete this board?'),
titleFieldLabel: __('Title'),
- titleFieldPlaceholder: s__('Board|Enter board name'),
+ titleFieldPlaceholder: s__('Boards|Enter board name'),
},
components: {
BoardScope: () => import('ee_component/boards/components/board_scope.vue'),
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 47c5a82cf82..f911b235844 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -25,10 +25,10 @@ export default {
name: 'BoardsSelector',
i18n: {
fetchBoardsError: s__('Boards|An error occurred while fetching boards. Please try again.'),
- headerText: s__('IssueBoards|Switch board'),
- noResultsText: s__('IssueBoards|No matching boards found'),
+ headerText: s__('Boards|Switch board'),
+ noResultsText: s__('Boards|No matching boards found'),
hiddenBoardsText: s__(
- 'IssueBoards|Some of your boards are hidden, add a license to see them again.',
+ 'Boards|Some of your boards are hidden, add a license to see them again.',
),
},
components: {
@@ -81,7 +81,7 @@ export default {
computed: {
boardName() {
- return this.board?.name || s__('IssueBoards|Select board');
+ return this.board?.name || s__('Boards|Select board');
},
boardId() {
return getIdFromGraphQLId(this.board.id) || '';
@@ -283,9 +283,7 @@ export default {
- {{
- s__('IssueBoards|Some of your boards are hidden, add a license to see them again.')
- }}
+ {{ s__('Boards|Some of your boards are hidden, add a license to see them again.') }}
- {{ s__('IssueBoards|Create new board') }}
+ {{ s__('Boards|Create new board') }}
diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue
index 24256dbaf32..7daf9cf0df9 100644
--- a/app/assets/javascripts/boards/components/config_toggle.vue
+++ b/app/assets/javascripts/boards/components/config_toggle.vue
@@ -16,7 +16,7 @@ export default {
inject: ['canAdminList'],
computed: {
buttonText() {
- return this.canAdminList ? s__('Board|Configure board') : s__('Board|Board configuration');
+ return this.canAdminList ? s__('Boards|Configure board') : s__('Boards|Board configuration');
},
},
methods: {
diff --git a/app/assets/javascripts/import/constants.js b/app/assets/javascripts/import/constants.js
index fc75877500e..485befcae8e 100644
--- a/app/assets/javascripts/import/constants.js
+++ b/app/assets/javascripts/import/constants.js
@@ -8,7 +8,7 @@ export const TYPE_SNIPPET = 'snippet';
export const BULK_IMPORT_STATIC_ITEMS = {
badges: __('Badge'),
- boards: s__('IssueBoards|Board'),
+ boards: s__('Boards|Board'),
epics: __('Epic'),
issues: __('Issue'),
labels: __('Label'),
diff --git a/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue b/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue
index 4650390a7ff..337a7f3656a 100644
--- a/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue
+++ b/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue
@@ -1,20 +1,48 @@
+
+
+
+
+ {{ heading }}
+
+
+
+
+
+
diff --git a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
index d198c834a82..f7c96699f92 100644
--- a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
@@ -52,6 +52,11 @@ export default {
type: Object,
required: true,
},
+ groupPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
workItemType() {
@@ -247,6 +252,7 @@ export default {
:work-item-id="workItem.id"
:work-item-type="workItemType"
:parent="workItemParent"
+ :group-path="groupPath"
@error="$emit('error', $event)"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 5b09e7bbe1d..162869f5184 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -79,7 +79,7 @@ export default {
WorkItemLoading,
},
mixins: [glFeatureFlagMixin()],
- inject: ['fullPath', 'isGroup', 'reportAbusePath'],
+ inject: ['fullPath', 'isGroup', 'reportAbusePath', 'groupPath'],
props: {
isModal: {
type: Boolean,
@@ -609,6 +609,7 @@ export default {
diff --git a/app/assets/javascripts/work_items/components/work_item_parent.vue b/app/assets/javascripts/work_items/components/work_item_parent.vue
index 0357ff752f2..36debe26ae9 100644
--- a/app/assets/javascripts/work_items/components/work_item_parent.vue
+++ b/app/assets/javascripts/work_items/components/work_item_parent.vue
@@ -14,6 +14,7 @@ import {
I18N_WORK_ITEM_ERROR_UPDATING,
sprintfWorkItem,
SUPPORTED_PARENT_TYPE_MAP,
+ WORK_ITEM_TYPE_VALUE_ISSUE,
} from '../constants';
export default {
@@ -57,6 +58,11 @@ export default {
required: false,
default: false,
},
+ groupPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -73,6 +79,9 @@ export default {
hasParent() {
return this.parent !== null;
},
+ isIssue() {
+ return this.workItemType === WORK_ITEM_TYPE_VALUE_ISSUE;
+ },
isLoading() {
return this.$apollo.queries.availableWorkItems.loading;
},
@@ -105,16 +114,19 @@ export default {
apollo: {
availableWorkItems: {
query() {
- return this.isGroup ? groupWorkItemsQuery : projectWorkItemsQuery;
+ // TODO: Remove the this.isIssue check once issues are migrated to work items
+ return this.isGroup || this.isIssue ? groupWorkItemsQuery : projectWorkItemsQuery;
},
variables() {
+ // TODO: Remove the this.isIssue check once issues are migrated to work items
return {
- fullPath: this.fullPath,
+ fullPath: this.isIssue ? this.groupPath : this.fullPath,
searchTerm: this.search,
types: this.parentType,
in: this.search ? 'TITLE' : undefined,
iid: null,
isNumber: false,
+ includeAncestors: true,
};
},
skip() {
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index f359d750bbd..1acf2192251 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -322,6 +322,7 @@ export const SUPPORTED_PARENT_TYPE_MAP = {
[WORK_ITEM_TYPE_VALUE_KEY_RESULT]: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
[WORK_ITEM_TYPE_VALUE_TASK]: [WORK_ITEM_TYPE_ENUM_ISSUE],
[WORK_ITEM_TYPE_VALUE_EPIC]: [WORK_ITEM_TYPE_ENUM_EPIC],
+ [WORK_ITEM_TYPE_VALUE_ISSUE]: [WORK_ITEM_TYPE_ENUM_EPIC],
};
export const LINKED_ITEMS_ANCHOR = 'linkeditems';
diff --git a/app/assets/javascripts/work_items/graphql/group_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/group_work_items.query.graphql
index bfee0452acd..567a8db46d7 100644
--- a/app/assets/javascripts/work_items/graphql/group_work_items.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/group_work_items.query.graphql
@@ -3,10 +3,11 @@ query groupWorkItems(
$fullPath: ID!
$types: [IssueType!]
$in: [IssuableSearchableField!]
+ $includeAncestors: Boolean = false
) {
workspace: group(fullPath: $fullPath) {
id
- workItems(search: $searchTerm, types: $types, in: $in) {
+ workItems(search: $searchTerm, types: $types, in: $in, includeAncestors: $includeAncestors) {
nodes {
id
iid
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index f803831ddf7..d6b4c9ab0c1 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -25,6 +25,7 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType } = {}) => {
const {
fullPath,
+ groupPath,
hasIssueWeightsFeature,
iid,
issuesListPath,
@@ -57,6 +58,7 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType } = {}) => {
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
newCommentTemplatePaths: JSON.parse(newCommentTemplatePaths),
reportAbusePath,
+ groupPath,
},
mounted() {
performanceMarkAndMeasure({
diff --git a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
index a2f11a728e4..b8a8ba56932 100644
--- a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
+++ b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
@@ -1,5 +1,5 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/work_items/list/index.js b/app/assets/javascripts/work_items/list/index.js
index 886400835cc..447145601b6 100644
--- a/app/assets/javascripts/work_items/list/index.js
+++ b/app/assets/javascripts/work_items/list/index.js
@@ -20,6 +20,7 @@ export const mountWorkItemsListApp = () => {
hasIssueWeightsFeature,
initialSort,
isSignedIn,
+ showNewIssueLink,
workItemType,
} = el.dataset;
@@ -37,6 +38,7 @@ export const mountWorkItemsListApp = () => {
initialSort,
isSignedIn: parseBoolean(isSignedIn),
isGroup: true,
+ showNewIssueLink: parseBoolean(showNewIssueLink),
workItemType,
},
render: (createComponent) => createComponent(WorkItemsListApp),
diff --git a/app/components/layouts/page_heading_component.haml b/app/components/layouts/page_heading_component.haml
new file mode 100644
index 00000000000..d79922e341f
--- /dev/null
+++ b/app/components/layouts/page_heading_component.haml
@@ -0,0 +1,7 @@
+.gl-flex.gl-flex-wrap.gl-items-center.gl-justify-between.gl-gap-y-2.gl-gap-x-5.gl-mt-5.gl-mb-3
+ %h1.gl-heading-1{ class: '!gl-m-0', data: { testid: 'page-heading' } }
+ = heading || @heading
+
+ - if actions?
+ .gl-flex.gl-items-center.gl-gap-5{ data: { testid: 'page-heading-actions' } }
+ = actions
diff --git a/app/components/layouts/page_heading_component.rb b/app/components/layouts/page_heading_component.rb
new file mode 100644
index 00000000000..3f0f2cab177
--- /dev/null
+++ b/app/components/layouts/page_heading_component.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Layouts
+ class PageHeadingComponent < ViewComponent::Base
+ # @param [String] heading
+ def initialize(heading)
+ @heading = heading
+ end
+
+ renders_one :heading
+ renders_one :actions
+ end
+end
diff --git a/app/graphql/mutations/pages/deployment/delete.rb b/app/graphql/mutations/pages/deployment/delete.rb
index 28710db7892..89ce4540de3 100644
--- a/app/graphql/mutations/pages/deployment/delete.rb
+++ b/app/graphql/mutations/pages/deployment/delete.rb
@@ -13,13 +13,18 @@ module Mutations
required: true,
description: 'ID of the Pages Deployment.'
+ field :pages_deployment, Types::PagesDeploymentType,
+ null: false,
+ description: 'Deleted Pages Deployment.'
+
def resolve(id:)
deployment = authorized_find!(id: id)
deployment.deactivate
{
- errors: errors_on_object(deployment)
+ errors: errors_on_object(deployment),
+ pages_deployment: deployment
}
end
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 9e9c0789a9e..b1efa39fba1 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -135,14 +135,12 @@ module IssuesHelper
{
autocomplete_award_emojis_path: autocomplete_award_emojis_path,
calendar_path: url_for(safe_params.merge(calendar_url_options)),
- empty_state_svg_path: image_path('illustrations/empty-state/empty-service-desk-md.svg'),
full_path: namespace.full_path,
initial_sort: current_user&.user_preference&.issues_sort,
is_issue_repositioning_disabled: issue_repositioning_disabled?.to_s,
is_public_visibility_restricted:
Gitlab::CurrentSettings.restricted_visibility_levels&.include?(Gitlab::VisibilityLevel::PUBLIC).to_s,
is_signed_in: current_user.present?.to_s,
- jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
rss_path: url_for(safe_params.merge(rss_url_options)),
sign_in_path: new_user_session_path,
has_issue_date_filter_feature: has_issue_date_filter_feature?(namespace, current_user).to_s
diff --git a/app/helpers/work_items_helper.rb b/app/helpers/work_items_helper.rb
index a1364c312f9..c8256ddf05f 100644
--- a/app/helpers/work_items_helper.rb
+++ b/app/helpers/work_items_helper.rb
@@ -6,6 +6,7 @@ module WorkItemsHelper
{
full_path: resource_parent.full_path,
+ group_path: group&.full_path,
issues_list_path:
resource_parent.is_a?(Group) ? issues_group_path(resource_parent) : project_issues_path(resource_parent),
register_path: new_user_registration_path(redirect_to_referer: 'yes'),
@@ -20,7 +21,8 @@ module WorkItemsHelper
{
full_path: group.full_path,
initial_sort: current_user&.user_preference&.issues_sort,
- is_signed_in: current_user.present?.to_s
+ is_signed_in: current_user.present?.to_s,
+ show_new_issue_link: can?(current_user, :create_work_item, group).to_s
}
end
end
diff --git a/app/models/ci/partition.rb b/app/models/ci/partition.rb
index 3fa079688ca..63489450ebf 100644
--- a/app/models/ci/partition.rb
+++ b/app/models/ci/partition.rb
@@ -48,6 +48,14 @@ module Ci
.order(id: :asc)
.first
end
+
+ def provisioning(partition_id)
+ Ci::Partition
+ .with_status(:preparing)
+ .id_after(partition_id)
+ .order(id: :asc)
+ .first
+ end
end
def above_threshold?(threshold)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index f65f634e036..3677be5359a 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -576,7 +576,9 @@ module Ci
def self.current_partition_value(project = nil)
Gitlab::SafeRequestStore.fetch(:ci_current_partition_value) do
- if Feature.enabled?(:ci_current_partition_value_102, project)
+ if Feature.enabled?(:ci_partitioning_automation, project)
+ Ci::Partition.current&.id || NEXT_PARTITION_VALUE
+ elsif Feature.enabled?(:ci_current_partition_value_102, project)
NEXT_PARTITION_VALUE
elsif Feature.enabled?(:ci_current_partition_value_101, project)
SECOND_PARTITION_VALUE
diff --git a/app/models/concerns/ci/partitionable.rb b/app/models/concerns/ci/partitionable.rb
index bfb92595e15..6e5e372ebc0 100644
--- a/app/models/concerns/ci/partitionable.rb
+++ b/app/models/concerns/ci/partitionable.rb
@@ -81,12 +81,19 @@ module Ci
partitioned_by :partition_id,
strategy: :ci_sliding_list,
next_partition_if: ->(latest_partition) do
- latest_partition.blank? ||
- ::Ci::Partitionable::Organizer.create_database_partition?(latest_partition)
+ latest_partition.blank? || create_database_partition?(latest_partition)
end,
detach_partition_if: proc { false },
analyze_interval: 3.days
end
+
+ def create_database_partition?(database_partition)
+ if Feature.enabled?(:ci_partitioning_automation, :instance)
+ Ci::Partition.provisioning(database_partition.values.max).present?
+ else
+ database_partition.before?(Ci::Pipeline::NEXT_PARTITION_VALUE)
+ end
+ end
end
end
end
diff --git a/app/models/concerns/ci/partitionable/organizer.rb b/app/models/concerns/ci/partitionable/organizer.rb
deleted file mode 100644
index a49615b10e4..00000000000
--- a/app/models/concerns/ci/partitionable/organizer.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- module Partitionable
- class Organizer
- class << self
- def create_database_partition?(database_partition)
- database_partition.before?(Ci::Pipeline::NEXT_PARTITION_VALUE)
- end
- end
- end
- end
-end
diff --git a/app/services/ci/partitions/setup_default_service.rb b/app/services/ci/partitions/setup_default_service.rb
index 09584d95e88..133d4efa506 100644
--- a/app/services/ci/partitions/setup_default_service.rb
+++ b/app/services/ci/partitions/setup_default_service.rb
@@ -11,6 +11,7 @@ module Ci
def execute
return unless Feature.enabled?(:ci_partitioning_first_records, :instance)
+ return if Ci::Partition.current
setup_default_partitions
end
@@ -18,7 +19,8 @@ module Ci
private
def setup_default_partitions
- setup_active_partitions && setup_current_partition
+ setup_active_partitions
+ setup_current_partition
end
def setup_active_partitions
diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
index 0fb36adeffb..2a32ccc251a 100644
--- a/app/views/dashboard/_activity_head.html.haml
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -1,5 +1,4 @@
-.page-title-holder.d-flex.gl-align-items-center
- %h1.page-title.gl-font-size-h-display= _('Activity')
+= render ::Layouts::PageHeadingComponent.new(_('Activity'))
.top-area
= gl_tabs_nav({ class: 'gl-border-b-0', data: { testid: 'dashboard-activity-tabs' } }) do
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index fa715b589f0..873b1da01eb 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -1,13 +1,11 @@
-.page-title-holder.d-flex.gl-align-items-center
- %h1.page-title.gl-font-size-h-display= _('Groups')
-
- .page-title-controls.gl-display-flex.gl-align-items-center.gl-gap-5
+= render ::Layouts::PageHeadingComponent.new(_('Groups')) do |c|
+ - c.with_actions do
= link_to _("Explore groups"), explore_groups_path
- if current_user.can_create_group?
= render Pajamas::ButtonComponent.new(href: new_group_path, variant: :confirm, button_options: { data: { testid: "new-group-button" } }) do
= _("New group")
-.gl-display-flex.gl-py-3.gl-gap-3
+.gl-flex.gl-py-3.gl-gap-3
.gl-w-full
= render 'shared/groups/search_form'
= render 'shared/groups/dropdown'
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index e7847269a5e..b8a22854948 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -2,17 +2,15 @@
= content_for :flash_message do
= render 'shared/project_limit'
-.page-title-holder.gl-display-flex.gl-align-items-center
- %h1.page-title.gl-font-size-h-display= _('Projects')
-
- .page-title-controls.gl-display-flex.gl-align-items-center.gl-gap-5
+= render ::Layouts::PageHeadingComponent.new(_('Projects')) do |c|
+ - c.with_actions do
= link_to _("Explore projects"), starred_explore_projects_path
- if current_user.can_create_project?
= render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm, button_options: { data: { testid: 'new-project-button' } }) do
= _("New project")
.top-area
- .scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-flex-basis-0.gl-min-w-0
+ .scrolling-tabs-container.inner-page-scroll-tabs.gl-grow.gl-basis-0.gl-min-w-0
%button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') }
= sprite_icon('chevron-lg-left', size: 12)
%button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') }
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index eea376a3281..53be0f168ad 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -1,7 +1,5 @@
-.page-title-holder.d-flex.gl-align-items-center
- %h1.page-title.gl-font-size-h-display= _('Snippets')
-
- .page-title-controls.gl-display-flex.gl-align-items-center.gl-gap-5
+= render ::Layouts::PageHeadingComponent.new(_('Snippets')) do |c|
+ - c.with_actions do
= link_to _("Explore snippets"), explore_snippets_path
- if can?(current_user, :create_snippet)
= render Pajamas::ButtonComponent.new(href: new_snippet_path, variant: :confirm, button_options: { title: _("New snippet") }) do
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index f7b2ba59549..cae3d58abac 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -9,11 +9,8 @@
= render_if_exists 'shared/dashboard/saml_reauth_notice',
groups_requiring_saml_reauth: user_groups_requiring_reauth
-.page-title-holder.gl-display-flex.gl-align-items-center
- %h1.page-title.gl-font-size-h-display= _('Issues')
-
- - if current_user
- .page-title-controls
- = render 'shared/new_project_item_vue_select'
+= render ::Layouts::PageHeadingComponent.new(_('Issues')) do |c|
+ - c.with_actions do
+ = render 'shared/new_project_item_vue_select'
.js-issues-dashboard{ data: dashboard_issues_list_data(current_user) }
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index b548e4d86e9..6975b8f903b 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -18,15 +18,12 @@
- if merge_request_dashboard_enabled?(current_user)
#js-merge-request-dashboard{ data: { initial_data: merge_request_dashboard_data.to_json } }
- .page-title-holder
- %h1.page-title.gl-font-size-h-display= _('Merge Requests')
+ = render ::Layouts::PageHeadingComponent.new(_('Merge requests'))
= gl_loading_icon(size: 'lg')
- else
- .page-title-holder.d-flex.align-items-start.flex-column.flex-sm-row.align-items-sm-center
- %h1.page-title.gl-font-size-h-display= title
-
- - if current_user
- .page-title-controls.ml-0.mb-3.ml-sm-auto.mb-sm-0
+ = render ::Layouts::PageHeadingComponent.new(title) do |c|
+ - c.with_actions do
+ - if current_user
= render 'shared/new_project_item_vue_select'
.top-area
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index 3f874feaa1b..fc5335d2016 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -1,11 +1,9 @@
- page_title _('Milestones')
- add_page_specific_style 'page_bundles/milestone'
-.page-title-holder.d-flex.gl-align-items-center
- %h1.page-title.gl-font-size-h-display= _('Milestones')
-
- - if current_user
- .page-title-controls
+= render ::Layouts::PageHeadingComponent.new(_('Milestones')) do |c|
+ - c.with_actions do
+ - if current_user
= render 'shared/new_project_item_vue_select'
- if @milestone_states.any? { |name, count| count > 0 }
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index bdfcbb4ec18..0e0ad965017 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -15,8 +15,7 @@
- show_header = @allowed_todos.any? || user_have_todos
- if show_header
- .page-title-holder.d-flex.gl-align-items-center
- %h1.page-title.gl-font-size-h-display= _("To-Do List")
+ = render ::Layouts::PageHeadingComponent.new(_('To-Do List'))
.js-todos-all
- if user_have_todos
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 50171d0834f..8bb992aca6c 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -3371,7 +3371,7 @@
:tags: []
- :name: merge
:worker_name: MergeWorker
- :feature_category: :source_code_management
+ :feature_category: :code_review_workflow
:has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
index 2979a77e19f..8fc9b14aab3 100644
--- a/app/workers/merge_worker.rb
+++ b/app/workers/merge_worker.rb
@@ -7,7 +7,7 @@ class MergeWorker # rubocop:disable Scalability/IdempotentWorker
sidekiq_options retry: 3
- feature_category :source_code_management
+ feature_category :code_review_workflow
urgency :high
weight 5
loggable_arguments 2
diff --git a/db/structure.sql b/db/structure.sql
index deaea149ff6..9d2c67b6284 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -881,22 +881,6 @@ RETURN NEW;
END
$$;
-CREATE FUNCTION trigger_8a38ce2327de() RETURNS trigger
- LANGUAGE plpgsql
- AS $$
-BEGIN
-IF NEW."group_id" IS NULL THEN
- SELECT "group_id"
- INTO NEW."group_id"
- FROM "epics"
- WHERE "epics"."id" = NEW."epic_id";
-END IF;
-
-RETURN NEW;
-
-END
-$$;
-
CREATE FUNCTION trigger_84d67ad63e93() RETURNS trigger
LANGUAGE plpgsql
AS $$
@@ -913,6 +897,22 @@ RETURN NEW;
END
$$;
+CREATE FUNCTION trigger_8a38ce2327de() RETURNS trigger
+ LANGUAGE plpgsql
+ AS $$
+BEGIN
+IF NEW."group_id" IS NULL THEN
+ SELECT "group_id"
+ INTO NEW."group_id"
+ FROM "epics"
+ WHERE "epics"."id" = NEW."epic_id";
+END IF;
+
+RETURN NEW;
+
+END
+$$;
+
CREATE FUNCTION trigger_8ac78f164b2d() RETURNS trigger
LANGUAGE plpgsql
AS $$
@@ -30413,10 +30413,10 @@ CREATE TRIGGER trigger_56d49f4ed623 BEFORE INSERT OR UPDATE ON workspace_variabl
CREATE TRIGGER trigger_7a8b08eed782 BEFORE INSERT OR UPDATE ON boards_epic_board_positions FOR EACH ROW EXECUTE FUNCTION trigger_7a8b08eed782();
-CREATE TRIGGER trigger_8a38ce2327de BEFORE INSERT OR UPDATE ON boards_epic_user_preferences FOR EACH ROW EXECUTE FUNCTION trigger_8a38ce2327de();
-
CREATE TRIGGER trigger_84d67ad63e93 BEFORE INSERT OR UPDATE ON wiki_page_slugs FOR EACH ROW EXECUTE FUNCTION trigger_84d67ad63e93();
+CREATE TRIGGER trigger_8a38ce2327de BEFORE INSERT OR UPDATE ON boards_epic_user_preferences FOR EACH ROW EXECUTE FUNCTION trigger_8a38ce2327de();
+
CREATE TRIGGER trigger_8ac78f164b2d BEFORE INSERT OR UPDATE ON design_management_repositories FOR EACH ROW EXECUTE FUNCTION trigger_8ac78f164b2d();
CREATE TRIGGER trigger_8e66b994e8f0 BEFORE INSERT OR UPDATE ON audit_events_streaming_event_type_filters FOR EACH ROW EXECUTE FUNCTION trigger_8e66b994e8f0();
diff --git a/doc/administration/geo/index.md b/doc/administration/geo/index.md
index d027b384177..13d95f86cb3 100644
--- a/doc/administration/geo/index.md
+++ b/doc/administration/geo/index.md
@@ -204,7 +204,6 @@ This list of limitations only reflects the latest version of GitLab. If you are
GitLab instances based on our [Reference Architectures](../reference_architectures/index.md), including automation of common daily tasks.
[Epic 1465](https://gitlab.com/groups/gitlab-org/-/epics/1465) proposes to improve Geo installation even more.
- Real-time updates of issues/merge requests (for example, via long polling) doesn't work on the **secondary** site.
-- Using Geo secondary sites to accelerate runners is not officially supported. Support for this functionality is planned and can be tracked in [epic 9779](https://gitlab.com/groups/gitlab-org/-/epics/9779). If a replication lag occurs between the primary and secondary site, and the pipeline ref is not available on the secondary site when the job is executed, the job will fail.
- [Selective synchronization](replication/configuration.md#selective-synchronization) only limits what repositories and files are replicated. The entire PostgreSQL data is still replicated. Selective synchronization is not built to accommodate compliance / export control use cases.
- [Pages access control](../../user/project/pages/pages_access_control.md) doesn't work on secondaries. See [GitLab issue #9336](https://gitlab.com/gitlab-org/gitlab/-/issues/9336) for details.
- [Disaster recovery](disaster_recovery/index.md) for deployments that have multiple secondary sites causes downtime due to the need to perform complete re-synchronization and re-configuration of all non-promoted secondaries to follow the new primary site.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 162ff6b7a2b..a98d44929ad 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -3998,6 +3998,7 @@ Input type: `DeletePagesDeploymentInput`
| ---- | ---- | ----------- |
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| `pagesDeployment` | [`PagesDeployment!`](#pagesdeployment) | Deleted Pages Deployment. |
### `Mutation.designManagementDelete`
diff --git a/doc/architecture/blueprints/pipeline_mini_graph/index.md b/doc/architecture/blueprints/pipeline_mini_graph/index.md
index 90c7c306856..2490bf55018 100644
--- a/doc/architecture/blueprints/pipeline_mini_graph/index.md
+++ b/doc/architecture/blueprints/pipeline_mini_graph/index.md
@@ -32,7 +32,7 @@ While the Pipeline Mini Graph primarily functions via REST, we are updating the
## Proposal
-To break down implementation, we are taking the following steps:
+To break down implementation, we are taking the following steps:
1. Separate the REST version and the GraphQL version of the component into 2 directories called `pipeline_mini_graph` and `legacy_pipeline_mini_graph`. This way, developers can contribute with more ease and we can easily remove the REST version once all apps are using GraphQL.
1. Finish updating the newer component to fully support GraphQL by adding a query for the stage dropdown.
diff --git a/doc/architecture/blueprints/work_items/index.md b/doc/architecture/blueprints/work_items/index.md
index 85f000a2caa..956779821ba 100644
--- a/doc/architecture/blueprints/work_items/index.md
+++ b/doc/architecture/blueprints/work_items/index.md
@@ -137,6 +137,16 @@ Parent-child relationships form the basis of **hierarchy** in work items. Each w
As types expand, and parent items have their own parent items, the hierarchy capability can grow exponentially.
+Currently, following are the allowed Parent-child relationships:
+
+| Type | Can be parent of | Can be child of |
+|------------|------------------|------------------|
+| Epic | Epic | Epic |
+| Issue | Task | Epic |
+| Task | None | Issue |
+| Objective | Objective | Objective |
+| Key result | None | Objective |
+
### Work Item view
The new frontend view that renders Work Items of any type using global Work Item `id` as an identifier.
diff --git a/doc/development/database/query_count_limits.md b/doc/development/database/query_count_limits.md
index 4575669c7af..0abb6b481b0 100644
--- a/doc/development/database/query_count_limits.md
+++ b/doc/development/database/query_count_limits.md
@@ -6,8 +6,9 @@ info: Any user with at least the Maintainer role can merge updates to this conte
# Query Count Limits
-Each controller or API endpoint is allowed to execute up to 100 SQL queries and
-in test environments we raise an error when this threshold is exceeded.
+Each controller, API endpoint and Sidekide worker is allowed to execute up to
+100 SQL queries and in test environments we raise an error when this threshold
+is exceeded.
## Solving Failing Tests
@@ -20,9 +21,7 @@ solutions to this problem:
You should only resort to disabling query limits when an existing controller or endpoint
is to blame as in this case reducing the number of SQL queries can take a lot of
effort. Newly added controllers and endpoints are not allowed to execute more
-than 100 SQL queries and no exceptions are made for this rule. _If_ a large
-number of SQL queries is necessary to perform certain work it's best to have
-this work performed by Sidekiq instead of doing this directly in a web request.
+than 100 SQL queries and no exceptions are made for this rule.
## Disable query limiting
@@ -68,3 +67,12 @@ get '/projects/:id/foo' do
# ...
end
```
+
+For Sidekiq workers, you will need to add the allowlist directly as well:
+
+```ruby
+def perform(args)
+ Gitlab::QueryLimiting.disable!('...')
+
+ # ...
+end
diff --git a/doc/development/fe_guide/view_component.md b/doc/development/fe_guide/view_component.md
index 53c389cef8c..9d6bec07b7b 100644
--- a/doc/development/fe_guide/view_component.md
+++ b/doc/development/fe_guide/view_component.md
@@ -211,6 +211,21 @@ Layout components can be used to create common layout patterns used in GitLab.
### Available components
+#### Page heading
+
+A standard page header with a page title and optional actions.
+
+**Example:**
+
+```haml
+= render ::Layouts::PageHeadingComponent.new(_('Page title')) do |c|
+ - c.with_actions do
+ = buttons
+```
+
+For the full list of options, see its
+[source](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/components/layouts/page_heading_component.rb).
+
#### Horizontal section
Many of the settings pages use a layout where the title and description are on the left and the settings fields are on the right. The `Layouts::HorizontalSectionComponent` can be used to create this layout.
diff --git a/doc/user/application_security/sast/img/sast_vulnerability_page_fp_detection_v15_2.png b/doc/user/application_security/sast/img/sast_vulnerability_page_fp_detection_v15_2.png
deleted file mode 100644
index 2a3e6e7e45f..00000000000
Binary files a/doc/user/application_security/sast/img/sast_vulnerability_page_fp_detection_v15_2.png and /dev/null differ
diff --git a/doc/user/application_security/secret_detection/pre_receive/index.md b/doc/user/application_security/secret_detection/pre_receive/index.md
index 01d0185ffa8..3daa24fd6e7 100644
--- a/doc/user/application_security/secret_detection/pre_receive/index.md
+++ b/doc/user/application_security/secret_detection/pre_receive/index.md
@@ -53,6 +53,19 @@ Prerequisites:
1. Expand **Secret Detection**.
1. Select the **Allow secret push protection** checkbox.
+## Coverage
+
+Secret push protection checks the content of each commit when it is pushed to GitLab.
+However, the following exclusions apply.
+
+Secret push protection does not check a file in a commit when:
+
+- The file is a binary file.
+- The file is larger than 1 MiB.
+- The file was renamed, deleted, or moved without changes to the content.
+- The content of the file is identical to the content of another file in the source code.
+- The file is contained in the initial push that created the repository.
+
## Resolve a blocked push
When secret push protection blocks a push, you can either:
@@ -128,17 +141,3 @@ To skip secret push protection when using any Git client:
For example, you are using the GitLab Web IDE and have several commits that are blocked from being
pushed because one of them contains a secret. To skip secret push protection, edit the latest
commit message and add `[skip secret detection]`, then push the commits.
-
-## Troubleshooting
-
-When working with secret push protection, you might encounter the following issues.
-
-### My file was not analyzed
-
-If your file was not scanned, it could be because:
-
-- The blob was binary.
-- The blob was larger than 1 MiB.
-- The file was renamed, deleted, or moved.
-- The content of the commit was identical to the content of another file already present in the source code.
-- The file was introduced when the repository was created.
diff --git a/doc/user/application_security/vulnerability_report/pipeline.md b/doc/user/application_security/vulnerability_report/pipeline.md
index ad8658c797d..64b85b627fb 100644
--- a/doc/user/application_security/vulnerability_report/pipeline.md
+++ b/doc/user/application_security/vulnerability_report/pipeline.md
@@ -207,3 +207,8 @@ You can find definitions for each scan type [`gitlab/lib/gitlab/ci/reports/secur
and [`gitlab/ee/lib/gitlab/ci/reports/security/locations`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/ee/lib/gitlab/ci/reports/security/locations).
For instance, for `container_scanning` type the location is defined by Docker image name without tag. However if the image tag contains at least one letter and/or is longer than 8 characters, it isn't considered a duplicate. So, locations `registry.gitlab.com/group-name/project-name/image1:12345019:libcrypto3` and `registry.gitlab.com/group-name/project-name/image1:libcrypto3` are treated as identical while `registry.gitlab.com/group-name/project-name/image1:v19202021:libcrypto3` and `registry.gitlab.com/group-name/project-name/image1:libcrypto3` are considered different.
+
+## Troubleshooting
+
+In some instances of GitLab 16.8 and earlier [dismissed vulnerabilities are sometimes still visible](https://gitlab.com/gitlab-org/gitlab/-/issues/367298).
+This can be resolved by upgrading to GitLab 16.9 and later.
diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md
index d42185b7c18..9c24b48ddf4 100644
--- a/doc/user/compliance/audit_event_types.md
+++ b/doc/user/compliance/audit_event_types.md
@@ -419,7 +419,7 @@ Audit event types belong to the following product categories.
| Name | Description | Saved to database | Streamed | Introduced in | Scope |
|:------------|:------------|:------------------|:---------|:--------------|:--------------|
-| [`skip_pre_receive_secret_detection`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147855) | Triggered when secret push protection is skipped by the user| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/441185) | Project |
+| [`skip_secret_push_protection`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147855) | Triggered when secret push protection is skipped by the user| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/441185) | Project |
### Security policy management
diff --git a/doc/user/product_analytics/index.md b/doc/user/product_analytics/index.md
index f41f49f1d81..a44ee58bf81 100644
--- a/doc/user/product_analytics/index.md
+++ b/doc/user/product_analytics/index.md
@@ -169,7 +169,7 @@ Prerequisites:
Prerequisites:
-- You must have at least the Developer role for the project or group the project belongs to.
+- You must have at least the Maintainer role for the project or group the project belongs to.
Onboarding a GitLab project means preparing it to receive events that are used for product analytics.
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index ddcbd2cf806..102706f3d36 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -105,8 +105,6 @@ You must define the following variables:
Variable names must contain only lowercase letters (`a-z`), numbers (`0-9`), or underscores (`_`).
You can define URL variables directly with the REST API.
-The host portion of the URL (such as `webhook.example.com`) must remain valid without using a mask variable.
-Otherwise, a `URI is invalid` or `Url is blocked` error occurs.
### Custom headers
diff --git a/doc/user/ssh.md b/doc/user/ssh.md
index 22fc88117e5..e65b7af0a1c 100644
--- a/doc/user/ssh.md
+++ b/doc/user/ssh.md
@@ -542,7 +542,7 @@ The GitLab SSH folder and files must have the following permissions:
- The `authorized_keys` file must have permissions set to `600`.
- The `authorized_keys.lock` file must have permissions set to `644`.
-To verify that these permissions are correct, run the following:
+To verify that these permissions are correct, run the following:
```shell
stat -c "%a %n" /var/opt/gitlab/.ssh/.
@@ -550,11 +550,11 @@ stat -c "%a %n" /var/opt/gitlab/.ssh/.
### Set permissions
-If the permissions are wrong, sign in to the application server and run:
+If the permissions are wrong, sign in to the application server and run:
```shell
cd /var/opt/gitlab/
-chown git:git /var/opt/gitlab/.ssh/
+chown git:git /var/opt/gitlab/.ssh/
chmod 700 /var/opt/gitlab/.ssh/
chmod 600 /var/opt/gitlab/.ssh/authorized_keys
chmod 644 /var/opt/gitlab/.ssh/authorized_keys.lock
@@ -584,7 +584,7 @@ This indicates that something is wrong with your SSH setup.
- Try to debug the connection by running `ssh -Tv git@example.com`.
Replace `example.com` with your GitLab URL.
- Ensure you followed all the instructions in [Use SSH on Microsoft Windows](#use-ssh-on-microsoft-windows).
-- Ensure that you have [Verify GitLab SSH ownership and permissions](#verify-gitlab-ssh-ownership-and-permissions). If you have several hosts ensure that permissions are correct in all hosts.
+- Ensure that you have [Verify GitLab SSH ownership and permissions](#verify-gitlab-ssh-ownership-and-permissions). If you have several hosts ensure that permissions are correct in all hosts.
### `Could not resolve hostname` error
diff --git a/lib/sidebars/groups/menus/issues_menu.rb b/lib/sidebars/groups/menus/issues_menu.rb
index ddc930d56ff..cc6726bc61a 100644
--- a/lib/sidebars/groups/menus/issues_menu.rb
+++ b/lib/sidebars/groups/menus/issues_menu.rb
@@ -81,7 +81,7 @@ module Sidebars
title = if context.is_super_sidebar
context.group.multiple_issue_boards_available? ? s_('Issue boards') : s_('Issue board')
else
- context.group.multiple_issue_boards_available? ? s_('IssueBoards|Boards') : s_('IssueBoards|Board')
+ context.group.multiple_issue_boards_available? ? s_('Boards|Boards') : s_('Boards|Board')
end
::Sidebars::MenuItem.new(
diff --git a/lib/sidebars/projects/menus/issues_menu.rb b/lib/sidebars/projects/menus/issues_menu.rb
index 9791a88cf9f..54c6fd0ff61 100644
--- a/lib/sidebars/projects/menus/issues_menu.rb
+++ b/lib/sidebars/projects/menus/issues_menu.rb
@@ -97,7 +97,7 @@ module Sidebars
title = if context.is_super_sidebar
multi_issue_boards? ? s_('Issue boards') : s_('Issue board')
else
- multi_issue_boards? ? s_('IssueBoards|Boards') : s_('IssueBoards|Board')
+ multi_issue_boards? ? s_('Boards|Boards') : s_('Boards|Board')
end
::Sidebars::MenuItem.new(
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index cf4c5c48b82..ef404135558 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8859,32 +8859,71 @@ msgstr ""
msgid "Boards|An error occurred while updating the list. Please try again."
msgstr ""
+msgid "Boards|Are you sure you want to delete this board?"
+msgstr ""
+
msgid "Boards|Blocked by %{blockedByCount} %{issuableType}"
msgid_plural "Boards|Blocked by %{blockedByCount} %{issuableType}s"
msgstr[0] ""
msgstr[1] ""
+msgid "Boards|Board"
+msgstr ""
+
+msgid "Boards|Board configuration"
+msgstr ""
+
+msgid "Boards|Boards"
+msgstr ""
+
msgid "Boards|Card options"
msgstr ""
msgid "Boards|Collapse"
msgstr ""
+msgid "Boards|Configure board"
+msgstr ""
+
+msgid "Boards|Create board"
+msgstr ""
+
+msgid "Boards|Create new board"
+msgstr ""
+
msgid "Boards|Create new epic"
msgstr ""
msgid "Boards|Create new issue"
msgstr ""
+msgid "Boards|Delete board"
+msgstr ""
+
msgid "Boards|Edit list settings"
msgstr ""
+msgid "Boards|Enter board name"
+msgstr ""
+
msgid "Boards|Expand"
msgstr ""
+msgid "Boards|Failed to delete board. Please try again."
+msgstr ""
+
msgid "Boards|Failed to fetch blocking %{issuableType}s"
msgstr ""
+msgid "Boards|Load more epics"
+msgstr ""
+
+msgid "Boards|Load more issues"
+msgstr ""
+
+msgid "Boards|Loading epics"
+msgstr ""
+
msgid "Boards|Move to end of list"
msgstr ""
@@ -8897,45 +8936,24 @@ msgstr ""
msgid "Boards|No cadence matches current iteration filter"
msgstr ""
+msgid "Boards|No matching boards found"
+msgstr ""
+
msgid "Boards|Retrieving blocking %{issuableType}s"
msgstr ""
+msgid "Boards|Select board"
+msgstr ""
+
+msgid "Boards|Some of your boards are hidden, add a license to see them again."
+msgstr ""
+
+msgid "Boards|Switch board"
+msgstr ""
+
msgid "Boards|View all blocking %{issuableType}s"
msgstr ""
-msgid "Board|Are you sure you want to delete this board?"
-msgstr ""
-
-msgid "Board|Board configuration"
-msgstr ""
-
-msgid "Board|Configure board"
-msgstr ""
-
-msgid "Board|Create board"
-msgstr ""
-
-msgid "Board|Create new board"
-msgstr ""
-
-msgid "Board|Delete board"
-msgstr ""
-
-msgid "Board|Enter board name"
-msgstr ""
-
-msgid "Board|Failed to delete board. Please try again."
-msgstr ""
-
-msgid "Board|Load more epics"
-msgstr ""
-
-msgid "Board|Load more issues"
-msgstr ""
-
-msgid "Board|Loading epics"
-msgstr ""
-
msgid "Bold (%{modifierKey}B)"
msgstr ""
@@ -28575,27 +28593,6 @@ msgstr ""
msgid "IssueAnalytics|Weight"
msgstr ""
-msgid "IssueBoards|Board"
-msgstr ""
-
-msgid "IssueBoards|Boards"
-msgstr ""
-
-msgid "IssueBoards|Create new board"
-msgstr ""
-
-msgid "IssueBoards|No matching boards found"
-msgstr ""
-
-msgid "IssueBoards|Select board"
-msgstr ""
-
-msgid "IssueBoards|Some of your boards are hidden, add a license to see them again."
-msgstr ""
-
-msgid "IssueBoards|Switch board"
-msgstr ""
-
msgid "IssueList|created %{timeAgoString} by %{user}"
msgstr ""
@@ -39456,6 +39453,9 @@ msgstr ""
msgid "ProductAnalytics|Add the script to the page and assign the client SDK to window:"
msgstr ""
+msgid "ProductAnalytics|Additional permissions required"
+msgstr ""
+
msgid "ProductAnalytics|After your application has been instrumented and data is being collected, you can visualize and monitor behaviors in your %{linkStart}analytics dashboards%{linkEnd}."
msgstr ""
@@ -39528,6 +39528,9 @@ msgstr ""
msgid "ProductAnalytics|Contact our sales team"
msgstr ""
+msgid "ProductAnalytics|Contact the GitLab administrator or project maintainer to onboard this project with product analytics. %{linkStart}Learn more%{linkEnd}."
+msgstr ""
+
msgid "ProductAnalytics|Continue set up"
msgstr ""
diff --git a/package.json b/package.json
index 70257edfdcc..7ccff1f6563 100644
--- a/package.json
+++ b/package.json
@@ -152,7 +152,7 @@
"gettext-parser": "^6.0.0",
"graphql": "^15.7.2",
"graphql-tag": "^2.11.0",
- "gridstack": "^10.1.2",
+ "gridstack": "^10.2.0",
"highlight.js": "^11.8.0",
"immer": "^9.0.15",
"ipaddr.js": "^1.9.1",
diff --git a/spec/components/layouts/page_heading_component_spec.rb b/spec/components/layouts/page_heading_component_spec.rb
new file mode 100644
index 00000000000..f044877ea78
--- /dev/null
+++ b/spec/components/layouts/page_heading_component_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Layouts::PageHeadingComponent, type: :component, feature_category: :shared do
+ let(:heading) { 'Page heading' }
+ let(:actions) { 'Page actions go here' }
+
+ describe 'slots' do
+ it 'renders heading' do
+ render_inline described_class.new(heading)
+
+ expect(page).to have_css('h1.gl-heading-1', text: heading)
+ end
+
+ it 'renders actions slot' do
+ render_inline described_class.new(heading) do |c|
+ c.with_actions { actions }
+ end
+
+ expect(page).to have_content(actions)
+ end
+ end
+end
diff --git a/spec/components/previews/layouts/page_heading_component_preview.rb b/spec/components/previews/layouts/page_heading_component_preview.rb
new file mode 100644
index 00000000000..6ecd879ad11
--- /dev/null
+++ b/spec/components/previews/layouts/page_heading_component_preview.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Layouts
+ class PageHeadingComponentPreview < ViewComponent::Preview
+ # @param heading text
+ # @param actions text
+ def default(
+ heading: 'Page heading',
+ actions: 'Page actions go here'
+ )
+ render(::Layouts::PageHeadingComponent.new(heading)) do |c|
+ c.with_actions { actions }
+ end
+ end
+ end
+end
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
index 96dab37eeec..ccd7224a9be 100644
--- a/spec/features/dashboard/shortcuts_spec.rb
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -71,6 +71,6 @@ RSpec.describe 'Dashboard shortcuts', :js, feature_category: :shared do
end
def check_page_title(title)
- expect(find('.page-title')).to have_content(title)
+ expect(find_by_testid('page-heading')).to have_content(title)
end
end
diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb
index 246b92615c2..d521d4c1cbe 100644
--- a/spec/features/dashboard/snippets_spec.rb
+++ b/spec/features/dashboard/snippets_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'Dashboard snippets', :js, feature_category: :source_code_managem
it_behaves_like 'paginated snippets'
it 'shows new snippet button in header' do
- parent_element = page.find('.page-title-controls')
+ parent_element = find_by_testid('page-heading-actions')
expect(parent_element).to have_link('New snippet')
end
end
diff --git a/spec/frontend/analytics/cycle_analytics/components/base_spec.js b/spec/frontend/analytics/cycle_analytics/components/base_spec.js
index 81eb38c4529..9ab5e822f88 100644
--- a/spec/frontend/analytics/cycle_analytics/components/base_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/components/base_spec.js
@@ -180,10 +180,10 @@ describe('Value stream analytics component', () => {
});
});
- it('renders a link to the value streams dashboard', () => {
+ it('renders a link to the value streams dashboard using the namespace path', () => {
expect(findOverviewMetrics().props('dashboardsPath')).toBeDefined();
expect(findOverviewMetrics().props('dashboardsPath')).toBe(
- '/groups/foo/-/analytics/dashboards/value_streams_dashboard',
+ '/full/path/to/foo/-/analytics/dashboards/value_streams_dashboard',
);
});
});
diff --git a/spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js
index 8f632ed12c5..78f0cb53e97 100644
--- a/spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js
+++ b/spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js
@@ -11,11 +11,11 @@ describe('EmptyStateWithAnyIssues component', () => {
wrapper = shallowMount(EmptyStateWithAnyIssues, {
propsData: {
hasSearch: true,
+ isEpic: false,
isOpenTab: true,
...props,
},
provide: {
- emptyStateSvgPath: 'empty/state/svg/path',
newIssuePath: 'new/issue/path',
showNewIssueLink: false,
},
@@ -23,42 +23,46 @@ describe('EmptyStateWithAnyIssues component', () => {
};
describe('when there is a search (with no results)', () => {
- beforeEach(() => {
- mountComponent({ hasSearch: true });
- });
-
it('shows empty state', () => {
+ mountComponent({ hasSearch: true });
+
expect(findGlEmptyState().props()).toMatchObject({
description: 'To widen your search, change or remove filters above',
title: 'Sorry, your filter produced no results',
- svgPath: 'empty/state/svg/path',
});
});
});
describe('when "Open" tab is active', () => {
- beforeEach(() => {
- mountComponent({ hasSearch: false, isOpenTab: true });
- });
-
it('shows empty state', () => {
- expect(findGlEmptyState().props()).toMatchObject({
- description: 'To keep this project going, create a new issue',
- title: 'There are no open issues',
- svgPath: 'empty/state/svg/path',
- });
+ mountComponent({ hasSearch: false, isOpenTab: true });
+
+ expect(findGlEmptyState().props('title')).toBe('There are no open issues');
});
});
describe('when "Closed" tab is active', () => {
- beforeEach(() => {
+ it('shows empty state', () => {
mountComponent({ hasSearch: false, isOpenTab: false });
+
+ expect(findGlEmptyState().props('title')).toBe('There are no closed issues');
+ });
+ });
+
+ describe('when epic', () => {
+ describe('when "Open" tab is active', () => {
+ it('shows empty state', () => {
+ mountComponent({ hasSearch: false, isEpic: true, isOpenTab: true });
+
+ expect(findGlEmptyState().props('title')).toBe('There are no open epics');
+ });
});
- it('shows empty state', () => {
- expect(findGlEmptyState().props()).toMatchObject({
- title: 'There are no closed issues',
- svgPath: 'empty/state/svg/path',
+ describe('when "Closed" tab is active', () => {
+ it('shows empty state', () => {
+ mountComponent({ hasSearch: false, isEpic: true, isOpenTab: false });
+
+ expect(findGlEmptyState().props('title')).toBe('There are no closed epics');
});
});
});
diff --git a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
index 956fd485d28..f0790d83f69 100644
--- a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
+++ b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
@@ -17,10 +17,8 @@ describe('EmptyStateWithoutAnyIssues component', () => {
const defaultProvide = {
canCreateProjects: false,
- emptyStateSvgPath: 'empty/state/svg/path',
fullPath: 'full/path',
isSignedIn: true,
- jiraIntegrationPath: 'jira/integration/path',
newIssuePath: 'new/issue/path',
newProjectPath: 'new/project/path',
showNewIssueLink: false,
@@ -64,10 +62,9 @@ describe('EmptyStateWithoutAnyIssues component', () => {
it('renders empty state', () => {
mountComponent();
- expect(findGlEmptyState().props()).toMatchObject({
- title: 'Use issues to collaborate on ideas, solve problems, and plan work',
- svgPath: defaultProvide.emptyStateSvgPath,
- });
+ expect(findGlEmptyState().props('title')).toBe(
+ 'Use issues to collaborate on ideas, solve problems, and plan work',
+ );
});
describe('description', () => {
@@ -280,7 +277,9 @@ describe('EmptyStateWithoutAnyIssues component', () => {
});
it('renders Jira integration docs link', () => {
- expect(findJiraDocsLink().attributes('href')).toBe(defaultProvide.jiraIntegrationPath);
+ expect(findJiraDocsLink().attributes('href')).toBe(
+ '/help/integration/jira/issues#view-jira-issues',
+ );
});
});
@@ -308,7 +307,6 @@ describe('EmptyStateWithoutAnyIssues component', () => {
it('renders empty state', () => {
expect(findGlEmptyState().props()).toMatchObject({
title: 'Use issues to collaborate on ideas, solve problems, and plan work',
- svgPath: defaultProvide.emptyStateSvgPath,
primaryButtonText: 'Register / Sign In',
primaryButtonLink: defaultProvide.signInPath,
});
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index 4f8285537de..c4e86e3e711 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -112,7 +112,6 @@ describe('CE IssuesListApp component', () => {
canCreateProjects: false,
canReadCrmContact: false,
canReadCrmOrganization: false,
- emptyStateSvgPath: 'empty-state.svg',
exportCsvPath: 'export/csv/path',
fullPath: 'path/to/project',
hasAnyIssues: true,
@@ -129,7 +128,6 @@ describe('CE IssuesListApp component', () => {
isProject: true,
isPublicVisibilityRestricted: false,
isSignedIn: true,
- jiraIntegrationPath: 'jira/integration/path',
newIssuePath: 'new/issue/path',
newProjectPath: 'new/project/path',
releasesPath: 'releases/path',
@@ -610,6 +608,7 @@ describe('CE IssuesListApp component', () => {
it('shows EmptyStateWithAnyIssues empty state', () => {
expect(wrapper.findComponent(EmptyStateWithAnyIssues).props()).toEqual({
hasSearch: false,
+ isEpic: false,
isOpenTab: true,
});
});
diff --git a/spec/frontend/vue_shared/components/page_heading_spec.js b/spec/frontend/vue_shared/components/page_heading_spec.js
new file mode 100644
index 00000000000..23e78081ca9
--- /dev/null
+++ b/spec/frontend/vue_shared/components/page_heading_spec.js
@@ -0,0 +1,44 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import PageHeading from '~/vue_shared/components/page_heading.vue';
+
+describe('Pagination links component', () => {
+ const template = `
+
+ Actions go here
+
+ `;
+
+ describe('Ordered Layout', () => {
+ let wrapper;
+
+ const createWrapper = () => {
+ wrapper = shallowMountExtended(PageHeading, {
+ scopedSlots: {
+ actions: template,
+ },
+ propsData: {
+ heading: 'Page heading',
+ },
+ });
+ };
+
+ const heading = () => wrapper.findByTestId('page-heading');
+ const actions = () => wrapper.findByTestId('page-heading-actions');
+
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ describe('rendering', () => {
+ it('renders the correct heading', () => {
+ expect(heading().text()).toBe('Page heading');
+ expect(heading().classes()).toEqual(['gl-heading-1', '!gl-m-0']);
+ expect(heading().element.tagName.toLowerCase()).toBe('h1');
+ });
+
+ it('renders its action slot content', () => {
+ expect(actions().text()).toBe('Actions go here');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
index 17a464b90a7..bfb2156b4a1 100644
--- a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
+++ b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
@@ -9,14 +9,7 @@ import WorkItemParent from '~/work_items/components/work_item_parent.vue';
import WorkItemTimeTracking from '~/work_items/components/work_item_time_tracking.vue';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue';
-import {
- workItemResponseFactory,
- taskType,
- objectiveType,
- keyResultType,
- issueType,
- epicType,
-} from '../mock_data';
+import { workItemResponseFactory } from '../mock_data';
describe('WorkItemAttributesWrapper component', () => {
let wrapper;
@@ -34,11 +27,13 @@ describe('WorkItemAttributesWrapper component', () => {
const createComponent = ({
workItem = workItemQueryResponse.data.workItem,
workItemsBeta = true,
+ groupPath = '',
} = {}) => {
wrapper = shallowMount(WorkItemAttributesWrapper, {
propsData: {
fullPath: 'group/project',
workItem,
+ groupPath,
},
provide: {
hasIssueWeightsFeature: true,
@@ -140,26 +135,9 @@ describe('WorkItemAttributesWrapper component', () => {
});
describe('parent widget', () => {
- describe.each`
- description | workItemType | exists
- ${'when work item type is task'} | ${taskType} | ${true}
- ${'when work item type is objective'} | ${objectiveType} | ${true}
- ${'when work item type is key result'} | ${keyResultType} | ${true}
- ${'when work item type is issue'} | ${issueType} | ${true}
- ${'when work item type is epic'} | ${epicType} | ${true}
- `('$description', ({ workItemType, exists }) => {
- it(`${exists ? 'renders' : 'does not render'} parent component`, async () => {
- const response = workItemResponseFactory({ workItemType });
- createComponent({ workItem: response.data.workItem });
-
- await waitForPromises();
-
- expect(findWorkItemParent().exists()).toBe(exists);
- });
- });
-
- it('renders WorkItemParent when workItemsBeta enabled', async () => {
- createComponent();
+ it(`renders parent component with proper data`, async () => {
+ const response = workItemResponseFactory();
+ createComponent({ workItem: response.data.workItem });
await waitForPromises();
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index 30feddcbb1b..64d699565ec 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -137,6 +137,7 @@ describe('WorkItemDetail component', () => {
hasIssuableHealthStatusFeature: true,
projectNamespace: 'namespace',
fullPath: 'group/project',
+ groupPath: 'group',
isGroup,
reportAbusePath: '/report/abuse/path',
},
diff --git a/spec/frontend/work_items/components/work_item_parent_spec.js b/spec/frontend/work_items/components/work_item_parent_spec.js
index 3462b3db75b..7a360c060db 100644
--- a/spec/frontend/work_items/components/work_item_parent_spec.js
+++ b/spec/frontend/work_items/components/work_item_parent_spec.js
@@ -292,6 +292,7 @@ describe('WorkItemParent component', () => {
isNumber: false,
searchByIid: false,
searchByText: true,
+ includeAncestors: true,
});
await findCollapsibleListbox().vm.$emit('search', 'Objective 101');
@@ -305,6 +306,7 @@ describe('WorkItemParent component', () => {
isNumber: false,
searchByIid: false,
searchByText: true,
+ includeAncestors: true,
});
await nextTick();
diff --git a/spec/frontend/work_items/list/components/work_items_list_app_spec.js b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
index dcb532c7f77..89644df305d 100644
--- a/spec/frontend/work_items/list/components/work_items_list_app_spec.js
+++ b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
@@ -1,3 +1,4 @@
+import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
import Vue, { nextTick } from 'vue';
@@ -33,6 +34,7 @@ jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
jest.mock('~/sentry/sentry_browser_wrapper');
describe('WorkItemsListApp component', () => {
+ /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */
let wrapper;
Vue.use(VueApollo);
@@ -63,44 +65,62 @@ describe('WorkItemsListApp component', () => {
});
};
- it('renders IssuableList component', () => {
+ it('renders loading icon when initially fetching work items', () => {
mountComponent();
- expect(findIssuableList().props()).toMatchObject({
- currentTab: STATUS_OPEN,
- error: '',
- initialSortBy: CREATED_DESC,
- issuables: [],
- issuablesLoading: true,
- namespace: 'work-items',
- recentSearchesStorageKey: 'issues',
- showWorkItemTypeIcon: true,
- sortOptions,
- tabs: WorkItemsListApp.issuableListTabs,
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ describe('when work items are fetched', () => {
+ beforeEach(async () => {
+ mountComponent();
+ await waitForPromises();
});
- });
- it('renders tab counts', async () => {
- mountComponent();
- await waitForPromises();
-
- expect(cloneDeep(findIssuableList().props('tabCounts'))).toEqual({
- all: 3,
- closed: 1,
- opened: 2,
+ it('renders IssuableList component', () => {
+ expect(findIssuableList().props()).toMatchObject({
+ currentTab: STATUS_OPEN,
+ error: '',
+ initialSortBy: CREATED_DESC,
+ namespace: 'work-items',
+ recentSearchesStorageKey: 'issues',
+ showWorkItemTypeIcon: true,
+ sortOptions,
+ tabs: WorkItemsListApp.issuableListTabs,
+ });
});
- });
- it('renders IssueCardStatistics component', () => {
- mountComponent();
+ it('renders tab counts', () => {
+ expect(cloneDeep(findIssuableList().props('tabCounts'))).toEqual({
+ all: 3,
+ closed: 1,
+ opened: 2,
+ });
+ });
- expect(findIssueCardStatistics().exists()).toBe(true);
- });
+ it('renders IssueCardStatistics component', () => {
+ expect(findIssueCardStatistics().exists()).toBe(true);
+ });
- it('renders IssueCardTimeInfo component', () => {
- mountComponent();
+ it('renders IssueCardTimeInfo component', () => {
+ expect(findIssueCardTimeInfo().exists()).toBe(true);
+ });
- expect(findIssueCardTimeInfo().exists()).toBe(true);
+ it('renders work items', () => {
+ expect(findIssuableList().props('issuables')).toEqual(
+ groupWorkItemsQueryResponse.data.group.workItems.nodes,
+ );
+ });
+
+ it('calls query to fetch work items', () => {
+ expect(defaultQueryHandler).toHaveBeenCalledWith({
+ fullPath: 'full/path',
+ sort: CREATED_DESC,
+ state: STATUS_OPEN,
+ firstPageSize: 20,
+ types: [null],
+ });
+ });
});
describe('pagination controls', () => {
@@ -122,53 +142,30 @@ describe('WorkItemsListApp component', () => {
});
});
- it('renders work items', async () => {
- mountComponent();
- await waitForPromises();
+ describe('when workItemType is provided', () => {
+ it('filters work items by workItemType', () => {
+ const type = 'EPIC';
+ mountComponent({ provide: { workItemType: type } });
- expect(findIssuableList().props('issuables')).toEqual(
- groupWorkItemsQueryResponse.data.group.workItems.nodes,
- );
- });
-
- it('fetches work items', () => {
- mountComponent();
-
- expect(defaultQueryHandler).toHaveBeenCalledWith({
- fullPath: 'full/path',
- sort: CREATED_DESC,
- state: STATUS_OPEN,
- firstPageSize: 20,
- types: [null],
- });
- });
-
- it('filters work items by workItemType', () => {
- const type = 'EPIC';
- mountComponent({
- provide: {
- workItemType: type,
- },
- });
-
- expect(defaultQueryHandler).toHaveBeenCalledWith({
- fullPath: 'full/path',
- sort: CREATED_DESC,
- state: STATUS_OPEN,
- firstPageSize: 20,
- types: [type],
+ expect(defaultQueryHandler).toHaveBeenCalledWith({
+ fullPath: 'full/path',
+ sort: CREATED_DESC,
+ state: STATUS_OPEN,
+ firstPageSize: 20,
+ types: [type],
+ });
});
});
describe('when there is an error fetching work items', () => {
+ const message = 'Something went wrong when fetching work items. Please try again.';
+
beforeEach(async () => {
mountComponent({ queryHandler: jest.fn().mockRejectedValue(new Error('ERROR')) });
await waitForPromises();
});
it('renders an error message', () => {
- const message = 'Something went wrong when fetching work items. Please try again.';
-
expect(findIssuableList().props('error')).toBe(message);
expect(Sentry.captureException).toHaveBeenCalledWith(new Error('ERROR'));
});
@@ -177,7 +174,22 @@ describe('WorkItemsListApp component', () => {
findIssuableList().vm.$emit('dismiss-alert');
await nextTick();
- expect(findIssuableList().props('error')).toBe('');
+ expect(wrapper.text()).not.toContain(message);
+ });
+ });
+
+ describe('watcher', () => {
+ describe('when eeCreatedWorkItemsCount is updated', () => {
+ it('refetches work items', async () => {
+ mountComponent();
+ await waitForPromises();
+
+ expect(defaultQueryHandler).toHaveBeenCalledTimes(1);
+
+ await wrapper.setProps({ eeCreatedWorkItemsCount: 1 });
+
+ expect(defaultQueryHandler).toHaveBeenCalledTimes(2);
+ });
});
});
@@ -189,7 +201,7 @@ describe('WorkItemsListApp component', () => {
avatar_url: 'avatar/url',
};
- beforeEach(() => {
+ beforeEach(async () => {
window.gon = {
current_user_id: mockCurrentUser.id,
current_user_fullname: mockCurrentUser.name,
@@ -197,6 +209,7 @@ describe('WorkItemsListApp component', () => {
current_user_avatar_url: mockCurrentUser.avatar_url,
};
mountComponent();
+ await waitForPromises();
});
it('renders all tokens', () => {
@@ -228,6 +241,7 @@ describe('WorkItemsListApp component', () => {
describe('when "filter" event is emitted by IssuableList', () => {
it('fetches filtered work items', async () => {
mountComponent();
+ await waitForPromises();
findIssuableList().vm.$emit('filter', [
{ type: FILTERED_SEARCH_TERM, value: { data: 'find issues', operator: 'undefined' } },
@@ -280,6 +294,7 @@ describe('WorkItemsListApp component', () => {
} else {
mountComponent();
}
+ await waitForPromises();
findIssuableList().vm.$emit('sort', sortKey);
await waitForPromises();
@@ -291,9 +306,10 @@ describe('WorkItemsListApp component', () => {
);
describe('when user is signed in', () => {
- it('calls mutation to save sort preference', () => {
+ it('calls mutation to save sort preference', async () => {
const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
mountComponent({ sortPreferenceMutationResponse: mutationMock });
+ await waitForPromises();
findIssuableList().vm.$emit('sort', UPDATED_DESC);
@@ -305,6 +321,7 @@ describe('WorkItemsListApp component', () => {
.fn()
.mockResolvedValue(setSortPreferenceMutationResponseWithErrors);
mountComponent({ sortPreferenceMutationResponse: mutationMock });
+ await waitForPromises();
findIssuableList().vm.$emit('sort', UPDATED_DESC);
await waitForPromises();
@@ -314,12 +331,13 @@ describe('WorkItemsListApp component', () => {
});
describe('when user is signed out', () => {
- it('does not call mutation to save sort preference', () => {
+ it('does not call mutation to save sort preference', async () => {
const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
mountComponent({
provide: { isSignedIn: false },
sortPreferenceMutationResponse: mutationMock,
});
+ await waitForPromises();
findIssuableList().vm.$emit('sort', CREATED_DESC);
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index aad2369ca6e..c0feb5796c5 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -3970,6 +3970,29 @@ export const groupWorkItemsQueryResponse = {
},
};
+export const emptyGroupWorkItemsQueryResponse = {
+ data: {
+ group: {
+ id: 'gid://gitlab/Group/3',
+ workItemStateCounts: {
+ all: 0,
+ closed: 0,
+ opened: 0,
+ },
+ workItems: {
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'startCursor',
+ endCursor: 'endCursor',
+ __typename: 'PageInfo',
+ },
+ nodes: [],
+ },
+ },
+ },
+};
+
export const updateWorkItemMutationResponseFactory = (options) => {
const response = workItemResponseFactory(options);
return {
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index c44e72323b0..475497f95bc 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -41,6 +41,7 @@ describe('Work items router', () => {
router,
provide: {
fullPath: 'full-path',
+ groupPath: '',
isGroup: false,
issuesListPath: 'full-path/-/issues',
hasIssueWeightsFeature: false,
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index f26cac05490..0a0d659f8e2 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -213,7 +213,6 @@ RSpec.describe IssuesHelper, feature_category: :team_planning do
can_import_issues: 'true',
email: current_user&.notification_email_or_default,
emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
- empty_state_svg_path: '#',
export_csv_path: export_csv_project_issues_path(project),
full_path: project.full_path,
has_any_issues: project_issues(project).exists?.to_s,
@@ -224,7 +223,6 @@ RSpec.describe IssuesHelper, feature_category: :team_planning do
is_project: 'true',
is_public_visibility_restricted: Gitlab::CurrentSettings.restricted_visibility_levels ? 'false' : '',
is_signed_in: current_user.present?.to_s,
- jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
markdown_help_path: help_page_path('user/markdown'),
max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes),
new_issue_path: new_project_issue_path(project),
@@ -281,12 +279,10 @@ RSpec.describe IssuesHelper, feature_category: :team_planning do
autocomplete_award_emojis_path: autocomplete_award_emojis_path,
calendar_path: '#',
can_create_projects: 'true',
- empty_state_svg_path: '#',
full_path: group.full_path,
has_any_issues: false.to_s,
has_any_projects: true.to_s,
is_signed_in: current_user.present?.to_s,
- jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
new_project_path: new_project_path(namespace_id: group.id),
rss_path: '#',
sign_in_path: new_user_session_path,
diff --git a/spec/helpers/work_items_helper_spec.rb b/spec/helpers/work_items_helper_spec.rb
index 587faa8dfca..2f2af548b52 100644
--- a/spec/helpers/work_items_helper_spec.rb
+++ b/spec/helpers/work_items_helper_spec.rb
@@ -3,6 +3,7 @@
require "spec_helper"
RSpec.describe WorkItemsHelper, feature_category: :team_planning do
+ include Devise::Test::ControllerHelpers
describe '#work_items_show_data' do
subject(:work_items_show_data) { helper.work_items_show_data(project) }
@@ -12,6 +13,7 @@ RSpec.describe WorkItemsHelper, feature_category: :team_planning do
expect(work_items_show_data).to include(
{
full_path: project.full_path,
+ group_path: nil,
issues_list_path: project_issues_path(project),
register_path: new_user_registration_path(redirect_to_referer: 'yes'),
sign_in_path: user_session_path(redirect_to_referer: 'yes'),
@@ -21,6 +23,21 @@ RSpec.describe WorkItemsHelper, feature_category: :team_planning do
}
)
end
+
+ context 'when project is under a group' do
+ let(:group) { build(:group) }
+ let(:group_project) { build(:project, group: group) }
+
+ subject(:work_items_show_data) { helper.work_items_show_data(group_project) }
+
+ it 'returns the expected group_path' do
+ expect(work_items_show_data).to include(
+ {
+ group_path: group_project.group.full_path
+ }
+ )
+ end
+ end
end
describe '#work_items_list_data' do
@@ -32,12 +49,14 @@ RSpec.describe WorkItemsHelper, feature_category: :team_planning do
it 'returns expected data' do
allow(helper).to receive(:current_user).and_return(current_user)
+ allow(helper).to receive(:can?).and_return(true)
expect(work_items_list_data).to include(
{
full_path: group.full_path,
initial_sort: current_user&.user_preference&.issues_sort,
- is_signed_in: current_user.present?.to_s
+ is_signed_in: current_user.present?.to_s,
+ show_new_issue_link: 'true'
}
)
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
index b18597807cc..d1044907801 100644
--- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics, feature_category: :shar
worker: 'MergeWorker',
urgency: 'high',
external_dependencies: 'no',
- feature_category: 'source_code_management',
+ feature_category: 'code_review_workflow',
boundary: '',
job_status: 'done',
destination_shard_redis: 'main' })
@@ -35,7 +35,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics, feature_category: :shar
worker: 'MergeWorker',
urgency: 'high',
external_dependencies: 'no',
- feature_category: 'source_code_management',
+ feature_category: 'code_review_workflow',
boundary: '',
job_status: 'fail',
destination_shard_redis: 'main' })
@@ -103,7 +103,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics, feature_category: :shar
{
worker: 'MergeWorker',
urgency: 'high',
- feature_category: 'source_code_management',
+ feature_category: 'code_review_workflow',
external_dependencies: 'no',
queue: 'merge',
destination_shard_redis: 'main'
diff --git a/spec/models/ci/partition_spec.rb b/spec/models/ci/partition_spec.rb
index 729e3a2c2aa..09b5ba3e2b3 100644
--- a/spec/models/ci/partition_spec.rb
+++ b/spec/models/ci/partition_spec.rb
@@ -95,6 +95,26 @@ RSpec.describe Ci::Partition, feature_category: :ci_scaling do
end
end
end
+
+ describe '.provisioning' do
+ subject(:provisioning) { described_class.provisioning(ci_partition.id) }
+
+ let!(:next_ci_partition) { create(:ci_partition) }
+
+ context 'when one partition is preparing' do
+ it { is_expected.to eq(next_ci_partition) }
+ end
+
+ context 'when multiple partitions are preparing' do
+ before do
+ create_list(:ci_partition, 2)
+ end
+
+ it 'returns the first ci_partition with status preparing' do
+ expect(provisioning).to eq(next_ci_partition)
+ end
+ end
+ end
end
describe 'state machine' do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index d789f262016..eccc0ee1e63 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -5805,29 +5805,53 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
describe '.current_partition_value' do
subject { described_class.current_partition_value }
- it { is_expected.to eq(102) }
+ context 'when not using ci partitioning automation' do
+ before do
+ stub_feature_flags(ci_partitioning_automation: false)
+ end
- it 'accepts an optional argument' do
- expect(described_class.current_partition_value(build_stubbed(:project))).to eq(102)
+ it { is_expected.to eq(102) }
+
+ it 'accepts an optional argument' do
+ expect(described_class.current_partition_value(build_stubbed(:project))).to eq(102)
+ end
+
+ it 'returns 100 when the flags are disabled' do
+ stub_feature_flags(ci_current_partition_value_101: false)
+ stub_feature_flags(ci_current_partition_value_102: false)
+
+ is_expected.to eq(100)
+ end
+
+ it 'returns 101 when the 102 flag is disabled' do
+ stub_feature_flags(ci_current_partition_value_102: false)
+
+ is_expected.to eq(101)
+ end
+
+ it 'returns 102 when the 101 flag is disabled' do
+ stub_feature_flags(ci_current_partition_value_101: false)
+
+ is_expected.to eq(102)
+ end
end
- it 'returns 100 when the flags are disabled' do
- stub_feature_flags(ci_current_partition_value_101: false)
- stub_feature_flags(ci_current_partition_value_102: false)
+ context 'when using ci partitioning automation' do
+ context 'when current ci_partition exists' do
+ before do
+ create(:ci_partition, :current)
+ end
- is_expected.to eq(100)
- end
+ it 'return the current partition value' do
+ expect(subject).to eq(Ci::Partition.current.id)
+ end
+ end
- it 'returns 101 when the 102 flag is disabled' do
- stub_feature_flags(ci_current_partition_value_102: false)
-
- is_expected.to eq(101)
- end
-
- it 'returns 102 when the 101 flag is disabled' do
- stub_feature_flags(ci_current_partition_value_101: false)
-
- is_expected.to eq(102)
+ context 'when current ci_partition does not exist' do
+ it 'return the default initial value' do
+ expect(subject).to eq(102)
+ end
+ end
end
end
diff --git a/spec/models/concerns/ci/partitionable/organizer_spec.rb b/spec/models/concerns/ci/partitionable/organizer_spec.rb
deleted file mode 100644
index 370e3b001c9..00000000000
--- a/spec/models/concerns/ci/partitionable/organizer_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Ci::Partitionable::Organizer, feature_category: :continuous_integration do
- describe '.create_database_partition?' do
- subject(:create_database_partition) { described_class.create_database_partition?(database_partition) }
-
- let(:parent_table_name) { 'p_ci_pipeline_variables' }
- let(:partition_name) { 'ci_pipeline_variables_102' }
- let(:schema) { 'gitlab_partitions_dynamic' }
- let(:database_partition) do
- Gitlab::Database::Partitioning::MultipleNumericListPartition.new(
- parent_table_name,
- [Ci::Pipeline::NEXT_PARTITION_VALUE],
- partition_name: partition_name,
- schema: schema
- )
- end
-
- context 'when partition size is greater than the current partition size' do
- it { is_expected.to eq(false) }
- end
-
- context 'when partition size is less than the current partition size' do
- before do
- allow(database_partition).to receive_message_chain(:values,
- :max).and_return(Ci::Pipeline::INITIAL_PARTITION_VALUE)
- end
-
- it { is_expected.to eq(true) }
- end
- end
-end
diff --git a/spec/models/concerns/ci/partitionable_spec.rb b/spec/models/concerns/ci/partitionable_spec.rb
index 09ae85ea338..473ca5876b7 100644
--- a/spec/models/concerns/ci/partitionable_spec.rb
+++ b/spec/models/concerns/ci/partitionable_spec.rb
@@ -96,38 +96,58 @@ RSpec.describe Ci::Partitionable, feature_category: :continuous_integration do
subject(:value) { partitioning_strategy.next_partition_if.call(active_partition) }
- context 'without any existing partitions' do
- it { is_expected.to eq(true) }
- end
-
- context 'with initial partition attached' do
+ context 'when not using ci partitioning automation' do
before do
- ci_model.connection.execute(<<~SQL)
- CREATE TABLE IF NOT EXISTS _test_table_name_100 PARTITION OF _test_table_name FOR VALUES IN (100);
- SQL
+ stub_feature_flags(ci_partitioning_automation: false)
end
- it { is_expected.to eq(true) }
- end
-
- context 'with an existing partition for partition_id = 101' do
- before do
- ci_model.connection.execute(<<~SQL)
- CREATE TABLE IF NOT EXISTS _test_table_name_101 PARTITION OF _test_table_name FOR VALUES IN (101);
- SQL
+ context 'without any existing partitions' do
+ it { is_expected.to eq(true) }
end
- it { is_expected.to eq(false) }
- end
+ context 'with initial partition attached' do
+ before do
+ ci_model.connection.execute(<<~SQL)
+ CREATE TABLE IF NOT EXISTS _test_table_name_100 PARTITION OF _test_table_name FOR VALUES IN (100);
+ SQL
+ end
- context 'with an existing partition for partition_id in 100, 101' do
- before do
- ci_model.connection.execute(<<~SQL)
- CREATE TABLE IF NOT EXISTS _test_table_name_101 PARTITION OF _test_table_name FOR VALUES IN (100, 101);
- SQL
+ it { is_expected.to eq(true) }
end
- it { is_expected.to eq(false) }
+ context 'with an existing partition for partition_id = 101' do
+ before do
+ ci_model.connection.execute(<<~SQL)
+ CREATE TABLE IF NOT EXISTS _test_table_name_101 PARTITION OF _test_table_name FOR VALUES IN (101);
+ SQL
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with an existing partition for partition_id in 100, 101' do
+ before do
+ ci_model.connection.execute(<<~SQL)
+ CREATE TABLE IF NOT EXISTS _test_table_name_101 PARTITION OF _test_table_name FOR VALUES IN (100, 101);
+ SQL
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when using ci partitioning automation' do
+ context 'when current ci_partition exists' do
+ before do
+ create_list(:ci_partition, 2)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when current ci_partition does not exist' do
+ it { is_expected.to eq(false) }
+ end
end
end
end
diff --git a/spec/requests/api/graphql/pages/delete_deployments_mutation_spec.rb b/spec/requests/api/graphql/pages/delete_deployments_mutation_spec.rb
index 4e7bdb9924b..7db86f7ec19 100644
--- a/spec/requests/api/graphql/pages/delete_deployments_mutation_spec.rb
+++ b/spec/requests/api/graphql/pages/delete_deployments_mutation_spec.rb
@@ -15,6 +15,11 @@ RSpec.describe 'DeletePagesDeployment mutation', feature_category: :pages do
<<~GRAPHQL
mutation DeletePagesDeployment {
deletePagesDeployment(input: { id: "#{pages_deployment_id}" }) {
+ pagesDeployment {
+ id
+ active
+ deletedAt
+ }
errors
}
}
@@ -36,6 +41,22 @@ RSpec.describe 'DeletePagesDeployment mutation', feature_category: :pages do
it 'does not throw an error' do
expect(graphql_errors).to be_nil
end
+
+ describe 'returned pages deployment', :freeze_time do
+ let(:returned_pages_deployment) { graphql_data_at(:deletePagesDeployment, :pages_deployment) }
+
+ it 'has the correct ID' do
+ expect(returned_pages_deployment["id"]).to eq(pages_deployment.to_global_id.to_s)
+ end
+
+ it 'has attribute active:false' do
+ expect(returned_pages_deployment["active"]).to be(false)
+ end
+
+ it 'has deleted_at set to the deletion time' do
+ expect(returned_pages_deployment["deletedAt"]).to eq(Time.now.utc.iso8601)
+ end
+ end
end
describe 'user is not authorized' do
diff --git a/spec/services/ci/partitions/setup_default_service_spec.rb b/spec/services/ci/partitions/setup_default_service_spec.rb
index 9a415c80919..b864a216f06 100644
--- a/spec/services/ci/partitions/setup_default_service_spec.rb
+++ b/spec/services/ci/partitions/setup_default_service_spec.rb
@@ -22,6 +22,16 @@ RSpec.describe Ci::Partitions::SetupDefaultService, feature_category: :ci_scalin
end
end
+ context 'when current ci_partition exists' do
+ let!(:current_partition) { create(:ci_partition, :current) }
+
+ it 'does not set up default values for ci_partitions' do
+ expect(service).not_to receive(:setup_default_partitions)
+
+ execute
+ end
+ end
+
context 'when default ci_partitions do not exist' do
it 'creates the default partitions', :aggregate_failures do
expect { execute }.to change { Ci::Partition.count }.by(3)
diff --git a/spec/tasks/gitlab/feature_categories_rake_spec.rb b/spec/tasks/gitlab/feature_categories_rake_spec.rb
index 1dee72eee46..4b65a0b9fe3 100644
--- a/spec/tasks/gitlab/feature_categories_rake_spec.rb
+++ b/spec/tasks/gitlab/feature_categories_rake_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe 'gitlab:feature_categories:index', :silence_stdout, feature_categ
)
),
'sidekiq_workers' => a_hash_including(
- 'source_code_management' => a_collection_including(
+ 'code_review_workflow' => a_collection_including(
klass: 'MergeWorker',
source_location: [
'app/workers/merge_worker.rb',
diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb
index 6e1beff68f0..e4aa5726331 100644
--- a/spec/workers/merge_worker_spec.rb
+++ b/spec/workers/merge_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeWorker, feature_category: :source_code_management do
+RSpec.describe MergeWorker, feature_category: :code_review_workflow do
describe "remove source branch" do
let!(:merge_request) { create(:merge_request, source_branch: "markdown") }
let!(:source_project) { merge_request.source_project }
diff --git a/yarn.lock b/yarn.lock
index f2f4e83f2ce..96a7f369e01 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7670,10 +7670,10 @@ graphql@^15.7.2:
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.7.2.tgz#85ab0eeb83722977151b3feb4d631b5f2ab287ef"
integrity sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A==
-gridstack@^10.1.2:
- version "10.1.2"
- resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.1.2.tgz#58b5ae0057a8aa5e4f6563041c4ca2def3aa4268"
- integrity sha512-Nn27XGQ68WtBC513cKQQ4t/dA2uuN/xnNUU50puXEJv6IFk5SzT0Dnsq68GpopO1n0tXUKZKm1Rw7uOUMDz1KQ==
+gridstack@^10.2.0:
+ version "10.2.0"
+ resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.2.0.tgz#4ba9c7ee69a730851721a9f5cb33dc55026ded1f"
+ integrity sha512-svKAOq/dfinpvhe/nnxdyZOOEd9qynXiOPHvL96PALE0yWChWp/6lechnqKwud0tL/rRyAfMJ6Hh/z2fS13pBA==
gzip-size@^6.0.0:
version "6.0.0"