diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-19 12:07:52 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-19 12:07:52 +0300 |
commit | ee3d5f16e3aa642944b121645764e33604a31307 (patch) | |
tree | 6b27cb8fca43bdac8d558d689b64c7298ea3cb37 | |
parent | 765ec2e3b2eb347314af5f806c6b70bad696265a (diff) |
Add latest changes from gitlab-org/gitlab@master
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: |