Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-09-01 15:10:20 +00:00
parent 2195019331
commit 9c191c0b94
34 changed files with 516 additions and 214 deletions

View file

@ -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)

View file

@ -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);

View file

@ -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 });
},

View file

@ -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

View file

@ -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"

View file

@ -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,
};

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -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')

View file

@ -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)

View file

@ -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?

View file

@ -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

View file

@ -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

View file

@ -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: []

View file

@ -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: []

View file

@ -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: []

View file

@ -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`.

View file

@ -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.

View file

@ -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

View file

@ -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/)

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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();

View file

@ -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>

View file

@ -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'),
});
});
});
});

View file

@ -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,
},
},

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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