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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-19 12:07:52 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-19 12:07:52 +0300
commitee3d5f16e3aa642944b121645764e33604a31307 (patch)
tree6b27cb8fca43bdac8d558d689b64c7298ea3cb37
parent765ec2e3b2eb347314af5f806c6b70bad696265a (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--CHANGELOG.md10
-rw-r--r--app/assets/javascripts/blame/streaming/index.js4
-rw-r--r--app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue4
-rw-r--r--app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue4
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_update_form.vue4
-rw-r--r--app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue4
-rw-r--r--app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue4
-rw-r--r--app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue4
-rw-r--r--app/assets/javascripts/layout_nav.js4
-rw-r--r--app/assets/javascripts/streaming/handle_streamed_relative_timestamps.js80
-rw-r--r--app/assets/javascripts/super_sidebar/components/brand_logo.vue38
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue26
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue2
-rw-r--r--app/models/project_setting.rb7
-rw-r--r--config/feature_flags/development/combined_analytics_dashboards_editor.yml8
-rw-r--r--db/migrate/20230504182314_add_pa_configurator_base_to_project_settings.rb14
-rw-r--r--db/schema_migrations/202305041823141
-rw-r--r--db/structure.sql2
-rw-r--r--doc/administration/silent_mode/index.md18
-rw-r--r--lib/gitlab/email/hook/silent_mode_interceptor.rb12
-rw-r--r--lib/gitlab/http.rb25
-rw-r--r--lib/gitlab/silent_mode.rb21
-rw-r--r--lib/product_analytics/settings.rb33
-rw-r--r--locale/gitlab.pot6
-rw-r--r--qa/qa/page/group/menu.rb4
-rw-r--r--qa/qa/page/sub_menus/super_sidebar/operate.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/dependency_proxy/dependency_proxy_spec.rb2
-rw-r--r--spec/frontend/__helpers__/mock_dom_observer.js4
-rw-r--r--spec/frontend/blame/streaming/index_spec.js9
-rw-r--r--spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js8
-rw-r--r--spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js9
-rw-r--r--spec/frontend/ci/runner/components/runner_update_form_spec.js11
-rw-r--r--spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js8
-rw-r--r--spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js9
-rw-r--r--spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js8
-rw-r--r--spec/frontend/streaming/handle_streamed_relative_timestamps_spec.js94
-rw-r--r--spec/frontend/super_sidebar/components/brand_logo_spec.js42
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js6
-rw-r--r--spec/lib/gitlab/http_spec.rb73
-rw-r--r--spec/lib/gitlab/silent_mode_spec.rb97
-rw-r--r--spec/lib/product_analytics/settings_spec.rb8
-rw-r--r--spec/requests/api/project_attributes.yml3
42 files changed, 647 insertions, 85 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cf3934c500f..06ded9f1390 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -774,6 +774,16 @@ entry.
- [Add index to group_group_links table](gitlab-org/gitlab@9a3f2c1a90b54074e61d0abf07101ce664198e81) ([merge request](gitlab-org/gitlab!117386))
- [Validate the projects.creator_id foregin key synchronously](gitlab-org/gitlab@ed9351984a16f20506babf6eab6706b917904ed1) ([merge request](gitlab-org/gitlab!117147))
+## 15.11.5 (2023-05-19)
+
+### Fixed (5 changes)
+
+- [Makes roadmap current day indicator & timeline locale aware](gitlab-org/gitlab@2dc71e59e277d017118d77743d8658be5b05ddf3) ([merge request](gitlab-org/gitlab!121104)) **GitLab Enterprise Edition**
+- [Fix height calculations with roadmap to prevent extra scrollers](gitlab-org/gitlab@58080e99cb0a551c41b557d5a0000d686c512fdf) ([merge request](gitlab-org/gitlab!120965)) **GitLab Enterprise Edition**
+- [Update by_parent filter in EpicsFinder](gitlab-org/gitlab@97115082a328bc01d04abc651e3b54913a19832a) ([merge request](gitlab-org/gitlab!120966)) **GitLab Enterprise Edition**
+- [Fix no_proxy not working when DNS rebinding protection enabled](gitlab-org/gitlab@84012b21559126cde51cfe341ebff44eda9b3d62) ([merge request](gitlab-org/gitlab!120809))
+- [Remove epic date fields authorization](gitlab-org/gitlab@5c36e497d1e43e4ccf05a0684c3388385b247e45) ([merge request](gitlab-org/gitlab!120290)) **GitLab Enterprise Edition**
+
## 15.11.4 (2023-05-16)
### Fixed (2 changes)
diff --git a/app/assets/javascripts/blame/streaming/index.js b/app/assets/javascripts/blame/streaming/index.js
index 935343cca2e..a88ef1c3e21 100644
--- a/app/assets/javascripts/blame/streaming/index.js
+++ b/app/assets/javascripts/blame/streaming/index.js
@@ -1,5 +1,6 @@
import { renderHtmlStreams } from '~/streaming/render_html_streams';
import { handleStreamedAnchorLink } from '~/streaming/handle_streamed_anchor_link';
+import { handleStreamedRelativeTimestamps } from '~/streaming/handle_streamed_relative_timestamps';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import { rateLimitStreamRequests } from '~/streaming/rate_limit_stream_requests';
@@ -11,6 +12,7 @@ export async function renderBlamePageStreams(firstStreamPromise) {
if (!element || !firstStreamPromise) return;
const stopAnchorObserver = handleStreamedAnchorLink(element);
+ const relativeTimestampsHandler = handleStreamedRelativeTimestamps(element);
const { dataset } = document.querySelector('#blob-content-holder');
const totalExtraPages = parseInt(dataset.totalExtraPages, 10);
const { pagesUrl } = dataset;
@@ -50,6 +52,8 @@ export async function renderBlamePageStreams(firstStreamPromise) {
});
throw error;
} finally {
+ const stopTimestampObserver = await relativeTimestampsHandler;
+ stopTimestampObserver();
stopAnchorObserver();
document.querySelector('#blame-stream-loading').remove();
}
diff --git a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
index e4d47fba464..4ec41381045 100644
--- a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
@@ -1,6 +1,6 @@
<script>
import { createAlert, VARIANT_SUCCESS } from '~/alert';
-import { redirectTo, setUrlParams } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
@@ -32,7 +32,7 @@ export default {
message: s__('Runners|Runner created.'),
variant: VARIANT_SUCCESS,
});
- redirectTo(ephemeralRegisterUrl); // eslint-disable-line import/no-deprecated
+ visitUrl(ephemeralRegisterUrl);
},
onError(error) {
createAlert({ message: error.message });
diff --git a/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue b/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue
index 668a55d2437..d385d32fd9d 100644
--- a/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue
@@ -2,7 +2,7 @@
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { visitUrl } from '~/lib/utils/url_utility';
import RunnerDeleteButton from '../components/runner_delete_button.vue';
import RunnerEditButton from '../components/runner_edit_button.vue';
@@ -71,7 +71,7 @@ export default {
},
onDeleted({ message }) {
saveAlertToLocalStorage({ message, variant: VARIANT_SUCCESS });
- redirectTo(this.runnersPath); // eslint-disable-line import/no-deprecated
+ visitUrl(this.runnersPath);
},
},
};
diff --git a/app/assets/javascripts/ci/runner/components/runner_update_form.vue b/app/assets/javascripts/ci/runner/components/runner_update_form.vue
index 2d34c551d6d..0b05969a551 100644
--- a/app/assets/javascripts/ci/runner/components/runner_update_form.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_update_form.vue
@@ -14,7 +14,7 @@ import {
runnerToModel,
} from 'ee_else_ce/ci/runner/runner_update_form_utils';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
-import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { captureException } from '~/ci/runner/sentry_utils';
import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants';
@@ -101,7 +101,7 @@ export default {
},
onSuccess() {
saveAlertToLocalStorage({ message: __('Changes saved.'), variant: VARIANT_SUCCESS });
- redirectTo(this.runnerPath); // eslint-disable-line import/no-deprecated
+ visitUrl(this.runnerPath);
},
onError(message) {
this.saving = false;
diff --git a/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
index 67d29daf66f..5965330c4eb 100644
--- a/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
@@ -1,6 +1,6 @@
<script>
import { createAlert, VARIANT_SUCCESS } from '~/alert';
-import { redirectTo, setUrlParams } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
@@ -38,7 +38,7 @@ export default {
message: s__('Runners|Runner created.'),
variant: VARIANT_SUCCESS,
});
- redirectTo(ephemeralRegisterUrl); // eslint-disable-line import/no-deprecated
+ visitUrl(ephemeralRegisterUrl);
},
onError(error) {
createAlert({ message: error.message });
diff --git a/app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue b/app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue
index 1318bf5a2e6..e885cf45c5a 100644
--- a/app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue
+++ b/app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue
@@ -2,7 +2,7 @@
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { visitUrl } from '~/lib/utils/url_utility';
import RunnerDeleteButton from '../components/runner_delete_button.vue';
import RunnerEditButton from '../components/runner_edit_button.vue';
@@ -76,7 +76,7 @@ export default {
},
onDeleted({ message }) {
saveAlertToLocalStorage({ message, variant: VARIANT_SUCCESS });
- redirectTo(this.runnersPath); // eslint-disable-line import/no-deprecated
+ visitUrl(this.runnersPath);
},
},
};
diff --git a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
index f0ae54c0232..715b0c28148 100644
--- a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
@@ -1,6 +1,6 @@
<script>
import { createAlert, VARIANT_SUCCESS } from '~/alert';
-import { redirectTo, setUrlParams } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
@@ -38,7 +38,7 @@ export default {
message: s__('Runners|Runner created.'),
variant: VARIANT_SUCCESS,
});
- redirectTo(ephemeralRegisterUrl); // eslint-disable-line import/no-deprecated
+ visitUrl(ephemeralRegisterUrl);
},
onError(error) {
createAlert({ message: error.message });
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 39eb1d934ce..63a1ba89fff 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -21,12 +21,12 @@ export function initScrollingTabs() {
if (el && parentElement) {
parentElement
.querySelector('button.fade-left')
- .addEventListener('click', function scrollLeft() {
+ ?.addEventListener('click', function scrollLeft() {
el.scrollBy({ left: -200, behavior: 'smooth' });
});
parentElement
.querySelector('button.fade-right')
- .addEventListener('click', function scrollRight() {
+ ?.addEventListener('click', function scrollRight() {
el.scrollBy({ left: 200, behavior: 'smooth' });
});
}
diff --git a/app/assets/javascripts/streaming/handle_streamed_relative_timestamps.js b/app/assets/javascripts/streaming/handle_streamed_relative_timestamps.js
new file mode 100644
index 00000000000..fa5fe02878c
--- /dev/null
+++ b/app/assets/javascripts/streaming/handle_streamed_relative_timestamps.js
@@ -0,0 +1,80 @@
+import { localTimeAgo } from '~/lib/utils/datetime_utility';
+
+const STREAMING_ELEMENT_NAME = 'streaming-element';
+const TIME_AGO_CLASS_NAME = 'js-timeago';
+
+// Callback handler for intersections observed on timestamps.
+const handleTimestampsIntersecting = (entries, observer) => {
+ entries.forEach((entry) => {
+ const { isIntersecting, target: timestamp } = entry;
+ if (isIntersecting) {
+ localTimeAgo([timestamp]);
+ observer.unobserve(timestamp);
+ }
+ });
+};
+
+// Finds nodes containing the `js-timeago` class within a mutation list.
+const findTimeAgoNodes = (mutationList) => {
+ return mutationList.reduce((acc, mutation) => {
+ [...mutation.addedNodes].forEach((node) => {
+ if (node.classList?.contains(TIME_AGO_CLASS_NAME)) {
+ acc.push(node);
+ }
+ });
+
+ return acc;
+ }, []);
+};
+
+// Callback handler for mutations observed on the streaming element.
+const handleStreamingElementMutation = (mutationList) => {
+ const timestamps = findTimeAgoNodes(mutationList);
+ const timestampIntersectionObserver = new IntersectionObserver(handleTimestampsIntersecting, {
+ rootMargin: `${window.innerHeight}px 0px`,
+ });
+
+ timestamps.forEach((timestamp) => timestampIntersectionObserver.observe(timestamp));
+};
+
+// Finds the streaming element within a mutation list.
+const findStreamingElement = (mutationList) =>
+ mutationList.find((mutation) =>
+ [...mutation.addedNodes].find((node) => node.localName === STREAMING_ELEMENT_NAME),
+ )?.target;
+
+// Waits for the streaming element to become available on the rootElement.
+const waitForStreamingElement = (rootElement) => {
+ return new Promise((resolve) => {
+ let element = document.querySelector(STREAMING_ELEMENT_NAME);
+
+ if (element) {
+ resolve(element);
+ return;
+ }
+
+ const rootElementObserver = new MutationObserver((mutations) => {
+ element = findStreamingElement(mutations);
+ if (element) {
+ resolve(element);
+ rootElementObserver.disconnect();
+ }
+ });
+
+ rootElementObserver.observe(rootElement, { childList: true, subtree: true });
+ });
+};
+
+/**
+ * Ensures relative (timeago) timestamps that are streamed are formatted correctly.
+ *
+ * Example: `May 12, 2020` → `3 years ago`
+ */
+export const handleStreamedRelativeTimestamps = async (rootElement) => {
+ const streamingElement = await waitForStreamingElement(rootElement); // wait for streaming to start
+ const streamingElementObserver = new MutationObserver(handleStreamingElementMutation);
+
+ streamingElementObserver.observe(streamingElement, { childList: true, subtree: true });
+
+ return () => streamingElementObserver.disconnect();
+};
diff --git a/app/assets/javascripts/super_sidebar/components/brand_logo.vue b/app/assets/javascripts/super_sidebar/components/brand_logo.vue
new file mode 100644
index 00000000000..4bb9614e97a
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/brand_logo.vue
@@ -0,0 +1,38 @@
+<script>
+import { __ } from '~/locale';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import logo from '../../../../views/shared/_logo.svg?raw';
+
+export default {
+ logo,
+ i18n: {
+ homepage: __('Homepage'),
+ },
+ directives: {
+ SafeHtml,
+ },
+ inject: ['rootPath'],
+ props: {
+ logoUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+};
+</script>
+
+<template>
+ <a
+ v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.homepage"
+ class="tanuki-logo-container"
+ :href="rootPath"
+ :title="$options.i18n.homepage"
+ data-track-action="click_link"
+ data-track-label="gitlab_logo_link"
+ data-track-property="nav_core_menu"
+ >
+ <img v-if="logoUrl" data-testid="brand-header-custom-logo" :src="logoUrl" class="gl-h-6" />
+ <span v-else v-safe-html="$options.logo" data-testid="brand-header-default-logo"></span>
+ </a>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
index 8cfdd8c8bf4..d3b2143aaa7 100644
--- a/app/assets/javascripts/super_sidebar/components/user_bar.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -1,13 +1,12 @@
<script>
import { GlBadge, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
-import SafeHtml from '~/vue_shared/directives/safe_html';
import {
destroyUserCountsManager,
createUserCountsManager,
userCounts,
} from '~/super_sidebar/user_counts_manager';
-import logo from '../../../../views/shared/_logo.svg?raw';
+import BrandLogo from 'jh_else_ce/super_sidebar/components/brand_logo.vue';
import { JS_TOGGLE_COLLAPSE_CLASS } from '../constants';
import CreateMenu from './create_menu.vue';
import Counter from './counter.vue';
@@ -20,7 +19,6 @@ export default {
// "GitLab Next" is a proper noun, so don't translate "Next"
/* eslint-disable-next-line @gitlab/require-i18n-strings */
NEXT_LABEL: 'Next',
- logo,
JS_TOGGLE_COLLAPSE_CLASS,
SEARCH_MODAL_ID,
components: {
@@ -35,6 +33,7 @@ export default {
/* webpackChunkName: 'global_search_modal' */ './global_search/components/global_search.vue'
),
SuperSidebarToggle,
+ BrandLogo,
},
i18n: {
createNew: __('Create new...'),
@@ -53,9 +52,8 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
GlModal: GlModalDirective,
- SafeHtml,
},
- inject: ['rootPath', 'isImpersonating'],
+ inject: ['isImpersonating'],
props: {
hasCollapseButton: {
default: true,
@@ -107,23 +105,7 @@ export default {
<template>
<div class="user-bar">
<div class="gl-display-flex gl-align-items-center gl-px-3 gl-py-2">
- <a
- v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.homepage"
- class="tanuki-logo-container"
- :href="rootPath"
- :title="$options.i18n.homepage"
- data-track-action="click_link"
- data-track-label="gitlab_logo_link"
- data-track-property="nav_core_menu"
- >
- <img
- v-if="sidebarData.logo_url"
- data-testid="brand-header-custom-logo"
- :src="sidebarData.logo_url"
- class="gl-h-6"
- />
- <span v-else v-safe-html="$options.logo"></span>
- </a>
+ <brand-logo :logo-url="sidebarData.logo_url" />
<gl-badge
v-if="sidebarData.gitlab_com_and_canary"
variant="success"
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index 279acc98cd4..988a28704d4 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -258,7 +258,7 @@ export default {
:markdown-docs-path="$options.markdownDocsPath"
:quick-actions-docs-path="$options.quickActionsDocsPath"
:autocomplete-data-sources="autocompleteDataSources"
- class="gl-px-3 bordered-box gl-mt-5"
+ class="gl-my-5"
>
<template #textarea>
<textarea
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index 1256ef0f2fc..7ca74d4e970 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -31,6 +31,13 @@ class ProjectSetting < ApplicationRecord
encode: false,
encode_iv: false
+ attr_encrypted :product_analytics_configurator_connection_string,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_32,
+ algorithm: 'aes-256-gcm',
+ encode: false,
+ encode_iv: false
+
enum squash_option: {
never: 0,
always: 1,
diff --git a/config/feature_flags/development/combined_analytics_dashboards_editor.yml b/config/feature_flags/development/combined_analytics_dashboards_editor.yml
new file mode 100644
index 00000000000..29f6e5387c4
--- /dev/null
+++ b/config/feature_flags/development/combined_analytics_dashboards_editor.yml
@@ -0,0 +1,8 @@
+---
+name: combined_analytics_dashboards_editor
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120609
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/411407
+milestone: '16.0'
+type: development
+group: group::product analytics
+default_enabled: false
diff --git a/db/migrate/20230504182314_add_pa_configurator_base_to_project_settings.rb b/db/migrate/20230504182314_add_pa_configurator_base_to_project_settings.rb
new file mode 100644
index 00000000000..a633f904692
--- /dev/null
+++ b/db/migrate/20230504182314_add_pa_configurator_base_to_project_settings.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class AddPaConfiguratorBaseToProjectSettings < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+ def up
+ add_column :project_settings, :encrypted_product_analytics_configurator_connection_string, :binary
+ add_column :project_settings, :encrypted_product_analytics_configurator_connection_string_iv, :binary
+ end
+
+ def down
+ remove_column :project_settings, :encrypted_product_analytics_configurator_connection_string
+ remove_column :project_settings, :encrypted_product_analytics_configurator_connection_string_iv
+ end
+end
diff --git a/db/schema_migrations/20230504182314 b/db/schema_migrations/20230504182314
new file mode 100644
index 00000000000..e460078f4a3
--- /dev/null
+++ b/db/schema_migrations/20230504182314
@@ -0,0 +1 @@
+33a5243e26cdcaa4151aa19e6e1837043303dc75295bc6d6468b7c5b849201d9 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 87510ecc9e7..7c6734152a0 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -21169,6 +21169,8 @@ CREATE TABLE project_settings (
cube_api_base_url text,
encrypted_cube_api_key bytea,
encrypted_cube_api_key_iv bytea,
+ encrypted_product_analytics_configurator_connection_string bytea,
+ encrypted_product_analytics_configurator_connection_string_iv bytea,
CONSTRAINT check_1a30456322 CHECK ((char_length(pages_unique_domain) <= 63)),
CONSTRAINT check_2981f15877 CHECK ((char_length(jitsu_key) <= 100)),
CONSTRAINT check_3a03e7557a CHECK ((char_length(previous_default_branch) <= 4096)),
diff --git a/doc/administration/silent_mode/index.md b/doc/administration/silent_mode/index.md
index e98aaaf4e0a..d51a06045a5 100644
--- a/doc/administration/silent_mode/index.md
+++ b/doc/administration/silent_mode/index.md
@@ -33,6 +33,8 @@ There are two ways to enable Silent Mode:
::Gitlab::CurrentSettings.update!(silent_mode_enabled: true)
```
+It may take up to a minute to take effect. [Issue 405433](https://gitlab.com/gitlab-org/gitlab/-/issues/405433) proposes removing this delay.
+
## Disable Silent Mode
Prerequisites:
@@ -53,12 +55,26 @@ There are two ways to disable Silent Mode:
::Gitlab::CurrentSettings.update!(silent_mode_enabled: false)
```
+It may take up to a minute to take effect. [Issue 405433](https://gitlab.com/gitlab-org/gitlab/-/issues/405433) proposes removing this delay.
+
## Behavior of GitLab features in Silent Mode
+This section documents the current behavior of GitLab when Silent Mode is enabled. While Silent Mode is an Experiment, the behavior may change without notice. The work for the first iteration of Silent Mode is tracked by [Epic 9826](https://gitlab.com/groups/gitlab-org/-/epics/9826).
+
### Service Desk
Incoming emails still raise issues, but the users who sent the emails to [Service Desk](../../user/project/service_desk.md) are not notified of issue creation or comments on their issues.
+### Project and group webhooks
+
+Project and group webhooks are suppressed. The relevant Sidekiq jobs fail 4 times and then disappear, while Silent Mode is enabled. [Issue 393639](https://gitlab.com/gitlab-org/gitlab/-/issues/393639) discusses preventing the Sidekiq jobs from running in the first place.
+
+Triggering webhook tests via the UI results in HTTP status 500 responses.
+
### Outbound emails
-Outbound emails are suppressed. It may take up to a minute to take effect after enabling Silent Mode. [Issue 405433](https://gitlab.com/gitlab-org/gitlab/-/issues/405433) proposes removing this delay.
+Outbound emails are suppressed.
+
+### Outbound HTTP requests
+
+Many outbound HTTP requests are suppressed. A list of unsuppressed requests does not exist at this time, since more suppression is planned.
diff --git a/lib/gitlab/email/hook/silent_mode_interceptor.rb b/lib/gitlab/email/hook/silent_mode_interceptor.rb
index 56f94119472..774d4ac1f45 100644
--- a/lib/gitlab/email/hook/silent_mode_interceptor.rb
+++ b/lib/gitlab/email/hook/silent_mode_interceptor.rb
@@ -5,19 +5,17 @@ module Gitlab
module Hook
class SilentModeInterceptor
def self.delivering_email(message)
- if Gitlab::CurrentSettings.silent_mode_enabled?
+ if ::Gitlab::SilentMode.enabled?
message.perform_deliveries = false
- Gitlab::AppJsonLogger.info(
+ ::Gitlab::SilentMode.log_info(
message: "SilentModeInterceptor prevented sending mail",
- mail_subject: message.subject,
- silent_mode_enabled: true
+ mail_subject: message.subject
)
else
- Gitlab::AppJsonLogger.debug(
+ ::Gitlab::SilentMode.log_debug(
message: "SilentModeInterceptor did nothing",
- mail_subject: message.subject,
- silent_mode_enabled: false
+ mail_subject: message.subject
)
end
end
diff --git a/lib/gitlab/http.rb b/lib/gitlab/http.rb
index c6cd5fbfced..8b19611e5c0 100644
--- a/lib/gitlab/http.rb
+++ b/lib/gitlab/http.rb
@@ -10,6 +10,7 @@ module Gitlab
RedirectionTooDeep = Class.new(StandardError)
ReadTotalTimeout = Class.new(Net::ReadTimeout)
HeaderReadTimeout = Class.new(Net::ReadTimeout)
+ SilentModeBlockedError = Class.new(StandardError)
HTTP_TIMEOUT_ERRORS = [
Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Gitlab::HTTP::ReadTotalTimeout
@@ -28,6 +29,13 @@ module Gitlab
}.freeze
DEFAULT_READ_TOTAL_TIMEOUT = 30.seconds
+ SILENT_MODE_ALLOWED_METHODS = [
+ Net::HTTP::Get,
+ Net::HTTP::Head,
+ Net::HTTP::Options,
+ Net::HTTP::Trace
+ ].freeze
+
include HTTParty # rubocop:disable Gitlab/HTTParty
class << self
@@ -37,6 +45,8 @@ module Gitlab
connection_adapter HTTPConnectionAdapter
def self.perform_request(http_method, path, options, &block)
+ raise_if_blocked_by_silent_mode(http_method)
+
log_info = options.delete(:extra_log_info)
options_with_timeouts =
if !options.has_key?(:timeout)
@@ -76,5 +86,20 @@ module Gitlab
rescue *HTTP_ERRORS
nil
end
+
+ def self.raise_if_blocked_by_silent_mode(http_method)
+ return unless blocked_by_silent_mode?(http_method)
+
+ ::Gitlab::SilentMode.log_info(
+ message: 'Outbound HTTP request blocked',
+ outbound_http_request_method: http_method.to_s
+ )
+
+ raise SilentModeBlockedError, 'only get, head, options, and trace methods are allowed in silent mode'
+ end
+
+ def self.blocked_by_silent_mode?(http_method)
+ ::Gitlab::SilentMode.enabled? && SILENT_MODE_ALLOWED_METHODS.exclude?(http_method)
+ end
end
end
diff --git a/lib/gitlab/silent_mode.rb b/lib/gitlab/silent_mode.rb
new file mode 100644
index 00000000000..7c7cbf8f1d9
--- /dev/null
+++ b/lib/gitlab/silent_mode.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SilentMode
+ def self.enabled?
+ Gitlab::CurrentSettings.silent_mode_enabled?
+ end
+
+ def self.log_info(data)
+ Gitlab::AppJsonLogger.info(**add_silent_mode_log_data(data))
+ end
+
+ def self.log_debug(data)
+ Gitlab::AppJsonLogger.debug(**add_silent_mode_log_data(data))
+ end
+
+ def self.add_silent_mode_log_data(data)
+ data.merge!({ silent_mode_enabled: enabled? })
+ end
+ end
+end
diff --git a/lib/product_analytics/settings.rb b/lib/product_analytics/settings.rb
index 5d52965f5be..a695cfd3db8 100644
--- a/lib/product_analytics/settings.rb
+++ b/lib/product_analytics/settings.rb
@@ -6,6 +6,11 @@ module ProductAnalytics
%w[product_analytics_data_collector_host product_analytics_clickhouse_connection_string] +
%w[cube_api_base_url cube_api_key]).freeze
+ SNOWPLOW_CONFIG_KEYS = %w[product_analytics_configurator_connection_string].freeze
+
+ ALL_CONFIG_KEYS = (ProductAnalytics::Settings::CONFIG_KEYS +
+ ProductAnalytics::Settings::SNOWPLOW_CONFIG_KEYS).freeze
+
def initialize(project:)
@project = project
end
@@ -14,25 +19,41 @@ module ProductAnalytics
::Gitlab::CurrentSettings.product_analytics_enabled? && configured?
end
- # rubocop:disable GitlabSecurity/PublicSend
def configured?
+ return unless configured_snowplow?
+
CONFIG_KEYS.all? do |key|
- @project.project_setting.public_send(key).present? ||
- ::Gitlab::CurrentSettings.public_send(key).present?
+ get_setting_value(key).present?
+ end
+ end
+
+ def configured_snowplow?
+ return true unless Feature.enabled?(:product_analytics_snowplow_support, @project)
+
+ SNOWPLOW_CONFIG_KEYS.all? do |key|
+ get_setting_value(key).present?
end
end
- CONFIG_KEYS.each do |key|
+ ALL_CONFIG_KEYS.each do |key|
define_method key.to_sym do
- @project.project_setting.public_send(key).presence || ::Gitlab::CurrentSettings.public_send(key)
+ get_setting_value(key)
end
end
- # rubocop:enable GitlabSecurity/PublicSend
class << self
def for_project(project)
ProductAnalytics::Settings.new(project: project)
end
end
+
+ private
+
+ # rubocop:disable GitlabSecurity/PublicSend
+ def get_setting_value(key)
+ @project.project_setting.public_send(key).presence ||
+ ::Gitlab::CurrentSettings.public_send(key)
+ end
+ # rubocop:enable GitlabSecurity/PublicSend
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b4c4de6927b..e9024645b23 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -35611,6 +35611,9 @@ msgstr ""
msgid "ProjectSettings|Private"
msgstr ""
+msgid "ProjectSettings|Product analytics configurator connection string"
+msgstr ""
+
msgid "ProjectSettings|Product analytics needs to be set up before your application can be instrumented. Follow the %{link_start}set up process%{link_end}."
msgstr ""
@@ -35716,6 +35719,9 @@ msgstr ""
msgid "ProjectSettings|The commit message used when squashing commits."
msgstr ""
+msgid "ProjectSettings|The connection string of your product analytics configurator instance for Snowplow configuration."
+msgstr ""
+
msgid "ProjectSettings|The default target project for merge requests created in this fork project."
msgstr ""
diff --git a/qa/qa/page/group/menu.rb b/qa/qa/page/group/menu.rb
index 157bc3abaf6..2850382b672 100644
--- a/qa/qa/page/group/menu.rb
+++ b/qa/qa/page/group/menu.rb
@@ -83,7 +83,9 @@ module QA
end
end
- def go_to_dependency_proxy
+ def go_to_group_dependency_proxy
+ return go_to_dependency_proxy if Runtime::Env.super_sidebar_enabled?
+
hover_group_packages do
within_submenu do
click_element(:sidebar_menu_item_link, menu_item: 'Dependency Proxy')
diff --git a/qa/qa/page/sub_menus/super_sidebar/operate.rb b/qa/qa/page/sub_menus/super_sidebar/operate.rb
index 1ffbb6872da..00f3fb368b3 100644
--- a/qa/qa/page/sub_menus/super_sidebar/operate.rb
+++ b/qa/qa/page/sub_menus/super_sidebar/operate.rb
@@ -16,7 +16,7 @@ module QA
end
def go_to_dependency_proxy
- open_operate_submenu('Dependency proxy')
+ open_operate_submenu('Dependency Proxy')
end
private
diff --git a/qa/qa/specs/features/browser_ui/5_package/dependency_proxy/dependency_proxy_spec.rb b/qa/qa/specs/features/browser_ui/5_package/dependency_proxy/dependency_proxy_spec.rb
index a0d283fd7ad..7e99cdba369 100644
--- a/qa/qa/specs/features/browser_ui/5_package/dependency_proxy/dependency_proxy_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/dependency_proxy/dependency_proxy_spec.rb
@@ -186,7 +186,7 @@ module QA
project.group.visit!
- Page::Group::Menu.perform(&:go_to_dependency_proxy)
+ Page::Group::Menu.perform(&:go_to_group_dependency_proxy)
Page::Group::DependencyProxy.perform do |index|
expect(index).to have_blob_count("Contains 1 blobs of images")
diff --git a/spec/frontend/__helpers__/mock_dom_observer.js b/spec/frontend/__helpers__/mock_dom_observer.js
index 8c9c435041e..fd3945adfd8 100644
--- a/spec/frontend/__helpers__/mock_dom_observer.js
+++ b/spec/frontend/__helpers__/mock_dom_observer.js
@@ -22,9 +22,9 @@ class MockObserver {
takeRecords() {}
- $_triggerObserve(node, { entry = {}, options = {} } = {}) {
+ $_triggerObserve(node, { entry = {}, observer = {}, options = {} } = {}) {
if (this.$_hasObserver(node, options)) {
- this.$_cb([{ target: node, ...entry }]);
+ this.$_cb([{ target: node, ...entry }], observer);
}
}
diff --git a/spec/frontend/blame/streaming/index_spec.js b/spec/frontend/blame/streaming/index_spec.js
index e048ce3f70e..29beb6beffa 100644
--- a/spec/frontend/blame/streaming/index_spec.js
+++ b/spec/frontend/blame/streaming/index_spec.js
@@ -4,12 +4,14 @@ import { setHTMLFixture } from 'helpers/fixtures';
import { renderHtmlStreams } from '~/streaming/render_html_streams';
import { rateLimitStreamRequests } from '~/streaming/rate_limit_stream_requests';
import { handleStreamedAnchorLink } from '~/streaming/handle_streamed_anchor_link';
+import { handleStreamedRelativeTimestamps } from '~/streaming/handle_streamed_relative_timestamps';
import { toPolyfillReadable } from '~/streaming/polyfills';
import { createAlert } from '~/alert';
jest.mock('~/streaming/render_html_streams');
jest.mock('~/streaming/rate_limit_stream_requests');
jest.mock('~/streaming/handle_streamed_anchor_link');
+jest.mock('~/streaming/handle_streamed_relative_timestamps');
jest.mock('~/streaming/polyfills');
jest.mock('~/sentry');
jest.mock('~/alert');
@@ -18,6 +20,7 @@ global.fetch = jest.fn();
describe('renderBlamePageStreams', () => {
let stopAnchor;
+ let stopTimetamps;
const PAGES_URL = 'https://example.com/';
const findStreamContainer = () => document.querySelector('#blame-stream-container');
const findStreamLoadingIndicator = () => document.querySelector('#blame-stream-loading');
@@ -34,6 +37,7 @@ describe('renderBlamePageStreams', () => {
};
handleStreamedAnchorLink.mockImplementation(() => stopAnchor);
+ handleStreamedRelativeTimestamps.mockImplementation(() => Promise.resolve(stopTimetamps));
rateLimitStreamRequests.mockImplementation(({ factory, total }) => {
return Array.from({ length: total }, (_, i) => {
return Promise.resolve(factory(i));
@@ -43,6 +47,7 @@ describe('renderBlamePageStreams', () => {
beforeEach(() => {
stopAnchor = jest.fn();
+ stopTimetamps = jest.fn();
fetch.mockClear();
});
@@ -50,6 +55,7 @@ describe('renderBlamePageStreams', () => {
await renderBlamePageStreams();
expect(handleStreamedAnchorLink).not.toHaveBeenCalled();
+ expect(handleStreamedRelativeTimestamps).not.toHaveBeenCalled();
expect(renderHtmlStreams).not.toHaveBeenCalled();
});
@@ -64,7 +70,9 @@ describe('renderBlamePageStreams', () => {
renderBlamePageStreams(stream);
expect(handleStreamedAnchorLink).toHaveBeenCalledTimes(1);
+ expect(handleStreamedRelativeTimestamps).toHaveBeenCalledTimes(1);
expect(stopAnchor).toHaveBeenCalledTimes(0);
+ expect(stopTimetamps).toHaveBeenCalledTimes(0);
expect(renderHtmlStreams).toHaveBeenCalledWith([stream], findStreamContainer());
expect(findStreamLoadingIndicator()).not.toBe(null);
@@ -72,6 +80,7 @@ describe('renderBlamePageStreams', () => {
await waitForPromises();
expect(stopAnchor).toHaveBeenCalledTimes(1);
+ expect(stopTimetamps).toHaveBeenCalledTimes(1);
expect(findStreamLoadingIndicator()).toBe(null);
});
diff --git a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
index 4c56dd74f1a..75bca68b888 100644
--- a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
+++ b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
@@ -16,14 +16,14 @@ import {
WINDOWS_PLATFORM,
} from '~/ci/runner/constants';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
-import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { visitUrl } from '~/lib/utils/url_utility';
import { runnerCreateResult } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
- redirectTo: jest.fn(),
+ visitUrl: jest.fn(),
}));
const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner;
@@ -87,7 +87,7 @@ describe('AdminNewRunnerApp', () => {
it('redirects to the registration page', () => {
const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`;
- expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ expect(visitUrl).toHaveBeenCalledWith(url);
});
});
@@ -100,7 +100,7 @@ describe('AdminNewRunnerApp', () => {
it('redirects to the registration page with the platform', () => {
const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`;
- expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ expect(visitUrl).toHaveBeenCalledWith(url);
});
});
diff --git a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
index 9787b1ef83f..a4ba9815c8d 100644
--- a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -5,7 +5,7 @@ import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_help
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
-import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { visitUrl } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/ci/runner/components/runner_header.vue';
@@ -26,7 +26,10 @@ import { runnerData } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
-jest.mock('~/lib/utils/url_utility');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ visitUrl: jest.fn(),
+}));
const mockRunner = runnerData.data.runner;
const mockRunnerGraphqlId = mockRunner.id;
@@ -180,7 +183,7 @@ describe('AdminRunnerShowApp', () => {
message: 'Runner deleted',
variant: VARIANT_SUCCESS,
});
- expect(redirectTo).toHaveBeenCalledWith(mockRunnersPath); // eslint-disable-line import/no-deprecated
+ expect(visitUrl).toHaveBeenCalledWith(mockRunnersPath);
});
});
diff --git a/spec/frontend/ci/runner/components/runner_update_form_spec.js b/spec/frontend/ci/runner/components/runner_update_form_spec.js
index db4c236bfff..ee37d6241b5 100644
--- a/spec/frontend/ci/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/ci/runner/components/runner_update_form_spec.js
@@ -6,7 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
-import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { visitUrl } from '~/lib/utils/url_utility';
import RunnerUpdateForm from '~/ci/runner/components/runner_update_form.vue';
import {
INSTANCE_TYPE,
@@ -23,7 +23,10 @@ import { runnerFormData } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
-jest.mock('~/lib/utils/url_utility');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ visitUrl: jest.fn(),
+}));
const mockRunner = runnerFormData.data.runner;
const mockRunnerPath = '/admin/runners/1';
@@ -86,7 +89,7 @@ describe('RunnerUpdateForm', () => {
variant: VARIANT_SUCCESS,
}),
);
- expect(redirectTo).toHaveBeenCalledWith(mockRunnerPath); // eslint-disable-line import/no-deprecated
+ expect(visitUrl).toHaveBeenCalledWith(mockRunnerPath);
};
beforeEach(() => {
@@ -278,7 +281,7 @@ describe('RunnerUpdateForm', () => {
expect(captureException).not.toHaveBeenCalled();
expect(saveAlertToLocalStorage).not.toHaveBeenCalled();
- expect(redirectTo).not.toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
+ expect(visitUrl).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js b/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
index 1c052b00fc3..177fd9bcd9a 100644
--- a/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
+++ b/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
@@ -16,7 +16,7 @@ import {
WINDOWS_PLATFORM,
} from '~/ci/runner/constants';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
-import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { visitUrl } from '~/lib/utils/url_utility';
import { runnerCreateResult } from '../mock_data';
const mockGroupId = 'gid://gitlab/Group/72';
@@ -25,7 +25,7 @@ jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
- redirectTo: jest.fn(),
+ visitUrl: jest.fn(),
}));
const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner;
@@ -92,7 +92,7 @@ describe('GroupRunnerRunnerApp', () => {
it('redirects to the registration page', () => {
const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`;
- expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ expect(visitUrl).toHaveBeenCalledWith(url);
});
});
@@ -105,7 +105,7 @@ describe('GroupRunnerRunnerApp', () => {
it('redirects to the registration page with the platform', () => {
const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`;
- expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ expect(visitUrl).toHaveBeenCalledWith(url);
});
});
diff --git a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
index 0c594e8005c..5a4c34fc374 100644
--- a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
@@ -5,7 +5,7 @@ import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_help
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
-import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { visitUrl } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/ci/runner/components/runner_header.vue';
@@ -26,7 +26,10 @@ import { runnerData } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
-jest.mock('~/lib/utils/url_utility');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ visitUrl: jest.fn(),
+}));
const mockRunner = runnerData.data.runner;
const mockRunnerGraphqlId = mockRunner.id;
@@ -185,7 +188,7 @@ describe('GroupRunnerShowApp', () => {
message: 'Runner deleted',
variant: VARIANT_SUCCESS,
});
- expect(redirectTo).toHaveBeenCalledWith(mockRunnersPath); // eslint-disable-line import/no-deprecated
+ expect(visitUrl).toHaveBeenCalledWith(mockRunnersPath);
});
});
});
diff --git a/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js b/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js
index 5bfbbfdc074..22d8e243f7b 100644
--- a/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js
+++ b/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js
@@ -16,7 +16,7 @@ import {
WINDOWS_PLATFORM,
} from '~/ci/runner/constants';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
-import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { visitUrl } from '~/lib/utils/url_utility';
import { runnerCreateResult, mockRegistrationToken } from '../mock_data';
const mockProjectId = 'gid://gitlab/Project/72';
@@ -25,7 +25,7 @@ jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
- redirectTo: jest.fn(),
+ visitUrl: jest.fn(),
}));
const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner;
@@ -93,7 +93,7 @@ describe('ProjectRunnerRunnerApp', () => {
it('redirects to the registration page', () => {
const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`;
- expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ expect(visitUrl).toHaveBeenCalledWith(url);
});
});
@@ -106,7 +106,7 @@ describe('ProjectRunnerRunnerApp', () => {
it('redirects to the registration page with the platform', () => {
const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`;
- expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ expect(visitUrl).toHaveBeenCalledWith(url);
});
});
diff --git a/spec/frontend/streaming/handle_streamed_relative_timestamps_spec.js b/spec/frontend/streaming/handle_streamed_relative_timestamps_spec.js
new file mode 100644
index 00000000000..12bd27488b1
--- /dev/null
+++ b/spec/frontend/streaming/handle_streamed_relative_timestamps_spec.js
@@ -0,0 +1,94 @@
+import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures';
+import waitForPromises from 'helpers/wait_for_promises';
+import { handleStreamedRelativeTimestamps } from '~/streaming/handle_streamed_relative_timestamps';
+import { localTimeAgo } from '~/lib/utils/datetime_utility';
+import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
+
+jest.mock('~/lib/utils/datetime_utility');
+
+const TIMESTAMP_MOCK = `<div class="js-timeago">Oct 2, 2019</div>`;
+
+describe('handleStreamedRelativeTimestamps', () => {
+ const findRoot = () => document.querySelector('#root');
+ const findStreamingElement = () => document.querySelector('streaming-element');
+ const findTimestamp = () => document.querySelector('.js-timeago');
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ describe('when element is present', () => {
+ beforeEach(() => {
+ setHTMLFixture(`<div id="root">${TIMESTAMP_MOCK}</div>`);
+ handleStreamedRelativeTimestamps(findRoot());
+ });
+
+ it('does nothing', async () => {
+ await waitForPromises();
+ expect(localTimeAgo).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when element is streamed', () => {
+ let relativeTimestampsHandler;
+ const { trigger: triggerIntersection } = useMockIntersectionObserver();
+
+ const insertStreamingElement = () =>
+ findRoot().insertAdjacentHTML('afterbegin', `<streaming-element></streaming-element>`);
+
+ beforeEach(() => {
+ setHTMLFixture('<div id="root"></div>');
+ relativeTimestampsHandler = handleStreamedRelativeTimestamps(findRoot());
+ });
+
+ it('formats and unobserved the timestamp when inserted and intersecting', async () => {
+ insertStreamingElement();
+ await waitForPromises();
+ findStreamingElement().insertAdjacentHTML('afterbegin', TIMESTAMP_MOCK);
+ await waitForPromises();
+
+ const timestamp = findTimestamp();
+ const unobserveMock = jest.fn();
+
+ triggerIntersection(findTimestamp(), {
+ entry: { isIntersecting: true },
+ observer: { unobserve: unobserveMock },
+ });
+
+ expect(unobserveMock).toHaveBeenCalled();
+ expect(localTimeAgo).toHaveBeenCalledWith([timestamp]);
+ });
+
+ it('does not format the timestamp when inserted but not intersecting', async () => {
+ insertStreamingElement();
+ await waitForPromises();
+ findStreamingElement().insertAdjacentHTML('afterbegin', TIMESTAMP_MOCK);
+ await waitForPromises();
+
+ const unobserveMock = jest.fn();
+
+ triggerIntersection(findTimestamp(), {
+ entry: { isIntersecting: false },
+ observer: { unobserve: unobserveMock },
+ });
+
+ expect(unobserveMock).not.toHaveBeenCalled();
+ expect(localTimeAgo).not.toHaveBeenCalled();
+ });
+
+ it('does not format the time when destroyed', async () => {
+ insertStreamingElement();
+
+ const stop = await relativeTimestampsHandler;
+ stop();
+
+ await waitForPromises();
+ findStreamingElement().insertAdjacentHTML('afterbegin', TIMESTAMP_MOCK);
+ await waitForPromises();
+
+ triggerIntersection(findTimestamp(), { entry: { isIntersecting: true } });
+
+ expect(localTimeAgo).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/brand_logo_spec.js b/spec/frontend/super_sidebar/components/brand_logo_spec.js
new file mode 100644
index 00000000000..63c4bb9668b
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/brand_logo_spec.js
@@ -0,0 +1,42 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createMockDirective } from 'helpers/vue_mock_directive';
+import BrandLogo from 'jh_else_ce/super_sidebar/components/brand_logo.vue';
+
+describe('Brand Logo component', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ logoUrl: 'path/to/logo',
+ };
+
+ const findBrandLogo = () => wrapper.findByTestId('brand-header-custom-logo');
+ const findDefaultLogo = () => wrapper.findByTestId('brand-header-default-logo');
+
+ const createWrapper = (props = {}) => {
+ wrapper = shallowMountExtended(BrandLogo, {
+ provide: {
+ rootPath: '/',
+ },
+ propsData: {
+ ...defaultPropsData,
+ ...props,
+ },
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ });
+ };
+
+ it('renders it', () => {
+ createWrapper();
+ expect(findBrandLogo().exists()).toBe(true);
+ expect(findBrandLogo().attributes('src')).toBe(defaultPropsData.logoUrl);
+ });
+
+ it('when logoUrl given empty', () => {
+ createWrapper({ logoUrl: '' });
+
+ expect(findBrandLogo().exists()).toBe(false);
+ expect(findDefaultLogo().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
index 6878e724c65..ae48c0f2a75 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -5,6 +5,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale';
import CreateMenu from '~/super_sidebar/components/create_menu.vue';
import SearchModal from '~/super_sidebar/components/global_search/components/global_search.vue';
+import BrandLogo from 'jh_else_ce/super_sidebar/components/brand_logo.vue';
import MergeRequestMenu from '~/super_sidebar/components/merge_request_menu.vue';
import Counter from '~/super_sidebar/components/counter.vue';
import UserBar from '~/super_sidebar/components/user_bar.vue';
@@ -23,7 +24,7 @@ describe('UserBar component', () => {
const findMRsCounter = () => findCounter(1);
const findTodosCounter = () => findCounter(2);
const findMergeRequestMenu = () => wrapper.findComponent(MergeRequestMenu);
- const findBrandLogo = () => wrapper.findByTestId('brand-header-custom-logo');
+ const findBrandLogo = () => wrapper.findComponent(BrandLogo);
const findCollapseButton = () => wrapper.findByTestId('super-sidebar-collapse-button');
const findSearchButton = () => wrapper.findByTestId('super-sidebar-search-button');
const findSearchModal = () => wrapper.findComponent(SearchModal);
@@ -47,7 +48,6 @@ describe('UserBar component', () => {
sidebarData: { ...sidebarData, ...extraSidebarData },
},
provide: {
- rootPath: '/',
toggleNewNavEndpoint: '/-/profile/preferences',
isImpersonating: false,
...provideOverrides,
@@ -116,7 +116,7 @@ describe('UserBar component', () => {
it('renders branding logo', () => {
expect(findBrandLogo().exists()).toBe(true);
- expect(findBrandLogo().attributes('src')).toBe(sidebarData.logo_url);
+ expect(findBrandLogo().props('logoUrl')).toBe(sidebarData.logo_url);
});
it('does not render the "Stop impersonating" button', () => {
diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb
index 57e4b4fc74b..133cd3b2f49 100644
--- a/spec/lib/gitlab/http_spec.rb
+++ b/spec/lib/gitlab/http_spec.rb
@@ -364,4 +364,77 @@ RSpec.describe Gitlab::HTTP do
end
end
end
+
+ describe 'silent mode', feature_category: :geo_replication do
+ before do
+ stub_full_request("http://example.org", method: :any)
+ stub_application_setting(silent_mode_enabled: silent_mode)
+ end
+
+ context 'when silent mode is enabled' do
+ let(:silent_mode) { true }
+
+ it 'allows GET requests' do
+ expect { described_class.get('http://example.org') }.not_to raise_error
+ end
+
+ it 'allows HEAD requests' do
+ expect { described_class.head('http://example.org') }.not_to raise_error
+ end
+
+ it 'allows OPTIONS requests' do
+ expect { described_class.options('http://example.org') }.not_to raise_error
+ end
+
+ it 'blocks POST requests' do
+ expect { described_class.post('http://example.org') }.to raise_error(Gitlab::HTTP::SilentModeBlockedError)
+ end
+
+ it 'blocks PUT requests' do
+ expect { described_class.put('http://example.org') }.to raise_error(Gitlab::HTTP::SilentModeBlockedError)
+ end
+
+ it 'blocks DELETE requests' do
+ expect { described_class.delete('http://example.org') }.to raise_error(Gitlab::HTTP::SilentModeBlockedError)
+ end
+
+ it 'logs blocked requests' do
+ expect(::Gitlab::AppJsonLogger).to receive(:info).with(
+ message: "Outbound HTTP request blocked",
+ outbound_http_request_method: 'Net::HTTP::Post',
+ silent_mode_enabled: true
+ )
+
+ expect { described_class.post('http://example.org') }.to raise_error(Gitlab::HTTP::SilentModeBlockedError)
+ end
+ end
+
+ context 'when silent mode is disabled' do
+ let(:silent_mode) { false }
+
+ it 'allows GET requests' do
+ expect { described_class.get('http://example.org') }.not_to raise_error
+ end
+
+ it 'allows HEAD requests' do
+ expect { described_class.head('http://example.org') }.not_to raise_error
+ end
+
+ it 'allows OPTIONS requests' do
+ expect { described_class.options('http://example.org') }.not_to raise_error
+ end
+
+ it 'blocks POST requests' do
+ expect { described_class.post('http://example.org') }.not_to raise_error
+ end
+
+ it 'blocks PUT requests' do
+ expect { described_class.put('http://example.org') }.not_to raise_error
+ end
+
+ it 'blocks DELETE requests' do
+ expect { described_class.delete('http://example.org') }.not_to raise_error
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/silent_mode_spec.rb b/spec/lib/gitlab/silent_mode_spec.rb
new file mode 100644
index 00000000000..bccf7033121
--- /dev/null
+++ b/spec/lib/gitlab/silent_mode_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::SilentMode, feature_category: :geo_replication do
+ before do
+ stub_application_setting(silent_mode_enabled: silent_mode)
+ end
+
+ describe '.enabled?' do
+ context 'when silent mode is enabled' do
+ let(:silent_mode) { true }
+
+ it { expect(described_class.enabled?).to be_truthy }
+ end
+
+ context 'when silent mode is disabled' do
+ let(:silent_mode) { false }
+
+ it { expect(described_class.enabled?).to be_falsey }
+ end
+ end
+
+ describe '.log_info' do
+ let(:log_args) do
+ {
+ message: 'foo',
+ bar: 'baz'
+ }
+ end
+
+ let(:expected_log_args) { log_args.merge(silent_mode_enabled: silent_mode) }
+
+ context 'when silent mode is enabled' do
+ let(:silent_mode) { true }
+
+ it 'logs to AppJsonLogger and adds the current state of silent mode' do
+ expect(Gitlab::AppJsonLogger).to receive(:info).with(expected_log_args)
+
+ described_class.log_info(log_args)
+ end
+ end
+
+ context 'when silent mode is disabled' do
+ let(:silent_mode) { false }
+
+ it 'logs to AppJsonLogger and adds the current state of silent mode' do
+ expect(Gitlab::AppJsonLogger).to receive(:info).with(expected_log_args)
+
+ described_class.log_info(log_args)
+ end
+
+ it 'overwrites silent_mode_enabled log key if call already contains it' do
+ expect(Gitlab::AppJsonLogger).to receive(:info).with(expected_log_args)
+
+ described_class.log_info(log_args.merge(silent_mode_enabled: 'foo'))
+ end
+ end
+ end
+
+ describe '.log_debug' do
+ let(:log_args) do
+ {
+ message: 'foo',
+ bar: 'baz'
+ }
+ end
+
+ let(:expected_log_args) { log_args.merge(silent_mode_enabled: silent_mode) }
+
+ context 'when silent mode is enabled' do
+ let(:silent_mode) { true }
+
+ it 'logs to AppJsonLogger and adds the current state of silent mode' do
+ expect(Gitlab::AppJsonLogger).to receive(:debug).with(expected_log_args)
+
+ described_class.log_debug(log_args)
+ end
+ end
+
+ context 'when silent mode is disabled' do
+ let(:silent_mode) { false }
+
+ it 'logs to AppJsonLogger and adds the current state of silent mode' do
+ expect(Gitlab::AppJsonLogger).to receive(:debug).with(expected_log_args)
+
+ described_class.log_debug(log_args)
+ end
+
+ it 'overwrites silent_mode_enabled log key if call already contains it' do
+ expect(Gitlab::AppJsonLogger).to receive(:debug).with(expected_log_args)
+
+ described_class.log_debug(log_args.merge(silent_mode_enabled: 'foo'))
+ end
+ end
+ end
+end
diff --git a/spec/lib/product_analytics/settings_spec.rb b/spec/lib/product_analytics/settings_spec.rb
index 8e6ac3cf0ad..9c33b8068d1 100644
--- a/spec/lib/product_analytics/settings_spec.rb
+++ b/spec/lib/product_analytics/settings_spec.rb
@@ -30,8 +30,8 @@ RSpec.describe ProductAnalytics::Settings, feature_category: :product_analytics
context 'when one configuration setting is missing' do
before do
- missing_key = ProductAnalytics::Settings::CONFIG_KEYS.last
- mock_settings('test', ProductAnalytics::Settings::CONFIG_KEYS - [missing_key])
+ missing_key = ProductAnalytics::Settings::ALL_CONFIG_KEYS.last
+ mock_settings('test', ProductAnalytics::Settings::ALL_CONFIG_KEYS - [missing_key])
allow(::Gitlab::CurrentSettings).to receive(missing_key).and_return('')
end
@@ -40,7 +40,7 @@ RSpec.describe ProductAnalytics::Settings, feature_category: :product_analytics
end
end
- ProductAnalytics::Settings::CONFIG_KEYS.each do |key|
+ ProductAnalytics::Settings::ALL_CONFIG_KEYS.each do |key|
it "can read #{key}" do
expect(::Gitlab::CurrentSettings).to receive(key).and_return('test')
@@ -93,7 +93,7 @@ RSpec.describe ProductAnalytics::Settings, feature_category: :product_analytics
private
- def mock_settings(setting, keys = ProductAnalytics::Settings::CONFIG_KEYS)
+ def mock_settings(setting, keys = ProductAnalytics::Settings::ALL_CONFIG_KEYS)
keys.each do |key|
allow(::Gitlab::CurrentSettings).to receive(key).and_return(setting)
end
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index e9581265bb0..bf233ed5929 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -180,6 +180,9 @@ project_setting:
- cube_api_key
- encrypted_cube_api_key
- encrypted_cube_api_key_iv
+ - encrypted_product_analytics_configurator_connection_string
+ - encrypted_product_analytics_configurator_connection_string_iv
+ - product_analytics_configurator_connection_string
build_service_desk_setting: # service_desk_setting
unexposed_attributes: