diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-01 18:10:20 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-01 18:10:20 +0300 |
commit | 9c191c0b942eb08360f4d64c038c435b1156e15f (patch) | |
tree | 18ac3c7c2d816ffa4898202102cb889c2c6ca5a7 | |
parent | 219501933150525be819f047d3196969ee914c47 (diff) |
Add latest changes from gitlab-org/gitlab@master
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> 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 |