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--.rubocop_todo/style/guard_clause.yml1
-rw-r--r--.rubocop_todo/style/redundant_self.yml1
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.checksum5
-rw-r--r--Gemfile.lock11
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue2
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_actions.vue23
-rw-r--r--app/assets/javascripts/work_items/components/work_item_created_updated.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue114
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state_toggle.vue (renamed from app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue)35
-rw-r--r--app/assets/javascripts/work_items/components/work_item_todos.vue2
-rw-r--r--app/assets/javascripts/work_items/constants.js1
-rw-r--r--config/feature_flags/development/use_primary_and_secondary_stores_for_shared_state.yml8
-rw-r--r--config/feature_flags/development/use_primary_store_as_default_for_shared_state.yml8
-rw-r--r--db/post_migrate/20220531233600_remove_sse_usage_data_from_redis.rb2
-rw-r--r--db/post_migrate/20230317004428_migrate_daily_redis_hll_events_to_weekly_aggregation.rb2
-rw-r--r--db/post_migrate/20230328111013_re_migrate_redis_slot_keys.rb2
-rw-r--r--doc/administration/gitaly/configure_gitaly.md39
-rw-r--r--doc/administration/gitaly/troubleshooting.md37
-rw-r--r--doc/user/application_security/dast/browser_based.md31
-rw-r--r--lib/gitlab/database/schema_cache_with_renamed_table.rb4
-rw-r--r--lib/gitlab/redis/multi_store.rb36
-rw-r--r--lib/gitlab/redis/shared_state.rb6
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/frontend/work_items/components/notes/work_item_comment_form_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_actions_spec.js24
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js40
-rw-r--r--spec/frontend/work_items/components/work_item_state_toggle_button_spec.js4
-rw-r--r--spec/helpers/sorting_helper_spec.rb2
-rw-r--r--spec/lib/gitlab/database/tables_truncate_spec.rb4
-rw-r--r--spec/lib/gitlab/job_waiter_spec.rb55
-rw-r--r--spec/lib/gitlab/redis/multi_store_spec.rb158
-rw-r--r--spec/support/db_cleaner.rb2
34 files changed, 554 insertions, 127 deletions
diff --git a/.rubocop_todo/style/guard_clause.yml b/.rubocop_todo/style/guard_clause.yml
index 47b6fecfdc1..7c70fde48f3 100644
--- a/.rubocop_todo/style/guard_clause.yml
+++ b/.rubocop_todo/style/guard_clause.yml
@@ -309,7 +309,6 @@ Style/GuardClause:
- 'ee/app/models/ee/project_group_link.rb'
- 'ee/app/models/ee/project_member.rb'
- 'ee/app/models/ee/user.rb'
- - 'ee/app/models/elasticsearch_indexed_project.rb'
- 'ee/app/models/epic/related_epic_link.rb'
- 'ee/app/models/epic_issue.rb'
- 'ee/app/models/geo_node.rb'
diff --git a/.rubocop_todo/style/redundant_self.yml b/.rubocop_todo/style/redundant_self.yml
index 481b6b756c0..7653091a3f6 100644
--- a/.rubocop_todo/style/redundant_self.yml
+++ b/.rubocop_todo/style/redundant_self.yml
@@ -172,7 +172,6 @@ Style/RedundantSelf:
- 'ee/app/models/concerns/ee/protected_ref.rb'
- 'ee/app/models/concerns/ee/protected_ref_access.rb'
- 'ee/app/models/concerns/elastic/application_versioned_search.rb'
- - 'ee/app/models/concerns/elastic/projects_search.rb'
- 'ee/app/models/concerns/elasticsearch_indexed_container.rb'
- 'ee/app/models/concerns/geo/replicable_model.rb'
- 'ee/app/models/concerns/geo/repository_replicator_strategy.rb'
diff --git a/Gemfile b/Gemfile
index 68010756679..7a6bb68e8bf 100644
--- a/Gemfile
+++ b/Gemfile
@@ -410,7 +410,7 @@ group :development, :test do
gem 'awesome_print', require: false # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'database_cleaner', '~> 1.7.0' # rubocop:todo Gemfile/MissingFeatureCategory
+ gem 'database_cleaner-active_record', '~> 2.1.0', feature_category: :database
gem 'factory_bot_rails', '~> 6.2.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'rspec-rails', '~> 6.0.3' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -448,7 +448,7 @@ group :development, :test do
end
group :development, :test, :danger do
- gem 'gitlab-dangerfiles', '~> 4.5.1', require: false, feature_category: :tooling
+ gem 'gitlab-dangerfiles', '~> 4.6.0', require: false, feature_category: :tooling
end
group :development, :test, :coverage do
diff --git a/Gemfile.checksum b/Gemfile.checksum
index cc5c849c8ec..af51bee0f08 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -99,7 +99,8 @@
{"name":"danger","version":"9.3.1","platform":"ruby","checksum":"9070fbac181eb45fb9b69ea25e6ea4faa86796ef33bf8d00346cab4385e51df5"},
{"name":"danger-gitlab","version":"8.0.0","platform":"ruby","checksum":"497dd7d0f6513913de651019223d8058cf494df10acbd17de92b175dfa04a3a8"},
{"name":"dartsass","version":"1.49.8","platform":"ruby","checksum":"267e7262a5655c8f0baa1ef663e976252bdbfa8bbf40c175153544a2dc8e1345"},
-{"name":"database_cleaner","version":"1.7.0","platform":"ruby","checksum":"bdf833c197afac7054015bcde2567c3834c366bbfe6a377c30151ca984b32016"},
+{"name":"database_cleaner-active_record","version":"2.1.0","platform":"ruby","checksum":"7384b973d67bcc1b5a850b876a4638aa83cca3bc88f9d87562fe25cd2dd60d8a"},
+{"name":"database_cleaner-core","version":"2.0.1","platform":"ruby","checksum":"8646574c32162e59ed7b5258a97a208d3c44551b854e510994f24683865d846c"},
{"name":"date","version":"3.3.3","platform":"java","checksum":"584e0a582d1eb2207b4eaac089d8a43f2ca10bea02682f286099642f15c56cce"},
{"name":"date","version":"3.3.3","platform":"ruby","checksum":"819792019d5712b748fb15f6dfaaedef14b0328723ef23583ea35f186774530f"},
{"name":"dead_end","version":"3.1.1","platform":"ruby","checksum":"1011df7f7c0149be004e11cbbc37747760227c55305cd902fd3c06e1394b2f5b"},
@@ -209,7 +210,7 @@
{"name":"gitaly","version":"16.5.0.pre.rc1","platform":"ruby","checksum":"ed17515ad04d4663a0efc15c8f2887b705f006133e8b10cc9321460eb0a38353"},
{"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"},
{"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"},
-{"name":"gitlab-dangerfiles","version":"4.5.1","platform":"ruby","checksum":"819212207b8dcb5dcf89901788c456a953316bc519563d4acf7c8eb601e197cf"},
+{"name":"gitlab-dangerfiles","version":"4.6.0","platform":"ruby","checksum":"441b37b17d1dad36268517490a30aaf57e43dffb2e9ebc1da38d3bc9fa20741e"},
{"name":"gitlab-experiment","version":"0.8.0","platform":"ruby","checksum":"b4e2f73e0af19cdd899a745f5a846c1318d44054e068a8f4ac887f6b1017d3f9"},
{"name":"gitlab-fog-azure-rm","version":"1.8.0","platform":"ruby","checksum":"e4f24b174b273b88849d12fbcfecb79ae1c09f56cbd614998714c7f0a81e6c28"},
{"name":"gitlab-labkit","version":"0.34.0","platform":"ruby","checksum":"ca5c504201390cd07ba1029e6ca3059f4e2e6005eb121ba8a103af1e166a3ecd"},
diff --git a/Gemfile.lock b/Gemfile.lock
index e8ff3d24c35..9f2b7608041 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -415,7 +415,10 @@ GEM
danger
gitlab (~> 4.2, >= 4.2.0)
dartsass (1.49.8)
- database_cleaner (1.7.0)
+ database_cleaner-active_record (2.1.0)
+ activerecord (>= 5.a)
+ database_cleaner-core (~> 2.0.0)
+ database_cleaner-core (2.0.1)
date (3.3.3)
dead_end (3.1.1)
debug_inspector (1.1.0)
@@ -655,7 +658,7 @@ GEM
terminal-table (>= 1.5.1)
gitlab-chronic (0.10.5)
numerizer (~> 0.2)
- gitlab-dangerfiles (4.5.1)
+ gitlab-dangerfiles (4.6.0)
danger (>= 9.3.0)
danger-gitlab (>= 8.0.0)
rake (~> 13.0)
@@ -1789,7 +1792,7 @@ DEPENDENCIES
crystalball (~> 0.7.0)
csv_builder!
cvss-suite (~> 3.0.1)
- database_cleaner (~> 1.7.0)
+ database_cleaner-active_record (~> 2.1.0)
deckar01-task_list (= 2.3.3)
declarative_policy (~> 1.1.0)
deprecation_toolkit (~> 1.5.1)
@@ -1832,7 +1835,7 @@ DEPENDENCIES
gettext_i18n_rails_js (~> 2.0.0)
gitaly (~> 16.5.0.pre.rc1)
gitlab-chronic (~> 0.10.5)
- gitlab-dangerfiles (~> 4.5.1)
+ gitlab-dangerfiles (~> 4.6.0)
gitlab-experiment (~> 0.8.0)
gitlab-fog-azure-rm (~> 1.8.0)
gitlab-http!
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index c50b8009284..32df19dfe44 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -468,7 +468,7 @@ export default {
><template #list-item>{{ copyMailAddressText }}</template></gl-disclosure-dropdown-item
>
</template>
- <gl-dropdown-divider v-if="showToggleIssueStateButton || canDestroyIssue || canReportSpam" />
+ <gl-dropdown-divider v-if="canDestroyIssue || canReportSpam || !isIssueAuthor" />
<gl-disclosure-dropdown-item
v-if="canReportSpam"
:item="submitSpamItem"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
index 62e1ebc50a8..1e6bd9ff1ac 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
@@ -8,7 +8,7 @@ import { STATE_OPEN, TRACKING_CATEGORY_SHOW, TASK_TYPE_NAME } from '~/work_items
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
+import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
import CommentFieldLayout from '~/notes/components/comment_field_layout.vue';
export default {
@@ -29,7 +29,7 @@ export default {
MarkdownEditor,
GlFormCheckbox,
GlIcon,
- WorkItemStateToggleButton,
+ WorkItemStateToggle,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -229,7 +229,7 @@ export default {
@click="$emit('submitForm', { commentText, isNoteInternal })"
>{{ commentButtonTextComputed }}
</gl-button>
- <work-item-state-toggle-button
+ <work-item-state-toggle
v-if="isNewDiscussion"
class="gl-ml-3"
:work-item-id="workItemId"
diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue
index 88f82fb6fb7..0a71fbc9a34 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -25,6 +25,7 @@ import {
TEST_ID_PROMOTE_ACTION,
TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
TEST_ID_COPY_REFERENCE_ACTION,
+ TEST_ID_TOGGLE_ACTION,
I18N_WORK_ITEM_ERROR_CONVERTING,
WORK_ITEM_TYPE_VALUE_KEY_RESULT,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
@@ -35,6 +36,7 @@ import {
import updateWorkItemNotificationsMutation from '../graphql/update_work_item_notifications.mutation.graphql';
import convertWorkItemMutation from '../graphql/work_item_convert.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
+import WorkItemStateToggle from './work_item_state_toggle.vue';
export default {
i18n: {
@@ -53,6 +55,7 @@ export default {
GlDropdownDivider,
GlModal,
GlToggle,
+ WorkItemStateToggle,
},
directives: {
GlModal: GlModalDirective,
@@ -65,12 +68,17 @@ export default {
copyCreateNoteEmailTestId: TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
deleteActionTestId: TEST_ID_DELETE_ACTION,
promoteActionTestId: TEST_ID_PROMOTE_ACTION,
+ stateToggleTestId: TEST_ID_TOGGLE_ACTION,
inject: ['isGroup'],
props: {
fullPath: {
type: String,
required: true,
},
+ workItemState: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: false,
@@ -126,6 +134,11 @@ export default {
required: false,
default: false,
},
+ workItemParentId: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
apollo: {
workItemTypes: {
@@ -310,6 +323,16 @@ export default {
<template #list-item>{{ confidentialItemText }}</template>
</gl-disclosure-dropdown-item>
+ <work-item-state-toggle
+ v-if="canUpdate"
+ :data-testid="$options.stateToggleTestId"
+ :work-item-id="workItemId"
+ :work-item-state="workItemState"
+ :work-item-parent-id="workItemParentId"
+ :work-item-type="workItemType"
+ show-as-dropdown-item
+ />
+
<gl-disclosure-dropdown-item
:data-testid="$options.copyReferenceTestId"
:data-clipboard-text="workItemReference"
diff --git a/app/assets/javascripts/work_items/components/work_item_created_updated.vue b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
index 460b5d35187..d352d66196a 100644
--- a/app/assets/javascripts/work_items/components/work_item_created_updated.vue
+++ b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
@@ -86,7 +86,7 @@ export default {
</script>
<template>
- <div class="gl-mb-3 gl-text-gray-700">
+ <div class="gl-mb-3 gl-text-gray-700 gl-mt-3">
<work-item-state-badge v-if="workItemState" :work-item-state="workItemState" />
<gl-loading-icon v-if="updateInProgress" :inline="true" class="gl-mr-3" />
<confidentiality-badge
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 c8ea2a12038..45d3aa564a5 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -50,7 +50,6 @@ import WorkItemDescription from './work_item_description.vue';
import WorkItemNotes from './work_item_notes.vue';
import WorkItemDetailModal from './work_item_detail_modal.vue';
import WorkItemAwardEmoji from './work_item_award_emoji.vue';
-import WorkItemStateToggleButton from './work_item_state_toggle_button.vue';
import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue';
import WorkItemTypeIcon from './work_item_type_icon.vue';
@@ -61,7 +60,6 @@ export default {
},
isLoggedIn: isLoggedIn(),
components: {
- WorkItemStateToggleButton,
GlAlert,
GlButton,
GlLoadingIcon,
@@ -274,6 +272,18 @@ export default {
showWorkItemLinkedItems() {
return this.hasLinkedWorkItems && this.workItemLinkedItems;
},
+ titleClassHeader() {
+ return {
+ 'gl-sm-display-none!': this.parentWorkItem,
+ 'gl-w-full': !this.parentWorkItem,
+ };
+ },
+ titleClassComponent() {
+ return {
+ 'gl-sm-display-block!': !this.parentWorkItem,
+ 'gl-display-none gl-sm-display-block!': this.parentWorkItem,
+ };
+ },
},
mounted() {
if (this.modalWorkItemIid) {
@@ -409,7 +419,20 @@ export default {
</gl-skeleton-loader>
</div>
<template v-else>
- <div class="gl-display-flex gl-align-items-center">
+ <div class="gl-sm-display-none! gl-display-flex">
+ <gl-button
+ v-if="isModal"
+ class="gl-ml-auto"
+ category="tertiary"
+ data-testid="work-item-close"
+ icon="close"
+ :aria-label="__('Close')"
+ @click="$emit('close')"
+ />
+ </div>
+ <div
+ class="gl-display-block gl-sm-display-flex! gl-align-items-flex-start gl-flex-direction-column gl-sm-flex-direction-row gl-gap-3 gl-pt-3"
+ >
<ul
v-if="parentWorkItem"
class="list-unstyled gl-display-flex gl-min-w-0 gl-mr-auto gl-mb-0 gl-z-index-0"
@@ -440,53 +463,55 @@ export default {
</li>
</ul>
<div
- v-else-if="!error && !workItemLoading"
- class="gl-mr-auto"
+ v-if="!error && !workItemLoading"
+ :class="titleClassHeader"
data-testid="work-item-type"
>
- <work-item-type-icon
- :work-item-icon-name="workItemIconName"
+ <work-item-title
+ v-if="workItem.title"
+ ref="title"
+ class="gl-sm-display-block!"
+ :work-item-id="workItem.id"
+ :work-item-title="workItem.title"
:work-item-type="workItemType"
- show-text
+ :work-item-parent-id="workItemParentId"
+ :can-update="canUpdate"
+ @error="updateError = $event"
+ />
+ </div>
+ <div class="detail-page-header-actions gl-display-flex gl-align-self-start gl-gap-3">
+ <work-item-todos
+ v-if="showWorkItemCurrentUserTodos"
+ :work-item-id="workItem.id"
+ :work-item-iid="workItemIid"
+ :work-item-fullpath="projectFullPath"
+ :current-user-todos="currentUserTodos"
+ @error="updateError = $event"
+ />
+ <work-item-actions
+ :full-path="fullPath"
+ :work-item-id="workItem.id"
+ :subscribed-to-notifications="workItemNotificationsSubscribed"
+ :work-item-type="workItemType"
+ :work-item-type-id="workItemTypeId"
+ :can-delete="canDelete"
+ :can-update="canUpdate"
+ :is-confidential="workItem.confidential"
+ :is-parent-confidential="parentWorkItemConfidentiality"
+ :work-item-reference="workItem.reference"
+ :work-item-create-note-email="workItem.createNoteEmail"
+ :is-modal="isModal"
+ :work-item-state="workItem.state"
+ :work-item-parent-id="workItemParentId"
+ @deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
+ @toggleWorkItemConfidentiality="toggleConfidentiality"
+ @error="updateError = $event"
+ @promotedToObjective="$emit('promotedToObjective', workItemIid)"
/>
- {{ workItemBreadcrumbReference }}
</div>
- <work-item-state-toggle-button
- v-if="canUpdate"
- :work-item-id="workItem.id"
- :work-item-state="workItem.state"
- :work-item-parent-id="workItemParentId"
- :work-item-type="workItemType"
- @error="updateError = $event"
- />
- <work-item-todos
- v-if="showWorkItemCurrentUserTodos"
- :work-item-id="workItem.id"
- :work-item-iid="workItemIid"
- :work-item-fullpath="projectFullPath"
- :current-user-todos="currentUserTodos"
- @error="updateError = $event"
- />
- <work-item-actions
- :full-path="fullPath"
- :work-item-id="workItem.id"
- :subscribed-to-notifications="workItemNotificationsSubscribed"
- :work-item-type="workItemType"
- :work-item-type-id="workItemTypeId"
- :can-delete="canDelete"
- :can-update="canUpdate"
- :is-confidential="workItem.confidential"
- :is-parent-confidential="parentWorkItemConfidentiality"
- :work-item-reference="workItem.reference"
- :work-item-create-note-email="workItem.createNoteEmail"
- :is-modal="isModal"
- @deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
- @toggleWorkItemConfidentiality="toggleConfidentiality"
- @error="updateError = $event"
- @promotedToObjective="$emit('promotedToObjective', workItemIid)"
- />
<gl-button
v-if="isModal"
+ class="gl-display-none gl-sm-display-block!"
category="tertiary"
data-testid="work-item-close"
icon="close"
@@ -496,8 +521,9 @@ export default {
</div>
<div>
<work-item-title
- v-if="workItem.title"
+ v-if="workItem.title && parentWorkItem"
ref="title"
+ :class="titleClassComponent"
:work-item-id="workItem.id"
:work-item-title="workItem.title"
:work-item-type="workItemType"
diff --git a/app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue b/app/assets/javascripts/work_items/components/work_item_state_toggle.vue
index 6d7a93f64c4..581ef9ec945 100644
--- a/app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue
+++ b/app/assets/javascripts/work_items/components/work_item_state_toggle.vue
@@ -1,9 +1,8 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlDisclosureDropdownItem, GlLoadingIcon } from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
-import { __, sprintf } from '~/locale';
-import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { __ } from '~/locale';
import { getUpdateWorkItemMutation } from '~/work_items/components/update_work_item';
import {
sprintfWorkItem,
@@ -17,6 +16,8 @@ import {
export default {
components: {
GlButton,
+ GlDisclosureDropdownItem,
+ GlLoadingIcon,
},
mixins: [Tracking.mixin()],
props: {
@@ -37,6 +38,11 @@ export default {
required: false,
default: null,
},
+ showAsDropdownItem: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -51,9 +57,7 @@ export default {
const baseText = this.isWorkItemOpen
? __('Close %{workItemType}')
: __('Reopen %{workItemType}');
- return capitalizeFirstCharacter(
- sprintf(baseText, { workItemType: this.workItemType.toLowerCase() }),
- );
+ return sprintfWorkItem(baseText, this.workItemType);
},
tracking() {
return {
@@ -62,6 +66,12 @@ export default {
property: `type_${this.workItemType}`,
};
},
+ toggleInProgressText() {
+ const baseText = this.isWorkItemOpen
+ ? __('Closing %{workItemType}')
+ : __('Reopening %{workItemType}');
+ return sprintfWorkItem(baseText, this.workItemType);
+ },
},
methods: {
async updateWorkItem() {
@@ -104,7 +114,18 @@ export default {
</script>
<template>
- <gl-button :loading="updateInProgress" @click="updateWorkItem">{{
+ <gl-disclosure-dropdown-item v-if="showAsDropdownItem" @action="updateWorkItem">
+ <template #list-item>
+ <template v-if="updateInProgress">
+ <gl-loading-icon inline size="sm" />
+ {{ toggleInProgressText }}
+ </template>
+ <template v-else>
+ {{ toggleWorkItemStateText }}
+ </template>
+ </template>
+ </gl-disclosure-dropdown-item>
+ <gl-button v-else :loading="updateInProgress" @click="updateWorkItem">{{
toggleWorkItemStateText
}}</gl-button>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_todos.vue b/app/assets/javascripts/work_items/components/work_item_todos.vue
index d5633adec9a..62518616398 100644
--- a/app/assets/javascripts/work_items/components/work_item_todos.vue
+++ b/app/assets/javascripts/work_items/components/work_item_todos.vue
@@ -177,7 +177,7 @@ export default {
v-gl-tooltip.hover
:loading="isLoading"
:title="buttonLabel"
- category="tertiary"
+ category="secondary"
:aria-label="buttonLabel"
@click="onToggle"
>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 9632165b8cb..2f2401bd9b3 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -258,6 +258,7 @@ export const TEST_ID_DELETE_ACTION = 'delete-action';
export const TEST_ID_PROMOTE_ACTION = 'promote-action';
export const TEST_ID_COPY_REFERENCE_ACTION = 'copy-reference-action';
export const TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION = 'copy-create-note-email-action';
+export const TEST_ID_TOGGLE_ACTION = 'state-toggle-action';
export const TODO_ADD_ICON = 'todo-add';
export const TODO_DONE_ICON = 'todo-done';
diff --git a/config/feature_flags/development/use_primary_and_secondary_stores_for_shared_state.yml b/config/feature_flags/development/use_primary_and_secondary_stores_for_shared_state.yml
new file mode 100644
index 00000000000..3e22d84d192
--- /dev/null
+++ b/config/feature_flags/development/use_primary_and_secondary_stores_for_shared_state.yml
@@ -0,0 +1,8 @@
+---
+name: use_primary_and_secondary_stores_for_shared_state
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134483
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429884
+milestone: '16.6'
+type: development
+group: group::scalability
+default_enabled: false
diff --git a/config/feature_flags/development/use_primary_store_as_default_for_shared_state.yml b/config/feature_flags/development/use_primary_store_as_default_for_shared_state.yml
new file mode 100644
index 00000000000..4c309144342
--- /dev/null
+++ b/config/feature_flags/development/use_primary_store_as_default_for_shared_state.yml
@@ -0,0 +1,8 @@
+---
+name: use_primary_store_as_default_for_shared_state
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134483
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429884
+milestone: '16.6'
+type: development
+group: group::scalability
+default_enabled: false
diff --git a/db/post_migrate/20220531233600_remove_sse_usage_data_from_redis.rb b/db/post_migrate/20220531233600_remove_sse_usage_data_from_redis.rb
index b7b02e483df..26ae9aed5cc 100644
--- a/db/post_migrate/20220531233600_remove_sse_usage_data_from_redis.rb
+++ b/db/post_migrate/20220531233600_remove_sse_usage_data_from_redis.rb
@@ -3,6 +3,8 @@
class RemoveSseUsageDataFromRedis < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
def up
Gitlab::Redis::SharedState.with { |r| r.del("USAGE_STATIC_SITE_EDITOR_VIEWS") }
Gitlab::Redis::SharedState.with { |r| r.del("USAGE_STATIC_SITE_EDITOR_COMMITS") }
diff --git a/db/post_migrate/20230317004428_migrate_daily_redis_hll_events_to_weekly_aggregation.rb b/db/post_migrate/20230317004428_migrate_daily_redis_hll_events_to_weekly_aggregation.rb
index 59bff26f964..22ef3381c17 100644
--- a/db/post_migrate/20230317004428_migrate_daily_redis_hll_events_to_weekly_aggregation.rb
+++ b/db/post_migrate/20230317004428_migrate_daily_redis_hll_events_to_weekly_aggregation.rb
@@ -3,6 +3,8 @@
class MigrateDailyRedisHllEventsToWeeklyAggregation < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
DAILY_EVENTS =
%w[g_edit_by_web_ide
g_edit_by_sfe
diff --git a/db/post_migrate/20230328111013_re_migrate_redis_slot_keys.rb b/db/post_migrate/20230328111013_re_migrate_redis_slot_keys.rb
index 17776d8e42e..a4061c3c7c6 100644
--- a/db/post_migrate/20230328111013_re_migrate_redis_slot_keys.rb
+++ b/db/post_migrate/20230328111013_re_migrate_redis_slot_keys.rb
@@ -3,6 +3,8 @@
class ReMigrateRedisSlotKeys < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
KEY_EXPIRY_LENGTH = 6.weeks
DAILY_EVENTS =
diff --git a/doc/administration/gitaly/configure_gitaly.md b/doc/administration/gitaly/configure_gitaly.md
index c4f064b5eba..5a7c7c70a42 100644
--- a/doc/administration/gitaly/configure_gitaly.md
+++ b/doc/administration/gitaly/configure_gitaly.md
@@ -665,6 +665,8 @@ Configure Gitaly with TLS in one of two ways:
```
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation).
+1. Run `sudo gitlab-rake gitlab:gitaly:check` on the Gitaly client (for example, the
+ Rails application) to confirm it can connect to Gitaly servers.
1. Verify Gitaly traffic is being served over TLS by
[observing the types of Gitaly connections](#observe-type-of-gitaly-connections).
1. Optional. Improve security by:
@@ -751,6 +753,43 @@ Configure Gitaly with TLS in one of two ways:
::EndTabs
+#### Update the certificates
+
+To update the Gitaly certificates after initial configuration:
+
+::Tabs
+
+:::TabTitle Linux package (Omnibus)
+
+If the content of your SSL certificates under the `/etc/gitlab/ssl` directory have been updated, but no configuration changes have been made to
+`/etc/gitlab/gitlab.rb`, then reconfiguring GitLab doesn’t affect Gitaly. Instead, you must restart Gitaly manually for the certificates to be loaded
+by the Gitaly process:
+
+```shell
+sudo gitlab-ctl restart gitaly
+```
+
+If you change or update the certificates in `/etc/gitlab/trusted-certs` without making changes to the `/etc/gitlab/gitlab.rb` file, you must:
+
+1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation) so the symlinks for the trusted certificates are updated.
+1. Restart Gitaly manually for the certificates to be loaded by the Gitaly process:
+
+ ```shell
+ sudo gitlab-ctl restart gitaly
+ ```
+
+:::TabTitle Self-compiled (source)
+
+If the content of your SSL certificates under the `/etc/gitlab/ssl` directory have been updated, you must
+[restart GitLab](../restart_gitlab.md#self-compiled-installations) for the certificates to be loaded by the Gitaly process.
+
+If you change or update the certificates in `/usr/local/share/ca-certificates`, you must:
+
+1. Run `sudo update-ca-certificates` to update the system's trusted store.
+1. [Restart GitLab](../restart_gitlab.md#self-compiled-installations) for the certificates to be loaded by the Gitaly process.
+
+::EndTabs
+
### Observe type of Gitaly connections
For information on observing the type of Gitaly connections being served, see the
diff --git a/doc/administration/gitaly/troubleshooting.md b/doc/administration/gitaly/troubleshooting.md
index 556bc29b76f..17687cbb181 100644
--- a/doc/administration/gitaly/troubleshooting.md
+++ b/doc/administration/gitaly/troubleshooting.md
@@ -387,6 +387,43 @@ If Git pushes are too slow when Dynatrace is enabled, disable Dynatrace.
One way to resolve this is to make sure the entry is correct for the GitLab internal API URL configured in `gitlab.rb` with `gitlab_rails['internal_api_url']`.
+### Changes (diffs) don't load for new merge requests when using Gitaly TLS
+
+After enabling [Gitaly with TLS](configure_gitaly.md#enable-tls-support), changes (diffs) for new merge requests are not generated
+and you see the following message in GitLab:
+
+```plaintext
+Building your merge request... This page will update when the build is complete
+```
+
+Gitaly must be able to connect to itself to complete some operations. If the Gitaly certificate is not trusted by the Gitaly server,
+merge request diffs can't be generated.
+
+If Gitaly can't connect to itself, you see messages in the [Gitaly logs](../../administration/logs/index.md#gitaly-logs) like the following messages:
+
+```json
+{
+ "level":"warning",
+ "msg":"[core] [Channel #16 SubChannel #17] grpc: addrConn.createTransport failed to connect to {Addr: \"ext-gitaly.example.com:9999\", ServerName: \"ext-gitaly.example.com:9999\", }. Err: connection error: desc = \"transport: authentication handshake failed: tls: failed to verify certificate: x509: certificate signed by unknown authority\"",
+ "pid":820,
+ "system":"system",
+ "time":"2023-11-06T05:40:04.169Z"
+}
+{
+ "level":"info",
+ "msg":"[core] [Server #3] grpc: Server.Serve failed to create ServerTransport: connection error: desc = \"ServerHandshake(\\\"x.x.x.x:x\\\") failed: wrapped server handshake: remote error: tls: bad certificate\"",
+ "pid":820,
+ "system":"system",
+ "time":"2023-11-06T05:40:04.169Z"
+}
+```
+
+To resolve the problem, ensure that you have added your Gitaly certificate to the `/etc/gitlab/trusted-certs` folder on the Gitaly server
+and:
+
+1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation) so the certificates are symlinked
+1. Restart Gitaly manually `sudo gitlab-ctl restart gitaly` for the certificates to be loaded by the Gitaly process.
+
## Gitaly fails to fork processes stored on `noexec` file systems
Because of changes [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5999) in GitLab 14.10, applying the `noexec` option to a mount
diff --git a/doc/user/application_security/dast/browser_based.md b/doc/user/application_security/dast/browser_based.md
index 26782c319b1..d8724272714 100644
--- a/doc/user/application_security/dast/browser_based.md
+++ b/doc/user/application_security/dast/browser_based.md
@@ -66,6 +66,8 @@ See [checks](checks/index.md) for more information about individual checks.
Active scans check for vulnerabilities by injecting attack payloads into HTTP requests recorded during the crawl phase of the scan.
Active scans are disabled by default due to the nature of their probing attacks.
+#### How active scans work
+
DAST analyzes each recorded HTTP request for injection locations, such as query values, header values, cookie values, form posts, and JSON string values.
Attack payloads are injected into the injection location, forming a new request.
DAST sends the request to the target application and uses the HTTP response to determine attack success.
@@ -84,6 +86,12 @@ A simplified timing attack works as follows:
1. The target application is vulnerable if it executes the query parameter value as a system command without validation, for example, `system(params[:search])`
1. DAST creates a finding if the response time takes longer than 10 seconds.
+#### Known issues
+
+Active scans do not use a browser to send HTTP requests in an effort to minimize scan time.
+
+Anti-CSRF tokens are not regenerated for attacks that submit forms. Please disable anti-CSRF tokens when running an active scan.
+
## Getting started
To run a DAST scan:
@@ -209,7 +217,7 @@ For authentication CI/CD variables, see [Authentication](authentication.md).
| `DAST_REQUEST_HEADERS` | string | `Cache-control:no-cache` | Set to a comma-separated list of request header names and values. |
| `DAST_SKIP_TARGET_CHECK` | boolean | `true` | Set to `true` to prevent DAST from checking that the target is available before scanning. Default: `false`. |
| `DAST_TARGET_AVAILABILITY_TIMEOUT` | number | `60` | Time limit in seconds to wait for target availability. |
-| `DAST_WEBSITE` | URL | `https://example.com` | The URL of the website to scan. |
+| `DAST_WEBSITE` | URL | `https://example.com` | The URL of the target application to scan. |
| `SECURE_ANALYZERS_PREFIX` | URL | `registry.organization.com` | Set the Docker registry base address from which to download the analyzer. |
## Managing scope
@@ -281,22 +289,17 @@ dast:
DAST_EXCLUDE_URLS: "https://my.site.com/user/logout" # don't visit this URL
```
-## Vulnerability detection
+## Vulnerability check migration
+
+A migration is underway that changes the browser-based analyzer from using the proxy-based analyzer Zed Attack Proxy (ZAP) active vulnerability checks, to using GitLab-built active vulnerability checks.
+
+The browser-based analyzer continues to use a combination of proxy-based analyzer and GitLab-built vulnerability checks until the migration is complete. See [browser-based vulnerability checks](checks/index.md) for details of which checks have been migrated.
-Vulnerability detection is gradually being migrated from the default Zed Attack Proxy (ZAP) solution
-to the browser-based analyzer. For details of the vulnerability detection already migrated, see
-[browser-based vulnerability checks](checks/index.md).
+### Why browser-based scans produce different results to proxy-based scans
-The crawler runs the target website in a browser with DAST/ZAP configured as the proxy server. This
-ensures that all requests and responses made by the browser are passively scanned by DAST/ZAP. When
-running a full scan, active vulnerability checks executed by DAST/ZAP do not use a browser. This
-difference in how vulnerabilities are checked can cause issues that require certain features of the
-target website to be disabled to ensure the scan works as intended.
+Browser-based and proxy-based scans do not produce the same results because they use a different set of vulnerability checks.
-For example, for a target website that contains forms with Anti-CSRF tokens, a passive scan works as
-intended because the browser displays pages and forms as if a user is viewing the page. However,
-active vulnerability checks that run in a full scan cannot submit forms containing Anti-CSRF tokens.
-In such cases, we recommend you disable Anti-CSRF tokens when running a full scan.
+The browser-based analyzer does not have an equivalent for proxy-based checks that create too many false positives, are not worth running because modern browsers don't allow the vulnerability to be exploited, or are no longer considered relevant. The browser-based analyzer includes checks that proxy-based analyzer does not.
## Managing scan time
diff --git a/lib/gitlab/database/schema_cache_with_renamed_table.rb b/lib/gitlab/database/schema_cache_with_renamed_table.rb
index 1a64bcbcf10..e110fc44b7b 100644
--- a/lib/gitlab/database/schema_cache_with_renamed_table.rb
+++ b/lib/gitlab/database/schema_cache_with_renamed_table.rb
@@ -11,8 +11,8 @@ module Gitlab
clear_renamed_tables_cache!
end
- def clear_data_source_cache!(name)
- super(name)
+ def clear_data_source_cache!(connection, table_name)
+ super(connection, table_name)
clear_renamed_tables_cache!
end
diff --git a/lib/gitlab/redis/multi_store.rb b/lib/gitlab/redis/multi_store.rb
index 95d27cdb500..f5576d7fe20 100644
--- a/lib/gitlab/redis/multi_store.rb
+++ b/lib/gitlab/redis/multi_store.rb
@@ -63,8 +63,12 @@ module Gitlab
hlen
hmget
hscan_each
+ llen
+ lrange
mapped_hmget
mget
+ pfcount
+ pttl
scan
scan_each
scard
@@ -72,20 +76,32 @@ module Gitlab
smembers
sscan
sscan_each
+ strlen
ttl
+ type
+ zcard
+ zcount
+ zrange
+ zrangebyscore
+ zrevrange
zscan_each
+ zscore
].freeze
WRITE_COMMANDS = %i[
+ decr
del
eval
expire
flushdb
hdel
+ hincrby
hset
incr
incrby
mapped_hmset
+ pfadd
+ pfmerge
publish
rpush
sadd
@@ -93,8 +109,15 @@ module Gitlab
set
setex
setnx
+ spop
srem
+ srem?
unlink
+ zadd
+ zpopmin
+ zrem
+ zremrangebyrank
+ zremrangebyscore
memory
].freeze
@@ -261,6 +284,19 @@ module Gitlab
end
end
+ # blpop blocks until an element to be popped exist in the list or after a timeout.
+ def blpop(*args)
+ result = default_store.blpop(*args)
+ if !!result && use_primary_and_secondary_stores?
+ # special case to accommodate Gitlab::JobWaiter as blpop is only used in JobWaiter
+ # 1s should be sufficient wait time to account for delays between 1st and 2nd lpush
+ # https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/2520#note_1630893702
+ non_default_store.blpop(args.first, timeout: 1)
+ end
+
+ result
+ end
+
private
# @return [Boolean]
diff --git a/lib/gitlab/redis/shared_state.rb b/lib/gitlab/redis/shared_state.rb
index fb3a143121b..d12d3e8c6aa 100644
--- a/lib/gitlab/redis/shared_state.rb
+++ b/lib/gitlab/redis/shared_state.rb
@@ -3,6 +3,12 @@
module Gitlab
module Redis
class SharedState < ::Gitlab::Redis::Wrapper
+ def self.redis
+ primary_store = ::Redis.new(ClusterSharedState.params)
+ secondary_store = ::Redis.new(params)
+
+ MultiStore.new(primary_store, secondary_store, store_name)
+ end
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 753a91ea0d4..2185dc9b941 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -10864,6 +10864,9 @@ msgstr ""
msgid "Closing %{issuableType}..."
msgstr ""
+msgid "Closing %{workItemType}"
+msgstr ""
+
msgid "Cloud Run"
msgstr ""
@@ -39935,6 +39938,9 @@ msgstr ""
msgid "Reopening %{issuableType}..."
msgstr ""
+msgid "Reopening %{workItemType}"
+msgstr ""
+
msgid "Reopening..."
msgstr ""
diff --git a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
index ee2b434bd75..fe89c525fea 100644
--- a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
@@ -10,7 +10,7 @@ import { STATE_OPEN } from '~/work_items/constants';
import * as confirmViaGlModal from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
+import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
Vue.use(VueApollo);
@@ -37,7 +37,7 @@ describe('Work item comment form component', () => {
const findConfirmButton = () => wrapper.find('[data-testid="confirm-button"]');
const findInternalNoteCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findInternalNoteTooltipIcon = () => wrapper.findComponent(GlIcon);
- const findWorkItemToggleStateButton = () => wrapper.findComponent(WorkItemStateToggleButton);
+ const findWorkItemToggleStateButton = () => wrapper.findComponent(WorkItemStateToggle);
const createComponent = ({
isSubmitting = false,
diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js
index b41014a236b..6cbb3c4384e 100644
--- a/spec/frontend/work_items/components/work_item_actions_spec.js
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -1,4 +1,10 @@
-import { GlDisclosureDropdown, GlDropdownDivider, GlModal, GlToggle } from '@gitlab/ui';
+import {
+ GlDisclosureDropdown,
+ GlDropdownDivider,
+ GlModal,
+ GlToggle,
+ GlDisclosureDropdownItem,
+} from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@@ -10,13 +16,16 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { isLoggedIn } from '~/lib/utils/common_utils';
import toast from '~/vue_shared/plugins/global_toast';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
+import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
import {
+ STATE_OPEN,
TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
TEST_ID_DELETE_ACTION,
TEST_ID_PROMOTE_ACTION,
TEST_ID_COPY_REFERENCE_ACTION,
TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
+ TEST_ID_TOGGLE_ACTION,
} from '~/work_items/constants';
import updateWorkItemNotificationsMutation from '~/work_items/graphql/update_work_item_notifications.mutation.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
@@ -46,6 +55,7 @@ describe('WorkItemActions component', () => {
const findDeleteButton = () => wrapper.findByTestId(TEST_ID_DELETE_ACTION);
const findPromoteButton = () => wrapper.findByTestId(TEST_ID_PROMOTE_ACTION);
const findCopyReferenceButton = () => wrapper.findByTestId(TEST_ID_COPY_REFERENCE_ACTION);
+ const findWorkItemToggleOption = () => wrapper.findComponent(WorkItemStateToggle);
const findCopyCreateNoteEmailButton = () =>
wrapper.findByTestId(TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION);
const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *');
@@ -105,6 +115,7 @@ describe('WorkItemActions component', () => {
[updateWorkItemNotificationsMutation, notificationsMutationHandler],
]),
propsData: {
+ workItemState: STATE_OPEN,
fullPath: 'gitlab-org/gitlab-test',
workItemId: 'gid://gitlab/WorkItem/1',
canUpdate,
@@ -129,6 +140,7 @@ describe('WorkItemActions component', () => {
show: jest.fn(),
},
}),
+ GlDisclosureDropdownItem,
GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
methods: {
close: modalShowSpy,
@@ -164,6 +176,10 @@ describe('WorkItemActions component', () => {
text: 'Turn on confidentiality',
},
{
+ testId: TEST_ID_TOGGLE_ACTION,
+ text: '',
+ },
+ {
testId: TEST_ID_COPY_REFERENCE_ACTION,
text: 'Copy reference',
},
@@ -363,4 +379,10 @@ describe('WorkItemActions component', () => {
expect(toast).toHaveBeenCalledWith('Email address copied');
});
});
+
+ it('renders the toggle status button', () => {
+ createComponent();
+
+ expect(findWorkItemToggleOption().exists()).toBe(true);
+ });
});
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 28826748cb0..acfe4571cd2 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -23,8 +23,6 @@ import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree
import WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.vue';
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
-import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
-import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import WorkItemTodos from '~/work_items/components/work_item_todos.vue';
import { i18n } from '~/work_items/constants';
@@ -55,10 +53,6 @@ describe('WorkItemDetail component', () => {
canUpdate: true,
canDelete: true,
});
- const workItemQueryResponseWithCannotUpdate = workItemByIidResponseFactory({
- canUpdate: false,
- canDelete: false,
- });
const workItemQueryResponseWithoutParent = workItemByIidResponseFactory({
parent: null,
canUpdate: true,
@@ -95,8 +89,6 @@ describe('WorkItemDetail component', () => {
const findWorkItemTwoColumnViewContainer = () => wrapper.findByTestId('work-item-overview');
const findRightSidebar = () => wrapper.findByTestId('work-item-overview-right-sidebar');
const triggerPageScroll = () => findIntersectionObserver().vm.$emit('disappear');
- const findWorkItemStateToggleButton = () => wrapper.findComponent(WorkItemStateToggleButton);
- const findWorkItemTypeIcon = () => wrapper.findComponent(WorkItemTypeIcon);
const createComponent = ({
isGroup = false,
@@ -212,25 +204,6 @@ describe('WorkItemDetail component', () => {
});
});
- describe('work item state toggle button', () => {
- describe.each`
- description | canUpdate
- ${'when user cannot update'} | ${false}
- ${'when user can update'} | ${true}
- `('$description', ({ canUpdate }) => {
- it(`${canUpdate ? 'is rendered' : 'is not rendered'}`, async () => {
- createComponent({
- handler: canUpdate
- ? jest.fn().mockResolvedValue(workItemQueryResponse)
- : jest.fn().mockResolvedValue(workItemQueryResponseWithCannotUpdate),
- });
- await waitForPromises();
-
- expect(findWorkItemStateToggleButton().exists()).toBe(canUpdate);
- });
- });
- });
-
describe('close button', () => {
describe('when isModal prop is false', () => {
it('does not render', async () => {
@@ -408,12 +381,11 @@ describe('WorkItemDetail component', () => {
expect(findParent().exists()).toBe(false);
});
- it('shows work item type with reference when there is no a parent', async () => {
+ it('shows title in the header when there is no parent', async () => {
createComponent({ handler: jest.fn().mockResolvedValue(workItemQueryResponseWithoutParent) });
await waitForPromises();
- expect(findWorkItemTypeIcon().props('showText')).toBe(true);
- expect(findWorkItemType().text()).toBe('#1');
+ expect(findWorkItemType().classes()).toEqual(['gl-w-full']);
});
describe('with parent', () => {
@@ -428,10 +400,6 @@ describe('WorkItemDetail component', () => {
expect(findParent().exists()).toBe(true);
});
- it('does not show work item type', () => {
- expect(findWorkItemType().exists()).toBe(false);
- });
-
it('shows parent breadcrumb icon', () => {
expect(findParentButton().props('icon')).toBe(mockParent.parent.workItemType.iconName);
});
@@ -468,6 +436,10 @@ describe('WorkItemDetail component', () => {
const { iid } = workItemQueryResponse.data.workspace.workItems.nodes[0];
expect(findParent().text()).toContain(`#${iid}`);
});
+
+ it('does not show title in the header when parent exists', () => {
+ expect(findWorkItemType().classes()).toEqual(['gl-sm-display-none!']);
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_state_toggle_button_spec.js b/spec/frontend/work_items/components/work_item_state_toggle_button_spec.js
index c0b206e5da4..a210bd50422 100644
--- a/spec/frontend/work_items/components/work_item_state_toggle_button_spec.js
+++ b/spec/frontend/work_items/components/work_item_state_toggle_button_spec.js
@@ -5,7 +5,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
+import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
import {
STATE_OPEN,
STATE_CLOSED,
@@ -33,7 +33,7 @@ describe('Work Item State toggle button component', () => {
workItemState = STATE_OPEN,
workItemType = 'Task',
} = {}) => {
- wrapper = shallowMount(WorkItemStateToggleButton, {
+ wrapper = shallowMount(WorkItemStateToggle, {
apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
propsData: {
workItemId: id,
diff --git a/spec/helpers/sorting_helper_spec.rb b/spec/helpers/sorting_helper_spec.rb
index 0f53cc98415..dccea889d55 100644
--- a/spec/helpers/sorting_helper_spec.rb
+++ b/spec/helpers/sorting_helper_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe SortingHelper do
+RSpec.describe SortingHelper, feature_category: :shared do
include ApplicationHelper
include IconsHelper
include ExploreHelper
diff --git a/spec/lib/gitlab/database/tables_truncate_spec.rb b/spec/lib/gitlab/database/tables_truncate_spec.rb
index 350e36ff1f1..352c2fff779 100644
--- a/spec/lib/gitlab/database/tables_truncate_spec.rb
+++ b/spec/lib/gitlab/database/tables_truncate_spec.rb
@@ -373,7 +373,9 @@ RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_ba
context 'with no main data in ci datatabase' do
before do
# Remove 'main' data in ci database
- ci_connection.truncate_tables([:_test_gitlab_main_items, :_test_gitlab_main_references])
+ ci_connection.execute(
+ "TRUNCATE TABLE _test_gitlab_main_items, _test_gitlab_main_references RESTART IDENTITY CASCADE;"
+ )
end
it { is_expected.to eq(false) }
diff --git a/spec/lib/gitlab/job_waiter_spec.rb b/spec/lib/gitlab/job_waiter_spec.rb
index 4b4c2e954b6..e3d7d59df04 100644
--- a/spec/lib/gitlab/job_waiter_spec.rb
+++ b/spec/lib/gitlab/job_waiter_spec.rb
@@ -62,6 +62,10 @@ RSpec.describe Gitlab::JobWaiter, :redis, feature_category: :shared do
before do
allow_any_instance_of(described_class).to receive(:wait).and_call_original
+ stub_feature_flags(
+ use_primary_and_secondary_stores_for_shared_state: false,
+ use_primary_store_as_default_for_shared_state: false
+ )
end
it 'returns when all jobs have been completed' do
@@ -82,5 +86,56 @@ RSpec.describe Gitlab::JobWaiter, :redis, feature_category: :shared do
expect(result).to contain_exactly('a')
end
+
+ context 'when migration is ongoing' do
+ let(:waiter) { described_class.new(3) }
+
+ shared_examples 'returns all jobs' do
+ it 'returns all jobs' do
+ result = nil
+ expect { Timeout.timeout(6) { result = waiter.wait(5) } }.not_to raise_error
+
+ expect(result).to contain_exactly('a', 'b', 'c')
+ end
+ end
+
+ context 'when using both stores' do
+ context 'with existing jobs in old store' do
+ before do
+ described_class.notify(waiter.key, 'a')
+ described_class.notify(waiter.key, 'b')
+ described_class.notify(waiter.key, 'c')
+ stub_feature_flags(use_primary_and_secondary_stores_for_shared_state: true)
+ end
+
+ it_behaves_like 'returns all jobs'
+ end
+
+ context 'with jobs in both stores' do
+ before do
+ stub_feature_flags(use_primary_and_secondary_stores_for_shared_state: true)
+ described_class.notify(waiter.key, 'a')
+ described_class.notify(waiter.key, 'b')
+ described_class.notify(waiter.key, 'c')
+ end
+
+ it_behaves_like 'returns all jobs'
+ end
+
+ context 'when using primary store as default store' do
+ before do
+ stub_feature_flags(
+ use_primary_and_secondary_stores_for_shared_state: true,
+ use_primary_store_as_default_for_shared_state: true
+ )
+ described_class.notify(waiter.key, 'a')
+ described_class.notify(waiter.key, 'b')
+ described_class.notify(waiter.key, 'c')
+ end
+
+ it_behaves_like 'returns all jobs'
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb
index 66860e8a443..9f94509e3f8 100644
--- a/spec/lib/gitlab/redis/multi_store_spec.rb
+++ b/spec/lib/gitlab/redis/multi_store_spec.rb
@@ -222,7 +222,8 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
it 'logs the exception and re-raises the error' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
- hash_including(:multi_store_error_message, instance_name: instance_name, command_name: name))
+ hash_including(:multi_store_error_message,
+ instance_name: instance_name, command_name: name))
expect { subject }.to raise_error(an_instance_of(StandardError))
end
@@ -559,7 +560,8 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
it 'logs the exception and execute on default instance', :aggregate_failures do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
- hash_including(:multi_store_error_message, command_name: name, instance_name: instance_name))
+ hash_including(:multi_store_error_message,
+ command_name: name, instance_name: instance_name))
expect(multi_store.default_store).to receive(name).with(*expected_args).and_call_original
subject
@@ -597,6 +599,88 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
# rubocop:enable RSpec/MultipleMemoizedHelpers
+ context 'with mocked redis commands' do
+ let(:args) { [1, 2, 3] }
+ let(:kwargs) { { foo: 'bar' } }
+
+ subject do
+ multi_store.send(command, *args, **kwargs)
+ end
+
+ context 'for read commands' do
+ described_class::READ_COMMANDS.each do |command|
+ describe command.to_s do
+ let(:command) { command }
+
+ where(
+ :use_primary_and_secondary_stores_ff,
+ :use_primary_store_as_default_ff,
+ :executed_store,
+ :non_executed_store,
+ :executed_store_name
+ ) do
+ false | false | ref(:secondary_store) | ref(:primary_store) | 'secondary_store'
+ true | false | ref(:secondary_store) | ref(:primary_store) | 'secondary_store'
+ true | true | ref(:primary_store) | ref(:secondary_store) | 'primary_store'
+ false | true | ref(:primary_store) | ref(:secondary_store) | 'primary_store'
+ end
+
+ with_them do
+ before do
+ stub_feature_flags(
+ use_primary_and_secondary_stores_for_test_store: use_primary_and_secondary_stores_ff,
+ use_primary_store_as_default_for_test_store: use_primary_store_as_default_ff
+ )
+ end
+
+ it "executes on #{params[:executed_store_name]}" do
+ expect(executed_store).to receive(command).with(*args, **kwargs)
+ expect(non_executed_store).not_to receive(command)
+
+ subject
+ end
+ end
+ end
+ end
+ end
+
+ context 'for write commands' do
+ described_class::WRITE_COMMANDS.each do |command|
+ describe command.to_s do
+ let(:command) { command }
+
+ where(
+ :use_primary_and_secondary_stores_ff,
+ :use_primary_store_as_default_ff,
+ :executed_stores,
+ :non_executed_store
+ ) do
+ false | false | [ref(:secondary_store)] | ref(:primary_store)
+ true | false | [ref(:secondary_store), ref(:primary_store)] | []
+ true | true | [ref(:primary_store), ref(:secondary_store)] | []
+ false | true | [ref(:primary_store)] | ref(:secondary_store)
+ end
+
+ with_them do
+ before do
+ stub_feature_flags(
+ use_primary_and_secondary_stores_for_test_store: use_primary_and_secondary_stores_ff,
+ use_primary_store_as_default_for_test_store: use_primary_store_as_default_ff
+ )
+ end
+
+ it "executes on executed_stores" do
+ expect(executed_stores).to all(receive(command).with(*args, **kwargs).ordered)
+ expect(non_executed_store).not_to receive(command)
+
+ subject
+ end
+ end
+ end
+ end
+ end
+ end
+
RSpec.shared_examples_for 'pipelined command' do |name|
let_it_be(:key1) { "redis:{1}:key_a" }
let_it_be(:value1) { "redis_value1" }
@@ -973,6 +1057,61 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
+ describe '#blpop' do
+ let_it_be(:key) { "mylist" }
+
+ subject { multi_store.blpop(key, timeout: 0.1) }
+
+ shared_examples 'calls blpop on default_store' do
+ it 'calls blpop on default_store' do
+ expect(multi_store.default_store).to receive(:blpop).with(key, { timeout: 0.1 })
+
+ subject
+ end
+ end
+
+ shared_examples 'does not call lpop on non_default_store' do
+ it 'does not call blpop on non_default_store' do
+ expect(multi_store.non_default_store).not_to receive(:blpop)
+
+ subject
+ end
+ end
+
+ context 'when using both stores' do
+ before do
+ allow(multi_store).to receive(:use_primary_and_secondary_stores?).and_return(true)
+ end
+
+ it_behaves_like 'calls blpop on default_store'
+
+ context "when an element exists in the default_store" do
+ before do
+ multi_store.default_store.lpush(key, 'abc')
+ end
+
+ it 'calls lpop on non_default_store' do
+ expect(multi_store.non_default_store).to receive(:blpop).with(key, { timeout: 1 })
+
+ subject
+ end
+ end
+
+ context 'when the list is empty in default_store' do
+ it_behaves_like 'does not call lpop on non_default_store'
+ end
+ end
+
+ context 'when using one store' do
+ before do
+ allow(multi_store).to receive(:use_primary_and_secondary_stores?).and_return(false)
+ end
+
+ it_behaves_like 'calls blpop on default_store'
+ it_behaves_like 'does not call lpop on non_default_store'
+ end
+ end
+
context 'with unsupported command' do
let(:counter) { Gitlab::Metrics::NullMetric.instance }
@@ -1255,4 +1394,19 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
end
+
+ describe '*_COMMANDS' do
+ it 'checks if every command is only defined once' do
+ commands = [
+ described_class::REDIS_CLIENT_COMMANDS,
+ described_class::PUBSUB_SUBSCRIBE_COMMANDS,
+ described_class::READ_COMMANDS,
+ described_class::WRITE_COMMANDS,
+ described_class::PIPELINED_COMMANDS
+ ].inject([], :concat)
+ duplicated_commands = commands.group_by { |c| c }.select { |k, v| v.size > 1 }.map(&:first)
+
+ expect(duplicated_commands).to be_empty, "commands #{duplicated_commands} defined more than once"
+ end
+ end
end
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index a1579ad1685..eeeb583cac1 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -20,7 +20,7 @@ module DbCleaner
def setup_database_cleaner
all_connection_classes.each do |connection_class|
- DatabaseCleaner[:active_record, { connection: connection_class }]
+ DatabaseCleaner[:active_record, db: connection_class]
end
end