Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2195019331
commit
9c191c0b94
34 changed files with 516 additions and 214 deletions
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 });
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
return this.queryVariables;
|
||||
},
|
||||
update: ({ project }) => project ?? {},
|
||||
error(error) {
|
||||
createFlash({ message: this.$options.i18n.errorFetchingCounts, captureError: true, error });
|
||||
},
|
||||
skip() {
|
||||
return !this.hasAnyIssues;
|
||||
},
|
||||
},
|
||||
countClosed: {
|
||||
...issuesCountSmartQueryBase,
|
||||
variables() {
|
||||
return {
|
||||
...this.queryVariables,
|
||||
state: IssuableStates.Closed,
|
||||
};
|
||||
},
|
||||
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"
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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: []
|
|
@ -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: []
|
|
@ -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: []
|
|
@ -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`.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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> Get to productivity faster with these [7 advanced GitLab CI workflow hacks](https://about.gitlab.com/webcast/7cicd-hacks/)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue