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:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-09-01 18:10:20 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-09-01 18:10:20 +0300
commit9c191c0b942eb08360f4d64c038c435b1156e15f (patch)
tree18ac3c7c2d816ffa4898202102cb889c2c6ca5a7
parent219501933150525be819f047d3196969ee914c47 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--CHANGELOG.md9
-rw-r--r--app/assets/javascripts/design_management/components/design_scaler.vue18
-rw-r--r--app/assets/javascripts/design_management/components/image.vue22
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue8
-rw-r--r--app/assets/javascripts/issues_list/components/issues_list_app.vue67
-rw-r--r--app/assets/javascripts/issues_list/constants.js14
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql30
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql57
-rw-r--r--app/helpers/issuables_helper.rb24
-rw-r--r--app/models/group.rb4
-rw-r--r--app/models/user.rb9
-rw-r--r--config/feature_flags/development/cached_issues_state_count.yml8
-rw-r--r--config/feature_flags/development/linear_user_groups_with_developer_maintainer_project_access.yml8
-rw-r--r--config/metrics/counts_28d/20210216183826_sast_scans.yml18
-rw-r--r--config/metrics/counts_28d/20210216183830_container_scanning_scans.yml18
-rw-r--r--config/metrics/counts_28d/20210216183834_secret_detection_scans.yml18
-rw-r--r--doc/ci/services/gitlab.md7
-rw-r--r--doc/ci/services/mysql.md12
-rw-r--r--doc/ci/services/postgres.md22
-rw-r--r--doc/ci/variables/index.md9
-rw-r--r--lib/gitlab/issuables_count_for_state.rb47
-rw-r--r--lib/sidebars/concerns/has_pill.rb4
-rw-r--r--lib/sidebars/groups/menus/issues_menu.rb2
-rw-r--r--lib/sidebars/groups/menus/merge_requests_menu.rb2
-rw-r--r--scripts/utils.sh3
-rw-r--r--spec/features/groups/issues_spec.rb35
-rw-r--r--spec/frontend/design_management/components/design_scaler_spec.js20
-rw-r--r--spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap8
-rw-r--r--spec/frontend/issues_list/components/issues_list_app_spec.js20
-rw-r--r--spec/frontend/issues_list/mock_data.js10
-rw-r--r--spec/helpers/issuables_helper_spec.rb34
-rw-r--r--spec/lib/gitlab/background_migration/migrate_pages_metadata_spec.rb14
-rw-r--r--spec/lib/gitlab/issuables_count_for_state_spec.rb102
-rw-r--r--spec/models/user_spec.rb45
34 files changed, 515 insertions, 213 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c9d7a0a6131..cc5796859fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,15 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 14.2.3 (2021-09-01)
+
+### Fixed (4 changes)
+
+- [Fix Live Markdown Preview in personal and subgroup projects](gitlab-org/gitlab@20553f93703c0bc076c8e1a4fbc4ce07e2e914b7) ([merge request](gitlab-org/gitlab!69316))
+- [Fix OrphanedInviteTokensCleanup migration](gitlab-org/gitlab@9c59b2fbdfeb250de66a9d2b9424cde9680f86c3) ([merge request](gitlab-org/gitlab!69316))
+- [Reset severity_levels default](gitlab-org/gitlab@34e65788679cfbdeec28357a01a8b303ba61418f) ([merge request](gitlab-org/gitlab!69316))
+- [Geo: Replicate multi-arch containers](gitlab-org/gitlab@fdf88767320016a84c83e896b9f9b90291de89e0) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67624)) **GitLab Enterprise Edition**
+
## 14.2.2 (2021-08-31)
### Security (9 changes)
diff --git a/app/assets/javascripts/design_management/components/design_scaler.vue b/app/assets/javascripts/design_management/components/design_scaler.vue
index c9273f97bed..af3d4453a6a 100644
--- a/app/assets/javascripts/design_management/components/design_scaler.vue
+++ b/app/assets/javascripts/design_management/components/design_scaler.vue
@@ -1,16 +1,21 @@
<script>
import { GlButtonGroup, GlButton } from '@gitlab/ui';
-const SCALE_STEP_SIZE = 0.2;
const DEFAULT_SCALE = 1;
const MIN_SCALE = 1;
-const MAX_SCALE = 2;
+const ZOOM_LEVELS = 5;
export default {
components: {
GlButtonGroup,
GlButton,
},
+ props: {
+ maxScale: {
+ type: Number,
+ required: true,
+ },
+ },
data() {
return {
scale: DEFAULT_SCALE,
@@ -24,7 +29,10 @@ export default {
return this.scale === DEFAULT_SCALE;
},
disableIncrease() {
- return this.scale >= MAX_SCALE;
+ return this.scale >= this.maxScale;
+ },
+ stepSize() {
+ return (this.maxScale - MIN_SCALE) / ZOOM_LEVELS;
},
},
methods: {
@@ -37,10 +45,10 @@ export default {
this.$emit('scale', this.scale);
},
incrementScale() {
- this.setScale(this.scale + SCALE_STEP_SIZE);
+ this.setScale(Math.min(this.scale + this.stepSize, this.maxScale));
},
decrementScale() {
- this.setScale(this.scale - SCALE_STEP_SIZE);
+ this.setScale(Math.max(this.scale - this.stepSize, MIN_SCALE));
},
resetScale() {
this.setScale(DEFAULT_SCALE);
diff --git a/app/assets/javascripts/design_management/components/image.vue b/app/assets/javascripts/design_management/components/image.vue
index 8ab94cd2c4b..5354c7756f5 100644
--- a/app/assets/javascripts/design_management/components/image.vue
+++ b/app/assets/javascripts/design_management/components/image.vue
@@ -57,6 +57,7 @@ export default {
methods: {
onImgLoad() {
requestIdleCallback(this.setBaseImageSize, { timeout: 1000 });
+ requestIdleCallback(this.setImageNaturalScale, { timeout: 1000 });
performanceMarkAndMeasure({
measures: [
{
@@ -79,6 +80,27 @@ export default {
};
this.onResize({ width: this.baseImageSize.width, height: this.baseImageSize.height });
},
+ setImageNaturalScale() {
+ const { contentImg } = this.$refs;
+
+ if (!contentImg) {
+ return;
+ }
+
+ const { naturalHeight, naturalWidth } = contentImg;
+
+ // In case image 404s
+ if (naturalHeight === 0 || naturalWidth === 0) {
+ return;
+ }
+
+ const { height, width } = this.baseImageSize;
+
+ this.$parent.$emit(
+ 'setMaxScale',
+ Math.round(((height + width) / (naturalHeight + naturalWidth)) * 100) / 100,
+ );
+ },
onResize({ width, height }) {
this.$emit('resize', { width, height });
},
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index c2e2fc4feae..38ea5406c02 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -46,6 +46,7 @@ import {
import { trackDesignDetailView, servicePingDesignDetailView } from '../../utils/tracking';
const DEFAULT_SCALE = 1;
+const DEFAULT_MAX_SCALE = 2;
export default {
components: {
@@ -96,6 +97,7 @@ export default {
scale: DEFAULT_SCALE,
resolvedDiscussionsExpanded: false,
prevCurrentUserTodos: null,
+ maxScale: DEFAULT_MAX_SCALE,
};
},
apollo: {
@@ -328,6 +330,9 @@ export default {
toggleResolvedComments() {
this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded;
},
+ setMaxScale(event) {
+ this.maxScale = 1 / event;
+ },
},
createImageDiffNoteMutation,
DESIGNS_ROUTE_NAME,
@@ -376,12 +381,13 @@ export default {
@openCommentForm="openCommentForm"
@closeCommentForm="closeCommentForm"
@moveNote="onMoveNote"
+ @setMaxScale="setMaxScale"
/>
<div
class="design-scaler-wrapper gl-absolute gl-mb-6 gl-display-flex gl-justify-content-center gl-align-items-center"
>
- <design-scaler @scale="scale = $event" />
+ <design-scaler :max-scale="maxScale" @scale="scale = $event" />
</div>
</div>
<design-sidebar
diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue
index 118faf85055..fd225223a7f 100644
--- a/app/assets/javascripts/issues_list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue
@@ -9,8 +9,8 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import { cloneDeep } from 'lodash';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
+import getIssuesCountsQuery from 'ee_else_ce/issues_list/queries/get_issues_counts.query.graphql';
import createFlash from '~/flash';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -21,7 +21,6 @@ import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
import {
CREATED_DESC,
i18n,
- issuesCountSmartQueryBase,
MAX_LIST_SIZE,
PAGE_SIZE,
PARAM_DUE_DATE,
@@ -164,18 +163,16 @@ export default {
},
},
data() {
- const filterTokens = getFilterTokens(window.location.search);
const state = getParameterByName(PARAM_STATE);
const sortKey = getSortKey(getParameterByName(PARAM_SORT));
const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
- this.initialFilterTokens = cloneDeep(filterTokens);
-
return {
dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
- filterTokens,
+ filterTokens: getFilterTokens(window.location.search),
issues: [],
+ issuesCounts: {},
pageInfo: {},
pageParams: getInitialPageParams(sortKey),
showBulkEditSidebar: false,
@@ -202,40 +199,21 @@ export default {
},
debounce: 200,
},
- countOpened: {
- ...issuesCountSmartQueryBase,
+ issuesCounts: {
+ query: getIssuesCountsQuery,
variables() {
- return {
- ...this.queryVariables,
- state: IssuableStates.Opened,
- };
- },
- skip() {
- return !this.hasAnyIssues;
+ return this.queryVariables;
},
- },
- countClosed: {
- ...issuesCountSmartQueryBase,
- variables() {
- return {
- ...this.queryVariables,
- state: IssuableStates.Closed,
- };
+ update: ({ project }) => project ?? {},
+ error(error) {
+ createFlash({ message: this.$options.i18n.errorFetchingCounts, captureError: true, error });
},
skip() {
return !this.hasAnyIssues;
},
- },
- countAll: {
- ...issuesCountSmartQueryBase,
- variables() {
- return {
- ...this.queryVariables,
- state: IssuableStates.All,
- };
- },
- skip() {
- return !this.hasAnyIssues;
+ debounce: 200,
+ context: {
+ isSingleRequest: true,
},
},
},
@@ -263,6 +241,9 @@ export default {
isOpenTab() {
return this.state === IssuableStates.Opened;
},
+ showCsvButtons() {
+ return this.isSignedIn;
+ },
apiFilterParams() {
return convertToApiParams(this.filterTokens);
},
@@ -405,10 +386,11 @@ export default {
return getSortOptions(this.hasIssueWeightsFeature, this.hasBlockedIssuesFeature);
},
tabCounts() {
+ const { openedIssues, closedIssues, allIssues } = this.issuesCounts;
return {
- [IssuableStates.Opened]: this.countOpened,
- [IssuableStates.Closed]: this.countClosed,
- [IssuableStates.All]: this.countAll,
+ [IssuableStates.Opened]: openedIssues?.count,
+ [IssuableStates.Closed]: closedIssues?.count,
+ [IssuableStates.All]: allIssues?.count,
};
},
currentTabCount() {
@@ -584,13 +566,13 @@ export default {
})
.then(() => {
const serializedVariables = JSON.stringify(this.queryVariables);
- this.$apollo.mutate({
+ return this.$apollo.mutate({
mutation: reorderIssuesMutation,
variables: { oldIndex, newIndex, serializedVariables },
});
})
- .catch(() => {
- createFlash({ message: this.$options.i18n.reorderError });
+ .catch((error) => {
+ createFlash({ message: this.$options.i18n.reorderError, captureError: true, error });
});
},
handleSort(sortKey) {
@@ -613,7 +595,7 @@ export default {
recent-searches-storage-key="issues"
:search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
- :initial-filter-value="initialFilterTokens"
+ :initial-filter-value="filterTokens"
:sort-options="sortOptions"
:initial-sort-by="sortKey"
:issuables="issues"
@@ -653,7 +635,7 @@ export default {
:aria-label="$options.i18n.calendarLabel"
/>
<csv-import-export-buttons
- v-if="isSignedIn"
+ v-if="showCsvButtons"
class="gl-md-mr-3"
:export-csv-path="exportCsvPathWithQuery"
:issuable-count="currentTabCount"
@@ -766,6 +748,7 @@ export default {
{{ $options.i18n.newIssueLabel }}
</gl-button>
<csv-import-export-buttons
+ v-if="showCsvButtons"
class="gl-mr-3"
:export-csv-path="exportCsvPathWithQuery"
:issuable-count="currentTabCount"
diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js
index 3f5b0d1feb5..5f769e8d0b3 100644
--- a/app/assets/javascripts/issues_list/constants.js
+++ b/app/assets/javascripts/issues_list/constants.js
@@ -1,5 +1,3 @@
-import getIssuesCountQuery from 'ee_else_ce/issues_list/queries/get_issues_count.query.graphql';
-import createFlash from '~/flash';
import { __, s__ } from '~/locale';
import {
FILTER_ANY,
@@ -351,15 +349,3 @@ export const filters = {
},
},
};
-
-export const issuesCountSmartQueryBase = {
- query: getIssuesCountQuery,
- context: {
- isSingleRequest: true,
- },
- update: ({ project }) => project?.issues.count,
- error(error) {
- createFlash({ message: i18n.errorFetchingCounts, captureError: true, error });
- },
- debounce: 200,
-};
diff --git a/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql
deleted file mode 100644
index 51cc6707bea..00000000000
--- a/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql
+++ /dev/null
@@ -1,30 +0,0 @@
-query getIssuesCount(
- $fullPath: ID!
- $search: String
- $state: IssuableState
- $assigneeId: String
- $assigneeUsernames: [String!]
- $authorUsername: String
- $labelName: [String]
- $milestoneTitle: [String]
- $milestoneWildcardId: MilestoneWildcardId
- $types: [IssueType!]
- $not: NegatedIssueFilterInput
-) {
- project(fullPath: $fullPath) {
- issues(
- search: $search
- state: $state
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- types: $types
- not: $not
- ) {
- count
- }
- }
-}
diff --git a/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
new file mode 100644
index 00000000000..a3765d39ed2
--- /dev/null
+++ b/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
@@ -0,0 +1,57 @@
+query getIssuesCount(
+ $fullPath: ID!
+ $search: String
+ $assigneeId: String
+ $assigneeUsernames: [String!]
+ $authorUsername: String
+ $labelName: [String]
+ $milestoneTitle: [String]
+ $milestoneWildcardId: MilestoneWildcardId
+ $types: [IssueType!]
+ $not: NegatedIssueFilterInput
+) {
+ project(fullPath: $fullPath) {
+ openedIssues: issues(
+ state: opened
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ types: $types
+ not: $not
+ ) {
+ count
+ }
+ closedIssues: issues(
+ state: closed
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ types: $types
+ not: $not
+ ) {
+ count
+ }
+ allIssues: issues(
+ state: all
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ types: $types
+ not: $not
+ ) {
+ count
+ }
+ }
+}
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 62a25164a47..45f4c968061 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -3,6 +3,7 @@
module IssuablesHelper
include GitlabRoutingHelper
include IssuablesDescriptionTemplatesHelper
+ include ::Sidebars::Concerns::HasPill
def sidebar_gutter_toggle_icon
content_tag(:span, class: 'js-sidebar-toggle-container', data: { is_expanded: !sidebar_gutter_collapsed? }) do
@@ -187,19 +188,18 @@ module IssuablesHelper
end
def issuables_state_counter_text(issuable_type, state, display_count)
- titles = {
- opened: "Open"
- }
-
+ titles = { opened: "Open" }
state_title = titles[state] || state.to_s.humanize
html = content_tag(:span, state_title)
return html.html_safe unless display_count
count = issuables_count_for_state(issuable_type, state)
-
if count != -1
- html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm')
+ html << " " << content_tag(:span,
+ format_count(issuable_type, count, Gitlab::IssuablesCountForState::THRESHOLD),
+ class: 'badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm'
+ )
end
html.html_safe
@@ -284,7 +284,9 @@ module IssuablesHelper
end
def issuables_count_for_state(issuable_type, state)
- Gitlab::IssuablesCountForState.new(finder)[state]
+ store_in_cache = parent.is_a?(Group) ? parent.cached_issues_state_count_enabled? : false
+
+ Gitlab::IssuablesCountForState.new(finder, store_in_redis_cache: store_in_cache)[state]
end
def close_issuable_path(issuable)
@@ -438,6 +440,14 @@ module IssuablesHelper
def parent
@project || @group
end
+
+ def format_count(issuable_type, count, threshold)
+ if issuable_type == :issues && parent.is_a?(Group) && parent.cached_issues_state_count_enabled?
+ format_cached_count(threshold, count)
+ else
+ number_with_delimiter(count)
+ end
+ end
end
IssuablesHelper.prepend_mod_with('IssuablesHelper')
diff --git a/app/models/group.rb b/app/models/group.rb
index 58215b57d8b..964ef46b39e 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -735,6 +735,10 @@ class Group < Namespace
Timelog.in_group(self)
end
+ def cached_issues_state_count_enabled?
+ Feature.enabled?(:cached_issues_state_count, self, default_enabled: :yaml)
+ end
+
private
def max_member_access(user_ids)
diff --git a/app/models/user.rb b/app/models/user.rb
index 2e5047b6292..e6f3ce38ba8 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -2117,9 +2117,12 @@ class User < ApplicationRecord
project_creation_levels << nil
end
- developer_groups_hierarchy = ::Gitlab::ObjectHierarchy.new(developer_groups).base_and_descendants
- ::Group.where(id: developer_groups_hierarchy.select(:id),
- project_creation_level: project_creation_levels)
+ if Feature.enabled?(:linear_user_groups_with_developer_maintainer_project_access, self, default_enabled: :yaml)
+ developer_groups.self_and_descendants.where(project_creation_level: project_creation_levels)
+ else
+ developer_groups_hierarchy = ::Gitlab::ObjectHierarchy.new(developer_groups).base_and_descendants
+ ::Group.where(id: developer_groups_hierarchy.select(:id), project_creation_level: project_creation_levels)
+ end
end
def no_recent_activity?
diff --git a/config/feature_flags/development/cached_issues_state_count.yml b/config/feature_flags/development/cached_issues_state_count.yml
new file mode 100644
index 00000000000..34d96b601d9
--- /dev/null
+++ b/config/feature_flags/development/cached_issues_state_count.yml
@@ -0,0 +1,8 @@
+---
+name: cached_issues_state_count
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67418
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/333089
+milestone: '14.3'
+type: development
+group: group::product planning
+default_enabled: false
diff --git a/config/feature_flags/development/linear_user_groups_with_developer_maintainer_project_access.yml b/config/feature_flags/development/linear_user_groups_with_developer_maintainer_project_access.yml
new file mode 100644
index 00000000000..09a910ba5f0
--- /dev/null
+++ b/config/feature_flags/development/linear_user_groups_with_developer_maintainer_project_access.yml
@@ -0,0 +1,8 @@
+---
+name: linear_user_groups_with_developer_maintainer_project_access
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68851
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339436
+milestone: '14.3'
+type: development
+group: group::access
+default_enabled: false
diff --git a/config/metrics/counts_28d/20210216183826_sast_scans.yml b/config/metrics/counts_28d/20210216183826_sast_scans.yml
deleted file mode 100644
index 0f645f5b1d7..00000000000
--- a/config/metrics/counts_28d/20210216183826_sast_scans.yml
+++ /dev/null
@@ -1,18 +0,0 @@
----
-data_category: operational
-key_path: usage_activity_by_stage_monthly.secure.sast_scans
-description: ''
-product_section: ''
-product_stage: ''
-product_group: ''
-product_category: ''
-value_type: number
-status: data_available
-time_frame: 28d
-data_source:
-distribution:
-- ce
-tier:
-- free
-skip_validation: true
-performance_indicator_type: []
diff --git a/config/metrics/counts_28d/20210216183830_container_scanning_scans.yml b/config/metrics/counts_28d/20210216183830_container_scanning_scans.yml
deleted file mode 100644
index 386c05ea5b2..00000000000
--- a/config/metrics/counts_28d/20210216183830_container_scanning_scans.yml
+++ /dev/null
@@ -1,18 +0,0 @@
----
-data_category: operational
-key_path: usage_activity_by_stage_monthly.secure.container_scanning_scans
-description: ''
-product_section: ''
-product_stage: ''
-product_group: ''
-product_category: ''
-value_type: number
-status: data_available
-time_frame: 28d
-data_source:
-distribution:
-- ce
-tier:
-- free
-skip_validation: true
-performance_indicator_type: []
diff --git a/config/metrics/counts_28d/20210216183834_secret_detection_scans.yml b/config/metrics/counts_28d/20210216183834_secret_detection_scans.yml
deleted file mode 100644
index fda980b8252..00000000000
--- a/config/metrics/counts_28d/20210216183834_secret_detection_scans.yml
+++ /dev/null
@@ -1,18 +0,0 @@
----
-data_category: operational
-key_path: usage_activity_by_stage_monthly.secure.secret_detection_scans
-description: ''
-product_section: ''
-product_stage: ''
-product_group: ''
-product_category: ''
-value_type: number
-status: data_available
-time_frame: 28d
-data_source:
-distribution:
-- ce
-tier:
-- free
-skip_validation: true
-performance_indicator_type: []
diff --git a/doc/ci/services/gitlab.md b/doc/ci/services/gitlab.md
index 558f53a9535..1258c33aa77 100644
--- a/doc/ci/services/gitlab.md
+++ b/doc/ci/services/gitlab.md
@@ -24,10 +24,9 @@ tests access to the GitLab API.
GITLAB_ROOT_PASSWORD: "password" # to access the api with user root:password
```
-1. To set values for the `GITLAB_HTTPS` and `GITLAB_ROOT_PASSWORD`,
- [assign them to a variable in the user interface](../variables/index.md#add-a-cicd-variable-to-a-project).
- Then assign that variable to the corresponding variable in your
- `.gitlab-ci.yml` file.
+NOTE:
+Variables set in the GitLab UI are not passed down to the service containers.
+[Learn more](../variables/index.md#).
Then, commands in `script:` sections in your `.gitlab-ci.yml` file can access the API at `http://gitlab/api/v4`.
diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md
index c691a6ef33d..3d9b93d3271 100644
--- a/doc/ci/services/mysql.md
+++ b/doc/ci/services/mysql.md
@@ -16,11 +16,9 @@ If you want to use a MySQL container, you can use [GitLab Runner](../runners/ind
This example shows you how to set a username and password that GitLab uses to access the MySQL container. If you do not set a username and password, you must use `root`.
-1. [Create CI/CD variables](../variables/index.md#custom-cicd-variables) for your
- MySQL database and password by going to **Settings > CI/CD**, expanding **Variables**,
- and clicking **Add Variable**.
-
- This example uses `$MYSQL_DB` and `$MYSQL_PASS` as the keys.
+NOTE:
+Variables set in the GitLab UI are not passed down to the service containers.
+[Learn more](../variables/index.md).
1. To specify a MySQL image, add the following to your `.gitlab-ci.yml` file:
@@ -39,8 +37,8 @@ This example shows you how to set a username and password that GitLab uses to ac
```yaml
variables:
# Configure mysql environment variables (https://hub.docker.com/_/mysql/)
- MYSQL_DATABASE: $MYSQL_DB
- MYSQL_ROOT_PASSWORD: $MYSQL_PASS
+ MYSQL_DATABASE: $MYSQL_DATABASE
+ MYSQL_ROOT_PASSWORD: $MYSQL_ROOT_PASSWORD
```
The MySQL container uses `MYSQL_DATABASE` and `MYSQL_ROOT_PASSWORD` to connect to the database.
diff --git a/doc/ci/services/postgres.md b/doc/ci/services/postgres.md
index 8d14e4795d2..cf44b01b2b8 100644
--- a/doc/ci/services/postgres.md
+++ b/doc/ci/services/postgres.md
@@ -16,6 +16,10 @@ do this with the Docker and Shell executors of GitLab Runner.
If you're using [GitLab Runner](../runners/index.md) with the Docker executor,
you basically have everything set up already.
+NOTE:
+Variables set in the GitLab UI are not passed down to the service containers.
+[Learn more](../variables/index.md).
+
First, in your `.gitlab-ci.yml` add:
```yaml
@@ -23,25 +27,19 @@ services:
- postgres:12.2-alpine
variables:
- POSTGRES_DB: nice_marmot
- POSTGRES_USER: runner
- POSTGRES_PASSWORD: ""
+ POSTGRES_DB: $POSTGRES_DB
+ POSTGRES_USER: $POSTGRES_USER
+ POSTGRES_PASSWORD: $POSTGRES_PASSWORD
POSTGRES_HOST_AUTH_METHOD: trust
```
-To set values for the `POSTGRES_DB`, `POSTGRES_USER`,
-`POSTGRES_PASSWORD` and `POSTGRES_HOST_AUTH_METHOD`,
-[assign them to a CI/CD variable in the user interface](../variables/index.md#custom-cicd-variables),
-then assign that variable to the corresponding variable in your
-`.gitlab-ci.yml` file.
-
And then configure your application to use the database, for example:
```yaml
Host: postgres
-User: runner
-Password: ''
-Database: nice_marmot
+User: $PG_USER
+Password: $PG_PASSWORD
+Database: $PG_DB
```
If you're wondering why we used `postgres` for the `Host`, read more at
diff --git a/doc/ci/variables/index.md b/doc/ci/variables/index.md
index 9b111e9b60a..7b909cf15f8 100644
--- a/doc/ci/variables/index.md
+++ b/doc/ci/variables/index.md
@@ -20,6 +20,15 @@ You can use [predefined CI/CD variables](#predefined-cicd-variables) or define c
- [Group CI/CD variables](#add-a-cicd-variable-to-a-group).
- [Instance CI/CD variables](#add-a-cicd-variable-to-an-instance).
+NOTE:
+Variables set in the GitLab UI are **not** passed down to [service containers](../docker/using_docker_images.md).
+To set them, assign them to variables in the UI, then re-assign them in your `.gitlab-ci.yml`:
+
+```yaml
+variables:
+ SA_PASSWORD: $SA_PASSWORD
+```
+
> For more information about advanced use of GitLab CI/CD:
>
> - <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>&nbsp;Get to productivity faster with these [7 advanced GitLab CI workflow hacks](https://about.gitlab.com/webcast/7cicd-hacks/)
diff --git a/lib/gitlab/issuables_count_for_state.rb b/lib/gitlab/issuables_count_for_state.rb
index 6b33b60e850..660877b964a 100644
--- a/lib/gitlab/issuables_count_for_state.rb
+++ b/lib/gitlab/issuables_count_for_state.rb
@@ -5,11 +5,14 @@ module Gitlab
class IssuablesCountForState
# The name of the Gitlab::SafeRequestStore cache key.
CACHE_KEY = :issuables_count_for_state
+ # The expiration time for the Rails cache.
+ CACHE_EXPIRES_IN = 10.minutes
+ THRESHOLD = 1000
# The state values that can be safely casted to a Symbol.
STATES = %w[opened closed merged all].freeze
- attr_reader :project
+ attr_reader :project, :finder
def self.declarative_policy_class
'IssuablePolicy'
@@ -18,11 +21,12 @@ module Gitlab
# finder - The finder class to use for retrieving the issuables.
# fast_fail - restrict counting to a shorter period, degrading gracefully on
# failure
- def initialize(finder, project = nil, fast_fail: false)
+ def initialize(finder, project = nil, fast_fail: false, store_in_redis_cache: false)
@finder = finder
@project = project
@fast_fail = fast_fail
@cache = Gitlab::SafeRequestStore[CACHE_KEY] ||= initialize_cache
+ @store_in_redis_cache = store_in_redis_cache
end
def for_state_or_opened(state = nil)
@@ -52,7 +56,16 @@ module Gitlab
private
def cache_for_finder
- @cache[@finder]
+ cached_counts = Rails.cache.read(redis_cache_key, cache_options) if cache_issues_count?
+
+ cached_counts ||= @cache[finder]
+ return cached_counts if cached_counts.empty?
+
+ if cache_issues_count? && cached_counts.values.all? { |count| count >= THRESHOLD }
+ Rails.cache.write(redis_cache_key, cached_counts, cache_options)
+ end
+
+ cached_counts
end
def cast_state_to_symbol?(state)
@@ -108,5 +121,33 @@ module Gitlab
"Count of failed calls to IssuableFinder#count_by_state with fast failure"
).increment
end
+
+ def cache_issues_count?
+ @store_in_redis_cache &&
+ finder.instance_of?(IssuesFinder) &&
+ parent_group.present? &&
+ !params_include_filters?
+ end
+
+ def parent_group
+ finder.params.group
+ end
+
+ def redis_cache_key
+ ['group', parent_group&.id, 'issues']
+ end
+
+ def cache_options
+ { expires_in: CACHE_EXPIRES_IN }
+ end
+
+ def params_include_filters?
+ non_filtering_params = %i[
+ scope state sort group_id include_subgroups
+ attempt_group_search_optimizations non_archived issue_types
+ ]
+
+ finder.params.except(*non_filtering_params).values.any?
+ end
end
end
diff --git a/lib/sidebars/concerns/has_pill.rb b/lib/sidebars/concerns/has_pill.rb
index 4bbf69bf16b..0a2e1f12008 100644
--- a/lib/sidebars/concerns/has_pill.rb
+++ b/lib/sidebars/concerns/has_pill.rb
@@ -21,8 +21,8 @@ module Sidebars
{}
end
- def format_cached_count(count_service, count)
- if count > count_service::CACHED_COUNT_THRESHOLD
+ def format_cached_count(threshold, count)
+ if count > threshold
number_to_human(
count,
units: { thousand: 'k', million: 'm' }, precision: 1, significant: false, format: '%n%u'
diff --git a/lib/sidebars/groups/menus/issues_menu.rb b/lib/sidebars/groups/menus/issues_menu.rb
index 5f0254a0529..4044cb1c716 100644
--- a/lib/sidebars/groups/menus/issues_menu.rb
+++ b/lib/sidebars/groups/menus/issues_menu.rb
@@ -38,7 +38,7 @@ module Sidebars
count_service = ::Groups::OpenIssuesCountService
count = count_service.new(context.group, context.current_user).count
- format_cached_count(count_service, count)
+ format_cached_count(count_service::CACHED_COUNT_THRESHOLD, count)
end
end
diff --git a/lib/sidebars/groups/menus/merge_requests_menu.rb b/lib/sidebars/groups/menus/merge_requests_menu.rb
index 7faf50305c6..050cba07641 100644
--- a/lib/sidebars/groups/menus/merge_requests_menu.rb
+++ b/lib/sidebars/groups/menus/merge_requests_menu.rb
@@ -37,7 +37,7 @@ module Sidebars
count_service = ::Groups::MergeRequestsCountService
count = count_service.new(context.group, context.current_user).count
- format_cached_count(count_service, count)
+ format_cached_count(count_service::CACHED_COUNT_THRESHOLD, count)
end
end
diff --git a/scripts/utils.sh b/scripts/utils.sh
index 700dad58779..d2e8c151438 100644
--- a/scripts/utils.sh
+++ b/scripts/utils.sh
@@ -37,8 +37,9 @@ function bundle_install_script() {
fi;
bundle --version
- bundle config set path 'vendor'
+ bundle config set path "$(pwd)/vendor"
bundle config set clean 'true'
+ test -d jh && bundle config set gemfile 'jh/Gemfile'
echo "${BUNDLE_WITHOUT}"
bundle config
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 6d04e3a227e..455d6638f2d 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -94,6 +94,41 @@ RSpec.describe 'Group issues page' do
expect(page).not_to have_content issue.title[0..80]
end
end
+
+ context 'when cached issues state count is enabled', :clean_gitlab_redis_cache do
+ before do
+ stub_feature_flags(cached_issues_state_count: true)
+ end
+
+ it 'truncates issue counts if over the threshold' do
+ allow(Rails.cache).to receive(:read).and_call_original
+ allow(Rails.cache).to receive(:read).with(
+ ['group', group.id, 'issues'],
+ { expires_in: Gitlab::IssuablesCountForState::CACHE_EXPIRES_IN }
+ ).and_return({ opened: 1050, closed: 500, all: 1550 })
+
+ visit issues_group_path(group)
+
+ expect(page).to have_text('Open 1.1k Closed 500 All 1.6k')
+ end
+ end
+
+ context 'when cached issues state count is disabled', :clean_gitlab_redis_cache do
+ before do
+ stub_feature_flags(cached_issues_state_count: false)
+ end
+
+ it 'does not truncate counts if they are over the threshold' do
+ allow_next_instance_of(IssuesFinder) do |finder|
+ allow(finder).to receive(:count_by_state).and_return(true)
+ .and_return({ opened: 1050, closed: 500, all: 1550 })
+ end
+
+ visit issues_group_path(group)
+
+ expect(page).to have_text('Open 1,050 Closed 500 All 1,550')
+ end
+ end
end
context 'manual ordering', :js do
diff --git a/spec/frontend/design_management/components/design_scaler_spec.js b/spec/frontend/design_management/components/design_scaler_spec.js
index 8a123b2d1e5..095c070e5e8 100644
--- a/spec/frontend/design_management/components/design_scaler_spec.js
+++ b/spec/frontend/design_management/components/design_scaler_spec.js
@@ -13,7 +13,11 @@ describe('Design management design scaler component', () => {
const setScale = (scale) => wrapper.vm.setScale(scale);
const createComponent = () => {
- wrapper = shallowMount(DesignScaler);
+ wrapper = shallowMount(DesignScaler, {
+ propsData: {
+ maxScale: 2,
+ },
+ });
};
beforeEach(() => {
@@ -61,6 +65,18 @@ describe('Design management design scaler component', () => {
expect(wrapper.emitted('scale')).toEqual([[1.2]]);
});
+ it('computes & increments correct stepSize based on maxScale', async () => {
+ wrapper.setProps({ maxScale: 11 });
+
+ await wrapper.vm.$nextTick();
+
+ getIncreaseScaleButton().vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.emitted().scale[0][0]).toBe(3);
+ });
+
describe('when `scale` value is 1', () => {
it('disables the "reset" button', () => {
const resetButton = getResetScaleButton();
@@ -77,7 +93,7 @@ describe('Design management design scaler component', () => {
});
});
- describe('when `scale` value is 2 (maximum)', () => {
+ describe('when `scale` value is maximum', () => {
beforeEach(async () => {
setScale(2);
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
index 57023c55878..3d04840b1f8 100644
--- a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
@@ -25,7 +25,9 @@ exports[`Design management design index page renders design index 1`] = `
<div
class="design-scaler-wrapper gl-absolute gl-mb-6 gl-display-flex gl-justify-content-center gl-align-items-center"
>
- <design-scaler-stub />
+ <design-scaler-stub
+ maxscale="2"
+ />
</div>
</div>
@@ -186,7 +188,9 @@ exports[`Design management design index page with error GlAlert is rendered in c
<div
class="design-scaler-wrapper gl-absolute gl-mb-6 gl-display-flex gl-justify-content-center gl-align-items-center"
>
- <design-scaler-stub />
+ <design-scaler-stub
+ maxscale="2"
+ />
</div>
</div>
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 f4b0e428582..72a30c30b50 100644
--- a/spec/frontend/issues_list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues_list/components/issues_list_app_spec.js
@@ -5,17 +5,17 @@ import { cloneDeep } from 'lodash';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
-import getIssuesCountQuery from 'ee_else_ce/issues_list/queries/get_issues_count.query.graphql';
+import getIssuesCountsQuery from 'ee_else_ce/issues_list/queries/get_issues_counts.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import {
+ getIssuesCountsQueryResponse,
getIssuesQueryResponse,
filteredTokens,
locationSearch,
urlParams,
- getIssuesCountQueryResponse,
} from 'jest/issues_list/mock_data';
import createFlash from '~/flash';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -97,12 +97,12 @@ describe('IssuesListApp component', () => {
const mountComponent = ({
provide = {},
issuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse),
- issuesQueryCountResponse = jest.fn().mockResolvedValue(getIssuesCountQueryResponse),
+ issuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse),
mountFn = shallowMount,
} = {}) => {
const requestHandlers = [
[getIssuesQuery, issuesQueryResponse],
- [getIssuesCountQuery, issuesQueryCountResponse],
+ [getIssuesCountsQuery, issuesCountsQueryResponse],
];
const apolloProvider = createMockApollo(requestHandlers);
@@ -571,9 +571,9 @@ describe('IssuesListApp component', () => {
describe('errors', () => {
describe.each`
- error | mountOption | message
- ${'fetching issues'} | ${'issuesQueryResponse'} | ${IssuesListApp.i18n.errorFetchingIssues}
- ${'fetching issue counts'} | ${'issuesQueryCountResponse'} | ${IssuesListApp.i18n.errorFetchingCounts}
+ error | mountOption | message
+ ${'fetching issues'} | ${'issuesQueryResponse'} | ${IssuesListApp.i18n.errorFetchingIssues}
+ ${'fetching issue counts'} | ${'issuesCountsQueryResponse'} | ${IssuesListApp.i18n.errorFetchingCounts}
`('when there is an error $error', ({ mountOption, message }) => {
beforeEach(() => {
wrapper = mountComponent({
@@ -696,7 +696,11 @@ describe('IssuesListApp component', () => {
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({ message: IssuesListApp.i18n.reorderError });
+ expect(createFlash).toHaveBeenCalledWith({
+ message: IssuesListApp.i18n.reorderError,
+ captureError: true,
+ error: new Error('Request failed with status code 500'),
+ });
});
});
});
diff --git a/spec/frontend/issues_list/mock_data.js b/spec/frontend/issues_list/mock_data.js
index d3f3f2f9f23..0f9677cff4b 100644
--- a/spec/frontend/issues_list/mock_data.js
+++ b/spec/frontend/issues_list/mock_data.js
@@ -70,10 +70,16 @@ export const getIssuesQueryResponse = {
},
};
-export const getIssuesCountQueryResponse = {
+export const getIssuesCountsQueryResponse = {
data: {
project: {
- issues: {
+ openedIssues: {
+ count: 1,
+ },
+ closedIssues: {
+ count: 1,
+ },
+ allIssues: {
count: 1,
},
},
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 679871b6672..3eb3c73cfcc 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -123,7 +123,7 @@ RSpec.describe IssuablesHelper do
end
describe '#issuables_state_counter_text' do
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
describe 'state text' do
context 'when number of issuables can be generated' do
@@ -159,6 +159,38 @@ RSpec.describe IssuablesHelper do
.to eq('<span>All</span>')
end
end
+
+ context 'when count is over the threshold' do
+ let_it_be(:group) { create(:group) }
+
+ before do
+ allow(helper).to receive(:issuables_count_for_state).and_return(1100)
+ allow(helper).to receive(:parent).and_return(group)
+ stub_const("Gitlab::IssuablesCountForState::THRESHOLD", 1000)
+ end
+
+ context 'when feature flag cached_issues_state_count is disabled' do
+ before do
+ stub_feature_flags(cached_issues_state_count: false)
+ end
+
+ it 'returns complete count' do
+ expect(helper.issuables_state_counter_text(:issues, :opened, true))
+ .to eq('<span>Open</span> <span class="badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm">1,100</span>')
+ end
+ end
+
+ context 'when feature flag cached_issues_state_count is enabled' do
+ before do
+ stub_feature_flags(cached_issues_state_count: true)
+ end
+
+ it 'returns truncated count' do
+ expect(helper.issuables_state_counter_text(:issues, :opened, true))
+ .to eq('<span>Open</span> <span class="badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm">1.1k</span>')
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/background_migration/migrate_pages_metadata_spec.rb b/spec/lib/gitlab/background_migration/migrate_pages_metadata_spec.rb
index 906a6a747c9..815dc2e73e5 100644
--- a/spec/lib/gitlab/background_migration/migrate_pages_metadata_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_pages_metadata_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigratePagesMetadata, schema: 201909
subject(:migrate_pages_metadata) { described_class.new }
- describe '#perform_on_relation' do
+ describe '#perform' do
let(:namespaces) { table(:namespaces) }
let(:builds) { table(:ci_builds) }
let(:pages_metadata) { table(:project_pages_metadata) }
@@ -23,9 +23,9 @@ RSpec.describe Gitlab::BackgroundMigration::MigratePagesMetadata, schema: 201909
not_migrated_no_pages = projects.create!(namespace_id: namespace.id, name: 'Not Migrated No Pages')
project_not_in_relation_scope = projects.create!(namespace_id: namespace.id, name: 'Other')
- projects_relation = projects.where(id: [not_migrated_with_pages, not_migrated_no_pages, migrated])
+ ids = [not_migrated_no_pages.id, not_migrated_with_pages.id, migrated.id]
- migrate_pages_metadata.perform_on_relation(projects_relation)
+ migrate_pages_metadata.perform(ids.min, ids.max)
expect(pages_metadata.find_by_project_id(not_migrated_with_pages.id).deployed).to eq(true)
expect(pages_metadata.find_by_project_id(not_migrated_no_pages.id).deployed).to eq(false)
@@ -33,12 +33,4 @@ RSpec.describe Gitlab::BackgroundMigration::MigratePagesMetadata, schema: 201909
expect(pages_metadata.find_by_project_id(project_not_in_relation_scope.id)).to be_nil
end
end
-
- describe '#perform' do
- it 'creates relation and delegates to #perform_on_relation' do
- expect(migrate_pages_metadata).to receive(:perform_on_relation).with(projects.where(id: 3..5))
-
- migrate_pages_metadata.perform(3, 5)
- end
- end
end
diff --git a/spec/lib/gitlab/issuables_count_for_state_spec.rb b/spec/lib/gitlab/issuables_count_for_state_spec.rb
index a6170c146ab..e377d1e1dde 100644
--- a/spec/lib/gitlab/issuables_count_for_state_spec.rb
+++ b/spec/lib/gitlab/issuables_count_for_state_spec.rb
@@ -66,4 +66,106 @@ RSpec.describe Gitlab::IssuablesCountForState do
end
end
end
+
+ context 'when store_in_redis_cache is `true`', :clean_gitlab_redis_cache do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ let(:cache_options) { { expires_in: 10.minutes } }
+ let(:cache_key) { ['group', group.id, 'issues'] }
+ let(:threshold) { described_class::THRESHOLD }
+ let(:states_count) { { opened: 1, closed: 1, all: 2 } }
+ let(:params) { {} }
+
+ subject { described_class.new(finder, fast_fail: true, store_in_redis_cache: true ) }
+
+ before do
+ allow(finder).to receive(:count_by_state).and_return(states_count)
+ allow_next_instance_of(described_class) do |counter|
+ allow(counter).to receive(:parent_group).and_return(group)
+ end
+ end
+
+ shared_examples 'calculating counts without caching' do
+ it 'does not store in redis store' do
+ expect(Rails.cache).not_to receive(:read)
+ expect(finder).to receive(:count_by_state)
+ expect(Rails.cache).not_to receive(:write)
+ expect(subject[:all]).to eq(states_count[:all])
+ end
+ end
+
+ context 'with Issues' do
+ let(:finder) { IssuesFinder.new(user, params) }
+
+ it 'returns -1 for the requested state' do
+ allow(finder).to receive(:count_by_state).and_raise(ActiveRecord::QueryCanceled)
+ expect(Rails.cache).not_to receive(:write)
+
+ expect(subject[:all]).to eq(-1)
+ end
+
+ context 'when parent group is not present' do
+ let(:group) { nil }
+
+ it_behaves_like 'calculating counts without caching'
+ end
+
+ context 'when params include search filters' do
+ let(:parent) { group }
+
+ before do
+ finder.params[:assignee_username] = [user.username, 'root']
+ end
+
+ it_behaves_like 'calculating counts without caching'
+ end
+
+ context 'when counts are stored in cache' do
+ before do
+ allow(Rails.cache).to receive(:read).with(cache_key, cache_options)
+ .and_return({ opened: 1000, closed: 1000, all: 2000 })
+ end
+
+ it 'does not call finder count_by_state' do
+ expect(finder).not_to receive(:count_by_state)
+
+ expect(subject[:all]).to eq(2000)
+ end
+ end
+
+ context 'when cache is empty' do
+ context 'when state counts are under threshold' do
+ let(:states_count) { { opened: 1, closed: 1, all: 2 } }
+
+ it 'does not store state counts in cache' do
+ expect(Rails.cache).to receive(:read).with(cache_key, cache_options)
+ expect(finder).to receive(:count_by_state)
+ expect(Rails.cache).not_to receive(:write)
+ expect(subject[:all]).to eq(states_count[:all])
+ end
+ end
+
+ context 'when state counts are over threshold' do
+ let(:states_count) do
+ { opened: threshold + 1, closed: threshold + 1, all: (threshold + 1) * 2 }
+ end
+
+ it 'stores state counts in cache' do
+ expect(Rails.cache).to receive(:read).with(cache_key, cache_options)
+ expect(finder).to receive(:count_by_state)
+ expect(Rails.cache).to receive(:write).with(cache_key, states_count, cache_options)
+
+ expect(subject[:all]).to eq((threshold + 1) * 2)
+ end
+ end
+ end
+ end
+
+ context 'with Merge Requests' do
+ let(:finder) { MergeRequestsFinder.new(user, params) }
+
+ it_behaves_like 'calculating counts without caching'
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 659be6f993d..7be90fdedbd 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -5999,4 +5999,49 @@ RSpec.describe User do
end
end
end
+
+ describe '#groups_with_developer_maintainer_project_access' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group1) { create(:group) }
+
+ let_it_be(:developer_group1) do
+ create(:group).tap do |g|
+ g.add_developer(user)
+ end
+ end
+
+ let_it_be(:developer_group2) do
+ create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS).tap do |g|
+ g.add_developer(user)
+ end
+ end
+
+ let_it_be(:guest_group1) do
+ create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS).tap do |g|
+ g.add_guest(user)
+ end
+ end
+
+ let_it_be(:developer_group1) do
+ create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS).tap do |g|
+ g.add_maintainer(user)
+ end
+ end
+
+ subject { user.send(:groups_with_developer_maintainer_project_access) }
+
+ shared_examples 'groups_with_developer_maintainer_project_access examples' do
+ specify { is_expected.to contain_exactly(developer_group2) }
+ end
+
+ it_behaves_like 'groups_with_developer_maintainer_project_access examples'
+
+ context 'when feature flag :linear_user_groups_with_developer_maintainer_project_access is disabled' do
+ before do
+ stub_feature_flags(linear_user_groups_with_developer_maintainer_project_access: false)
+ end
+
+ it_behaves_like 'groups_with_developer_maintainer_project_access examples'
+ end
+ end
end