Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/issue_templates/Security developer workflow.md3
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue1
-rw-r--r--app/assets/javascripts/monitoring/constants.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js10
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js4
-rw-r--r--app/assets/javascripts/releases/components/app_edit_new.vue37
-rw-r--r--app/assets/javascripts/releases/components/tag_field.vue20
-rw-r--r--app/assets/javascripts/releases/components/tag_field_existing.vue52
-rw-r--r--app/assets/javascripts/releases/components/tag_field_new.vue8
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/getters.js8
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/state.js5
-rw-r--r--app/models/commit_status_enums.rb5
-rw-r--r--app/models/concerns/counter_attribute.rb143
-rw-r--r--app/services/metrics/dashboard/self_monitoring_dashboard_service.rb4
-rw-r--r--app/services/metrics/dashboard/system_dashboard_service.rb2
-rw-r--r--app/workers/all_queues.yml8
-rw-r--r--app/workers/flush_counter_increments_worker.rb26
-rw-r--r--changelogs/unreleased/231177-enforce-storage-limit-app-setting.yml5
-rw-r--r--changelogs/unreleased/232980-pasting-an-image-into-a-comment-still-uploades-a-design.yml5
-rw-r--r--changelogs/unreleased/astoicescu-changeDefaultDashboardNameToOverview.yml5
-rw-r--r--changelogs/unreleased/clickable-component-diagram.yml5
-rw-r--r--changelogs/unreleased/efficient-counters-redis.yml5
-rw-r--r--config/prometheus/self_monitoring_default.yml2
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/migrate/20200728175710_add_enforce_namespace_storage_limit_to_application_settings.rb9
-rw-r--r--db/schema_migrations/202007281757101
-rw-r--r--db/structure.sql1
-rw-r--r--doc/administration/logs.md5
-rw-r--r--doc/administration/monitoring/gitlab_self_monitoring_project/img/self_monitoring_overview_dashboard.png (renamed from doc/administration/monitoring/gitlab_self_monitoring_project/img/self_monitoring_default_dashboard.png)bin51508 -> 51508 bytes
-rw-r--r--doc/administration/monitoring/gitlab_self_monitoring_project/index.md4
-rw-r--r--doc/api/settings.md1
-rw-r--r--doc/development/architecture.md157
-rw-r--r--doc/operations/metrics/embed.md2
-rw-r--r--doc/university/bookclub/booklist.md117
-rw-r--r--doc/university/bookclub/index.md23
-rw-r--r--doc/university/glossary/README.md10
-rw-r--r--doc/user/group/saml_sso/scim_setup.md4
-rw-r--r--lib/gitlab/metrics/subscribers/active_record.rb2
-rw-r--r--lib/gitlab/usage_data.rb37
-rw-r--r--locale/gitlab.pot3
-rw-r--r--rubocop/rubocop-migrations.yml1
-rw-r--r--spec/controllers/concerns/metrics_dashboard_spec.rb2
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb84
-rw-r--r--spec/frontend/design_management/pages/index_spec.js70
-rw-r--r--spec/frontend/monitoring/components/dashboard_header_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js12
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js2
-rw-r--r--spec/frontend/monitoring/mock_data.js2
-rw-r--r--spec/frontend/monitoring/store/getters_spec.js6
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js23
-rw-r--r--spec/frontend/releases/components/tag_field_exsting_spec.js78
-rw-r--r--spec/frontend/releases/components/tag_field_new_spec.js34
-rw-r--r--spec/frontend/releases/components/tag_field_spec.js60
-rw-r--r--spec/frontend/releases/stores/modules/detail/getters_spec.js14
-rw-r--r--spec/lib/gitlab/metrics/dashboard/finder_spec.rb4
-rw-r--r--spec/models/concerns/counter_attribute_spec.rb52
-rw-r--r--spec/models/project_statistics_spec.rb4
-rw-r--r--spec/services/metrics/dashboard/dynamic_embed_service_spec.rb2
-rw-r--r--spec/support/counter_attribute.rb14
-rw-r--r--spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb176
-rw-r--r--spec/workers/flush_counter_increments_worker_spec.rb41
62 files changed, 1008 insertions, 420 deletions
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md
index 7de137bd2e2..89539289dcb 100644
--- a/.gitlab/issue_templates/Security developer workflow.md
+++ b/.gitlab/issue_templates/Security developer workflow.md
@@ -53,7 +53,7 @@ After your merge request has been approved according to our [approval guidelines
| Description | Details | Further details|
| -------- | -------- | -------- |
| Versions affected | X.Y | |
-| GitLab EE only | Yes/No | |
+| GitLab EE only | Yes/No | |
| Upgrade notes | | |
| GitLab Settings updated | Yes/No| |
| Migration required | Yes/No | |
@@ -62,7 +62,6 @@ After your merge request has been approved according to our [approval guidelines
[security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md
[secpick documentation]: https://gitlab.com/gitlab-org/release/docs/-/blob/master/general/security/utilities/secpick_script.md
[security Release merge request template]: https://gitlab.com/gitlab-org/security/gitlab/blob/master/.gitlab/merge_request_templates/Security%20Release.md
-[code review process]: https://docs.gitlab.com/ee/development/code_review.html
[approval guidelines]: https://docs.gitlab.com/ee/development/code_review.html#approval-guidelines
[issue as related]: https://docs.gitlab.com/ee/user/project/issues/related_issues.html#adding-a-related-issue
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 679b48858e9..4ac4d83a3bf 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-3876ecd3e4f6bf756621ad07de5e033f8a5b6129
+40b90823b0d55561059d27249e02db426b428786
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index 916c1681041..e35d1a8c419 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -106,7 +106,6 @@ export default {
},
},
mounted() {
- this.toggleOnPasteListener(this.$route.name);
if (this.$route.path === '/designs') {
this.$el.scrollIntoView();
}
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index afeb3318eb9..2ddee67db8c 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -213,7 +213,7 @@ export const annotationsSymbolIcon = 'path://m5 229 5 8h-10z';
* This technical debt is being tracked here
* https://gitlab.com/gitlab-org/gitlab/-/issues/214671
*/
-export const DEFAULT_DASHBOARD_PATH = 'config/prometheus/common_metrics.yml';
+export const OVERVIEW_DASHBOARD_PATH = 'config/prometheus/common_metrics.yml';
/**
* GitLab provide metrics dashboards that are available to a user once
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 0c8f97f2e29..a569eec6f91 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -17,7 +17,7 @@ import { convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale';
import { getDashboard, getPrometheusQueryData, getPanelJson } from '../requests';
-import { ENVIRONMENT_AVAILABLE_STATE, DEFAULT_DASHBOARD_PATH, VARIABLE_TYPES } from '../constants';
+import { ENVIRONMENT_AVAILABLE_STATE, OVERVIEW_DASHBOARD_PATH, VARIABLE_TYPES } from '../constants';
function prometheusMetricQueryParams(timeRange) {
const { start, end } = convertToFixedRange(timeRange);
@@ -298,7 +298,7 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => {
export const fetchAnnotations = ({ state, dispatch, getters }) => {
const { start } = convertToFixedRange(state.timeRange);
- const dashboardPath = getters.fullDashboardPath || DEFAULT_DASHBOARD_PATH;
+ const dashboardPath = getters.fullDashboardPath || OVERVIEW_DASHBOARD_PATH;
return gqClient
.mutate({
mutation: getAnnotations,
@@ -331,12 +331,12 @@ export const receiveAnnotationsFailure = ({ commit }) => commit(types.RECEIVE_AN
export const fetchDashboardValidationWarnings = ({ state, dispatch, getters }) => {
/**
- * Normally, the default dashboard won't throw any validation warnings.
+ * Normally, the overview dashboard won't throw any validation warnings.
*
- * However, if a bug sneaks into the default dashboard making it invalid,
+ * However, if a bug sneaks into the overview dashboard making it invalid,
* this might come handy for our clients
*/
- const dashboardPath = getters.fullDashboardPath || DEFAULT_DASHBOARD_PATH;
+ const dashboardPath = getters.fullDashboardPath || OVERVIEW_DASHBOARD_PATH;
return gqClient
.mutate({
mutation: getDashboardValidationWarnings,
diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js
index 51562593ee8..29c477c2c41 100644
--- a/app/assets/javascripts/monitoring/stores/utils.js
+++ b/app/assets/javascripts/monitoring/stores/utils.js
@@ -465,9 +465,9 @@ export const addPrefixToCustomVariableParams = name => `variables[${name}]`;
* metrics dashboard to work with custom dashboard file names instead
* of the entire path.
*
- * If dashboard is empty, it is the default dashboard.
+ * If dashboard is empty, it is the overview dashboard.
* If dashboard is set, it usually is a custom dashboard unless
- * explicitly it is set to default dashboard path.
+ * explicitly it is set to overview dashboard path.
*
* @param {String} dashboard dashboard path
* @param {String} dashboardPrefix custom dashboard directory prefix
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index fd3491a9c62..09fdd9e4438 100644
--- a/app/assets/javascripts/releases/components/app_edit_new.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -1,7 +1,6 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
-import { escape } from 'lodash';
import { __, sprintf } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
@@ -10,6 +9,7 @@ import { getParameterByName } from '~/lib/utils/common_utils';
import AssetLinksForm from './asset_links_form.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MilestoneCombobox from '~/milestones/project_milestone_combobox.vue';
+import TagField from './tag_field.vue';
export default {
name: 'ReleaseEditNewApp',
@@ -20,6 +20,7 @@ export default {
MarkdownField,
AssetLinksForm,
MilestoneCombobox,
+ TagField,
},
directives: {
autofocusonshow,
@@ -55,23 +56,6 @@ export default {
false,
);
},
- tagName() {
- return this.$store.state.detail.release.tagName;
- },
- tagNameHintText() {
- return sprintf(
- __(
- 'Changing a Release tag is only supported via Releases API. %{linkStart}More information%{linkEnd}',
- ),
- {
- linkStart: `<a href="${escape(
- this.updateReleaseApiDocsPath,
- )}" target="_blank" rel="noopener noreferrer">`,
- linkEnd: '</a>',
- },
- false,
- );
- },
releaseTitle: {
get() {
return this.$store.state.detail.release.name;
@@ -136,22 +120,7 @@ export default {
<div class="d-flex flex-column">
<p class="pt-3 js-subtitle-text" v-html="subtitleText"></p>
<form v-if="showForm" @submit.prevent="updateRelease()">
- <gl-form-group>
- <div class="row">
- <div class="col-md-6 col-lg-5 col-xl-4">
- <label for="git-ref">{{ __('Tag name') }}</label>
- <gl-form-input
- id="git-ref"
- v-model="tagName"
- type="text"
- class="form-control"
- aria-describedby="tag-name-help"
- disabled
- />
- </div>
- </div>
- <div id="tag-name-help" class="form-text text-muted" v-html="tagNameHintText"></div>
- </gl-form-group>
+ <tag-field />
<gl-form-group>
<label for="release-title">{{ __('Release title') }}</label>
<gl-form-input
diff --git a/app/assets/javascripts/releases/components/tag_field.vue b/app/assets/javascripts/releases/components/tag_field.vue
new file mode 100644
index 00000000000..ed8d6e62926
--- /dev/null
+++ b/app/assets/javascripts/releases/components/tag_field.vue
@@ -0,0 +1,20 @@
+<script>
+import { mapGetters } from 'vuex';
+import TagFieldExisting from './tag_field_existing.vue';
+import TagFieldNew from './tag_field_new.vue';
+
+export default {
+ components: {
+ TagFieldExisting,
+ TagFieldNew,
+ },
+ computed: {
+ ...mapGetters('detail', ['isExistingRelease']),
+ },
+};
+</script>
+
+<template>
+ <tag-field-existing v-if="isExistingRelease" />
+ <tag-field-new v-else />
+</template>
diff --git a/app/assets/javascripts/releases/components/tag_field_existing.vue b/app/assets/javascripts/releases/components/tag_field_existing.vue
new file mode 100644
index 00000000000..6267e7088c4
--- /dev/null
+++ b/app/assets/javascripts/releases/components/tag_field_existing.vue
@@ -0,0 +1,52 @@
+<script>
+import { mapState } from 'vuex';
+import { uniqueId } from 'lodash';
+import { GlFormGroup, GlFormInput, GlLink, GlSprintf } from '@gitlab/ui';
+
+export default {
+ name: 'TagFieldExisting',
+ components: { GlFormGroup, GlFormInput, GlSprintf, GlLink },
+ computed: {
+ ...mapState('detail', ['release', 'updateReleaseApiDocsPath']),
+ inputId() {
+ return uniqueId('tag-name-input-');
+ },
+ helpId() {
+ return uniqueId('tag-name-help-');
+ },
+ },
+};
+</script>
+<template>
+ <gl-form-group :label="__('Tag name')" :label-for="inputId">
+ <div class="row">
+ <div class="col-md-6 col-lg-5 col-xl-4">
+ <gl-form-input
+ :id="inputId"
+ :value="release.tagName"
+ type="text"
+ class="form-control"
+ :aria-describedby="helpId"
+ disabled
+ />
+ </div>
+ </div>
+ <template #description>
+ <div :id="helpId" data-testid="tag-name-help">
+ <gl-sprintf
+ :message="
+ __(
+ 'Changing a Release tag is only supported via Releases API. %{linkStart}More information%{linkEnd}',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link :href="updateReleaseApiDocsPath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ </template>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue
new file mode 100644
index 00000000000..594ae77bfb0
--- /dev/null
+++ b/app/assets/javascripts/releases/components/tag_field_new.vue
@@ -0,0 +1,8 @@
+<script>
+export default {
+ name: 'TagFieldNew',
+};
+</script>
+<template>
+ <div></div>
+</template>
diff --git a/app/assets/javascripts/releases/stores/modules/detail/getters.js b/app/assets/javascripts/releases/stores/modules/detail/getters.js
index 84dc2fca4be..ffbbc756f39 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/getters.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/getters.js
@@ -2,6 +2,14 @@ import { isEmpty } from 'lodash';
import { hasContent } from '~/lib/utils/text_utility';
/**
+ * @returns {Boolean} `true` if the app is editing an existing release.
+ * `false` if the app is creating a new release.
+ */
+export const isExistingRelease = state => {
+ return Boolean(state.originalRelease);
+};
+
+/**
* @param {Object} link The link to test
* @returns {Boolean} `true` if the release link is empty, i.e. it has
* empty (or whitespace-only) values for both `url` and `name`.
diff --git a/app/assets/javascripts/releases/stores/modules/detail/state.js b/app/assets/javascripts/releases/stores/modules/detail/state.js
index 966c1c00ef5..1e634992d05 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/state.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/state.js
@@ -19,7 +19,12 @@ export default ({
manageMilestonesPath,
newMilestonePath,
+ /**
+ * The name of the tag associated with the release, provided by the backend.
+ * When creating a new release, this value is null.
+ */
tagName,
+
releasesPagePath,
defaultBranch,
diff --git a/app/models/commit_status_enums.rb b/app/models/commit_status_enums.rb
index f6b8c08b8e2..caebff91022 100644
--- a/app/models/commit_status_enums.rb
+++ b/app/models/commit_status_enums.rb
@@ -19,14 +19,13 @@ module CommitStatusEnums
scheduler_failure: 11,
data_integrity_failure: 12,
forward_deployment_failure: 13,
- protected_environment_failure: 1_000,
insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003,
- upstream_bridge_project_not_found: 1_004,
- insufficient_upstream_permissions: 1_005,
bridge_pipeline_is_child_pipeline: 1_006,
downstream_pipeline_creation_failed: 1_007
}
end
end
+
+CommitStatusEnums.prepend_if_ee('EE::CommitStatusEnums')
diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb
new file mode 100644
index 00000000000..a5c7393e8f7
--- /dev/null
+++ b/app/models/concerns/counter_attribute.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+# Add capabilities to increment a numeric model attribute efficiently by
+# using Redis and flushing the increments asynchronously to the database
+# after a period of time (10 minutes).
+# When an attribute is incremented by a value, the increment is added
+# to a Redis key. Then, FlushCounterIncrementsWorker will execute
+# `flush_increments_to_database!` which removes increments from Redis for a
+# given model attribute and updates the values in the database.
+#
+# @example:
+#
+# class ProjectStatistics
+# include CounterAttribute
+#
+# counter_attribute :commit_count
+# counter_attribute :storage_size
+# end
+#
+# To increment the counter we can use the method:
+# delayed_increment_counter(:commit_count, 3)
+#
+module CounterAttribute
+ extend ActiveSupport::Concern
+ extend AfterCommitQueue
+ include Gitlab::ExclusiveLeaseHelpers
+
+ LUA_STEAL_INCREMENT_SCRIPT = <<~EOS.freeze
+ local increment_key, flushed_key = KEYS[1], KEYS[2]
+ local increment_value = redis.call("get", increment_key) or 0
+ local flushed_value = redis.call("incrby", flushed_key, increment_value)
+ if flushed_value == 0 then
+ redis.call("del", increment_key, flushed_key)
+ else
+ redis.call("del", increment_key)
+ end
+ return flushed_value
+ EOS
+
+ WORKER_DELAY = 10.minutes
+ WORKER_LOCK_TTL = 10.minutes
+
+ class_methods do
+ def counter_attribute(attribute)
+ counter_attributes << attribute
+ end
+
+ def counter_attributes
+ @counter_attributes ||= Set.new
+ end
+ end
+
+ # This method must only be called by FlushCounterIncrementsWorker
+ # because it should run asynchronously and with exclusive lease.
+ # This will
+ # 1. temporarily move the pending increment for a given attribute
+ # to a relative "flushed" Redis key, delete the increment key and return
+ # the value. If new increments are performed at this point, the increment
+ # key is recreated as part of `delayed_increment_counter`.
+ # The "flushed" key is used to ensure that we can keep incrementing
+ # counters in Redis while flushing existing values.
+ # 2. then the value is used to update the counter in the database.
+ # 3. finally the "flushed" key is deleted.
+ def flush_increments_to_database!(attribute)
+ lock_key = counter_lock_key(attribute)
+
+ with_exclusive_lease(lock_key) do
+ increment_key = counter_key(attribute)
+ flushed_key = counter_flushed_key(attribute)
+ increment_value = steal_increments(increment_key, flushed_key)
+
+ next if increment_value == 0
+
+ transaction do
+ unsafe_update_counters(id, attribute => increment_value)
+ redis_state { |redis| redis.del(flushed_key) }
+ end
+ end
+ end
+
+ def delayed_increment_counter(attribute, increment)
+ return if increment == 0
+
+ run_after_commit_or_now do
+ if counter_attribute_enabled?(attribute)
+ redis_state do |redis|
+ redis.incrby(counter_key(attribute), increment)
+ end
+
+ FlushCounterIncrementsWorker.perform_in(WORKER_DELAY, self.class.name, self.id, attribute)
+ else
+ legacy_increment!(attribute, increment)
+ end
+ end
+
+ true
+ end
+
+ def counter_key(attribute)
+ "project:{#{project_id}}:counters:#{self.class}:#{id}:#{attribute}"
+ end
+
+ def counter_flushed_key(attribute)
+ counter_key(attribute) + ':flushed'
+ end
+
+ def counter_lock_key(attribute)
+ counter_key(attribute) + ':lock'
+ end
+
+ private
+
+ def counter_attribute_enabled?(attribute)
+ Feature.enabled?(:efficient_counter_attribute, project) &&
+ self.class.counter_attributes.include?(attribute)
+ end
+
+ def steal_increments(increment_key, flushed_key)
+ redis_state do |redis|
+ redis.eval(LUA_STEAL_INCREMENT_SCRIPT, keys: [increment_key, flushed_key])
+ end
+ end
+
+ def legacy_increment!(attribute, increment)
+ increment!(attribute, increment)
+ end
+
+ def unsafe_update_counters(id, increments)
+ self.class.update_counters(id, increments)
+ end
+
+ def redis_state(&block)
+ Gitlab::Redis::SharedState.with(&block)
+ end
+
+ def with_exclusive_lease(lock_key)
+ in_lock(lock_key, ttl: WORKER_LOCK_TTL) do
+ yield
+ end
+ rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
+ # a worker is already updating the counters
+ end
+end
diff --git a/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb b/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb
index 9097caade4b..eb0575c9d23 100644
--- a/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb
+++ b/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb
@@ -6,10 +6,10 @@ module Metrics
module Dashboard
class SelfMonitoringDashboardService < ::Metrics::Dashboard::PredefinedDashboardService
DASHBOARD_PATH = 'config/prometheus/self_monitoring_default.yml'
- DASHBOARD_NAME = N_('Default dashboard')
+ DASHBOARD_NAME = N_('Overview')
# SHA256 hash of dashboard content
- DASHBOARD_VERSION = '1dff3e3cb76e73c8e368823c98b34c61aec0d141978450dea195a3b3dc2415d6'
+ DASHBOARD_VERSION = '0f7ade2022e09f1a1da8e883cc95d84b9557e1e0e9b015c51eb964296aa73098'
SEQUENCE = [
STAGES::CustomMetricsInserter,
diff --git a/app/services/metrics/dashboard/system_dashboard_service.rb b/app/services/metrics/dashboard/system_dashboard_service.rb
index 5c3562b8ca0..54ff81fd5f5 100644
--- a/app/services/metrics/dashboard/system_dashboard_service.rb
+++ b/app/services/metrics/dashboard/system_dashboard_service.rb
@@ -6,7 +6,7 @@ module Metrics
module Dashboard
class SystemDashboardService < ::Metrics::Dashboard::PredefinedDashboardService
DASHBOARD_PATH = 'config/prometheus/common_metrics.yml'
- DASHBOARD_NAME = N_('Default dashboard')
+ DASHBOARD_NAME = N_('Overview')
# SHA256 hash of dashboard content
DASHBOARD_VERSION = '4685fe386c25b1a786b3be18f79bb2ee9828019003e003816284cdb634fa3e13'
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 3d243cfd00c..14488aa7f59 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1340,6 +1340,14 @@
:weight: 1
:idempotent:
:tags: []
+- :name: flush_counter_increments
+ :feature_category: :not_owned
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: git_garbage_collect
:feature_category: :gitaly
:has_external_dependencies:
diff --git a/app/workers/flush_counter_increments_worker.rb b/app/workers/flush_counter_increments_worker.rb
new file mode 100644
index 00000000000..b7e3c0c134d
--- /dev/null
+++ b/app/workers/flush_counter_increments_worker.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+# Invoked by CounterAttribute concern when incrementing counter
+# attributes. The method `flush_increments_to_database!` that
+# this worker uses is itself idempotent as it runs with exclusive
+# lease to ensure that only one instance at the time can flush
+# increments from Redis to the database.
+class FlushCounterIncrementsWorker
+ include ApplicationWorker
+
+ feature_category_not_owned!
+ urgency :low
+ deduplicate :until_executing, including_scheduled: true
+
+ idempotent!
+
+ def perform(model_name, model_id, attribute)
+ return unless self.class.const_defined?(model_name)
+
+ model_class = model_name.constantize
+ model = model_class.find_by_id(model_id)
+ return unless model
+
+ model.flush_increments_to_database!(attribute)
+ end
+end
diff --git a/changelogs/unreleased/231177-enforce-storage-limit-app-setting.yml b/changelogs/unreleased/231177-enforce-storage-limit-app-setting.yml
new file mode 100644
index 00000000000..32f78ed7996
--- /dev/null
+++ b/changelogs/unreleased/231177-enforce-storage-limit-app-setting.yml
@@ -0,0 +1,5 @@
+---
+title: Enforce namespace storage limit via app setting
+merge_request: 38094
+author:
+type: changed
diff --git a/changelogs/unreleased/232980-pasting-an-image-into-a-comment-still-uploades-a-design.yml b/changelogs/unreleased/232980-pasting-an-image-into-a-comment-still-uploades-a-design.yml
new file mode 100644
index 00000000000..e64f276c618
--- /dev/null
+++ b/changelogs/unreleased/232980-pasting-an-image-into-a-comment-still-uploades-a-design.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Pasting an image into a comment still uploades a design
+merge_request: 38280
+author:
+type: fixed
diff --git a/changelogs/unreleased/astoicescu-changeDefaultDashboardNameToOverview.yml b/changelogs/unreleased/astoicescu-changeDefaultDashboardNameToOverview.yml
new file mode 100644
index 00000000000..fe6937764f9
--- /dev/null
+++ b/changelogs/unreleased/astoicescu-changeDefaultDashboardNameToOverview.yml
@@ -0,0 +1,5 @@
+---
+title: In metrics view, change default dashboard name to Overview
+merge_request: 38292
+author:
+type: changed
diff --git a/changelogs/unreleased/clickable-component-diagram.yml b/changelogs/unreleased/clickable-component-diagram.yml
new file mode 100644
index 00000000000..501784ebb16
--- /dev/null
+++ b/changelogs/unreleased/clickable-component-diagram.yml
@@ -0,0 +1,5 @@
+---
+title: Making component diagram click-friendly
+merge_request: 37147
+author: Arjun Pravin @Sgt.Arjun
+type: other
diff --git a/changelogs/unreleased/efficient-counters-redis.yml b/changelogs/unreleased/efficient-counters-redis.yml
new file mode 100644
index 00000000000..b745716246e
--- /dev/null
+++ b/changelogs/unreleased/efficient-counters-redis.yml
@@ -0,0 +1,5 @@
+---
+title: Add mechanism that efficiently increments ActiveRecord counters using Redis
+merge_request: 35878
+author:
+type: performance
diff --git a/config/prometheus/self_monitoring_default.yml b/config/prometheus/self_monitoring_default.yml
index 50e6f4585e4..024733bf2f0 100644
--- a/config/prometheus/self_monitoring_default.yml
+++ b/config/prometheus/self_monitoring_default.yml
@@ -1,4 +1,4 @@
-dashboard: 'Default dashboard'
+dashboard: 'Overview'
priority: 1
templating:
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 8dd60bcd65c..a4d25ce1578 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -108,6 +108,8 @@
- 1
- - file_hook
- 1
+- - flush_counter_increments
+ - 1
- - gcp_cluster
- 1
- - geo
diff --git a/db/migrate/20200728175710_add_enforce_namespace_storage_limit_to_application_settings.rb b/db/migrate/20200728175710_add_enforce_namespace_storage_limit_to_application_settings.rb
new file mode 100644
index 00000000000..adbc86ec621
--- /dev/null
+++ b/db/migrate/20200728175710_add_enforce_namespace_storage_limit_to_application_settings.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddEnforceNamespaceStorageLimitToApplicationSettings < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :enforce_namespace_storage_limit, :boolean, default: false, null: false
+ end
+end
diff --git a/db/schema_migrations/20200728175710 b/db/schema_migrations/20200728175710
new file mode 100644
index 00000000000..0ba117d0ead
--- /dev/null
+++ b/db/schema_migrations/20200728175710
@@ -0,0 +1 @@
+a3a6d4e488c9979efd61890a15fdfe4ccea044a0b030b392ad39885cc807f22d \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 84044a0ad56..4b1f18073c1 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9248,6 +9248,7 @@ CREATE TABLE public.application_settings (
maintenance_mode_message text,
wiki_page_max_content_bytes bigint DEFAULT 52428800 NOT NULL,
elasticsearch_indexed_file_size_limit_kb integer DEFAULT 1024 NOT NULL,
+ enforce_namespace_storage_limit boolean DEFAULT false NOT NULL,
CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),
CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)),
CONSTRAINT check_d03919528d CHECK ((char_length(container_registry_vendor) <= 255)),
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 3db9d32563e..e3182e2df6e 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -14,6 +14,11 @@ Find more about them [in Audit Events documentation](audit_events.md).
System log files are typically plain text in a standard log file format.
This guide talks about how to read and use these system log files.
+[Read more about how to customise logging on Omnibus GitLab
+installations](https://docs.gitlab.com/omnibus/settings/logs.html)
+including adjusting log retention, log forwarding,
+switching logs from JSON to plain text logging, and more.
+
## `production_json.log`
This file lives in `/var/log/gitlab/gitlab-rails/production_json.log` for
diff --git a/doc/administration/monitoring/gitlab_self_monitoring_project/img/self_monitoring_default_dashboard.png b/doc/administration/monitoring/gitlab_self_monitoring_project/img/self_monitoring_overview_dashboard.png
index 1d61823ce41..1d61823ce41 100644
--- a/doc/administration/monitoring/gitlab_self_monitoring_project/img/self_monitoring_default_dashboard.png
+++ b/doc/administration/monitoring/gitlab_self_monitoring_project/img/self_monitoring_overview_dashboard.png
Binary files differ
diff --git a/doc/administration/monitoring/gitlab_self_monitoring_project/index.md b/doc/administration/monitoring/gitlab_self_monitoring_project/index.md
index 636301898a2..e272cccb7ce 100644
--- a/doc/administration/monitoring/gitlab_self_monitoring_project/index.md
+++ b/doc/administration/monitoring/gitlab_self_monitoring_project/index.md
@@ -55,7 +55,7 @@ panels, provide a regular expression in the **Instance label regex** field.
The dashboard uses metrics available in
[Omnibus GitLab](https://docs.gitlab.com/omnibus/) installations.
-![GitLab self monitoring default dashboard](img/self_monitoring_default_dashboard.png)
+![GitLab self monitoring overview dashboard](img/self_monitoring_overview_dashboard.png)
You can also
[create your own dashboards](../../../operations/metrics/dashboards/index.md).
@@ -83,7 +83,7 @@ Once the webhook is setup, you can
You can add custom metrics in the self monitoring project by:
-1. [Duplicating](../../../operations/metrics/dashboards/index.md#duplicate-a-gitlab-defined-dashboard) the default dashboard.
+1. [Duplicating](../../../operations/metrics/dashboards/index.md#duplicate-a-gitlab-defined-dashboard) the overview dashboard.
1. [Editing](../../../operations/metrics/index.md) the newly created dashboard file and configuring it with [dashboard YAML properties](../../../operations/metrics/dashboards/yaml.md).
## Troubleshooting
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 2169ff0f67b..69721de72d2 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -251,6 +251,7 @@ are listed in the descriptions of the relevant settings.
| `email_additional_text` | string | no | **(PREMIUM)** Additional text added to the bottom of every email for legal/auditing/compliance reasons |
| `email_author_in_body` | boolean | no | Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. |
| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. |
+| `enforce_namespace_storage_limit` | boolean | no | Enabling this permits enforcement of namespace storage limits. |
| `enforce_terms` | boolean | no | (**If enabled, requires:** `terms`) Enforce application ToS to all users. |
| `external_auth_client_cert` | string | no | (**If enabled, requires:** `external_auth_client_key`) The certificate to use to authenticate with the external authorization service |
| `external_auth_client_key_pass` | string | no | Passphrase to use for the private key when authenticating with the external service this is encrypted when stored |
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 80533065e7f..ebe43940e1a 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -46,68 +46,101 @@ https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/
```mermaid
graph TB
- HTTP[HTTP/HTTPS] -- TCP 80, 443 --> NGINX[NGINX]
- SSH -- TCP 22 --> GitLabShell[GitLab Shell]
- SMTP[SMTP Gateway]
- Geo[GitLab Geo Node] -- TCP 22, 80, 443 --> NGINX
-
- GitLabShell --TCP 8080 -->Unicorn["Unicorn (GitLab Rails)"]
- GitLabShell --> Praefect
- Unicorn --> PgBouncer[PgBouncer]
- Unicorn --> Redis
- Unicorn --> Praefect
- Sidekiq --> Redis
- Sidekiq --> PgBouncer
- Sidekiq --> Praefect
- GitLabWorkhorse[GitLab Workhorse] --> Unicorn
- GitLabWorkhorse --> Redis
- GitLabWorkhorse --> Praefect
- Praefect --> Gitaly
- NGINX --> GitLabWorkhorse
- NGINX -- TCP 8090 --> GitLabPages[GitLab Pages]
- NGINX --> Grafana[Grafana]
- Grafana -- TCP 9090 --> Prometheus[Prometheus]
- Prometheus -- TCP 80, 443 --> Unicorn
- RedisExporter[Redis Exporter] --> Redis
- Prometheus -- TCP 9121 --> RedisExporter
- PostgreSQLExporter[PostgreSQL Exporter] --> PostgreSQL
- PgBouncerExporter[PgBouncer Exporter] --> PgBouncer
- Prometheus -- TCP 9187 --> PostgreSQLExporter
- Prometheus -- TCP 9100 --> NodeExporter[Node Exporter]
- Prometheus -- TCP 9168 --> GitLabExporter[GitLab Exporter]
- Prometheus -- TCP 9127 --> PgBouncerExporter
- GitLabExporter --> PostgreSQL
- GitLabExporter --> GitLabShell
- GitLabExporter --> Sidekiq
- PgBouncer --> Consul
- PostgreSQL --> Consul
- PgBouncer --> PostgreSQL
- NGINX --> Registry
- Unicorn --> Registry
- NGINX --> Mattermost
- Mattermost --- Unicorn
- Prometheus --> Alertmanager
- Migrations --> PostgreSQL
- Runner -- TCP 443 --> NGINX
- Unicorn -- TCP 9200 --> Elasticsearch
- Sidekiq -- TCP 9200 --> Elasticsearch
- Sidekiq -- TCP 80, 443 --> Sentry
- Unicorn -- TCP 80, 443 --> Sentry
- Sidekiq -- UDP 6831 --> Jaeger
- Unicorn -- UDP 6831 --> Jaeger
- Gitaly -- UDP 6831 --> Jaeger
- GitLabShell -- UDP 6831 --> Jaeger
- GitLabWorkhorse -- UDP 6831 --> Jaeger
- Alertmanager -- TCP 25 --> SMTP
- Sidekiq -- TCP 25 --> SMTP
- Unicorn -- TCP 25 --> SMTP
- Unicorn -- TCP 369 --> LDAP
- Sidekiq -- TCP 369 --> LDAP
- Unicorn -- TCP 443 --> ObjectStorage["Object Storage"]
- Sidekiq -- TCP 443 --> ObjectStorage
- GitLabWorkhorse -- TCP 443 --> ObjectStorage
- Registry -- TCP 443 --> ObjectStorage
- Geo -- TCP 5432 --> PostgreSQL
+HTTP[HTTP/HTTPS] -- TCP 80, 443 --> NGINX[NGINX]
+SSH -- TCP 22 --> GitLabShell[GitLab Shell]
+SMTP[SMTP Gateway]
+Geo[GitLab Geo Node] -- TCP 22, 80, 443 --> NGINX
+
+GitLabShell --TCP 8080 -->Unicorn["Unicorn (GitLab Rails)"]
+GitLabShell --> Praefect
+Unicorn --> PgBouncer[PgBouncer]
+Unicorn --> Redis
+Unicorn --> Praefect
+Sidekiq --> Redis
+Sidekiq --> PgBouncer
+Sidekiq --> Praefect
+GitLabWorkhorse[GitLab Workhorse] --> Unicorn
+GitLabWorkhorse --> Redis
+GitLabWorkhorse --> Praefect
+Praefect --> Gitaly
+NGINX --> GitLabWorkhorse
+NGINX -- TCP 8090 --> GitLabPages[GitLab Pages]
+NGINX --> Grafana[Grafana]
+Grafana -- TCP 9090 --> Prometheus[Prometheus]
+Prometheus -- TCP 80, 443 --> Unicorn
+RedisExporter[Redis Exporter] --> Redis
+Prometheus -- TCP 9121 --> RedisExporter
+PostgreSQLExporter[PostgreSQL Exporter] --> PostgreSQL
+PgBouncerExporter[PgBouncer Exporter] --> PgBouncer
+Prometheus -- TCP 9187 --> PostgreSQLExporter
+Prometheus -- TCP 9100 --> NodeExporter[Node Exporter]
+Prometheus -- TCP 9168 --> GitLabExporter[GitLab Exporter]
+Prometheus -- TCP 9127 --> PgBouncerExporter
+GitLabExporter --> PostgreSQL
+GitLabExporter --> GitLabShell
+GitLabExporter --> Sidekiq
+PgBouncer --> Consul
+PostgreSQL --> Consul
+PgBouncer --> PostgreSQL
+NGINX --> Registry
+Unicorn --> Registry
+NGINX --> Mattermost
+Mattermost --- Unicorn
+Prometheus --> Alertmanager
+Migrations --> PostgreSQL
+Runner -- TCP 443 --> NGINX
+Unicorn -- TCP 9200 --> Elasticsearch
+Sidekiq -- TCP 9200 --> Elasticsearch
+Sidekiq -- TCP 80, 443 --> Sentry
+Unicorn -- TCP 80, 443 --> Sentry
+Sidekiq -- UDP 6831 --> Jaeger
+Unicorn -- UDP 6831 --> Jaeger
+Gitaly -- UDP 6831 --> Jaeger
+GitLabShell -- UDP 6831 --> Jaeger
+GitLabWorkhorse -- UDP 6831 --> Jaeger
+Alertmanager -- TCP 25 --> SMTP
+Sidekiq -- TCP 25 --> SMTP
+Unicorn -- TCP 25 --> SMTP
+Unicorn -- TCP 369 --> LDAP
+Sidekiq -- TCP 369 --> LDAP
+Unicorn -- TCP 443 --> ObjectStorage["Object Storage"]
+Sidekiq -- TCP 443 --> ObjectStorage
+GitLabWorkhorse -- TCP 443 --> ObjectStorage
+Registry -- TCP 443 --> ObjectStorage
+Geo -- TCP 5432 --> PostgreSQL
+
+click Alertmanager "./architecture.html#alertmanager"
+click Praefect "./architecture.html#praefect"
+click Geo "./architecture.html#gitlab-geo"
+click NGINX "./architecture.html#nginx"
+click Runner "./architecture.html#gitlab-runner"
+click Registry "./architecture.html#registry"
+click ObjectStorage "./architecture.html#minio"
+click Mattermost "./architecture.html#mattermost"
+click Gitaly "./architecture.html#gitaly"
+click Jaeger "./architecture.html#jaeger"
+click GitLabWorkhorse "./architecture.html#gitlab-workhorse"
+click LDAP "./architecture.html#ldap-authentication"
+click Unicorn "./architecture.html#unicorn"
+click GitLabShell "./architecture.html#gitlab-shell"
+click SSH "./architecture.html#ssh-request-22"
+click Sidekiq "./architecture.html#sidekiq"
+click Sentry "./architecture.html#sentry"
+click GitLabExporter "./architecture.html#gitlab-exporter"
+click Elasticsearch "./architecture.html#elasticsearch"
+click Migrations "./architecture.html#database-migrations"
+click PostgreSQL "./architecture.html#postgresql"
+click Consul "./architecture.html#consul"
+click PgBouncer "./architecture.html#pgbouncer"
+click PgBouncerExporter "./architecture.html#pgbouncer-exporter"
+click RedisExporter "./architecture.html#redis-exporter"
+click Redis "./architecture.html#redis"
+click Prometheus "./architecture.html#prometheus"
+click Grafana "./architecture.html#grafana"
+click GitLabPages "./architecture.html#gitlab-pages"
+click PostgreSQLExporter "./architecture.html#postgresql-exporter"
+click SMTP "./architecture.html#outbound-email"
+click NodeExporter "./architecture.html#node-exporter"
```
### Component legend
diff --git a/doc/operations/metrics/embed.md b/doc/operations/metrics/embed.md
index 26a8a4628d1..62d60921c85 100644
--- a/doc/operations/metrics/embed.md
+++ b/doc/operations/metrics/embed.md
@@ -57,7 +57,7 @@ You can open the link directly into your browser for a
## Embedding metrics in issue templates
-You can also embed either the default dashboard metrics or individual metrics in
+You can also embed either the overview dashboard metrics or individual metrics in
issue templates. For charts to render side-by-side, separate links to the entire metrics
dashboard or individual metrics by either a comma or a space.
diff --git a/doc/university/bookclub/booklist.md b/doc/university/bookclub/booklist.md
index 33298e45393..c0251229916 100644
--- a/doc/university/bookclub/booklist.md
+++ b/doc/university/bookclub/booklist.md
@@ -1,118 +1,5 @@
---
-comments: false
-type: index
+redirect_to: 'https://docs.gitlab.com'
---
-# Books
-
-List of books and resources that may be worth reading.
-
-## Papers
-
-1. **The Humble Programmer**
-
- Edsger W. Dijkstra, 1972 ([paper](https://dl.acm.org/citation.cfm?id=361591))
-
-## Programming
-
-1. **Design Patterns: Elements of Reusable Object-Oriented Software**
-
- Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, 1994 ([amazon](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612))
-
-1. **Clean Code: A Handbook of Agile Software Craftsmanship**
-
- Robert C. "Uncle Bob" Martin, 2008 ([amazon](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882))
-
-1. **Code Complete: A Practical Handbook of Software Construction**, 2nd Edition
-
- Steve McConnell, 2004 ([amazon](https://www.amazon.com/Code-Complete-Practical-Handbook-Construction/dp/0735619670))
-
-1. **The Pragmatic Programmer: From Journeyman to Master**
-
- Andrew Hunt, David Thomas, 1999 ([amazon](https://www.amazon.com/Pragmatic-Programmer-Journeyman-Master/dp/020161622X))
-
-1. **Working Effectively with Legacy Code**
-
- Michael Feathers, 2004 ([amazon](https://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052))
-
-1. **Eloquent Ruby**
-
- Russ Olsen, 2011 ([amazon](https://www.amazon.com/Eloquent-Ruby-Addison-Wesley-Professional/dp/0321584104))
-
-1. **Domain-Driven Design: Tackling Complexity in the Heart of Software**
-
- Eric Evans, 2003 ([amazon](https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215))
-
-1. **How to Solve It: A New Aspect of Mathematical Method**
-
- Polya G. 1957 ([amazon](https://www.amazon.com/How-Solve-Mathematical-Princeton-Science/dp/069116407X))
-
-1. **Software Creativity 2.0**
-
- Robert L. Glass, 2006 ([amazon](https://www.amazon.com/Software-Creativity-2-0-Robert-Glass/dp/0977213315))
-
-1. **Object-Oriented Software Construction**
-
- Bertrand Meyer, 1997 ([amazon](https://www.amazon.com/Object-Oriented-Software-Construction-Book-CD-ROM/dp/0136291554))
-
-1. **Refactoring: Improving the Design of Existing Code**
-
- Martin Fowler, Kent Beck, 1999 ([amazon](https://www.amazon.com/Refactoring-Improving-Design-Existing-Code/dp/0201485672))
-
-1. **Test Driven Development: By Example**
-
- Kent Beck, 2002 ([amazon](https://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530))
-
-1. **Algorithms in C++: Fundamentals, Data Structure, Sorting, Searching**
-
- Robert Sedgewick, 1990 ([amazon](https://www.amazon.com/Algorithms-Parts-1-4-Fundamentals-Structure/dp/0201350882))
-
-1. **Effective C++**
-
- Scott Mayers, 1996 ([amazon](https://www.amazon.com/Effective-Specific-Improve-Programs-Designs/dp/0321334876))
-
-1. **Extreme Programming Explained: Embrace Change**
-
- Kent Beck, 1999 ([amazon](https://www.amazon.com/Extreme-Programming-Explained-Embrace-Change/dp/0321278658))
-
-1. **The Art of Computer Programming**
-
- Donald E. Knuth, 1997 ([amazon](https://www.amazon.com/Computer-Programming-Volumes-1-4A-Boxed/dp/0321751043))
-
-1. **Writing Efficient Programs**
-
- Jon Louis Bentley, 1982 ([amazon](https://www.amazon.com/Writing-Efficient-Programs-Prentice-Hall-Software/dp/013970244X))
-
-1. **The Mythical Man-Month: Essays on Software Engineering**
-
- Frederick Phillips Brooks, 1975 ([amazon](https://www.amazon.com/Mythical-Man-Month-Essays-Software-Engineering/dp/0201006502))
-
-1. **Peopleware: Productive Projects and Teams** 3rd Edition
-
- Tom DeMarco, Tim Lister, 2013 ([amazon](https://www.amazon.com/Peopleware-Productive-Projects-Teams-3rd/dp/0321934113))
-
-1. **Principles Of Software Engineering Management**
-
- Tom Gilb, 1988 ([amazon](https://www.amazon.com/Principles-Software-Engineering-Management-Gilb/dp/0201192462))
-
-## Other
-
-1. **Thinking, Fast and Slow**
-
- Daniel Kahneman, 2013 ([amazon](https://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374533555))
-
-1. **The Social Animal** 11th Edition
-
- Elliot Aronson, 2011 ([amazon](https://www.amazon.com/Social-Animal-Elliot-Aronson/dp/1429233419))
-
-1. **Influence: Science and Practice** 5th Edition
-
- Robert B. Cialdini, 2008 ([amazon](https://www.amazon.com/Influence-Practice-Robert-B-Cialdini/dp/0205609996))
-
-1. **Getting to Yes: Negotiating Agreement Without Giving In**
-
- Roger Fisher, William L. Ury, Bruce Patton, 2011 ([amazon](https://www.amazon.com/Getting-Yes-Negotiating-Agreement-Without/dp/0143118757))
-
-1. **How to Win Friends & Influence People**
-
- Dale Carnegie, 1981 ([amazon](https://www.amazon.com/How-Win-Friends-Influence-People/dp/0671027034))
+Visit our [documentation page](https://docs.gitlab.com) for information about GitLab.
diff --git a/doc/university/bookclub/index.md b/doc/university/bookclub/index.md
index 71dfe7fc3cb..c0251229916 100644
--- a/doc/university/bookclub/index.md
+++ b/doc/university/bookclub/index.md
@@ -1,24 +1,5 @@
---
-comments: false
-type: index
+redirect_to: 'https://docs.gitlab.com'
---
-# The GitLab Book Club
-
-The Book Club is a casual meet-up to read and discuss books we like.
-We'll find a time that suits most, if not all.
-
-See the [book list](booklist.md) for additional recommendations.
-
-## Currently reading : Books about remote work
-
-1. **Remote: Office not required**
-
- David Heinemeier Hansson and Jason Fried, 2013
- ([Amazon](https://www.amazon.co.uk/dp/0091954673/ref=cm_sw_r_tw_dp_x_0yy9EbZ2WXJ6Y))
-
-1. **The Year Without Pants**
-
- Scott Berkun, 2013 ([ScottBerkun.com](https://scottberkun.com/yearwithoutpants/))
-
-Any other books you'd like to suggest? Edit this page and add them to the queue.
+Visit our [documentation page](https://docs.gitlab.com) for information about GitLab.
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index 297b841b283..c0251229916 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -1,11 +1,5 @@
---
-comments: false
+redirect_to: 'https://docs.gitlab.com'
---
-# Glossary
-
-This page has been removed after an effort to ensure that all applicable GitLab-specific
-terms are available in context on the relevant [GitLab Documentation](https://docs.gitlab.com/)
-or <https://about.gitlab.com/> pages.
-
-If you are looking for a definition of a specific term, please search these sites.
+Visit our [documentation page](https://docs.gitlab.com) for information about GitLab.
diff --git a/doc/user/group/saml_sso/scim_setup.md b/doc/user/group/saml_sso/scim_setup.md
index 13e9d623e2c..1e37acf6ab3 100644
--- a/doc/user/group/saml_sso/scim_setup.md
+++ b/doc/user/group/saml_sso/scim_setup.md
@@ -217,6 +217,10 @@ As a workaround, try an alternate mapping:
#### How do I diagnose why a user is unable to sign in
+Ensure that the user has been added to the SCIM app.
+
+If you receive "User is not linked to a SAML account", then most likely the user already exists in GitLab. Have the user follow the [User access and linking setup](#user-access-and-linking-setup) instructions.
+
The **Identity** (`extern_uid`) value stored by GitLab is updated by SCIM whenever `id` or `externalId` changes. Users won't be able to sign in unless the GitLab Identity (`extern_uid`) value matches the `NameId` sent by SAML.
This value is also used by SCIM to match users on the `id`, and is updated by SCIM whenever the `id` or `externalId` values change.
diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb
index 43c523cf801..346a2f9a461 100644
--- a/lib/gitlab/metrics/subscribers/active_record.rb
+++ b/lib/gitlab/metrics/subscribers/active_record.rb
@@ -22,7 +22,7 @@ module Gitlab
return if payload[:name] == 'SCHEMA' || IGNORABLE_SQL.include?(payload[:sql])
current_transaction.observe(:gitlab_sql_duration_seconds, event.duration / 1000.0) do
- buckets [0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
+ buckets [0.05, 0.1]
end
increment_db_counters(payload)
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index c34030063d6..a067b03534d 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -12,8 +12,6 @@
# redis_usage_data { ::Gitlab::UsageCounters::PodLogs.usage_totals[:total] }
module Gitlab
class UsageData
- BATCH_SIZE = 100
-
class << self
include Gitlab::Utils::UsageData
include Gitlab::Utils::StrongMemoize
@@ -353,29 +351,25 @@ module Gitlab
results
end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def services_usage
# rubocop: disable UsageData/LargeTable:
- Service.available_services_names.without('jira').each_with_object({}) do |service_name, response|
+ Service.available_services_names.each_with_object({}) do |service_name, response|
response["projects_#{service_name}_active".to_sym] = count(Service.active.where(template: false, type: "#{service_name}_service".camelize))
- end.merge(jira_usage).merge(jira_import_usage)
+ end.merge(jira_usage, jira_import_usage)
# rubocop: enable UsageData/LargeTable:
end
def jira_usage
# Jira Cloud does not support custom domains as per https://jira.atlassian.com/browse/CLOUD-6999
# so we can just check for subdomains of atlassian.net
-
results = {
projects_jira_server_active: 0,
- projects_jira_cloud_active: 0,
- projects_jira_active: 0
+ projects_jira_cloud_active: 0
}
# rubocop: disable UsageData/LargeTable:
- JiraService.active.includes(:jira_tracker_data).find_in_batches(batch_size: BATCH_SIZE) do |services|
+ JiraService.active.includes(:jira_tracker_data).find_in_batches(batch_size: 100) do |services|
counts = services.group_by do |service|
# TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
service_url = service.data_fields&.url || (service.properties && service.properties['url'])
@@ -384,22 +378,12 @@ module Gitlab
results[:projects_jira_server_active] += counts[:server].size if counts[:server]
results[:projects_jira_cloud_active] += counts[:cloud].size if counts[:cloud]
- results[:projects_jira_active] += services.size
end
# rubocop: enable UsageData/LargeTable:
results
rescue ActiveRecord::StatementInvalid
- { projects_jira_server_active: FALLBACK, projects_jira_cloud_active: FALLBACK, projects_jira_active: FALLBACK }
- end
-
- # rubocop: disable UsageData/LargeTable
- def successful_deployments_with_cluster(scope)
- scope
- .joins(cluster: :deployments)
- .merge(Clusters::Cluster.enabled)
- .merge(Deployment.success)
+ { projects_jira_server_active: FALLBACK, projects_jira_cloud_active: FALLBACK }
end
- # rubocop: enable UsageData/LargeTable
# rubocop: enable CodeReuse/ActiveRecord
def jira_import_usage
@@ -414,6 +398,17 @@ module Gitlab
# rubocop: enable UsageData/LargeTable
end
+ # rubocop: disable CodeReuse/ActiveRecord
+ # rubocop: disable UsageData/LargeTable
+ def successful_deployments_with_cluster(scope)
+ scope
+ .joins(cluster: :deployments)
+ .merge(Clusters::Cluster.enabled)
+ .merge(Deployment.success)
+ end
+ # rubocop: enable UsageData/LargeTable
+ # rubocop: enable CodeReuse/ActiveRecord
+
def user_preferences_usage
{} # augmented in EE
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 60fc471eea9..689e96a765b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -7603,9 +7603,6 @@ msgstr ""
msgid "Default classification label"
msgstr ""
-msgid "Default dashboard"
-msgstr ""
-
msgid "Default deletion adjourned period"
msgstr ""
diff --git a/rubocop/rubocop-migrations.yml b/rubocop/rubocop-migrations.yml
index 8699f7b9c68..f8820c0c6aa 100644
--- a/rubocop/rubocop-migrations.yml
+++ b/rubocop/rubocop-migrations.yml
@@ -28,7 +28,6 @@ Migration/UpdateLargeTable:
- :resource_label_events
- :routes
- :sent_notifications
- - :services
- :system_note_metadata
- :taggings
- :todos
diff --git a/spec/controllers/concerns/metrics_dashboard_spec.rb b/spec/controllers/concerns/metrics_dashboard_spec.rb
index f0c9874965e..99f1aff6eeb 100644
--- a/spec/controllers/concerns/metrics_dashboard_spec.rb
+++ b/spec/controllers/concerns/metrics_dashboard_spec.rb
@@ -165,7 +165,7 @@ RSpec.describe MetricsDashboard do
it 'adds starred dashboard information and sorts the list' do
all_dashboards = json_response['all_dashboards'].map { |dashboard| dashboard.slice('display_name', 'starred', 'user_starred_path') }
expected_response = [
- { "display_name" => "Default dashboard", "starred" => false, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: 'config/prometheus/common_metrics.yml' }) },
+ { "display_name" => "Overview", "starred" => false, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: 'config/prometheus/common_metrics.yml' }) },
{ "display_name" => "anomaly.yml", "starred" => false, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: '.gitlab/dashboards/anomaly.yml' }) },
{ "display_name" => "errors.yml", "starred" => true, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: '.gitlab/dashboards/errors.yml' }) },
{ "display_name" => "test.yml", "starred" => true, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: '.gitlab/dashboards/test.yml' }) }
diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb
index b100e035d38..39801e265e0 100644
--- a/spec/features/snippets/user_creates_snippet_spec.rb
+++ b/spec/features/snippets/user_creates_snippet_spec.rb
@@ -2,7 +2,17 @@
require 'spec_helper'
-RSpec.shared_examples_for 'snippet editor' do
+RSpec.describe 'User creates snippet', :js do
+ include DropzoneHelper
+
+ let_it_be(:user) { create(:user) }
+
+ let(:title) { 'My Snippet Title' }
+ let(:file_content) { 'Hello World!' }
+ let(:md_description) { 'My Snippet **Description**' }
+ let(:description) { 'My Snippet Description' }
+ let(:created_snippet) { Snippet.last }
+
before do
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
@@ -14,15 +24,15 @@ RSpec.shared_examples_for 'snippet editor' do
end
def fill_form
- fill_in 'personal_snippet_title', with: 'My Snippet Title'
+ fill_in 'personal_snippet_title', with: title
# Click placeholder first to expand full description field
description_field.click
- fill_in 'personal_snippet_description', with: 'My Snippet **Description**'
+ fill_in 'personal_snippet_description', with: md_description
page.within('.file-editor') do
el = find('.inputarea')
- el.send_keys 'Hello World!'
+ el.send_keys file_content
end
end
@@ -34,12 +44,12 @@ RSpec.shared_examples_for 'snippet editor' do
click_button('Create snippet')
wait_for_requests
- expect(page).to have_content('My Snippet Title')
+ expect(page).to have_content(title)
page.within('.snippet-header .description') do
- expect(page).to have_content('My Snippet Description')
+ expect(page).to have_content(description)
expect(page).to have_selector('strong')
end
- expect(page).to have_content('Hello World!')
+ expect(page).to have_content(file_content)
end
it 'previews a snippet with file' do
@@ -57,7 +67,7 @@ RSpec.shared_examples_for 'snippet editor' do
link = find('a.no-attachment-icon img.js-lazy-loaded[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/user/#{user.id}/\h{32}/banana_sample\.gif\z})
- # Adds a cache buster for checking if the image exists as Selenium is now handling the cached regquests
+ # Adds a cache buster for checking if the image exists as Selenium is now handling the cached requests
# not anymore as requests when they come straight from memory cache.
reqs = inspect_requests { visit("#{link}?ran=#{SecureRandom.base64(20)}") }
expect(reqs.first.status_code).to eq(200)
@@ -99,15 +109,10 @@ RSpec.shared_examples_for 'snippet editor' do
wait_for_requests
end
- it 'displays the error' do
+ it 'renders the new page and displays the error' do
expect(page).to have_content(error)
- end
-
- it 'renders new page' do
expect(page).to have_content('New Snippet')
- end
- it 'has the correct action path' do
action = find('form.snippet-form')['action']
expect(action).to match(%r{/snippets\z})
end
@@ -116,46 +121,10 @@ RSpec.shared_examples_for 'snippet editor' do
it 'validation fails for the first time' do
visit new_snippet_path
- fill_in 'personal_snippet_title', with: 'My Snippet Title'
+ fill_in 'personal_snippet_title', with: title
click_button('Create snippet')
expect(page).to have_selector('#error_explanation')
-
- fill_form
- dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
-
- click_button('Create snippet')
- wait_for_requests
-
- expect(page).to have_content('My Snippet Title')
- page.within('.snippet-header .description') do
- expect(page).to have_content('My Snippet Description')
- expect(page).to have_selector('strong')
- end
- expect(page).to have_content('Hello World!')
- link = find('a.no-attachment-icon img.js-lazy-loaded[alt="banana_sample"]')['src']
- expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
-
- reqs = inspect_requests { visit("#{link}?ran=#{SecureRandom.base64(20)}") }
- expect(reqs.first.status_code).to eq(200)
- end
-
- it 'Authenticated user creates a snippet with + in filename' do
- visit new_snippet_path
-
- fill_in 'personal_snippet_title', with: 'My Snippet Title'
- page.within('.file-editor') do
- find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name'
- el = find('.inputarea')
- el.send_keys 'Hello World!'
- end
-
- click_button 'Create snippet'
- wait_for_requests
-
- expect(page).to have_content('My Snippet Title')
- expect(page).to have_content('snippet+file+name')
- expect(page).to have_content('Hello World!')
end
context 'when snippets default visibility level is restricted' do
@@ -172,20 +141,7 @@ RSpec.shared_examples_for 'snippet editor' do
click_button('Create snippet')
wait_for_requests
- visit snippets_path
- click_link('Internal')
-
- expect(page).to have_content('My Snippet Title')
- created_snippet = Snippet.last
expect(created_snippet.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
end
end
-
-RSpec.describe 'User creates snippet', :js do
- include DropzoneHelper
-
- let_it_be(:user) { create(:user) }
-
- it_behaves_like "snippet editor"
-end
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 69f757a6f88..8982766bdbb 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -72,6 +72,7 @@ describe('Design management index page', () => {
const dropzoneClasses = () => findDropzone().classes();
const findDropzoneWrapper = () => wrapper.find('[data-testid="design-dropzone-wrapper"]');
const findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1);
+ const findDesignsWrapper = () => wrapper.find('[data-testid="designs-root"]');
function createComponent({
loading = false,
@@ -508,6 +509,10 @@ describe('Design management index page', () => {
});
event = new Event('paste');
+ event.clipboardData = {
+ files: [{ name: 'image.png', type: 'image/png' }],
+ getData: () => 'test.png',
+ };
router.replace({
name: DESIGNS_ROUTE_NAME,
@@ -517,43 +522,52 @@ describe('Design management index page', () => {
});
});
- it('calls onUploadDesign with valid paste', () => {
- event.clipboardData = {
- files: [{ name: 'image.png', type: 'image/png' }],
- getData: () => 'test.png',
- };
-
+ it('does not call paste event if designs wrapper is not hovered', () => {
document.dispatchEvent(event);
- expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
- expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([
- new File([{ name: 'image.png' }], 'test.png'),
- ]);
+ expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled();
});
- it('renames a design if it has an image.png filename', () => {
- event.clipboardData = {
- files: [{ name: 'image.png', type: 'image/png' }],
- getData: () => 'image.png',
- };
+ describe('when designs wrapper is hovered', () => {
+ beforeEach(() => {
+ findDesignsWrapper().trigger('mouseenter');
+ });
- document.dispatchEvent(event);
+ it('calls onUploadDesign with valid paste', () => {
+ document.dispatchEvent(event);
- expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
- expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([
- new File([{ name: 'image.png' }], `design_${Date.now()}.png`),
- ]);
- });
+ expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([
+ new File([{ name: 'image.png' }], 'test.png'),
+ ]);
+ });
- it('does not call onUploadDesign with invalid paste', () => {
- event.clipboardData = {
- items: [{ type: 'text/plain' }, { type: 'text' }],
- files: [],
- };
+ it('renames a design if it has an image.png filename', () => {
+ document.dispatchEvent(event);
- document.dispatchEvent(event);
+ expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([
+ new File([{ name: 'image.png' }], `design_${Date.now()}.png`),
+ ]);
+ });
- expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled();
+ it('does not call onUploadDesign with invalid paste', () => {
+ event.clipboardData = {
+ items: [{ type: 'text/plain' }, { type: 'text' }],
+ files: [],
+ };
+
+ document.dispatchEvent(event);
+
+ expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled();
+ });
+
+ it('removes onPaste listener after mouseleave event', async () => {
+ findDesignsWrapper().trigger('mouseleave');
+ document.dispatchEvent(event);
+
+ expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js
index 3c231a79a14..7a704de427e 100644
--- a/spec/frontend/monitoring/components/dashboard_header_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_header_spec.js
@@ -257,7 +257,7 @@ describe('Dashboard header', () => {
});
const duplicableCases = [
- null, // When no path is specified, it uses the default dashboard path.
+ null, // When no path is specified, it uses the overview dashboard path.
dashboardGitResponse[0].path,
dashboardGitResponse[2].path,
selfMonitoringDashboardGitResponse[0].path,
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 591e89bc41d..66a894b1479 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -886,7 +886,7 @@ describe('Dashboard', () => {
return wrapper.vm.$nextTick();
});
- it('is not present for the default dashboard', () => {
+ it('is not present for the overview dashboard', () => {
expect(findEditLink().exists()).toBe(false);
});
@@ -905,7 +905,7 @@ describe('Dashboard', () => {
describe('document title', () => {
const originalTitle = 'Original Title';
- const defaultDashboardName = dashboardGitResponse[0].display_name;
+ const overviewDashboardName = dashboardGitResponse[0].display_name;
beforeEach(() => {
document.title = originalTitle;
@@ -916,11 +916,11 @@ describe('Dashboard', () => {
document.title = '';
});
- it('is prepended with default dashboard name by default', () => {
+ it('is prepended with the overview dashboard name by default', () => {
setupAllDashboards(store);
return wrapper.vm.$nextTick().then(() => {
- expect(document.title.startsWith(`${defaultDashboardName} · `)).toBe(true);
+ expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true);
});
});
@@ -935,11 +935,11 @@ describe('Dashboard', () => {
});
});
- it('is prepended with default dashboard name is path is not known', () => {
+ it('is prepended with the overview dashboard name if path is not known', () => {
setupAllDashboards(store, 'unknown/path');
return wrapper.vm.$nextTick().then(() => {
- expect(document.title.startsWith(`${defaultDashboardName} · `)).toBe(true);
+ expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true);
});
});
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
index 413b2cde93e..cc1849b4426 100644
--- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
+++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
@@ -73,7 +73,7 @@ describe('DashboardsDropdown', () => {
});
it('filters dropdown items when searched for item exists in the list', () => {
- const searchTerm = 'Default';
+ const searchTerm = 'Overview';
setSearchTerm(searchTerm);
return wrapper.vm.$nextTick().then(() => {
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index cd7f93f6876..adb64b83dfa 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -170,7 +170,7 @@ export const environmentData = [
export const dashboardGitResponse = [
{
default: true,
- display_name: 'Default',
+ display_name: 'Overview',
can_edit: false,
system_dashboard: true,
out_of_the_box_dashboard: true,
diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js
index 69f1d6b4dad..509de8a4596 100644
--- a/spec/frontend/monitoring/store/getters_spec.js
+++ b/spec/frontend/monitoring/store/getters_spec.js
@@ -380,7 +380,7 @@ describe('Monitoring store Getters', () => {
);
});
- it('returns a non-default dashboard', () => {
+ it('returns a dashboard different from the overview dashboard', () => {
const localState = {
allDashboards: dashboardGitResponse,
currentDashboard: dashboardGitResponse[1].path,
@@ -391,7 +391,7 @@ describe('Monitoring store Getters', () => {
);
});
- it('returns a default dashboard when no dashboard is selected', () => {
+ it('returns the overview dashboard when no dashboard is selected', () => {
const localState = {
allDashboards: dashboardGitResponse,
currentDashboard: null,
@@ -402,7 +402,7 @@ describe('Monitoring store Getters', () => {
);
});
- it('returns a default dashboard when dashboard cannot be found', () => {
+ it('returns the overview dashboard when dashboard cannot be found', () => {
const localState = {
allDashboards: dashboardGitResponse,
currentDashboard: 'wrong_path',
diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index 2f0a35bf06a..2b73fb7f30f 100644
--- a/spec/frontend/releases/components/app_edit_new_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -34,6 +34,7 @@ describe('Release edit/new component', () => {
getters = {
isValid: () => true,
+ isExistingRelease: () => true,
validationErrors: () => ({
assets: {
links: [],
@@ -96,28 +97,6 @@ describe('Release edit/new component', () => {
);
});
- it('renders the correct tag name in the "Tag name" field', () => {
- expect(wrapper.find('#git-ref').element.value).toBe(release.tagName);
- });
-
- it('renders the correct help text under the "Tag name" field', () => {
- const helperText = wrapper.find('#tag-name-help');
- const helperTextLink = helperText.find('a');
- const helperTextLinkAttrs = helperTextLink.attributes();
-
- expect(helperText.text()).toBe(
- 'Changing a Release tag is only supported via Releases API. More information',
- );
- expect(helperTextLink.text()).toBe('More information');
- expect(helperTextLinkAttrs).toEqual(
- expect.objectContaining({
- href: state.updateReleaseApiDocsPath,
- rel: 'noopener noreferrer',
- target: '_blank',
- }),
- );
- });
-
it('renders the correct release title in the "Release title" field', () => {
expect(wrapper.find('#release-title').element.value).toBe(release.name);
});
diff --git a/spec/frontend/releases/components/tag_field_exsting_spec.js b/spec/frontend/releases/components/tag_field_exsting_spec.js
new file mode 100644
index 00000000000..0a04f68bd67
--- /dev/null
+++ b/spec/frontend/releases/components/tag_field_exsting_spec.js
@@ -0,0 +1,78 @@
+import { GlFormInput } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
+import TagFieldExisting from '~/releases/components/tag_field_existing.vue';
+import createStore from '~/releases/stores';
+import createDetailModule from '~/releases/stores/modules/detail';
+
+const TEST_TAG_NAME = 'test-tag-name';
+const TEST_DOCS_PATH = '/help/test/docs/path';
+
+describe('releases/components/tag_field_existing', () => {
+ let store;
+ let wrapper;
+
+ const createComponent = (mountFn = shallowMount) => {
+ wrapper = mountFn(TagFieldExisting, {
+ store,
+ });
+ };
+
+ const findInput = () => wrapper.find(GlFormInput);
+ const findHelp = () => wrapper.find('[data-testid="tag-name-help"]');
+ const findHelpLink = () => {
+ const link = findHelp().find('a');
+
+ return {
+ text: link.text(),
+ href: link.attributes('href'),
+ target: link.attributes('target'),
+ };
+ };
+
+ beforeEach(() => {
+ store = createStore({
+ modules: {
+ detail: createDetailModule({
+ updateReleaseApiDocsPath: TEST_DOCS_PATH,
+ tagName: TEST_TAG_NAME,
+ }),
+ },
+ });
+
+ store.state.detail.release = {
+ tagName: TEST_TAG_NAME,
+ };
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('default', () => {
+ it('shows the tag name', () => {
+ createComponent();
+
+ expect(findInput().attributes()).toMatchObject({
+ disabled: '',
+ value: TEST_TAG_NAME,
+ });
+ });
+
+ it('shows help', () => {
+ createComponent(mount);
+
+ expect(findHelp().text()).toMatchInterpolatedText(
+ 'Changing a Release tag is only supported via Releases API. More information',
+ );
+
+ const helpLink = findHelpLink();
+
+ expect(helpLink).toEqual({
+ text: 'More information',
+ href: TEST_DOCS_PATH,
+ target: '_blank',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js
new file mode 100644
index 00000000000..dbd762abb0d
--- /dev/null
+++ b/spec/frontend/releases/components/tag_field_new_spec.js
@@ -0,0 +1,34 @@
+import { shallowMount } from '@vue/test-utils';
+import TagFieldNew from '~/releases/components/tag_field_new.vue';
+import createStore from '~/releases/stores';
+import createDetailModule from '~/releases/stores/modules/detail';
+
+describe('releases/components/tag_field_new', () => {
+ let store;
+ let wrapper;
+
+ const createComponent = (mountFn = shallowMount) => {
+ wrapper = mountFn(TagFieldNew, {
+ store,
+ });
+ };
+
+ beforeEach(() => {
+ store = createStore({
+ modules: {
+ detail: createDetailModule({}),
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders a placeholder component', () => {
+ createComponent();
+
+ expect(wrapper.exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/releases/components/tag_field_spec.js b/spec/frontend/releases/components/tag_field_spec.js
new file mode 100644
index 00000000000..aaec685e822
--- /dev/null
+++ b/spec/frontend/releases/components/tag_field_spec.js
@@ -0,0 +1,60 @@
+import { shallowMount } from '@vue/test-utils';
+import TagField from '~/releases/components/tag_field.vue';
+import TagFieldNew from '~/releases/components/tag_field_new.vue';
+import TagFieldExisting from '~/releases/components/tag_field_existing.vue';
+import createStore from '~/releases/stores';
+import createDetailModule from '~/releases/stores/modules/detail';
+
+describe('releases/components/tag_field', () => {
+ let store;
+ let wrapper;
+
+ const createComponent = ({ originalRelease }) => {
+ store = createStore({
+ modules: {
+ detail: createDetailModule({}),
+ },
+ });
+
+ store.state.detail.originalRelease = originalRelease;
+
+ wrapper = shallowMount(TagField, { store });
+ };
+
+ const findTagFieldNew = () => wrapper.find(TagFieldNew);
+ const findTagFieldExisting = () => wrapper.find(TagFieldExisting);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when an existing release is being edited', () => {
+ beforeEach(() => {
+ const originalRelease = { name: 'Version 1.0' };
+ createComponent({ originalRelease });
+ });
+
+ it('renders the TagFieldExisting component', () => {
+ expect(findTagFieldExisting().exists()).toBe(true);
+ });
+
+ it('does not render the TagFieldNew component', () => {
+ expect(findTagFieldNew().exists()).toBe(false);
+ });
+ });
+
+ describe('when a new release is being created', () => {
+ beforeEach(() => {
+ createComponent({ originalRelease: null });
+ });
+
+ it('renders the TagFieldNew component', () => {
+ expect(findTagFieldNew().exists()).toBe(true);
+ });
+
+ it('does not render the TagFieldExisting component', () => {
+ expect(findTagFieldExisting().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js
index 8945ad97c93..d8776ef44d2 100644
--- a/spec/frontend/releases/stores/modules/detail/getters_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js
@@ -1,6 +1,20 @@
import * as getters from '~/releases/stores/modules/detail/getters';
describe('Release detail getters', () => {
+ describe('isExistingRelease', () => {
+ it('returns true if the release is an existing release that already exists in the database', () => {
+ const state = { originalRelease: { name: 'The first release' } };
+
+ expect(getters.isExistingRelease(state)).toBe(true);
+ });
+
+ it('returns false if the release is a new release that has not yet been saved to the database', () => {
+ const state = { originalRelease: null };
+
+ expect(getters.isExistingRelease(state)).toBe(false);
+ });
+ });
+
describe('releaseLinksToCreate', () => {
it("returns an empty array if state.release doesn't exist", () => {
const state = {};
diff --git a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
index ee54a45f648..021fe7734c0 100644
--- a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
@@ -142,7 +142,7 @@ RSpec.describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store
describe '.find_all_paths' do
let(:all_dashboard_paths) { described_class.find_all_paths(project) }
- let(:system_dashboard) { { path: system_dashboard_path, display_name: 'Default dashboard', default: true, system_dashboard: true, out_of_the_box_dashboard: true } }
+ let(:system_dashboard) { { path: system_dashboard_path, display_name: 'Overview', default: true, system_dashboard: true, out_of_the_box_dashboard: true } }
it 'includes only the system dashboard by default' do
expect(all_dashboard_paths).to eq([system_dashboard])
@@ -163,7 +163,7 @@ RSpec.describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store
let(:self_monitoring_dashboard) do
{
path: self_monitoring_dashboard_path,
- display_name: 'Default dashboard',
+ display_name: 'Overview',
default: true,
system_dashboard: true,
out_of_the_box_dashboard: true
diff --git a/spec/models/concerns/counter_attribute_spec.rb b/spec/models/concerns/counter_attribute_spec.rb
new file mode 100644
index 00000000000..f23865a5dbb
--- /dev/null
+++ b/spec/models/concerns/counter_attribute_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe CounterAttribute, :counter_attribute, :clean_gitlab_redis_shared_state do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:project_statistics) { create(:project_statistics) }
+ let(:model) { CounterAttributeModel.find(project_statistics.id) }
+
+ it_behaves_like CounterAttribute, [:build_artifacts_size, :commit_count] do
+ let(:model) { CounterAttributeModel.find(project_statistics.id) }
+ end
+
+ describe '.steal_increments' do
+ let(:increment_key) { 'counters:Model:123:attribute' }
+ let(:flushed_key) { 'counter:Model:123:attribute:flushed' }
+
+ subject { model.send(:steal_increments, increment_key, flushed_key) }
+
+ where(:increment, :flushed, :result, :flushed_key_present) do
+ nil | nil | 0 | false
+ nil | 0 | 0 | false
+ 0 | 0 | 0 | false
+ 1 | 0 | 1 | true
+ 1 | nil | 1 | true
+ 1 | 1 | 2 | true
+ 1 | -2 | -1 | true
+ -1 | 1 | 0 | false
+ end
+
+ with_them do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(increment_key, increment) if increment
+ redis.set(flushed_key, flushed) if flushed
+ end
+ end
+
+ it { is_expected.to eq(result) }
+
+ it 'drops the increment key and creates the flushed key if it does not exist' do
+ subject
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.exists(increment_key)).to be_falsey
+ expect(redis.exists(flushed_key)).to eq(flushed_key_present)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index 3659e6b973e..5f66de3a63c 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -328,8 +328,8 @@ RSpec.describe ProjectStatistics do
it 'increases also storage size by that amount' do
expect { described_class.increment_statistic(project.id, stat, 20) }
- .to change { statistics.reload.storage_size }
- .by(20)
+ .to change { statistics.reload.storage_size }
+ .by(20)
end
end
diff --git a/spec/services/metrics/dashboard/dynamic_embed_service_spec.rb b/spec/services/metrics/dashboard/dynamic_embed_service_spec.rb
index 72b356be60f..3c533b0c464 100644
--- a/spec/services/metrics/dashboard/dynamic_embed_service_spec.rb
+++ b/spec/services/metrics/dashboard/dynamic_embed_service_spec.rb
@@ -132,7 +132,7 @@ RSpec.describe Metrics::Dashboard::DynamicEmbedService, :use_clean_rails_memory_
end
shared_examples 'uses system dashboard' do
- it 'uses the default dashboard' do
+ it 'uses the overview dashboard' do
expect(Gitlab::Metrics::Dashboard::Finder)
.to receive(:find_raw)
.with(project, dashboard_path: system_dashboard_path)
diff --git a/spec/support/counter_attribute.rb b/spec/support/counter_attribute.rb
new file mode 100644
index 00000000000..ea71b25b4c0
--- /dev/null
+++ b/spec/support/counter_attribute.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ config.before(:each, :counter_attribute) do
+ stub_const('CounterAttributeModel', Class.new(ProjectStatistics))
+
+ CounterAttributeModel.class_eval do
+ include CounterAttribute
+
+ counter_attribute :build_artifacts_size
+ counter_attribute :commit_count
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
new file mode 100644
index 00000000000..99a09993900
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
@@ -0,0 +1,176 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.shared_examples_for CounterAttribute do |counter_attributes|
+ it 'defines a Redis counter_key' do
+ expect(model.counter_key(:counter_name))
+ .to eq("project:{#{model.project_id}}:counters:CounterAttributeModel:#{model.id}:counter_name")
+ end
+
+ it 'defines a method to store counters' do
+ expect(model.class.counter_attributes.to_a).to eq(counter_attributes)
+ end
+
+ counter_attributes.each do |attribute|
+ describe attribute do
+ describe '#delayed_increment_counter', :redis do
+ let(:increment) { 10 }
+
+ subject { model.delayed_increment_counter(attribute, increment) }
+
+ context 'when attribute is a counter attribute' do
+ where(:increment) { [10, -3] }
+
+ with_them do
+ it 'increments the counter in Redis' do
+ subject
+
+ Gitlab::Redis::SharedState.with do |redis|
+ counter = redis.get(model.counter_key(attribute))
+ expect(counter).to eq(increment.to_s)
+ end
+ end
+
+ it 'does not increment the counter for the record' do
+ expect { subject }.not_to change { model.reset.read_attribute(attribute) }
+ end
+
+ it 'schedules a worker to flush counter increments asynchronously' do
+ expect(FlushCounterIncrementsWorker).to receive(:perform_in)
+ .with(CounterAttribute::WORKER_DELAY, model.class.name, model.id, attribute)
+ .and_call_original
+
+ subject
+ end
+ end
+
+ context 'when increment is 0' do
+ let(:increment) { 0 }
+
+ it 'does nothing' do
+ expect(FlushCounterIncrementsWorker).not_to receive(:perform_in)
+ expect(model).not_to receive(:update!)
+
+ subject
+ end
+ end
+ end
+
+ context 'when attribute is not a counter attribute' do
+ it 'delegates to ActiveRecord update!' do
+ expect { model.delayed_increment_counter(:unknown_attribute, 10) }
+ .to raise_error(ActiveModel::MissingAttributeError)
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(efficient_counter_attribute: false)
+ end
+
+ it 'delegates to ActiveRecord update!' do
+ expect { subject }
+ .to change { model.reset.read_attribute(attribute) }.by(increment)
+ end
+
+ it 'does not increment the counter in Redis' do
+ subject
+
+ Gitlab::Redis::SharedState.with do |redis|
+ counter = redis.get(model.counter_key(attribute))
+ expect(counter).to be_nil
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe '.flush_increments_to_database!', :redis do
+ let(:incremented_attribute) { counter_attributes.first }
+
+ subject { model.flush_increments_to_database!(incremented_attribute) }
+
+ it 'obtains an exclusive lease during processing' do
+ expect(model)
+ .to receive(:in_lock)
+ .with(model.counter_lock_key(incremented_attribute), ttl: described_class::WORKER_LOCK_TTL)
+ .and_call_original
+
+ subject
+ end
+
+ context 'when there is a counter to flush' do
+ before do
+ model.delayed_increment_counter(incremented_attribute, 10)
+ model.delayed_increment_counter(incremented_attribute, -3)
+ end
+
+ it 'updates the record' do
+ expect { subject }.to change { model.reset.read_attribute(incremented_attribute) }.by(7)
+ end
+
+ it 'removes the increment entry from Redis' do
+ Gitlab::Redis::SharedState.with do |redis|
+ key_exists = redis.exists(model.counter_key(incremented_attribute))
+ expect(key_exists).to be_truthy
+ end
+
+ subject
+
+ Gitlab::Redis::SharedState.with do |redis|
+ key_exists = redis.exists(model.counter_key(incremented_attribute))
+ expect(key_exists).to be_falsey
+ end
+ end
+ end
+
+ context 'when there are no counters to flush' do
+ context 'when there are no counters in the relative :flushed key' do
+ it 'does not change the record' do
+ expect { subject }.not_to change { model.reset.attributes }
+ end
+ end
+
+ # This can be the case where updating counters in the database fails with error
+ # and retrying the worker will retry flushing the counters but the main key has
+ # disappeared and the increment has been moved to the "<...>:flushed" key.
+ context 'when there are counters in the relative :flushed key' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.incrby(model.counter_flushed_key(incremented_attribute), 10)
+ end
+ end
+
+ it 'updates the record' do
+ expect { subject }.to change { model.reset.read_attribute(incremented_attribute) }.by(10)
+ end
+
+ it 'deletes the relative :flushed key' do
+ subject
+
+ Gitlab::Redis::SharedState.with do |redis|
+ key_exists = redis.exists(model.counter_flushed_key(incremented_attribute))
+ expect(key_exists).to be_falsey
+ end
+ end
+ end
+ end
+
+ context 'when deleting :flushed key fails' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.incrby(model.counter_flushed_key(incremented_attribute), 10)
+
+ expect(redis).to receive(:del).and_raise('could not delete key')
+ end
+ end
+
+ it 'does a rollback of the counter update' do
+ expect { subject }.to raise_error('could not delete key')
+
+ expect(model.reset.read_attribute(incremented_attribute)).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/workers/flush_counter_increments_worker_spec.rb b/spec/workers/flush_counter_increments_worker_spec.rb
new file mode 100644
index 00000000000..14b49b97ac3
--- /dev/null
+++ b/spec/workers/flush_counter_increments_worker_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe FlushCounterIncrementsWorker, :counter_attribute do
+ let(:project_statistics) { create(:project_statistics) }
+ let(:model) { CounterAttributeModel.find(project_statistics.id) }
+
+ describe '#perform', :redis do
+ let(:attribute) { model.class.counter_attributes.first }
+ let(:worker) { described_class.new }
+
+ subject { worker.perform(model.class.name, model.id, attribute) }
+
+ it 'flushes increments to database' do
+ expect(model.class).to receive(:find_by_id).and_return(model)
+ expect(model)
+ .to receive(:flush_increments_to_database!)
+ .with(attribute)
+ .and_call_original
+
+ subject
+ end
+
+ context 'when model class does not exist' do
+ subject { worker.perform('non-existend-model') }
+
+ it 'does nothing' do
+ expect(worker).not_to receive(:in_lock)
+ end
+ end
+
+ context 'when record does not exist' do
+ subject { worker.perform(model.class.name, model.id + 100, attribute) }
+
+ it 'does nothing' do
+ expect(worker).not_to receive(:in_lock)
+ end
+ end
+ end
+end