summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-03-06 18:08:12 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-03-06 18:08:12 +0000
commite22c3819ad2321a0cf825877fe3b60e41268c5b3 (patch)
treefcd143b30bdd7b42d439cd0b2fc5c6c4268d8d97
parent49b16b71778148e9f9c579bf7bf69853c780c827 (diff)
downloadgitlab-ce-e22c3819ad2321a0cf825877fe3b60e41268c5b3.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--README.md2
-rw-r--r--app/assets/javascripts/blame/streaming/index.js56
-rw-r--r--app/assets/javascripts/pages/projects/blame/show/index.js7
-rw-r--r--app/assets/javascripts/streaming/chunk_writer.js144
-rw-r--r--app/assets/javascripts/streaming/constants.js9
-rw-r--r--app/assets/javascripts/streaming/handle_streamed_anchor_link.js26
-rw-r--r--app/assets/javascripts/streaming/html_stream.js33
-rw-r--r--app/assets/javascripts/streaming/polyfills.js5
-rw-r--r--app/assets/javascripts/streaming/rate_limit_stream_requests.js87
-rw-r--r--app/assets/javascripts/streaming/render_balancer.js36
-rw-r--r--app/assets/javascripts/streaming/render_html_streams.js40
-rw-r--r--app/assets/stylesheets/framework/files.scss28
-rw-r--r--app/controllers/admin/applications_controller.rb2
-rw-r--r--app/controllers/admin/identities_controller.rb2
-rw-r--r--app/controllers/admin/impersonation_tokens_controller.rb2
-rw-r--r--app/controllers/admin/impersonations_controller.rb2
-rw-r--r--app/controllers/admin/keys_controller.rb2
-rw-r--r--app/controllers/admin/sessions_controller.rb2
-rw-r--r--app/controllers/confirmations_controller.rb2
-rw-r--r--app/controllers/groups/settings/access_tokens_controller.rb2
-rw-r--r--app/controllers/groups/settings/applications_controller.rb2
-rw-r--r--app/controllers/import/github_controller.rb6
-rw-r--r--app/controllers/invites_controller.rb2
-rw-r--r--app/controllers/jwt_controller.rb2
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb2
-rw-r--r--app/controllers/passwords_controller.rb2
-rw-r--r--app/controllers/profiles/accounts_controller.rb2
-rw-r--r--app/controllers/profiles/active_sessions_controller.rb2
-rw-r--r--app/controllers/profiles/passwords_controller.rb2
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb2
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb2
-rw-r--r--app/controllers/profiles/u2f_registrations_controller.rb2
-rw-r--r--app/controllers/profiles/webauthn_registrations_controller.rb2
-rw-r--r--app/controllers/profiles_controller.rb2
-rw-r--r--app/controllers/projects/blame_controller.rb38
-rw-r--r--app/controllers/projects/settings/access_tokens_controller.rb2
-rw-r--r--app/controllers/registrations/welcome_controller.rb2
-rw-r--r--app/controllers/registrations_controller.rb2
-rw-r--r--app/controllers/sessions_controller.rb2
-rw-r--r--app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb62
-rw-r--r--app/graphql/types/analytics/cycle_analytics/flow_metrics.rb5
-rw-r--r--app/policies/group_policy.rb2
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/services/projects/blame_service.rb42
-rw-r--r--app/services/protected_branches/base_service.rb2
-rw-r--r--app/views/layouts/_head.html.haml1
-rw-r--r--app/views/layouts/component_preview.html.haml4
-rw-r--r--app/views/projects/blame/show.html.haml27
-rw-r--r--app/views/users/_profile_basic_info.html.haml8
-rw-r--r--app/views/users/show.html.haml146
-rw-r--r--app/workers/all_queues.yml28
-rw-r--r--app/workers/authorized_project_update/project_recalculate_per_user_worker.rb2
-rw-r--r--app/workers/authorized_project_update/project_recalculate_worker.rb2
-rw-r--r--app/workers/authorized_project_update/user_refresh_from_replica_worker.rb2
-rw-r--r--app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb2
-rw-r--r--app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb2
-rw-r--r--app/workers/authorized_projects_worker.rb2
-rw-r--r--app/workers/delete_user_worker.rb2
-rw-r--r--app/workers/groups/update_two_factor_requirement_for_members_worker.rb2
-rw-r--r--app/workers/members_destroyer/unassign_issuables_worker.rb2
-rw-r--r--app/workers/personal_access_tokens/expired_notification_worker.rb2
-rw-r--r--app/workers/personal_access_tokens/expiring_worker.rb2
-rw-r--r--app/workers/remove_expired_group_links_worker.rb2
-rw-r--r--app/workers/remove_expired_members_worker.rb2
-rw-r--r--app/workers/remove_unaccepted_member_invites_worker.rb2
-rw-r--r--config/environments/development.rb1
-rw-r--r--config/events/1666038724_Gitlab__Tracking__Helpers__WeakPasswordErrorEvent_track_weak_password_error.yml2
-rw-r--r--config/feature_categories.yml3
-rw-r--r--config/feature_flags/development/blame_page_streaming.yml (renamed from config/feature_flags/development/github_client_fetch_repos_via_graphql.yml)12
-rw-r--r--config/feature_flags/development/create_runner_workflow_for_namespace.yml8
-rw-r--r--config/metrics/counts_28d/20210216183627_omniauth_providers.yml2
-rw-r--r--config/metrics/counts_28d/20210910132229_user_auth_by_provider.yml2
-rw-r--r--config/metrics/counts_all/20210216180752_keys.yml2
-rw-r--r--config/metrics/counts_all/20210216183400_omniauth_providers.yml2
-rw-r--r--config/metrics/counts_all/20210910132001_user_auth_by_provider.yml2
-rw-r--r--config/metrics/settings/20210204124906_ldap_enabled.yml2
-rw-r--r--config/metrics/settings/20210204124910_omniauth_enabled.yml2
-rw-r--r--config/metrics/settings/20210204124918_signup_enabled.yml2
-rw-r--r--config/routes/repository.rb1
-rw-r--r--db/docs/application_setting_terms.yml2
-rw-r--r--db/docs/atlassian_identities.yml2
-rw-r--r--db/docs/authentication_events.yml2
-rw-r--r--db/docs/banned_users.yml2
-rw-r--r--db/docs/group_group_links.yml2
-rw-r--r--db/docs/identities.yml2
-rw-r--r--db/docs/ip_restrictions.yml2
-rw-r--r--db/docs/keys.yml2
-rw-r--r--db/docs/ldap_group_links.yml2
-rw-r--r--db/docs/namespace_admin_notes.yml2
-rw-r--r--db/docs/namespace_ldap_settings.yml2
-rw-r--r--db/docs/oauth_access_grants.yml2
-rw-r--r--db/docs/oauth_access_tokens.yml2
-rw-r--r--db/docs/oauth_applications.yml2
-rw-r--r--db/docs/oauth_openid_requests.yml2
-rw-r--r--db/docs/personal_access_tokens.yml2
-rw-r--r--db/docs/project_access_tokens.yml2
-rw-r--r--db/docs/project_authorizations.yml2
-rw-r--r--db/docs/project_group_links.yml2
-rw-r--r--db/docs/saml_group_links.yml2
-rw-r--r--db/docs/saml_providers.yml2
-rw-r--r--db/docs/scim_identities.yml2
-rw-r--r--db/docs/scim_oauth_access_tokens.yml2
-rw-r--r--db/docs/smartcard_identities.yml2
-rw-r--r--db/docs/term_agreements.yml2
-rw-r--r--db/docs/token_with_ivs.yml2
-rw-r--r--db/docs/u2f_registrations.yml2
-rw-r--r--db/docs/user_canonical_emails.yml2
-rw-r--r--db/docs/user_highest_roles.yml2
-rw-r--r--db/docs/user_permission_export_uploads.yml2
-rw-r--r--db/docs/user_synced_attributes_metadata.yml2
-rw-r--r--db/docs/webauthn_registrations.yml2
-rw-r--r--db/post_migrate/20230223082752_schedule_fk_validation_for_p_ci_builds_metadata_partitions_and_ci_builds.rb11
-rw-r--r--db/post_migrate/20230306071456_validate_partitioning_fk_on_p_ci_builds_metadata_partitions.rb20
-rw-r--r--db/post_migrate/20230306072532_add_partitioned_fk_to_p_ci_builds_metadata_on_partition_id_and_build_id.rb36
-rw-r--r--db/post_migrate/20230306082852_remove_fk_to_ci_builds_p_ci_builds_metadata_on_build_id.rb32
-rw-r--r--db/schema_migrations/202303060714561
-rw-r--r--db/schema_migrations/202303060725321
-rw-r--r--db/schema_migrations/202303060828521
-rw-r--r--db/structure.sql5
-rw-r--r--doc/administration/dedicated/index.md2
-rw-r--r--doc/api/graphql/reference/index.md27
-rw-r--r--doc/api/keys.md32
-rw-r--r--doc/api/namespaces.md5
-rw-r--r--doc/architecture/blueprints/runner_tokens/index.md2
-rw-r--r--doc/user/group/saml_sso/index.md262
-rw-r--r--doc/user/profile/notifications.md10
-rw-r--r--lib/api/access_requests.rb2
-rw-r--r--lib/api/applications.rb2
-rw-r--r--lib/api/internal/base.rb12
-rw-r--r--lib/api/keys.rb2
-rw-r--r--lib/api/personal_access_tokens.rb2
-rw-r--r--lib/api/personal_access_tokens/self_information.rb2
-rw-r--r--lib/api/projects.rb6
-rw-r--r--lib/api/resource_access_tokens.rb2
-rw-r--r--lib/api/users.rb66
-rw-r--r--lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens.rb2
-rw-r--r--lib/gitlab/background_migration/backfill_namespace_ldap_settings.rb2
-rw-r--r--lib/gitlab/github_import/clients/proxy.rb14
-rw-r--r--lib/gitlab/github_import/clients/search_repos.rb8
-rw-r--r--lib/gitlab/metrics/requests_rack_middleware.rb2
-rw-r--r--locale/gitlab.pot33
-rw-r--r--package.json2
-rw-r--r--qa/qa/specs/features/api/1_manage/group_access_token_spec.rb2
-rw-r--r--qa/qa/specs/features/api/1_manage/project_access_token_spec.rb2
-rw-r--r--qa/qa/specs/features/api/1_manage/user_access_termination_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/group/group_access_token_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/project_access_token_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/user/impersonation_token_spec.rb2
-rw-r--r--spec/controllers/import/github_controller_spec.rb16
-rw-r--r--spec/db/schema_spec.rb5
-rw-r--r--spec/features/merge_request/real_time_merge_widget_spec.rb87
-rw-r--r--spec/features/projects/blobs/blame_spec.rb47
-rw-r--r--spec/frontend/__helpers__/shared_test_setup.js4
-rw-r--r--spec/frontend/__mocks__/lodash/debounce.js19
-rw-r--r--spec/frontend/__mocks__/lodash/throttle.js2
-rw-r--r--spec/frontend/blame/streaming/index_spec.js110
-rw-r--r--spec/frontend/ide/components/commit_sidebar/editor_header_spec.js39
-rw-r--r--spec/frontend/streaming/chunk_writer_spec.js214
-rw-r--r--spec/frontend/streaming/handle_streamed_anchor_link_spec.js132
-rw-r--r--spec/frontend/streaming/html_stream_spec.js46
-rw-r--r--spec/frontend/streaming/rate_limit_stream_requests_spec.js155
-rw-r--r--spec/frontend/streaming/render_balancer_spec.js69
-rw-r--r--spec/frontend/streaming/render_html_streams_spec.js96
-rw-r--r--spec/helpers/device_registration_helper_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb42
-rw-r--r--spec/lib/gitlab/github_import/clients/proxy_spec.rb62
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb8
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb6
-rw-r--r--spec/policies/group_policy_spec.rb10
-rw-r--r--spec/policies/project_policy_spec.rb8
-rw-r--r--spec/requests/api/admin/ci/variables_spec.rb22
-rw-r--r--spec/requests/api/admin/instance_clusters_spec.rb21
-rw-r--r--spec/requests/api/admin/plan_limits_spec.rb4
-rw-r--r--spec/requests/api/appearance_spec.rb4
-rw-r--r--spec/requests/api/applications_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/flow_metrics_spec.rb14
-rw-r--r--spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb81
-rw-r--r--spec/support/shared_examples/features/real_time_merge_widget_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/requests/admin_mode_shared_examples.rb20
-rw-r--r--spec/tasks/gitlab/feature_categories_rake_spec.rb2
-rw-r--r--yarn.lock5
191 files changed, 2390 insertions, 647 deletions
diff --git a/README.md b/README.md
index 29d5d599972..b61af1b1f42 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,7 @@ Instructions on how to start GitLab and how to run the tests can be found in the
GitLab is a Ruby on Rails application that runs on the following software:
- Ubuntu/Debian/CentOS/RHEL/OpenSUSE
-- Ruby (MRI) 2.7.7
+- Ruby (MRI) 3.0.5
- Git 2.33+
- Redis 5.0+
- PostgreSQL 12+
diff --git a/app/assets/javascripts/blame/streaming/index.js b/app/assets/javascripts/blame/streaming/index.js
new file mode 100644
index 00000000000..a74e01b6423
--- /dev/null
+++ b/app/assets/javascripts/blame/streaming/index.js
@@ -0,0 +1,56 @@
+import { renderHtmlStreams } from '~/streaming/render_html_streams';
+import { handleStreamedAnchorLink } from '~/streaming/handle_streamed_anchor_link';
+import { createAlert } from '~/flash';
+import { __ } from '~/locale';
+import { rateLimitStreamRequests } from '~/streaming/rate_limit_stream_requests';
+import { toPolyfillReadable } from '~/streaming/polyfills';
+
+export async function renderBlamePageStreams(firstStreamPromise) {
+ const element = document.querySelector('#blame-stream-container');
+
+ if (!element || !firstStreamPromise) return;
+
+ const stopAnchorObserver = handleStreamedAnchorLink(element);
+ const { dataset } = document.querySelector('#blob-content-holder');
+ const totalExtraPages = parseInt(dataset.totalExtraPages, 10);
+ const { pagesUrl } = dataset;
+
+ const remainingStreams = rateLimitStreamRequests({
+ factory: (index) => {
+ const url = new URL(pagesUrl);
+ // page numbers start with 1
+ // the first page is already rendered in the document
+ // the second page is passed with the 'firstStreamPromise'
+ url.searchParams.set('page', index + 3);
+ return fetch(url).then((response) => toPolyfillReadable(response.body));
+ },
+ // we don't want to overload gitaly with concurrent requests
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/391842#note_1281695095
+ // using 5 as a good starting point
+ maxConcurrentRequests: 5,
+ total: totalExtraPages,
+ });
+
+ try {
+ await renderHtmlStreams(
+ [firstStreamPromise.then(toPolyfillReadable), ...remainingStreams],
+ element,
+ );
+ } catch (error) {
+ createAlert({
+ message: __('Blame could not be loaded as a single page.'),
+ primaryButton: {
+ text: __('View blame as separate pages'),
+ clickHandler() {
+ const newUrl = new URL(window.location);
+ newUrl.searchParams.delete('streaming');
+ window.location.href = newUrl;
+ },
+ },
+ });
+ throw error;
+ } finally {
+ stopAnchorObserver();
+ document.querySelector('#blame-stream-loading').remove();
+ }
+}
diff --git a/app/assets/javascripts/pages/projects/blame/show/index.js b/app/assets/javascripts/pages/projects/blame/show/index.js
index 1e4b9de90f2..f0fdd18c828 100644
--- a/app/assets/javascripts/pages/projects/blame/show/index.js
+++ b/app/assets/javascripts/pages/projects/blame/show/index.js
@@ -1,5 +1,10 @@
import initBlob from '~/pages/projects/init_blob';
import redirectToCorrectPage from '~/blame/blame_redirect';
+import { renderBlamePageStreams } from '~/blame/streaming';
-redirectToCorrectPage();
+if (new URLSearchParams(window.location.search).get('streaming')) {
+ renderBlamePageStreams(window.blamePageStream);
+} else {
+ redirectToCorrectPage();
+}
initBlob();
diff --git a/app/assets/javascripts/streaming/chunk_writer.js b/app/assets/javascripts/streaming/chunk_writer.js
new file mode 100644
index 00000000000..4bbd0a5f843
--- /dev/null
+++ b/app/assets/javascripts/streaming/chunk_writer.js
@@ -0,0 +1,144 @@
+import { throttle } from 'lodash';
+import { RenderBalancer } from '~/streaming/render_balancer';
+import {
+ BALANCE_RATE,
+ HIGH_FRAME_TIME,
+ LOW_FRAME_TIME,
+ MAX_CHUNK_SIZE,
+ MIN_CHUNK_SIZE,
+ TIMEOUT,
+} from '~/streaming/constants';
+
+const defaultConfig = {
+ balanceRate: BALANCE_RATE,
+ minChunkSize: MIN_CHUNK_SIZE,
+ maxChunkSize: MAX_CHUNK_SIZE,
+ lowFrameTime: LOW_FRAME_TIME,
+ highFrameTime: HIGH_FRAME_TIME,
+ timeout: TIMEOUT,
+};
+
+function concatUint8Arrays(a, b) {
+ const array = new Uint8Array(a.length + b.length);
+ array.set(a, 0);
+ array.set(b, a.length);
+ return array;
+}
+
+// This class is used to write chunks with a balanced size
+// to avoid blocking main thread for too long.
+//
+// A chunk can be:
+// 1. Too small
+// 2. Too large
+// 3. Delayed in time
+//
+// This class resolves all these problems by
+// 1. Splitting or concatenating chunks to met the size criteria
+// 2. Rendering current chunk buffer immediately if enough time has passed
+//
+// The size of the chunk is determined by RenderBalancer,
+// It measures execution time for each chunk write and adjusts next chunk size.
+export class ChunkWriter {
+ buffer = null;
+ decoder = new TextDecoder('utf-8');
+ timeout = null;
+
+ constructor(htmlStream, config) {
+ this.htmlStream = htmlStream;
+
+ const { balanceRate, minChunkSize, maxChunkSize, lowFrameTime, highFrameTime, timeout } = {
+ ...defaultConfig,
+ ...config,
+ };
+
+ // ensure we still render chunks over time if the size criteria is not met
+ this.scheduleAccumulatorFlush = throttle(this.flushAccumulator.bind(this), timeout);
+
+ const averageSize = Math.round((maxChunkSize + minChunkSize) / 2);
+ this.size = Math.max(averageSize, minChunkSize);
+
+ this.balancer = new RenderBalancer({
+ lowFrameTime,
+ highFrameTime,
+ decrease: () => {
+ this.size = Math.round(Math.max(this.size / balanceRate, minChunkSize));
+ },
+ increase: () => {
+ this.size = Math.round(Math.min(this.size * balanceRate, maxChunkSize));
+ },
+ });
+ }
+
+ write(chunk) {
+ this.scheduleAccumulatorFlush.cancel();
+
+ if (this.buffer) {
+ this.buffer = concatUint8Arrays(this.buffer, chunk);
+ } else {
+ this.buffer = chunk;
+ }
+
+ // accumulate chunks until the size is fulfilled
+ if (this.size > this.buffer.length) {
+ this.scheduleAccumulatorFlush();
+ return Promise.resolve();
+ }
+
+ return this.balancedWrite();
+ }
+
+ balancedWrite() {
+ let cursor = 0;
+
+ return this.balancer.render(() => {
+ const chunkPart = this.buffer.subarray(cursor, cursor + this.size);
+ // accumulate chunks until the size is fulfilled
+ // this is a hot path for the last chunkPart of the chunk
+ if (chunkPart.length < this.size) {
+ this.buffer = chunkPart;
+ this.scheduleAccumulatorFlush();
+ return false;
+ }
+
+ this.writeToDom(chunkPart);
+
+ cursor += this.size;
+ if (cursor >= this.buffer.length) {
+ this.buffer = null;
+ return false;
+ }
+ // continue render
+ return true;
+ });
+ }
+
+ writeToDom(chunk, stream = true) {
+ // stream: true allows us to split chunks with multi-part words
+ const decoded = this.decoder.decode(chunk, { stream });
+ this.htmlStream.write(decoded);
+ }
+
+ flushAccumulator() {
+ if (this.buffer) {
+ this.writeToDom(this.buffer);
+ this.buffer = null;
+ }
+ }
+
+ close() {
+ this.scheduleAccumulatorFlush.cancel();
+ if (this.buffer) {
+ // last chunk should have stream: false to indicate the end of the stream
+ this.writeToDom(this.buffer, false);
+ this.buffer = null;
+ }
+ this.htmlStream.close();
+ }
+
+ abort() {
+ this.scheduleAccumulatorFlush.cancel();
+ this.buffer = null;
+ this.htmlStream.abort();
+ }
+}
diff --git a/app/assets/javascripts/streaming/constants.js b/app/assets/javascripts/streaming/constants.js
new file mode 100644
index 00000000000..224d93a7ac1
--- /dev/null
+++ b/app/assets/javascripts/streaming/constants.js
@@ -0,0 +1,9 @@
+// Lower min chunk numbers can make the page loading take incredibly long
+export const MIN_CHUNK_SIZE = 128 * 1024;
+export const MAX_CHUNK_SIZE = 2048 * 1024;
+export const LOW_FRAME_TIME = 32;
+// Tasks that take more than 50ms are considered Long
+// https://web.dev/optimize-long-tasks/
+export const HIGH_FRAME_TIME = 64;
+export const BALANCE_RATE = 1.2;
+export const TIMEOUT = 500;
diff --git a/app/assets/javascripts/streaming/handle_streamed_anchor_link.js b/app/assets/javascripts/streaming/handle_streamed_anchor_link.js
new file mode 100644
index 00000000000..315dc9bb0a0
--- /dev/null
+++ b/app/assets/javascripts/streaming/handle_streamed_anchor_link.js
@@ -0,0 +1,26 @@
+import { throttle } from 'lodash';
+import { scrollToElement } from '~/lib/utils/common_utils';
+import LineHighlighter from '~/blob/line_highlighter';
+
+const noop = () => {};
+
+export function handleStreamedAnchorLink(rootElement) {
+ // "#L100-200" → ['L100', 'L200']
+ const [anchorStart, end] = window.location.hash.substring(1).split('-');
+ const anchorEnd = end ? `L${end}` : anchorStart;
+ if (!anchorStart || document.getElementById(anchorEnd)) return noop;
+
+ const handler = throttle((mutationList, instance) => {
+ if (!document.getElementById(anchorEnd)) return;
+ scrollToElement(document.getElementById(anchorStart));
+ // eslint-disable-next-line no-new
+ new LineHighlighter();
+ instance.disconnect();
+ }, 300);
+
+ const observer = new MutationObserver(handler);
+
+ observer.observe(rootElement, { childList: true, subtree: true });
+
+ return () => observer.disconnect();
+}
diff --git a/app/assets/javascripts/streaming/html_stream.js b/app/assets/javascripts/streaming/html_stream.js
new file mode 100644
index 00000000000..8182f69a607
--- /dev/null
+++ b/app/assets/javascripts/streaming/html_stream.js
@@ -0,0 +1,33 @@
+import { ChunkWriter } from '~/streaming/chunk_writer';
+
+export class HtmlStream {
+ constructor(element) {
+ const streamDocument = document.implementation.createHTMLDocument('stream');
+
+ streamDocument.open();
+ streamDocument.write('<streaming-element>');
+
+ const virtualStreamingElement = streamDocument.querySelector('streaming-element');
+ element.appendChild(document.adoptNode(virtualStreamingElement));
+
+ this.streamDocument = streamDocument;
+ }
+
+ withChunkWriter(config) {
+ return new ChunkWriter(this, config);
+ }
+
+ write(chunk) {
+ // eslint-disable-next-line no-unsanitized/method
+ this.streamDocument.write(chunk);
+ }
+
+ close() {
+ this.streamDocument.write('</streaming-element>');
+ this.streamDocument.close();
+ }
+
+ abort() {
+ this.streamDocument.close();
+ }
+}
diff --git a/app/assets/javascripts/streaming/polyfills.js b/app/assets/javascripts/streaming/polyfills.js
new file mode 100644
index 00000000000..a9a044a3e99
--- /dev/null
+++ b/app/assets/javascripts/streaming/polyfills.js
@@ -0,0 +1,5 @@
+import { createReadableStreamWrapper } from '@mattiasbuelens/web-streams-adapter';
+import { ReadableStream as PolyfillReadableStream } from 'web-streams-polyfill';
+
+// TODO: remove this when our WebStreams API reaches 100% support
+export const toPolyfillReadable = createReadableStreamWrapper(PolyfillReadableStream);
diff --git a/app/assets/javascripts/streaming/rate_limit_stream_requests.js b/app/assets/javascripts/streaming/rate_limit_stream_requests.js
new file mode 100644
index 00000000000..04a592baa16
--- /dev/null
+++ b/app/assets/javascripts/streaming/rate_limit_stream_requests.js
@@ -0,0 +1,87 @@
+const consumeReadableStream = (stream) => {
+ return new Promise((resolve, reject) => {
+ stream.pipeTo(
+ new WritableStream({
+ close: resolve,
+ abort: reject,
+ }),
+ );
+ });
+};
+
+const wait = (timeout) =>
+ new Promise((resolve) => {
+ setTimeout(resolve, timeout);
+ });
+
+// this rate-limiting approach is specific to Web Streams
+// because streams only resolve when they're fully consumed
+// so we need to split each stream into two pieces:
+// one for the rate-limiter (wait for all the bytes to be sent)
+// another for the original consumer
+export const rateLimitStreamRequests = ({
+ factory,
+ total,
+ maxConcurrentRequests,
+ immediateCount = maxConcurrentRequests,
+ timeout = 0,
+}) => {
+ if (total === 0) return [];
+
+ const unsettled = [];
+
+ const pushUnsettled = (promise) => {
+ let res;
+ let rej;
+ const consume = new Promise((resolve, reject) => {
+ res = resolve;
+ rej = reject;
+ });
+ unsettled.push(consume);
+ return promise.then((stream) => {
+ const [first, second] = stream.tee();
+ // eslint-disable-next-line promise/no-nesting
+ consumeReadableStream(first)
+ .then(() => {
+ unsettled.splice(unsettled.indexOf(consume), 1);
+ res();
+ })
+ .catch(rej);
+ return second;
+ }, rej);
+ };
+
+ const immediate = Array.from({ length: Math.min(immediateCount, total) }, (_, i) =>
+ pushUnsettled(factory(i)),
+ );
+
+ const queue = [];
+ const flushQueue = () => {
+ const promises =
+ unsettled.length > maxConcurrentRequests ? unsettled : [...unsettled, wait(timeout)];
+ // errors are handled by the caller
+ // eslint-disable-next-line promise/catch-or-return
+ Promise.race(promises).then(() => {
+ const cb = queue.shift();
+ cb?.();
+ if (queue.length !== 0) {
+ // wait for stream consumer promise to be removed from unsettled
+ queueMicrotask(flushQueue);
+ }
+ });
+ };
+
+ const throttled = Array.from({ length: total - immediateCount }, (_, i) => {
+ return new Promise((resolve, reject) => {
+ queue.push(() => {
+ pushUnsettled(factory(i + immediateCount))
+ .then(resolve)
+ .catch(reject);
+ });
+ });
+ });
+
+ flushQueue();
+
+ return [...immediate, ...throttled];
+};
diff --git a/app/assets/javascripts/streaming/render_balancer.js b/app/assets/javascripts/streaming/render_balancer.js
new file mode 100644
index 00000000000..66929ff3a54
--- /dev/null
+++ b/app/assets/javascripts/streaming/render_balancer.js
@@ -0,0 +1,36 @@
+export class RenderBalancer {
+ previousTimestamp = undefined;
+
+ constructor({ increase, decrease, highFrameTime, lowFrameTime }) {
+ this.increase = increase;
+ this.decrease = decrease;
+ this.highFrameTime = highFrameTime;
+ this.lowFrameTime = lowFrameTime;
+ }
+
+ render(fn) {
+ return new Promise((resolve) => {
+ const callback = (timestamp) => {
+ this.throttle(timestamp);
+ if (fn()) requestAnimationFrame(callback);
+ else resolve();
+ };
+ requestAnimationFrame(callback);
+ });
+ }
+
+ throttle(timestamp) {
+ const { previousTimestamp } = this;
+ this.previousTimestamp = timestamp;
+ if (previousTimestamp === undefined) return;
+
+ const duration = Math.round(timestamp - previousTimestamp);
+ if (!duration) return;
+
+ if (duration >= this.highFrameTime) {
+ this.decrease();
+ } else if (duration < this.lowFrameTime) {
+ this.increase();
+ }
+ }
+}
diff --git a/app/assets/javascripts/streaming/render_html_streams.js b/app/assets/javascripts/streaming/render_html_streams.js
new file mode 100644
index 00000000000..7201e541777
--- /dev/null
+++ b/app/assets/javascripts/streaming/render_html_streams.js
@@ -0,0 +1,40 @@
+import { HtmlStream } from '~/streaming/html_stream';
+
+async function pipeStreams(domWriter, streamPromises) {
+ try {
+ for await (const stream of streamPromises.slice(0, -1)) {
+ await stream.pipeTo(domWriter, { preventClose: true });
+ }
+ const stream = await streamPromises[streamPromises.length - 1];
+ await stream.pipeTo(domWriter);
+ } catch (error) {
+ domWriter.abort(error);
+ }
+}
+
+// this function (and the rest of the pipeline) expects polyfilled streams
+// do not pass native streams here unless our browser support allows for it
+// TODO: remove this notice when our WebStreams API support reaches 100%
+export function renderHtmlStreams(streamPromises, element, config) {
+ if (streamPromises.length === 0) return Promise.resolve();
+
+ const chunkedHtmlStream = new HtmlStream(element).withChunkWriter(config);
+
+ return new Promise((resolve, reject) => {
+ const domWriter = new WritableStream({
+ write(chunk) {
+ return chunkedHtmlStream.write(chunk);
+ },
+ close() {
+ chunkedHtmlStream.close();
+ resolve();
+ },
+ abort(error) {
+ chunkedHtmlStream.abort();
+ reject(error);
+ },
+ });
+
+ pipeStreams(domWriter, streamPromises);
+ });
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 9ea5a66b3bc..b292adf9eac 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -580,3 +580,31 @@ span.idiff {
padding: 0;
border-radius: 0 0 $border-radius-default $border-radius-default;
}
+
+.blame-stream-container {
+ border-top: 1px solid $border-color;
+}
+
+.blame-stream-loading {
+ $gradient-size: 16px;
+ position: sticky;
+ bottom: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: -$gradient-size;
+ height: $gl-spacing-scale-10;
+ border-top: $gradient-size solid transparent;
+ background-color: $white;
+ box-sizing: content-box;
+ background-clip: content-box;
+
+ .gradient {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: -$gradient-size;
+ height: $gradient-size;
+ background: linear-gradient(to top, $white, transparentize($white, 1));
+ }
+}
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index 91fc1bf489d..53d302f60ee 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -6,7 +6,7 @@ class Admin::ApplicationsController < Admin::ApplicationController
before_action :set_application, only: [:show, :edit, :update, :renew, :destroy]
before_action :load_scopes, only: [:new, :create, :edit, :update]
- feature_category :authentication_and_authorization
+ feature_category :system_access
def index
applications = ApplicationsFinder.new.execute
diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb
index dcec50e882d..0745ba328c6 100644
--- a/app/controllers/admin/identities_controller.rb
+++ b/app/controllers/admin/identities_controller.rb
@@ -4,7 +4,7 @@ class Admin::IdentitiesController < Admin::ApplicationController
before_action :user
before_action :identity, except: [:index, :new, :create]
- feature_category :authentication_and_authorization
+ feature_category :system_access
def new
@identity = Identity.new
diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb
index ddc555add5c..dae3337d19b 100644
--- a/app/controllers/admin/impersonation_tokens_controller.rb
+++ b/app/controllers/admin/impersonation_tokens_controller.rb
@@ -4,7 +4,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
before_action :user
before_action :verify_impersonation_enabled!
- feature_category :authentication_and_authorization
+ feature_category :user_management
def index
set_index_vars
diff --git a/app/controllers/admin/impersonations_controller.rb b/app/controllers/admin/impersonations_controller.rb
index 6c45b03455e..c1a6cb350ec 100644
--- a/app/controllers/admin/impersonations_controller.rb
+++ b/app/controllers/admin/impersonations_controller.rb
@@ -4,7 +4,7 @@ class Admin::ImpersonationsController < Admin::ApplicationController
skip_before_action :authenticate_admin!
before_action :authenticate_impersonator!
- feature_category :authentication_and_authorization
+ feature_category :user_management
def destroy
original_user = stop_impersonation
diff --git a/app/controllers/admin/keys_controller.rb b/app/controllers/admin/keys_controller.rb
index 03383604e30..e4a756ec12d 100644
--- a/app/controllers/admin/keys_controller.rb
+++ b/app/controllers/admin/keys_controller.rb
@@ -3,7 +3,7 @@
class Admin::KeysController < Admin::ApplicationController
before_action :user, only: [:show, :destroy]
- feature_category :authentication_and_authorization
+ feature_category :user_management
def show
@key = user.keys.find(params[:id])
diff --git a/app/controllers/admin/sessions_controller.rb b/app/controllers/admin/sessions_controller.rb
index 63579421573..bb275532170 100644
--- a/app/controllers/admin/sessions_controller.rb
+++ b/app/controllers/admin/sessions_controller.rb
@@ -7,7 +7,7 @@ class Admin::SessionsController < ApplicationController
before_action :user_is_admin!
- feature_category :authentication_and_authorization
+ feature_category :system_access
def new
if current_user_mode.admin_mode?
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index a0ba5b9c8a4..4d1cbd8becc 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -10,7 +10,7 @@ class ConfirmationsController < Devise::ConfirmationsController
prepend_before_action :check_recaptcha, only: :create
before_action :load_recaptcha, only: :new
- feature_category :authentication_and_authorization
+ feature_category :user_management
def almost_there
flash[:notice] = nil
diff --git a/app/controllers/groups/settings/access_tokens_controller.rb b/app/controllers/groups/settings/access_tokens_controller.rb
index d86ddcfe2d0..ff07e881bfa 100644
--- a/app/controllers/groups/settings/access_tokens_controller.rb
+++ b/app/controllers/groups/settings/access_tokens_controller.rb
@@ -7,7 +7,7 @@ module Groups
include AccessTokensActions
layout 'group_settings'
- feature_category :authentication_and_authorization
+ feature_category :system_access
alias_method :resource, :group
diff --git a/app/controllers/groups/settings/applications_controller.rb b/app/controllers/groups/settings/applications_controller.rb
index 6fb2b65feb8..b174ba9a6ad 100644
--- a/app/controllers/groups/settings/applications_controller.rb
+++ b/app/controllers/groups/settings/applications_controller.rb
@@ -9,7 +9,7 @@ module Groups
before_action :set_application, only: [:show, :edit, :update, :renew, :destroy]
before_action :load_scopes, only: [:index, :create, :edit, :update]
- feature_category :authentication_and_authorization
+ feature_category :system_access
def index
set_index_vars
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 0bee1faccf5..2729b11fcff 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -245,11 +245,7 @@ class Import::GithubController < Import::BaseController
{
before: params[:before].presence,
after: params[:after].presence,
- first: PAGE_LENGTH,
- # TODO: remove after rollout FF github_client_fetch_repos_via_graphql
- # https://gitlab.com/gitlab-org/gitlab/-/issues/385649
- page: [1, params[:page].to_i].max,
- per_page: PAGE_LENGTH
+ first: PAGE_LENGTH
}
end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 2a7f2d42e2a..0a2c98af8ec 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -13,7 +13,7 @@ class InvitesController < ApplicationController
respond_to :html
- feature_category :authentication_and_authorization
+ feature_category :system_access
def show
accept if skip_invitation_prompt?
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 7211eebdb4b..d299613f498 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -8,7 +8,7 @@ class JwtController < ApplicationController
# Add this before other actions, since we want to have the user or project
prepend_before_action :auth_user, :authenticate_project_or_user
- feature_category :authentication_and_authorization
+ feature_category :system_access
# https://gitlab.com/gitlab-org/gitlab/-/issues/357037
urgency :low
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 4046433f8ea..e450151fd82 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -12,7 +12,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
protect_from_forgery except: [:cas3, :failure] + AuthHelper.saml_providers, with: :exception, prepend: true
- feature_category :authentication_and_authorization
+ feature_category :system_access
def handle_omniauth
omniauth_flow(Gitlab::Auth::OAuth)
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index 38cdb16c350..38839497fb6 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -12,7 +12,7 @@ class PasswordsController < Devise::PasswordsController
before_action :check_password_authentication_available, only: [:create]
before_action :throttle_reset, only: [:create]
- feature_category :authentication_and_authorization
+ feature_category :system_access
# rubocop: disable CodeReuse/ActiveRecord
def edit
diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb
index cb8b2783000..eb64016379d 100644
--- a/app/controllers/profiles/accounts_controller.rb
+++ b/app/controllers/profiles/accounts_controller.rb
@@ -3,7 +3,7 @@
class Profiles::AccountsController < Profiles::ApplicationController
include AuthHelper
- feature_category :authentication_and_authorization
+ feature_category :system_access
urgency :low, [:show]
def show
diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/profiles/active_sessions_controller.rb
index 2607ba7d404..5a86179b89f 100644
--- a/app/controllers/profiles/active_sessions_controller.rb
+++ b/app/controllers/profiles/active_sessions_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Profiles::ActiveSessionsController < Profiles::ApplicationController
- feature_category :authentication_and_authorization
+ feature_category :system_access
def index
@sessions = ActiveSession.list(current_user).reject(&:is_impersonated)
diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb
index 738c41207d5..7a0dfbbba0d 100644
--- a/app/controllers/profiles/passwords_controller.rb
+++ b/app/controllers/profiles/passwords_controller.rb
@@ -11,7 +11,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController
layout :determine_layout
- feature_category :authentication_and_authorization
+ feature_category :system_access
def new
end
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 1663aa61f62..8d5c690fbfe 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -3,7 +3,7 @@
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
include RenderAccessTokens
- feature_category :authentication_and_authorization
+ feature_category :system_access
before_action :check_personal_access_tokens_enabled
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index aded295bfab..89151068696 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -12,7 +12,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
push_frontend_feature_flag(:webauthn)
end
- feature_category :authentication_and_authorization
+ feature_category :system_access
def show
setup_show_page
diff --git a/app/controllers/profiles/u2f_registrations_controller.rb b/app/controllers/profiles/u2f_registrations_controller.rb
index 32ca303e722..2ee0e9fe960 100644
--- a/app/controllers/profiles/u2f_registrations_controller.rb
+++ b/app/controllers/profiles/u2f_registrations_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Profiles::U2fRegistrationsController < Profiles::ApplicationController
- feature_category :authentication_and_authorization
+ feature_category :system_access
def destroy
u2f_registration = current_user.u2f_registrations.find(params[:id])
diff --git a/app/controllers/profiles/webauthn_registrations_controller.rb b/app/controllers/profiles/webauthn_registrations_controller.rb
index a4a6d84f1ae..345d7bdbca8 100644
--- a/app/controllers/profiles/webauthn_registrations_controller.rb
+++ b/app/controllers/profiles/webauthn_registrations_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Profiles::WebauthnRegistrationsController < Profiles::ApplicationController
- feature_category :authentication_and_authorization
+ feature_category :system_access
def destroy
webauthn_registration = current_user.webauthn_registrations.find(params[:id])
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 45b274fc920..70487915707 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -17,7 +17,7 @@ class ProfilesController < Profiles::ApplicationController
feature_category :user_profile, [:show, :update, :reset_incoming_email_token, :reset_feed_token,
:reset_static_object_token, :update_username]
- feature_category :authentication_and_authorization, [:audit_log]
+ feature_category :system_access, [:audit_log]
urgency :low, [:show, :update]
def show
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index cfff281604e..d41b347dc5a 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -23,13 +23,47 @@ class Projects::BlameController < Projects::ApplicationController
environment_params[:find_latest] = true
@environment = ::Environments::EnvironmentsByDeploymentsFinder.new(@project, current_user, environment_params).execute.last
- blame_service = Projects::BlameService.new(@blob, @commit, params.permit(:page, :no_pagination))
+ permitted_params = params.permit(:page, :no_pagination, :streaming)
+ blame_service = Projects::BlameService.new(@blob, @commit, permitted_params)
@blame = Gitlab::View::Presenter::Factory.new(blame_service.blame, project: @project, path: @path, page: blame_service.page).fabricate!
- @blame_pagination = blame_service.pagination
+ @entire_blame_path = full_blame_path(no_pagination: true)
+ @blame_pages_url = blame_pages_url(permitted_params)
+ if blame_service.streaming_possible
+ @entire_blame_path = full_blame_path(streaming: true)
+ end
+
+ @streaming_enabled = blame_service.streaming_enabled
+ @blame_pagination = blame_service.pagination unless @streaming_enabled
@blame_per_page = blame_service.per_page
+
+ render locals: { total_extra_pages: blame_service.total_extra_pages }
+ end
+
+ def page
+ @blob = @repository.blob_at(@commit.id, @path)
+
+ environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
+ environment_params[:find_latest] = true
+ @environment = ::Environments::EnvironmentsByDeploymentsFinder.new(@project, current_user, environment_params).execute.last
+
+ blame_service = Projects::BlameService.new(@blob, @commit, params.permit(:page, :streaming))
+
+ @blame = Gitlab::View::Presenter::Factory.new(blame_service.blame, project: @project, path: @path, page: blame_service.page).fabricate!
+
+ render partial: 'page'
+ end
+
+ private
+
+ def full_blame_path(params)
+ namespace_project_blame_path(namespace_id: @project.namespace, project_id: @project, id: @id, **params)
+ end
+
+ def blame_pages_url(params)
+ namespace_project_blame_page_url(namespace_id: @project.namespace, project_id: @project, id: @id, **params)
end
end
diff --git a/app/controllers/projects/settings/access_tokens_controller.rb b/app/controllers/projects/settings/access_tokens_controller.rb
index 0884816ef62..af1527ba6a3 100644
--- a/app/controllers/projects/settings/access_tokens_controller.rb
+++ b/app/controllers/projects/settings/access_tokens_controller.rb
@@ -7,7 +7,7 @@ module Projects
include AccessTokensActions
layout 'project_settings'
- feature_category :authentication_and_authorization
+ feature_category :system_access
alias_method :resource, :project
diff --git a/app/controllers/registrations/welcome_controller.rb b/app/controllers/registrations/welcome_controller.rb
index cfb4e939b35..78c00a81a93 100644
--- a/app/controllers/registrations/welcome_controller.rb
+++ b/app/controllers/registrations/welcome_controller.rb
@@ -10,7 +10,7 @@ module Registrations
skip_before_action :authenticate_user!, :required_signup_info, :check_two_factor_requirement, only: [:show, :update]
before_action :require_current_user
- feature_category :authentication_and_authorization
+ feature_category :user_management
def show
return redirect_to path_for_signed_in_user(current_user) if completed_welcome_step?
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 420ca6a2286..edc74dd71fc 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -27,7 +27,7 @@ class RegistrationsController < Devise::RegistrationsController
push_frontend_feature_flag(:gitlab_gtm_datalayer, type: :ops)
end
- feature_category :authentication_and_authorization
+ feature_category :user_management
def new
@resource = build_resource
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index b6aba04c877..83034e3faa6 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -56,7 +56,7 @@ class SessionsController < Devise::SessionsController
# token mismatch.
protect_from_forgery with: :exception, prepend: true, except: :destroy
- feature_category :authentication_and_authorization
+ feature_category :system_access
urgency :low
CAPTCHA_HEADER = 'X-GitLab-Show-Login-Captcha'
diff --git a/app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb
new file mode 100644
index 00000000000..be17601e7a2
--- /dev/null
+++ b/app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Analytics
+ module CycleAnalytics
+ class DeploymentCountResolver < BaseResolver
+ type Types::Analytics::CycleAnalytics::MetricType, null: true
+
+ argument :from, Types::TimeType,
+ required: true,
+ description: 'Deployments finished after the date.'
+
+ argument :to, Types::TimeType,
+ required: true,
+ description: 'Deployments finished before the date.'
+
+ def resolve(**args)
+ value = count(args)
+ {
+ value: value,
+ title: n_('Deploy', 'Deploys', value.to_i),
+ identifier: 'deploys',
+ links: []
+ }
+ end
+
+ private
+
+ def count(args)
+ finder = DeploymentsFinder.new({
+ finished_after: args[:from],
+ finished_before: args[:to],
+ project: object.project,
+ status: :success,
+ order_by: :finished_at
+ })
+
+ finder.execute.count
+ end
+
+ # :project level: no customization, returning the original resolver
+ # :group level: add the project_ids argument
+ def self.[](context = :project)
+ case context
+ when :project
+ self
+ when :group
+ Class.new(self) do
+ argument :project_ids, [GraphQL::Types::ID],
+ required: false,
+ description: 'Project IDs within the group hierarchy.'
+ end
+
+ end
+ end
+ end
+ end
+ end
+end
+
+mod = Resolvers::Analytics::CycleAnalytics::DeploymentCountResolver
+mod.prepend_mod_with('Resolvers::Analytics::CycleAnalytics::DeploymentCountResolver')
diff --git a/app/graphql/types/analytics/cycle_analytics/flow_metrics.rb b/app/graphql/types/analytics/cycle_analytics/flow_metrics.rb
index d320cd6cfc6..2645a86a9f8 100644
--- a/app/graphql/types/analytics/cycle_analytics/flow_metrics.rb
+++ b/app/graphql/types/analytics/cycle_analytics/flow_metrics.rb
@@ -14,6 +14,11 @@ module Types
null: true,
description: 'Number of issues opened in the given period.',
resolver: Resolvers::Analytics::CycleAnalytics::IssueCountResolver[context]
+ field :deployment_count,
+ Types::Analytics::CycleAnalytics::MetricType,
+ null: true,
+ description: 'Number of production deployments in the given period.',
+ resolver: Resolvers::Analytics::CycleAnalytics::DeploymentCountResolver[context]
end
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index a11e2c00cf8..ba6e760fd2b 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -85,7 +85,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
condition(:crm_enabled, score: 0, scope: :subject) { @subject.crm_enabled? }
condition(:create_runner_workflow_enabled) do
- Feature.enabled?(:create_runner_workflow_for_admin)
+ Feature.enabled?(:create_runner_workflow_for_namespace, group)
end
condition(:achievements_enabled, scope: :subject) do
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index cab3dff0400..fbe0f9bfeea 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -235,7 +235,7 @@ class ProjectPolicy < BasePolicy
end
condition(:create_runner_workflow_enabled) do
- Feature.enabled?(:create_runner_workflow_for_admin)
+ Feature.enabled?(:create_runner_workflow_for_namespace, project.namespace)
end
# `:read_project` may be prevented in EE, but `:read_project_for_iids` should
diff --git a/app/services/projects/blame_service.rb b/app/services/projects/blame_service.rb
index 58e146e5a32..1ea16040655 100644
--- a/app/services/projects/blame_service.rb
+++ b/app/services/projects/blame_service.rb
@@ -5,15 +5,19 @@
module Projects
class BlameService
PER_PAGE = 1000
+ STREAMING_FIRST_PAGE_SIZE = 200
+ STREAMING_PER_PAGE = 2000
def initialize(blob, commit, params)
@blob = blob
@commit = commit
- @page = extract_page(params)
+ @streaming_enabled = streaming_state(params)
@pagination_enabled = pagination_state(params)
+ @page = extract_page(params)
+ @params = params
end
- attr_reader :page
+ attr_reader :page, :streaming_enabled
def blame
Gitlab::Blame.new(blob, commit, range: blame_range)
@@ -28,7 +32,22 @@ module Projects
end
def per_page
- PER_PAGE
+ streaming_enabled ? STREAMING_PER_PAGE : PER_PAGE
+ end
+
+ def total_pages
+ total = (blob_lines_count.to_f / per_page).ceil
+ return total unless streaming_enabled
+
+ ([blob_lines_count - STREAMING_FIRST_PAGE_SIZE, 0].max.to_f / per_page).ceil + 1
+ end
+
+ def total_extra_pages
+ [total_pages - 1, 0].max
+ end
+
+ def streaming_possible
+ Feature.enabled?(:blame_page_streaming, commit.project)
end
private
@@ -36,9 +55,16 @@ module Projects
attr_reader :blob, :commit, :pagination_enabled
def blame_range
- return unless pagination_enabled
+ return unless pagination_enabled || streaming_enabled
first_line = (page - 1) * per_page + 1
+
+ if streaming_enabled
+ return 1..STREAMING_FIRST_PAGE_SIZE if page == 1
+
+ first_line = STREAMING_FIRST_PAGE_SIZE + (page - 2) * per_page + 1
+ end
+
last_line = (first_line + per_page).to_i - 1
first_line..last_line
@@ -52,6 +78,12 @@ module Projects
page
end
+ def streaming_state(params)
+ return false unless streaming_possible
+
+ Gitlab::Utils.to_boolean(params[:streaming], default: false)
+ end
+
def pagination_state(params)
return false if Gitlab::Utils.to_boolean(params[:no_pagination], default: false)
@@ -59,7 +91,7 @@ module Projects
end
def overlimit?(page)
- page * per_page >= blob_lines_count + per_page
+ page > total_pages
end
def blob_lines_count
diff --git a/app/services/protected_branches/base_service.rb b/app/services/protected_branches/base_service.rb
index 951017b2d01..6906ab2b642 100644
--- a/app/services/protected_branches/base_service.rb
+++ b/app/services/protected_branches/base_service.rb
@@ -21,3 +21,5 @@ module ProtectedBranches
end
end
end
+
+ProtectedBranches::BaseService.prepend_mod
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index dd441d0d155..f0c1b090140 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -10,6 +10,7 @@
%meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' }
= render 'layouts/startup_js'
+ = yield :startup_js
- if page_canonical_link
%link{ rel: 'canonical', href: page_canonical_link }
diff --git a/app/views/layouts/component_preview.html.haml b/app/views/layouts/component_preview.html.haml
index a1b1304f994..8217ac13c52 100644
--- a/app/views/layouts/component_preview.html.haml
+++ b/app/views/layouts/component_preview.html.haml
@@ -1,12 +1,12 @@
%head
- - if params[:lookbook][:display][:theme] == 'light'
+ - if params[:lookbook][:display][:theme] == "light"
= stylesheet_link_tag "application"
= stylesheet_link_tag "application_utilities"
- else
= stylesheet_link_tag "application_dark"
= stylesheet_link_tag "application_utilities_dark"
%body
- .container.gl-mt-6
+ .gl-mt-6{ class: (params[:lookbook][:display][:layout] == "fluid" ? "container-fluid" : "container") }
- if params[:lookbook][:display][:bg_dark]
.bg-dark.rounded.shadow.p-4
= yield
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 74b85a93c8e..ee7ca9cd351 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -1,6 +1,15 @@
- page_title _("Blame"), @blob.path, @ref
- add_page_specific_style 'page_bundles/tree'
-- dataset = { testid: 'blob-content-holder', qa_selector: 'blame_file_content', per_page: @blame_per_page }
+- if @streaming_enabled && total_extra_pages > 0
+ - content_for :startup_js do
+ = javascript_tag do
+ :plain
+ window.blamePageStream = (() => {
+ const url = new URL("#{@blame_pages_url}");
+ url.searchParams.set('page', 2);
+ return fetch(url).then(response => response.body);
+ })();
+- dataset = { testid: 'blob-content-holder', qa_selector: 'blame_file_content', per_page: @blame_per_page, total_extra_pages: total_extra_pages - 1, pages_url: @blame_pages_url }
#blob-content-holder.tree-holder.js-per-page{ data: dataset }
= render "projects/blob/breadcrumb", blob: @blob, blame: true
@@ -26,11 +35,21 @@
.blame-table-wrapper
= render partial: 'page'
+ - if @streaming_enabled
+ #blame-stream-container.blame-stream-container
+
- if @blame_pagination && @blame_pagination.total_pages > 1
.gl-display-flex.gl-justify-content-center.gl-flex-direction-column.gl-align-items-center.gl-p-3.gl-bg-gray-50.gl-border-t-solid.gl-border-t-1.gl-border-gray-100
- = _('For faster browsing, not all history is shown.')
- = render Pajamas::ButtonComponent.new(href: namespace_project_blame_path(namespace_id: @project.namespace, project_id: @project, id: @id, no_pagination: true), size: :small, button_options: { class: 'gl-mt-3' }) do |c|
- = _('View entire blame')
+ = render Pajamas::ButtonComponent.new(href: @entire_blame_path, size: :small, button_options: { class: 'gl-mt-3' }) do |c|
+ = _('Show full blame')
+
+ - if @streaming_enabled
+ #blame-stream-loading.blame-stream-loading
+ .gradient
+ = gl_loading_icon(size: 'sm')
+ %span.gl-mx-2
+ = _('Loading full blame...')
- if @blame_pagination
= paginate(@blame_pagination, theme: "gitlab")
+
diff --git a/app/views/users/_profile_basic_info.html.haml b/app/views/users/_profile_basic_info.html.haml
index b62440fcbde..c916b6c3d45 100644
--- a/app/views/users/_profile_basic_info.html.haml
+++ b/app/views/users/_profile_basic_info.html.haml
@@ -1,9 +1,9 @@
-.gl-text-gray-900.gl-mt-4
- = render 'middle_dot_divider' do
+.gl-text-gray-900
+ = render 'middle_dot_divider', stacking: true do
@#{@user.username}
- if can?(current_user, :read_user_profile, @user)
- = render 'middle_dot_divider' do
+ = render 'middle_dot_divider', stacking: true do
= s_('UserProfile|User ID: %{id}') % { id: @user.id }
= clipboard_button(title: s_('UserProfile|Copy user ID'), text: @user.id)
- = render 'middle_dot_divider' do
+ = render 'middle_dot_divider', stacking: true do
= s_('Member since %{date}') % { date: @user.created_at.to_date.to_s(:long) }
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index dd84b303655..29fd894cebf 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -43,80 +43,82 @@
= _('Follow')
.profile-header{ class: [('with-no-profile-tabs' if profile_tabs.empty?)] }
- .avatar-holder
- = link_to avatar_icon_for_user(@user, 400, current_user: current_user), target: '_blank', rel: 'noopener noreferrer' do
- = render Pajamas::AvatarComponent.new(@user, alt: "", size: 96, avatar_options: { itemprop: "image" })
-
- - if @user.blocked? || !@user.confirmed?
- .user-info
- %h1.cover-title
- = user_display_name(@user)
- = render "users/profile_basic_info"
- - else
- .user-info
- %h1.cover-title{ itemprop: 'name' }
- = @user.name
- - if @user.pronouns.present?
- %span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle
- = "(#{@user.pronouns})"
- - if @user.status&.busy?
- %span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle= s_("UserProfile|(Busy)")
-
- - if @user.pronunciation.present?
- .gl-align-items-center
- %p.gl-mb-4.gl-text-gray-500.gl-max-w-80.gl-mx-auto= s_("UserProfile|Pronounced as: %{pronunciation}") % { pronunciation: @user.pronunciation }
-
- - if @user.status&.customized?
- .cover-status.gl-display-inline-flex.gl-align-items-center
- = emoji_icon(@user.status.emoji, class: 'gl-mr-2')
- = markdown_field(@user.status, :message)
+ .gl-display-inline-block.gl-mx-8.gl-vertical-align-top
+ .avatar-holder
+ = link_to avatar_icon_for_user(@user, 400, current_user: current_user), target: '_blank', rel: 'noopener noreferrer' do
+ = render Pajamas::AvatarComponent.new(@user, alt: "", size: 96, avatar_options: { itemprop: "image" })
+ .gl-display-inline-block.gl-vertical-align-top.gl-text-left
+ - if @user.blocked? || !@user.confirmed?
+ .user-info
+ %h1.cover-title.gl-my-0
+ = user_display_name(@user)
= render "users/profile_basic_info"
- .gl-text-gray-900.mb-1.mb-sm-2
- - unless @user.location.blank?
- = render 'middle_dot_divider', stacking: true, itemprop: 'address', itemscope: true, itemtype: 'https://schema.org/PostalAddress' do
- = sprite_icon('location', css_class: 'fgray')
- %span{ itemprop: 'addressLocality' }
- = @user.location
+ - else
+ .user-info
+ %h1.cover-title.gl-my-0{ itemprop: 'name' }
+ = @user.name
+ - if @user.pronouns.present?
+ %span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle
+ = "(#{@user.pronouns})"
+ - if @user.status&.busy?
+ %span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle= s_("UserProfile|(Busy)")
+
+ - if @user.pronunciation.present?
+ .gl-align-items-center
+ %p.gl-mb-4.gl-text-gray-500.gl-max-w-80.gl-mx-auto= s_("UserProfile|Pronounced as: %{pronunciation}") % { pronunciation: @user.pronunciation }
+
+ - if @user.status&.customized?
+ .cover-status.gl-display-inline-flex.gl-align-items-center.gl-mb-3
+ = emoji_icon(@user.status.emoji, class: 'gl-mr-2')
+ = markdown_field(@user.status, :message)
+ = render "users/profile_basic_info"
- user_local_time = local_time(@user.timezone)
- - unless user_local_time.nil?
- = render 'middle_dot_divider', stacking: true, data: { testid: 'user-local-time' } do
- = sprite_icon('clock', css_class: 'fgray')
- %span
- = user_local_time
- - unless work_information(@user).blank?
- = render 'middle_dot_divider', stacking: true do
- = sprite_icon('work', css_class: 'fgray')
- %span
- = work_information(@user, with_schema_markup: true)
- .gl-text-gray-900
- - unless @user.skype.blank?
- = render 'middle_dot_divider' do
- = link_to "skype:#{@user.skype}", class: 'gl-hover-text-decoration-none', title: "Skype" do
- = sprite_icon('skype', css_class: 'skype-icon')
- - unless @user.linkedin.blank?
- = render 'middle_dot_divider' do
- = link_to linkedin_url(@user), class: 'gl-hover-text-decoration-none', title: "LinkedIn", target: '_blank', rel: 'noopener noreferrer nofollow' do
- = sprite_icon('linkedin', css_class: 'linkedin-icon')
- - unless @user.twitter.blank?
- = render 'middle_dot_divider', breakpoint: 'sm' do
- = link_to twitter_url(@user), class: 'gl-hover-text-decoration-none', title: "Twitter", target: '_blank', rel: 'noopener noreferrer nofollow' do
- = sprite_icon('twitter', css_class: 'twitter-icon')
- - unless @user.discord.blank?
- = render 'middle_dot_divider', breakpoint: 'sm' do
- = link_to discord_url(@user), class: 'gl-hover-text-decoration-none', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow' do
- = sprite_icon('discord', css_class: 'discord-icon')
- - unless @user.website_url.blank?
- = render 'middle_dot_divider', stacking: true do
- - if Feature.enabled?(:security_auto_fix) && @user.bot?
- = sprite_icon('question', css_class: 'gl-text-blue-600')
- = link_to @user.short_website_url, @user.full_website_url, target: '_blank', rel: 'me noopener noreferrer nofollow', itemprop: 'url'
- - if display_public_email?(@user)
- = render 'middle_dot_divider', stacking: true do
- = link_to @user.public_email, "mailto:#{@user.public_email}", itemprop: 'email'
- - if @user.bio.present?
- .gl-text-gray-900
- .profile-user-bio
- = @user.bio
+ - if @user.location.present? || user_local_time.present? || work_information(@user).present?
+ .gl-text-gray-900
+ - if @user.location.present?
+ = render 'middle_dot_divider', stacking: true, itemprop: 'address', itemscope: true, itemtype: 'https://schema.org/PostalAddress' do
+ = sprite_icon('location', css_class: 'fgray')
+ %span{ itemprop: 'addressLocality' }
+ = @user.location
+ - if user_local_time.present?
+ = render 'middle_dot_divider', stacking: true, data: { testid: 'user-local-time' } do
+ = sprite_icon('clock', css_class: 'fgray')
+ %span
+ = user_local_time
+ - if work_information(@user).present?
+ = render 'middle_dot_divider', stacking: true do
+ = sprite_icon('work', css_class: 'fgray')
+ %span
+ = work_information(@user, with_schema_markup: true)
+ .gl-text-gray-900
+ - if @user.skype.present?
+ = render 'middle_dot_divider' do
+ = link_to "skype:#{@user.skype}", class: 'gl-hover-text-decoration-none', title: "Skype" do
+ = sprite_icon('skype', css_class: 'skype-icon')
+ - if @user.linkedin.present?
+ = render 'middle_dot_divider' do
+ = link_to linkedin_url(@user), class: 'gl-hover-text-decoration-none', title: "LinkedIn", target: '_blank', rel: 'noopener noreferrer nofollow' do
+ = sprite_icon('linkedin', css_class: 'linkedin-icon')
+ - if @user.twitter.present?
+ = render 'middle_dot_divider', breakpoint: 'sm' do
+ = link_to twitter_url(@user), class: 'gl-hover-text-decoration-none', title: "Twitter", target: '_blank', rel: 'noopener noreferrer nofollow' do
+ = sprite_icon('twitter', css_class: 'twitter-icon')
+ - if @user.discord.present?
+ = render 'middle_dot_divider', breakpoint: 'sm' do
+ = link_to discord_url(@user), class: 'gl-hover-text-decoration-none', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow' do
+ = sprite_icon('discord', css_class: 'discord-icon')
+ - if @user.website_url.present?
+ = render 'middle_dot_divider', stacking: true do
+ - if Feature.enabled?(:security_auto_fix) && @user.bot?
+ = sprite_icon('question', css_class: 'gl-text-blue-600')
+ = link_to @user.short_website_url, @user.full_website_url, target: '_blank', rel: 'me noopener noreferrer nofollow', itemprop: 'url'
+ - if display_public_email?(@user)
+ = render 'middle_dot_divider', stacking: true do
+ = link_to @user.public_email, "mailto:#{@user.public_email}", itemprop: 'email'
+ - if @user.bio.present? && @user.confirmed? && !@user.blocked?
+ .gl-text-gray-900.gl-mx-5
+ .profile-user-bio.gl-text-left
+ = @user.bio
- if !profile_tabs.empty? && !Feature.enabled?(:profile_tabs_vue, current_user)
.scrolling-tabs-container
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index fbb348811e0..16930548124 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -5,7 +5,7 @@
---
- :name: authorized_project_update:authorized_project_update_project_recalculate
:worker_name: AuthorizedProjectUpdate::ProjectRecalculateWorker
- :feature_category: :authentication_and_authorization
+ :feature_category: :system_access
:has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
@@ -14,7 +14,7 @@
:tags: []
- :name: authorized_project_update:authorized_project_update_project_recalculate_per_user
:worker_name: AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker
- :feature_category: :authentication_and_authorization
+ :feature_category: :system_access
:has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
@@ -23,7 +23,7 @@
:tags: []
- :name: authorized_project_update:authorized_project_update_user_refresh_from_replica
:worker_name: AuthorizedProjectUpdate::UserRefreshFromReplicaWorker
- :feature_category: :authentication_and_authorization
+ :feature_category: :system_access
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
@@ -32,7 +32,7 @@
:tags: []
- :name: authorized_project_update:authorized_project_update_user_refresh_over_user_range
:worker_name: AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker
- :feature_category: :authentication_and_authorization
+ :feature_category: :system_access
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
@@ -41,7 +41,7 @@
:tags: []
- :name: authorized_project_update:authorized_project_update_user_refresh_with_low_urgency
:worker_name: AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker
- :feature_category: :authentication_and_authorization
+ :feature_category: :system_access
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
@@ -617,7 +617,7 @@
:tags: []
- :name: cronjob:personal_access_tokens_expired_notification
:worker_name: PersonalAccessTokens::ExpiredNotificationWorker
- :feature_category: :authentication_and_authorization
+ :feature_category: :system_access
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
@@ -626,7 +626,7 @@
:tags: []
- :name: cronjob:personal_access_tokens_expiring
:worker_name: PersonalAccessTokens::ExpiringWorker
- :feature_category: :authentication_and_authorization
+ :feature_category: :system_access
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
@@ -680,7 +680,7 @@
:tags: []
- :name: cronjob:remove_expired_group_links
:worker_name: RemoveExpiredGroupLinksWorker
- :feature_category: :authentication_and_authorization
+ :feature_category: :system_access
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
@@ -689,7 +689,7 @@
:tags: []
- :name: cronjob:remove_expired_members
:worker_name: RemoveExpiredMembersWorker
- :feature_category: :authentication_and_authorization
+ :feature_category: :system_access
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :cpu
@@ -698,7 +698,7 @@
:tags: []
- :name: cronjob:remove_unaccepted_member_invites
:worker_name: RemoveUnacceptedMemberInvitesWorker
- :feature_category: :authentication_and_authorization
+ :feature_category: :system_access
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
@@ -2165,7 +2165,7 @@
:tags: []
- :name: unassign_issuables:members_destroyer_unassign_issuables
:worker_name: MembersDestroyer::UnassignIssuablesWorker
- :feature_category: :authentication_and_authorization
+ :feature_category: :user_management
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
@@ -2219,7 +2219,7 @@
:tags: []
- :name: authorized_projects
:worker_name: AuthorizedProjectsWorker
- :feature_category: :authentication_and_authorization
+ :feature_category: :system_access
:has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
@@ -2417,7 +2417,7 @@
:tags: []
- :name: delete_user
:worker_name: DeleteUserWorker
- :feature_category: :authentication_and_authorization
+ :feature_category: :user_management
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
@@ -2642,7 +2642,7 @@
:tags: []
- :name: groups_update_two_factor_requirement_for_members
:worker_name: Groups::UpdateTwoFactorRequirementForMembersWorker
- :feature_category: :authentication_and_authorization
+ :feature_category: :system_access
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
diff --git a/app/workers/authorized_project_update/project_recalculate_per_user_worker.rb b/app/workers/authorized_project_update/project_recalculate_per_user_worker.rb
index 352c82e5021..96647cc671c 100644
--- a/app/workers/authorized_project_update/project_recalculate_per_user_worker.rb
+++ b/app/workers/authorized_project_update/project_recalculate_per_user_worker.rb
@@ -4,7 +4,7 @@ module AuthorizedProjectUpdate
class ProjectRecalculatePerUserWorker < ProjectRecalculateWorker
data_consistency :always
- feature_category :authentication_and_authorization
+ feature_category :system_access
urgency :high
queue_namespace :authorized_project_update
diff --git a/app/workers/authorized_project_update/project_recalculate_worker.rb b/app/workers/authorized_project_update/project_recalculate_worker.rb
index 1b5faee0b6f..cbf068f0b85 100644
--- a/app/workers/authorized_project_update/project_recalculate_worker.rb
+++ b/app/workers/authorized_project_update/project_recalculate_worker.rb
@@ -9,7 +9,7 @@ module AuthorizedProjectUpdate
prepend WaitableWorker
- feature_category :authentication_and_authorization
+ feature_category :system_access
urgency :high
queue_namespace :authorized_project_update
diff --git a/app/workers/authorized_project_update/user_refresh_from_replica_worker.rb b/app/workers/authorized_project_update/user_refresh_from_replica_worker.rb
index daebb23baae..cdc0a097c92 100644
--- a/app/workers/authorized_project_update/user_refresh_from_replica_worker.rb
+++ b/app/workers/authorized_project_update/user_refresh_from_replica_worker.rb
@@ -5,7 +5,7 @@ module AuthorizedProjectUpdate
include ApplicationWorker
sidekiq_options retry: 3
- feature_category :authentication_and_authorization
+ feature_category :system_access
urgency :low
data_consistency :always
queue_namespace :authorized_project_update
diff --git a/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb b/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb
index 8452f2a7821..ae243a94d3d 100644
--- a/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb
+++ b/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb
@@ -16,7 +16,7 @@ module AuthorizedProjectUpdate
sidekiq_options retry: 3
- feature_category :authentication_and_authorization
+ feature_category :system_access
urgency :low
queue_namespace :authorized_project_update
diff --git a/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb b/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb
index 7ca59a72adf..d6b41ba949c 100644
--- a/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb
+++ b/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb
@@ -2,7 +2,7 @@
module AuthorizedProjectUpdate
class UserRefreshWithLowUrgencyWorker < ::AuthorizedProjectsWorker
- feature_category :authentication_and_authorization
+ feature_category :system_access
urgency :low
queue_namespace :authorized_project_update
deduplicate :until_executing, including_scheduled: true
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index 4312ba41367..b553a2cd14e 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -8,7 +8,7 @@ class AuthorizedProjectsWorker
sidekiq_options retry: 3
prepend WaitableWorker
- feature_category :authentication_and_authorization
+ feature_category :system_access
urgency :high
weight 2
idempotent!
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
index 0af084caf86..bca156ff84c 100644
--- a/app/workers/delete_user_worker.rb
+++ b/app/workers/delete_user_worker.rb
@@ -7,7 +7,7 @@ class DeleteUserWorker # rubocop:disable Scalability/IdempotentWorker
sidekiq_options retry: 3
- feature_category :authentication_and_authorization
+ feature_category :user_management
loggable_arguments 2
def perform(current_user_id, delete_user_id, options = {})
diff --git a/app/workers/groups/update_two_factor_requirement_for_members_worker.rb b/app/workers/groups/update_two_factor_requirement_for_members_worker.rb
index ac1d3589516..ca68a82ec66 100644
--- a/app/workers/groups/update_two_factor_requirement_for_members_worker.rb
+++ b/app/workers/groups/update_two_factor_requirement_for_members_worker.rb
@@ -9,7 +9,7 @@ module Groups
idempotent!
- feature_category :authentication_and_authorization
+ feature_category :system_access
def perform(group_id)
group = Group.find_by_id(group_id)
diff --git a/app/workers/members_destroyer/unassign_issuables_worker.rb b/app/workers/members_destroyer/unassign_issuables_worker.rb
index 915551d6e30..2e6ce0005fc 100644
--- a/app/workers/members_destroyer/unassign_issuables_worker.rb
+++ b/app/workers/members_destroyer/unassign_issuables_worker.rb
@@ -11,7 +11,7 @@ module MembersDestroyer
ENTITY_TYPES = %w(Group Project).freeze
queue_namespace :unassign_issuables
- feature_category :authentication_and_authorization
+ feature_category :user_management
idempotent!
diff --git a/app/workers/personal_access_tokens/expired_notification_worker.rb b/app/workers/personal_access_tokens/expired_notification_worker.rb
index b119957fa2c..f86bd604cdf 100644
--- a/app/workers/personal_access_tokens/expired_notification_worker.rb
+++ b/app/workers/personal_access_tokens/expired_notification_worker.rb
@@ -8,7 +8,7 @@ module PersonalAccessTokens
include CronjobQueue
- feature_category :authentication_and_authorization
+ feature_category :system_access
MAX_TOKENS = 100
diff --git a/app/workers/personal_access_tokens/expiring_worker.rb b/app/workers/personal_access_tokens/expiring_worker.rb
index f4afa9f8994..de0bda82573 100644
--- a/app/workers/personal_access_tokens/expiring_worker.rb
+++ b/app/workers/personal_access_tokens/expiring_worker.rb
@@ -8,7 +8,7 @@ module PersonalAccessTokens
include CronjobQueue
- feature_category :authentication_and_authorization
+ feature_category :system_access
MAX_TOKENS = 100
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
index 37298c53a5c..f1da5f37945 100644
--- a/app/workers/remove_expired_group_links_worker.rb
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -7,7 +7,7 @@ class RemoveExpiredGroupLinksWorker # rubocop:disable Scalability/IdempotentWork
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
- feature_category :authentication_and_authorization
+ feature_category :system_access
def perform
ProjectGroupLink.expired.find_each do |link|
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
index c9eb715a522..b5031f4cda6 100644
--- a/app/workers/remove_expired_members_worker.rb
+++ b/app/workers/remove_expired_members_worker.rb
@@ -7,7 +7,7 @@ class RemoveExpiredMembersWorker # rubocop:disable Scalability/IdempotentWorker
include CronjobQueue
- feature_category :authentication_and_authorization
+ feature_category :system_access
worker_resource_boundary :cpu
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/workers/remove_unaccepted_member_invites_worker.rb b/app/workers/remove_unaccepted_member_invites_worker.rb
index 7fe45b26094..96f60b5fa12 100644
--- a/app/workers/remove_unaccepted_member_invites_worker.rb
+++ b/app/workers/remove_unaccepted_member_invites_worker.rb
@@ -7,7 +7,7 @@ class RemoveUnacceptedMemberInvitesWorker # rubocop:disable Scalability/Idempote
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
- feature_category :authentication_and_authorization
+ feature_category :system_access
urgency :low
idempotent!
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 6b44af3b658..91184004f80 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -47,6 +47,7 @@ Rails.application.configure do
config.lookbook.page_paths = ["#{config.root}/spec/components/docs"]
config.lookbook.preview_params_options_eval = true
config.lookbook.preview_display_options = {
+ layout: %w[fixed fluid],
theme: ["light", "dark (alpha)"]
}
diff --git a/config/events/1666038724_Gitlab__Tracking__Helpers__WeakPasswordErrorEvent_track_weak_password_error.yml b/config/events/1666038724_Gitlab__Tracking__Helpers__WeakPasswordErrorEvent_track_weak_password_error.yml
index d19db52074b..4fc127ebfb1 100644
--- a/config/events/1666038724_Gitlab__Tracking__Helpers__WeakPasswordErrorEvent_track_weak_password_error.yml
+++ b/config/events/1666038724_Gitlab__Tracking__Helpers__WeakPasswordErrorEvent_track_weak_password_error.yml
@@ -16,7 +16,7 @@ identifiers:
product_section: dev
product_stage: manage
product_group: group::authentication and authorization
-product_category: authentication_and_authorization
+product_category: system_access
milestone: "15.6"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/100237
distributions:
diff --git a/config/feature_categories.yml b/config/feature_categories.yml
index 3a75ba4f2ab..1a4d6e59f2d 100644
--- a/config/feature_categories.yml
+++ b/config/feature_categories.yml
@@ -15,7 +15,6 @@
- application_performance
- attack_emulation
- audit_events
-- authentication_and_authorization
- auto_devops
- backup_restore
- billing_and_payments
@@ -36,7 +35,6 @@
- continuous_delivery
- continuous_integration
- continuous_verification
-- credential_management
- customersdot_application
- database
- dataops
@@ -95,7 +93,6 @@
- onboarding
- package_registry
- pages
-- permissions
- pipeline_composition
- planning_analytics
- pods
diff --git a/config/feature_flags/development/github_client_fetch_repos_via_graphql.yml b/config/feature_flags/development/blame_page_streaming.yml
index 2d045e8ca06..44d64800dab 100644
--- a/config/feature_flags/development/github_client_fetch_repos_via_graphql.yml
+++ b/config/feature_flags/development/blame_page_streaming.yml
@@ -1,8 +1,8 @@
---
-name: github_client_fetch_repos_via_graphql
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105824
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/385649
-milestone: '15.7'
+name: blame_page_streaming
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110208
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/392890
+milestone: '15.10'
type: development
-group: group::import
-default_enabled: true
+group: group::source code
+default_enabled: false
diff --git a/config/feature_flags/development/create_runner_workflow_for_namespace.yml b/config/feature_flags/development/create_runner_workflow_for_namespace.yml
new file mode 100644
index 00000000000..783bb9803a1
--- /dev/null
+++ b/config/feature_flags/development/create_runner_workflow_for_namespace.yml
@@ -0,0 +1,8 @@
+---
+name: create_runner_workflow_for_namespace
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113535
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/393919
+milestone: '15.10'
+type: development
+group: group::runner
+default_enabled: false
diff --git a/config/metrics/counts_28d/20210216183627_omniauth_providers.yml b/config/metrics/counts_28d/20210216183627_omniauth_providers.yml
index b65141906e8..ab7e89ce449 100644
--- a/config/metrics/counts_28d/20210216183627_omniauth_providers.yml
+++ b/config/metrics/counts_28d/20210216183627_omniauth_providers.yml
@@ -5,7 +5,7 @@ description: List of unique OmniAuth providers
product_section: dev
product_stage: manage
product_group: authentication_and_authorization
-product_category: authentication_and_authorization
+product_category: system_access
value_type: object
status: active
time_frame: 28d
diff --git a/config/metrics/counts_28d/20210910132229_user_auth_by_provider.yml b/config/metrics/counts_28d/20210910132229_user_auth_by_provider.yml
index 0ee3d694317..7aad160f01d 100644
--- a/config/metrics/counts_28d/20210910132229_user_auth_by_provider.yml
+++ b/config/metrics/counts_28d/20210910132229_user_auth_by_provider.yml
@@ -5,7 +5,7 @@ description: Number of unique user logins using two factor authentication for av
product_section: dev
product_stage: manage
product_group: authentication_and_authorization
-product_category: authentication_and_authorization
+product_category: system_access
value_type: object
status: active
milestone: "14.3"
diff --git a/config/metrics/counts_all/20210216180752_keys.yml b/config/metrics/counts_all/20210216180752_keys.yml
index dad2a777d26..afa2559310d 100644
--- a/config/metrics/counts_all/20210216180752_keys.yml
+++ b/config/metrics/counts_all/20210216180752_keys.yml
@@ -5,7 +5,7 @@ description: Number of keys.
product_section: dev
product_stage: manage
product_group: authentication_and_authorization
-product_category: authentication_and_authorization
+product_category: system_access
value_type: number
status: active
time_frame: all
diff --git a/config/metrics/counts_all/20210216183400_omniauth_providers.yml b/config/metrics/counts_all/20210216183400_omniauth_providers.yml
index f4d6e2bc57b..aa314730665 100644
--- a/config/metrics/counts_all/20210216183400_omniauth_providers.yml
+++ b/config/metrics/counts_all/20210216183400_omniauth_providers.yml
@@ -5,7 +5,7 @@ description: List of unique OmniAuth providers
product_section: dev
product_stage: manage
product_group: authentication_and_authorization
-product_category: authentication_and_authorization
+product_category: system_access
value_type: object
status: active
time_frame: all
diff --git a/config/metrics/counts_all/20210910132001_user_auth_by_provider.yml b/config/metrics/counts_all/20210910132001_user_auth_by_provider.yml
index 98ac9ace52f..c183edf1836 100644
--- a/config/metrics/counts_all/20210910132001_user_auth_by_provider.yml
+++ b/config/metrics/counts_all/20210910132001_user_auth_by_provider.yml
@@ -5,7 +5,7 @@ description: Number of unique user logins using two factor authentication for av
product_section: dev
product_stage: manage
product_group: authentication_and_authorization
-product_category: authentication_and_authorization
+product_category: system_access
value_type: object
status: active
milestone: "14.3"
diff --git a/config/metrics/settings/20210204124906_ldap_enabled.yml b/config/metrics/settings/20210204124906_ldap_enabled.yml
index 2c506cb40fc..d25cb1d2628 100644
--- a/config/metrics/settings/20210204124906_ldap_enabled.yml
+++ b/config/metrics/settings/20210204124906_ldap_enabled.yml
@@ -5,7 +5,7 @@ description: Whether LDAP is enabled
product_section: dev
product_stage: manage
product_group: authentication_and_authorization
-product_category: authentication_and_authorization
+product_category: system_access
value_type: boolean
status: active
time_frame: none
diff --git a/config/metrics/settings/20210204124910_omniauth_enabled.yml b/config/metrics/settings/20210204124910_omniauth_enabled.yml
index 83ea666a331..0939ce43903 100644
--- a/config/metrics/settings/20210204124910_omniauth_enabled.yml
+++ b/config/metrics/settings/20210204124910_omniauth_enabled.yml
@@ -5,7 +5,7 @@ description: Whether OmniAuth is enabled
product_section: dev
product_stage: manage
product_group: authentication_and_authorization
-product_category: authentication_and_authorization
+product_category: system_access
value_type: boolean
status: active
time_frame: none
diff --git a/config/metrics/settings/20210204124918_signup_enabled.yml b/config/metrics/settings/20210204124918_signup_enabled.yml
index df7f03a7d2e..9371a08613d 100644
--- a/config/metrics/settings/20210204124918_signup_enabled.yml
+++ b/config/metrics/settings/20210204124918_signup_enabled.yml
@@ -5,7 +5,7 @@ description: Whether public signup is enabled
product_section: dev
product_stage: manage
product_group: authentication_and_authorization
-product_category: authentication_and_authorization
+product_category: system_access
value_type: boolean
status: active
time_frame: none
diff --git a/config/routes/repository.rb b/config/routes/repository.rb
index 0202eb80b23..60d3d37bdc8 100644
--- a/config/routes/repository.rb
+++ b/config/routes/repository.rb
@@ -75,6 +75,7 @@ scope format: false do
get '/tree/*id', to: 'tree#show', as: :tree
get '/raw/*id', to: 'raw#show', as: :raw
+ get '/blame_page/*id', to: 'blame#page', as: :blame_page
get '/blame/*id', to: 'blame#show', as: :blame
get '/commits', to: 'commits#commits_root', as: :commits_root
diff --git a/db/docs/application_setting_terms.yml b/db/docs/application_setting_terms.yml
index 046231b13a4..d58d4d67569 100644
--- a/db/docs/application_setting_terms.yml
+++ b/db/docs/application_setting_terms.yml
@@ -3,7 +3,7 @@ table_name: application_setting_terms
classes:
- ApplicationSetting::Term
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/cf37bef287d7dd5d2dce3e2276489767b8c0671f
milestone: '10.8'
diff --git a/db/docs/atlassian_identities.yml b/db/docs/atlassian_identities.yml
index e43c8018d5c..e24c316c0f6 100644
--- a/db/docs/atlassian_identities.yml
+++ b/db/docs/atlassian_identities.yml
@@ -3,7 +3,7 @@ table_name: atlassian_identities
classes:
- Atlassian::Identity
feature_categories:
-- authentication_and_authorization
+- system_access
description: Stores Atlassian credentials that are used to integrate with Atlassian API
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40176
milestone: '13.4'
diff --git a/db/docs/authentication_events.yml b/db/docs/authentication_events.yml
index eaede3b7cd4..440ca695ad2 100644
--- a/db/docs/authentication_events.yml
+++ b/db/docs/authentication_events.yml
@@ -3,7 +3,7 @@ table_name: authentication_events
classes:
- AuthenticationEvent
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39652
milestone: '13.4'
diff --git a/db/docs/banned_users.yml b/db/docs/banned_users.yml
index d14b6d77234..33c5c9024cd 100644
--- a/db/docs/banned_users.yml
+++ b/db/docs/banned_users.yml
@@ -3,7 +3,7 @@ table_name: banned_users
classes:
- Users::BannedUser
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64728
milestone: '14.2'
diff --git a/db/docs/group_group_links.yml b/db/docs/group_group_links.yml
index f1541871795..1fa70ec02a6 100644
--- a/db/docs/group_group_links.yml
+++ b/db/docs/group_group_links.yml
@@ -3,7 +3,7 @@ table_name: group_group_links
classes:
- GroupGroupLink
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17117
milestone: '12.5'
diff --git a/db/docs/identities.yml b/db/docs/identities.yml
index 149907a419e..f2790c53466 100644
--- a/db/docs/identities.yml
+++ b/db/docs/identities.yml
@@ -3,7 +3,7 @@ table_name: identities
classes:
- Identity
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/1a80d13a3990937580c97e2b0ba8fb98f69bc055
milestone: '7.6'
diff --git a/db/docs/ip_restrictions.yml b/db/docs/ip_restrictions.yml
index 93f0da0505a..fbf90135d0a 100644
--- a/db/docs/ip_restrictions.yml
+++ b/db/docs/ip_restrictions.yml
@@ -3,7 +3,7 @@ table_name: ip_restrictions
classes:
- IpRestriction
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/12669
milestone: '12.0'
diff --git a/db/docs/keys.yml b/db/docs/keys.yml
index 4e626b1465c..41f6786a6cc 100644
--- a/db/docs/keys.yml
+++ b/db/docs/keys.yml
@@ -5,7 +5,7 @@ classes:
- Key
- LDAPKey
feature_categories:
-- authentication_and_authorization
+- system_access
- continuous_delivery
description: SSH keys used by users or for deployments.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/9ba1224867665844b117fa037e1465bb706b3685
diff --git a/db/docs/ldap_group_links.yml b/db/docs/ldap_group_links.yml
index d9a1b0acca5..74cc1a13d69 100644
--- a/db/docs/ldap_group_links.yml
+++ b/db/docs/ldap_group_links.yml
@@ -3,7 +3,7 @@ table_name: ldap_group_links
classes:
- LdapGroupLink
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/b017947ac91655f8ae6593fb63c3423cd1b439f4
milestone: '7.3'
diff --git a/db/docs/namespace_admin_notes.yml b/db/docs/namespace_admin_notes.yml
index 6d6710f7ee4..50ca72b270c 100644
--- a/db/docs/namespace_admin_notes.yml
+++ b/db/docs/namespace_admin_notes.yml
@@ -3,7 +3,7 @@ table_name: namespace_admin_notes
classes:
- Namespace::AdminNote
feature_categories:
-- authentication_and_authorization
+- system_access
- subgroups
description: Contains notes about groups that are visible to server administrators.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47825
diff --git a/db/docs/namespace_ldap_settings.yml b/db/docs/namespace_ldap_settings.yml
index d30a835b55f..e2ebbf54fde 100644
--- a/db/docs/namespace_ldap_settings.yml
+++ b/db/docs/namespace_ldap_settings.yml
@@ -3,7 +3,7 @@ table_name: namespace_ldap_settings
classes:
- Namespaces::LdapSetting
feature_categories:
- - authentication_and_authorization
+ - system_access
description: Used to store LDAP settings for namespaces
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108908
milestone: '15.10'
diff --git a/db/docs/oauth_access_grants.yml b/db/docs/oauth_access_grants.yml
index 197d4fc59bd..8339863cca7 100644
--- a/db/docs/oauth_access_grants.yml
+++ b/db/docs/oauth_access_grants.yml
@@ -4,7 +4,7 @@ classes:
- Doorkeeper::AccessGrant
- OauthAccessGrant
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/e41dadcb33fda44ee274daa673bd933e13aa90eb
milestone: '7.7'
diff --git a/db/docs/oauth_access_tokens.yml b/db/docs/oauth_access_tokens.yml
index f409762f483..4f68fe5b6c6 100644
--- a/db/docs/oauth_access_tokens.yml
+++ b/db/docs/oauth_access_tokens.yml
@@ -4,7 +4,7 @@ classes:
- Doorkeeper::AccessToken
- OauthAccessToken
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/e41dadcb33fda44ee274daa673bd933e13aa90eb
milestone: '7.7'
diff --git a/db/docs/oauth_applications.yml b/db/docs/oauth_applications.yml
index ac13ab3319a..e24578c3272 100644
--- a/db/docs/oauth_applications.yml
+++ b/db/docs/oauth_applications.yml
@@ -3,7 +3,7 @@ table_name: oauth_applications
classes:
- Doorkeeper::Application
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/e41dadcb33fda44ee274daa673bd933e13aa90eb
milestone: '7.7'
diff --git a/db/docs/oauth_openid_requests.yml b/db/docs/oauth_openid_requests.yml
index 011b91a758a..59de50597c3 100644
--- a/db/docs/oauth_openid_requests.yml
+++ b/db/docs/oauth_openid_requests.yml
@@ -3,7 +3,7 @@ table_name: oauth_openid_requests
classes:
- Doorkeeper::OpenidConnect::Request
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/c4982890489d254da2fe998aab30bf257767ed5e
milestone: '9.0'
diff --git a/db/docs/personal_access_tokens.yml b/db/docs/personal_access_tokens.yml
index 8241f4234d8..2739db8371f 100644
--- a/db/docs/personal_access_tokens.yml
+++ b/db/docs/personal_access_tokens.yml
@@ -3,7 +3,7 @@ table_name: personal_access_tokens
classes:
- PersonalAccessToken
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/3a609038748055a27c7e01cf4b55d8249709c9cc
milestone: '8.9'
diff --git a/db/docs/project_access_tokens.yml b/db/docs/project_access_tokens.yml
index ddaca744571..3c19e4dc19f 100644
--- a/db/docs/project_access_tokens.yml
+++ b/db/docs/project_access_tokens.yml
@@ -2,7 +2,7 @@
table_name: project_access_tokens
classes: []
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33272
milestone: '13.1'
diff --git a/db/docs/project_authorizations.yml b/db/docs/project_authorizations.yml
index b37634047f0..b81235d4aac 100644
--- a/db/docs/project_authorizations.yml
+++ b/db/docs/project_authorizations.yml
@@ -4,7 +4,7 @@ classes:
- ProjectAuthorization
feature_categories:
- projects
-- authentication_and_authorization
+- system_access
description: Stores maximal access to the project per user
introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6839
milestone: '8.14'
diff --git a/db/docs/project_group_links.yml b/db/docs/project_group_links.yml
index c03141058b6..aa981adb745 100644
--- a/db/docs/project_group_links.yml
+++ b/db/docs/project_group_links.yml
@@ -3,7 +3,7 @@ table_name: project_group_links
classes:
- ProjectGroupLink
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/3ac5a759e93e632539438d4564582c645a9f6799
milestone: "<6.0"
diff --git a/db/docs/saml_group_links.yml b/db/docs/saml_group_links.yml
index 5fd2372a22d..4dfb33e37a5 100644
--- a/db/docs/saml_group_links.yml
+++ b/db/docs/saml_group_links.yml
@@ -3,7 +3,7 @@ table_name: saml_group_links
classes:
- SamlGroupLink
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45061
milestone: '13.5'
diff --git a/db/docs/saml_providers.yml b/db/docs/saml_providers.yml
index 6fcc0e0e370..21ef2ed3a26 100644
--- a/db/docs/saml_providers.yml
+++ b/db/docs/saml_providers.yml
@@ -3,7 +3,7 @@ table_name: saml_providers
classes:
- SamlProvider
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/4549
milestone: '10.7'
diff --git a/db/docs/scim_identities.yml b/db/docs/scim_identities.yml
index 6ad69d9b4cc..16fec8da041 100644
--- a/db/docs/scim_identities.yml
+++ b/db/docs/scim_identities.yml
@@ -3,7 +3,7 @@ table_name: scim_identities
classes:
- ScimIdentity
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26124
milestone: '12.9'
diff --git a/db/docs/scim_oauth_access_tokens.yml b/db/docs/scim_oauth_access_tokens.yml
index e26cd94f4cd..addd4c49ed5 100644
--- a/db/docs/scim_oauth_access_tokens.yml
+++ b/db/docs/scim_oauth_access_tokens.yml
@@ -3,7 +3,7 @@ table_name: scim_oauth_access_tokens
classes:
- ScimOauthAccessToken
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/e9b2253fe3538234d1c4d173c4549a955233d836
milestone: '11.10'
diff --git a/db/docs/smartcard_identities.yml b/db/docs/smartcard_identities.yml
index 76b8d1a1368..905811768c1 100644
--- a/db/docs/smartcard_identities.yml
+++ b/db/docs/smartcard_identities.yml
@@ -3,7 +3,7 @@ table_name: smartcard_identities
classes:
- SmartcardIdentity
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/b6316689fdc2d142af85b17d511d39e50712b420
milestone: '11.6'
diff --git a/db/docs/term_agreements.yml b/db/docs/term_agreements.yml
index 502adad8ac0..bc2abea809e 100644
--- a/db/docs/term_agreements.yml
+++ b/db/docs/term_agreements.yml
@@ -3,7 +3,7 @@ table_name: term_agreements
classes:
- TermAgreement
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/82eeb72c8c03727540b902d40e7e657d0a5ecb4c
milestone: '10.8'
diff --git a/db/docs/token_with_ivs.yml b/db/docs/token_with_ivs.yml
index 2acdff0dad1..521e26baac0 100644
--- a/db/docs/token_with_ivs.yml
+++ b/db/docs/token_with_ivs.yml
@@ -3,7 +3,7 @@ table_name: token_with_ivs
classes:
- TokenWithIv
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/37b80b4048190c2e1a35ec399e4aeb35d511090e
milestone: '13.9'
diff --git a/db/docs/u2f_registrations.yml b/db/docs/u2f_registrations.yml
index 27b0ca3f2f5..b1aaa8148bd 100644
--- a/db/docs/u2f_registrations.yml
+++ b/db/docs/u2f_registrations.yml
@@ -3,7 +3,7 @@ table_name: u2f_registrations
classes:
- U2fRegistration
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/791cc9138be6ea1783e3c3853370cf0290f4d41e
milestone: '8.9'
diff --git a/db/docs/user_canonical_emails.yml b/db/docs/user_canonical_emails.yml
index aeb1c3d830f..df3240b52fa 100644
--- a/db/docs/user_canonical_emails.yml
+++ b/db/docs/user_canonical_emails.yml
@@ -3,7 +3,7 @@ table_name: user_canonical_emails
classes:
- UserCanonicalEmail
feature_categories:
-- authentication_and_authorization
+- system_access
description: stores the canonical version of user's primary email address
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27722
milestone: '13.0'
diff --git a/db/docs/user_highest_roles.yml b/db/docs/user_highest_roles.yml
index cc12e3080ff..cfe4c2e5ce0 100644
--- a/db/docs/user_highest_roles.yml
+++ b/db/docs/user_highest_roles.yml
@@ -3,7 +3,7 @@ table_name: user_highest_roles
classes:
- UserHighestRole
feature_categories:
-- authentication_and_authorization
+- system_access
description: Stores highest role per User they have in a Group or a Project. If a User has an open invite or pending access request or no membership the highest role will be set to nil.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26987
milestone: '12.9'
diff --git a/db/docs/user_permission_export_uploads.yml b/db/docs/user_permission_export_uploads.yml
index 217ede5bad2..fe76f1fa618 100644
--- a/db/docs/user_permission_export_uploads.yml
+++ b/db/docs/user_permission_export_uploads.yml
@@ -3,7 +3,7 @@ table_name: user_permission_export_uploads
classes:
- UserPermissionExportUpload
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47846
milestone: '13.7'
diff --git a/db/docs/user_synced_attributes_metadata.yml b/db/docs/user_synced_attributes_metadata.yml
index efc0ad1ec95..a2162c071c9 100644
--- a/db/docs/user_synced_attributes_metadata.yml
+++ b/db/docs/user_synced_attributes_metadata.yml
@@ -3,7 +3,7 @@ table_name: user_synced_attributes_metadata
classes:
- UserSyncedAttributesMetadata
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/4df54f260751a832ebf0b8c18524020d6604994b
milestone: '10.0'
diff --git a/db/docs/webauthn_registrations.yml b/db/docs/webauthn_registrations.yml
index fc983ea60ca..1ec27e1bb3b 100644
--- a/db/docs/webauthn_registrations.yml
+++ b/db/docs/webauthn_registrations.yml
@@ -3,7 +3,7 @@ table_name: webauthn_registrations
classes:
- WebauthnRegistration
feature_categories:
-- authentication_and_authorization
+- system_access
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35797
milestone: '13.2'
diff --git a/db/post_migrate/20230223082752_schedule_fk_validation_for_p_ci_builds_metadata_partitions_and_ci_builds.rb b/db/post_migrate/20230223082752_schedule_fk_validation_for_p_ci_builds_metadata_partitions_and_ci_builds.rb
index 583a9cd31f7..bcb1147605e 100644
--- a/db/post_migrate/20230223082752_schedule_fk_validation_for_p_ci_builds_metadata_partitions_and_ci_builds.rb
+++ b/db/post_migrate/20230223082752_schedule_fk_validation_for_p_ci_builds_metadata_partitions_and_ci_builds.rb
@@ -1,14 +1,17 @@
# frozen_string_literal: true
class ScheduleFkValidationForPCiBuildsMetadataPartitionsAndCiBuilds < Gitlab::Database::Migration[2.1]
- TABLE_NAME = :p_ci_builds_metadata
- FK_NAME = :fk_e20479742e_p
+ # This migration was used to validate the foreign keys on partitions introduced by
+ # db/post_migrate/20230221125148_add_fk_to_p_ci_builds_metadata_partitions_on_partition_id_and_build_id.rb
+ # but executing the rollback of
+ # db/post_migrate/20230306072532_add_partitioned_fk_to_p_ci_builds_metadata_on_partition_id_and_build_id.rb
+ # would also remove the FKs on partitions and this would errors out.
def up
- prepare_partitioned_async_foreign_key_validation TABLE_NAME, name: FK_NAME
+ # No-op
end
def down
- unprepare_partitioned_async_foreign_key_validation TABLE_NAME, name: FK_NAME
+ # No-op
end
end
diff --git a/db/post_migrate/20230306071456_validate_partitioning_fk_on_p_ci_builds_metadata_partitions.rb b/db/post_migrate/20230306071456_validate_partitioning_fk_on_p_ci_builds_metadata_partitions.rb
new file mode 100644
index 00000000000..f07175e82f9
--- /dev/null
+++ b/db/post_migrate/20230306071456_validate_partitioning_fk_on_p_ci_builds_metadata_partitions.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class ValidatePartitioningFkOnPCiBuildsMetadataPartitions < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ SOURCE_TABLE_NAME = :p_ci_builds_metadata
+ FK_NAME = :fk_e20479742e_p
+
+ def up
+ Gitlab::Database::PostgresPartitionedTable.each_partition(SOURCE_TABLE_NAME) do |partition|
+ next unless foreign_key_exists?(partition.identifier, name: FK_NAME)
+
+ validate_foreign_key(partition.identifier, nil, name: FK_NAME)
+ end
+ end
+
+ def down
+ # No-op
+ end
+end
diff --git a/db/post_migrate/20230306072532_add_partitioned_fk_to_p_ci_builds_metadata_on_partition_id_and_build_id.rb b/db/post_migrate/20230306072532_add_partitioned_fk_to_p_ci_builds_metadata_on_partition_id_and_build_id.rb
new file mode 100644
index 00000000000..9328e3ff00e
--- /dev/null
+++ b/db/post_migrate/20230306072532_add_partitioned_fk_to_p_ci_builds_metadata_on_partition_id_and_build_id.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class AddPartitionedFkToPCiBuildsMetadataOnPartitionIdAndBuildId < Gitlab::Database::Migration[2.1]
+ SOURCE_TABLE_NAME = :p_ci_builds_metadata
+ TARGET_TABLE_NAME = :ci_builds
+ FK_NAME = :fk_e20479742e_p
+
+ disable_ddl_transaction!
+
+ def up
+ return if foreign_key_exists?(SOURCE_TABLE_NAME, TARGET_TABLE_NAME, name: FK_NAME)
+
+ with_lock_retries do
+ execute("LOCK TABLE #{TARGET_TABLE_NAME}, #{SOURCE_TABLE_NAME} IN SHARE ROW EXCLUSIVE MODE")
+
+ execute(<<~SQL.squish)
+ ALTER TABLE #{SOURCE_TABLE_NAME}
+ ADD CONSTRAINT #{FK_NAME}
+ FOREIGN KEY (partition_id, build_id)
+ REFERENCES #{TARGET_TABLE_NAME} (partition_id, id)
+ ON UPDATE CASCADE ON DELETE CASCADE;
+ SQL
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key_if_exists(
+ SOURCE_TABLE_NAME,
+ TARGET_TABLE_NAME,
+ name: FK_NAME,
+ reverse_lock_order: true
+ )
+ end
+ end
+end
diff --git a/db/post_migrate/20230306082852_remove_fk_to_ci_builds_p_ci_builds_metadata_on_build_id.rb b/db/post_migrate/20230306082852_remove_fk_to_ci_builds_p_ci_builds_metadata_on_build_id.rb
new file mode 100644
index 00000000000..108a92aec3b
--- /dev/null
+++ b/db/post_migrate/20230306082852_remove_fk_to_ci_builds_p_ci_builds_metadata_on_build_id.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class RemoveFkToCiBuildsPCiBuildsMetadataOnBuildId < Gitlab::Database::Migration[2.1]
+ include Gitlab::Database::PartitioningMigrationHelpers
+
+ disable_ddl_transaction!
+
+ SOURCE_TABLE_NAME = :p_ci_builds_metadata
+ TARGET_TABLE_NAME = :ci_builds
+ FK_NAME = :fk_e20479742e
+
+ def up
+ with_lock_retries do
+ remove_foreign_key_if_exists(
+ SOURCE_TABLE_NAME,
+ TARGET_TABLE_NAME,
+ name: FK_NAME,
+ reverse_lock_order: true
+ )
+ end
+ end
+
+ def down
+ add_concurrent_partitioned_foreign_key(
+ SOURCE_TABLE_NAME,
+ TARGET_TABLE_NAME,
+ column: :build_id,
+ on_delete: :cascade,
+ name: FK_NAME
+ )
+ end
+end
diff --git a/db/schema_migrations/20230306071456 b/db/schema_migrations/20230306071456
new file mode 100644
index 00000000000..b4ac086f125
--- /dev/null
+++ b/db/schema_migrations/20230306071456
@@ -0,0 +1 @@
+7f431d6dd4f9dc237623c18465995fa59c9902187f433375baa03194f7a6b88f \ No newline at end of file
diff --git a/db/schema_migrations/20230306072532 b/db/schema_migrations/20230306072532
new file mode 100644
index 00000000000..f1604aa84a7
--- /dev/null
+++ b/db/schema_migrations/20230306072532
@@ -0,0 +1 @@
+f6613d1fd3b99fa0e8ea059c6d53e8d226ce3fd8c07e44a024b065d8d110876f \ No newline at end of file
diff --git a/db/schema_migrations/20230306082852 b/db/schema_migrations/20230306082852
new file mode 100644
index 00000000000..bbbe7cb27ef
--- /dev/null
+++ b/db/schema_migrations/20230306082852
@@ -0,0 +1 @@
+580efa96f235c47de1bcea172544e51e8207dd0a81bd888567b30ce02e453f7d \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 64beb9476f9..8341c91b5e6 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -34838,10 +34838,7 @@ ALTER TABLE ONLY ci_sources_pipelines
ADD CONSTRAINT fk_e1bad85861 FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
ALTER TABLE p_ci_builds_metadata
- ADD CONSTRAINT fk_e20479742e FOREIGN KEY (build_id) REFERENCES ci_builds(id) ON DELETE CASCADE;
-
-ALTER TABLE ONLY ci_builds_metadata
- ADD CONSTRAINT fk_e20479742e_p FOREIGN KEY (partition_id, build_id) REFERENCES ci_builds(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE NOT VALID;
+ ADD CONSTRAINT fk_e20479742e_p FOREIGN KEY (partition_id, build_id) REFERENCES ci_builds(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE ONLY gitlab_subscriptions
ADD CONSTRAINT fk_e2595d00a1 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
diff --git a/doc/administration/dedicated/index.md b/doc/administration/dedicated/index.md
index 22b8cc13637..926500090dc 100644
--- a/doc/administration/dedicated/index.md
+++ b/doc/administration/dedicated/index.md
@@ -97,7 +97,7 @@ Prerequisites:
To activate SAML for your GitLab Dedicated instance:
-1. Read the [GitLab documentation about SAML](../../integration/saml.md#https://docs.gitlab.com/ee/integration/saml.html#configure-saml-on-your-idp) to gather all data your identity provider requires for configuration. You can also find some providers and their requirements in the [group SAML documentation](../../user/group/saml_sso/index.md#providers).
+1. Read the [GitLab documentation about SAML](../../integration/saml.md#https://docs.gitlab.com/ee/integration/saml.html#configure-saml-on-your-idp) to gather all data your identity provider requires for configuration. You can also find some providers and their requirements in the [group SAML documentation](../../user/group/saml_sso/index.md#set-up-identity-provider).
1. To make the necessary changes, include in your [support ticket](#configuration-changes) the desired [SAML configuration block](../../integration/saml.md#configure-saml-support-in-gitlab) that will be set on the GitLab application. At a minimum, GitLab needs the following information to enable SAML for your instance:
- Assertion consumer service URL
- Certificate fingerprint or certificate
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 1ce6c1784a2..701070b11d7 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -15073,6 +15073,20 @@ Exposes aggregated value stream flow metrics.
#### Fields with arguments
+##### `GroupValueStreamAnalyticsFlowMetrics.deploymentCount`
+
+Number of production deployments in the given period.
+
+Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric).
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="groupvaluestreamanalyticsflowmetricsdeploymentcountfrom"></a>`from` | [`Time!`](#time) | Deployments finished after the date. |
+| <a id="groupvaluestreamanalyticsflowmetricsdeploymentcountprojectids"></a>`projectIds` | [`[ID!]`](#id) | Project IDs within the group hierarchy. |
+| <a id="groupvaluestreamanalyticsflowmetricsdeploymentcountto"></a>`to` | [`Time!`](#time) | Deployments finished before the date. |
+
##### `GroupValueStreamAnalyticsFlowMetrics.issueCount`
Number of issues opened in the given period.
@@ -19509,6 +19523,19 @@ Exposes aggregated value stream flow metrics.
#### Fields with arguments
+##### `ProjectValueStreamAnalyticsFlowMetrics.deploymentCount`
+
+Number of production deployments in the given period.
+
+Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric).
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="projectvaluestreamanalyticsflowmetricsdeploymentcountfrom"></a>`from` | [`Time!`](#time) | Deployments finished after the date. |
+| <a id="projectvaluestreamanalyticsflowmetricsdeploymentcountto"></a>`to` | [`Time!`](#time) | Deployments finished before the date. |
+
##### `ProjectValueStreamAnalyticsFlowMetrics.issueCount`
Number of issues opened in the given period.
diff --git a/doc/api/keys.md b/doc/api/keys.md
index e7bdc70017c..90144310238 100644
--- a/doc/api/keys.md
+++ b/doc/api/keys.md
@@ -7,6 +7,8 @@ type: reference, api
# Keys API **(FREE)**
+If using a SHA256 fingerprint in an API call, you should URL-encode the fingerprint.
+
## Get SSH key with user by ID of an SSH key
Get SSH key with user by ID of an SSH key. Note only administrators can lookup SSH key with user by ID of an SSH key.
@@ -22,7 +24,8 @@ GET /keys/:id
Example request:
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/keys/1"
+curl --header "PRIVATE-TOKEN: <your_access_token>" \
+ "https://gitlab.example.com/api/v4/keys/1"
```
```json
@@ -82,15 +85,8 @@ GET /keys
Example request:
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/keys?fingerprint=ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1"
-```
-
-If using sha256 fingerprint API calls, make sure that the fingerprint is URL-encoded.
-
-For example, `/` is represented by `%2F` and `:` is represented by`%3A`:
-
-```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/keys?fingerprint=SHA256%3AnUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo%2FlCg"
+curl --header "PRIVATE-TOKEN: <your_access_token>" \
+ "https://gitlab.example.com/api/v4/keys?fingerprint=ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1"
```
Example response:
@@ -142,15 +138,21 @@ Example response:
## Get user by deploy key fingerprint
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/119209) in GitLab 12.7.
+Deploy keys are bound to the creating user. If you query with a deploy key
+fingerprint, you get additional information about the projects using that key.
-Deploy keys are bound to the creating user, so if you query with a deploy key
-fingerprint you get additional information about the projects using that key.
+Example request with an MD5 fingerprint:
-Example request:
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" \
+ "https://gitlab.example.com/api/v4/keys?fingerprint=ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1"
+```
+
+In this SHA256 example, `/` is represented by `%2F` and `:` is represented by`%3A`:
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/keys?fingerprint=SHA256%3AnUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo%2FlCg"
+curl --header "PRIVATE-TOKEN: <your_access_token>" \
+ "https://gitlab.example.com/api/v4/keys?fingerprint=SHA256%3AnUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo%2FlCg"
```
Example response:
diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md
index cbf7ea079bc..25c26ee9339 100644
--- a/doc/api/namespaces.md
+++ b/doc/api/namespaces.md
@@ -100,9 +100,9 @@ Owners also see the `plan` property associated with a namespace:
]
```
-Users on GitLab.com also see `max_seats_used` and `seats_in_use` parameters.
+Users on GitLab.com also see `max_seats_used`, `seats_in_use` and `max_seats_used_changed_at` parameters.
`max_seats_used` is the highest number of users the group had. `seats_in_use` is
-the number of license seats currently being used. Both values are updated
+the number of license seats currently being used. `max_seats_used_changed_at` shows the date when the `max_seats_used` value changed. All the values are updated
once a day.
`max_seats_used` and `seats_in_use` are non-zero only for namespaces on paid plans.
@@ -114,6 +114,7 @@ once a day.
"name": "user1",
"billable_members_count": 2,
"max_seats_used": 3,
+ "max_seats_used_changed_at":"2023-02-13T12:00:02.000Z",
"seats_in_use": 2,
...
}
diff --git a/doc/architecture/blueprints/runner_tokens/index.md b/doc/architecture/blueprints/runner_tokens/index.md
index c6e1e143e96..b5be1689785 100644
--- a/doc/architecture/blueprints/runner_tokens/index.md
+++ b/doc/architecture/blueprints/runner_tokens/index.md
@@ -393,7 +393,7 @@ scope.
| GitLab Rails app | `%15.9` | Implement new GraphQL user-authenticated API to create a new runner. |
| GitLab Rails app | `%15.10` | Return token and runner ID information from `/runners/verify` REST endpoint. |
| GitLab Runner | `%15.10` | [Modify register command to allow new flow with glrt- prefixed authentication tokens](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/29613). |
-| GitLab Rails app | `%15.11` | Define feature flag and policies for "New Runner creation workflow" for groups and projects. |
+| GitLab Rails app | `%15.10` | Define feature flag and policies for "New Runner creation workflow" for groups and projects. |
| GitLab Rails app | `%15.11` | Update service and mutation to accept groups and projects. |
| GitLab Rails app | `%15.11` | Implement UI to create new runner. |
| GitLab Rails app | `%15.11` | GraphQL changes to `CiRunner` type. (?) |
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index e80e2caf636..5dfad57facf 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -8,17 +8,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> Introduced in GitLab 11.0.
-This page describes SAML for groups. For instance-wide SAML on self-managed GitLab instances, see [SAML SSO for self-managed GitLab instances](../../../integration/saml.md).
-[View the differences between SaaS and Self-Managed Authentication and Authorization Options](../../../administration/auth/index.md#saas-vs-self-managed-comparison).
+Users can sign in to GitLab through their SAML identity provider.
-SAML on GitLab.com allows users to sign in through their SAML identity provider. If the user is not already a member, the sign-in process automatically adds the user to the appropriate group.
+[SCIM](scim_setup.md) synchronizes users with the group on GitLab.com.
-User synchronization of SAML SSO groups is supported through [SCIM](scim_setup.md). SCIM supports adding and removing users from the GitLab group automatically.
-For example, if you remove a user from the SCIM app, SCIM removes that same user from the GitLab group.
+- When you add or remove a user from the SCIM app, SCIM adds or removes the user
+ from the GitLab group.
+- If the user is not already a group member, the user is added to the group as part of the sign-in process.
-SAML SSO is only configurable at the top-level group.
-
-If required, you can find [a glossary of common terms](../../../integration/saml.md#glossary-of-common-terms).
+You can configure SAML SSO for the top-level group only.
## Configure your identity provider
@@ -28,7 +26,7 @@ If required, you can find [a glossary of common terms](../../../integration/saml
1. Note the **Assertion consumer service URL**, **Identifier**, and **GitLab single sign-on URL**.
1. Configure your SAML identity provider app using the noted details.
Alternatively, GitLab provides a [metadata XML configuration](#metadata-configuration).
- See [specific identity provider documentation](#providers) for more details.
+ See [specific identity provider documentation](#set-up-identity-provider) for more details.
1. Configure the SAML response to include a [NameID](#nameid) that uniquely identifies each user.
1. Configure the required [user attributes](#user-attributes), ensuring you include the user's email address.
1. While the default is enabled for most SAML providers, ensure the app is set to have service provider
@@ -40,6 +38,121 @@ If required, you can find [a glossary of common terms](../../../integration/saml
If your account is the only owner in the group after SAML is set up, you can't unlink the account. To [unlink the account](#unlinking-accounts),
set up another user as a group owner.
+## Set up identity provider
+
+The SAML standard means that you can use a wide range of identity providers with GitLab. Your identity provider might have relevant documentation. It can be generic SAML documentation or specifically targeted for GitLab.
+
+When [configuring your identity provider](#configure-your-identity-provider), consider the notes below for specific providers to help avoid common issues and as a guide for terminology used.
+
+For providers not listed below, you can refer to the [instance SAML notes on configuring an identity provider](../../../integration/saml.md#configure-saml-on-your-idp)
+for additional guidance on information your identity provider may require.
+
+GitLab provides the following information for guidance only.
+If you have any questions on configuring the SAML app, contact your provider's support.
+
+### Set up Azure
+
+Follow the Azure documentation on [configuring single sign-on to applications](https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/add-application-portal-setup-sso), and use the following notes when needed.
+
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For a demo of the Azure SAML setup including SCIM, see [SCIM Provisioning on Azure Using SAML SSO for Groups Demo](https://youtu.be/24-ZxmTeEBU).
+The video is outdated in regard to objectID mapping and you should follow the [SCIM documentation](scim_setup.md#configure-azure-active-directory).
+
+| GitLab Setting | Azure Field |
+| ------------------------------------ | ------------------------------------------ |
+| Identifier | Identifier (Entity ID) |
+| Assertion consumer service URL | Reply URL (Assertion Consumer Service URL) |
+| GitLab single sign-on URL | Sign on URL |
+| Identity provider single sign-on URL | Login URL |
+| Certificate fingerprint | Thumbprint |
+
+You should set the following attributes:
+
+- **Unique User Identifier (Name identifier)** to `user.objectID`.
+- **nameid-format** to persistent.
+- Additional claims to [supported attributes](#user-attributes).
+
+If using [Group Sync](#group-sync), customize the name of the group claim to match the required attribute.
+
+See our [example configuration page](example_saml_config.md#azure-active-directory).
+
+### Set up Google Workspace
+
+1. [Set up SSO with Google as your identity provider](https://support.google.com/a/answer/6087519?hl=en).
+ The following GitLab settings correspond to the Google Workspace fields.
+
+ | GitLab setting | Google Workspace field |
+ |:-------------------------------------|:-----------------------|
+ | Identifier | **Entity ID** |
+ | Assertion consumer service URL | **ACS URL** |
+ | GitLab single sign-on URL | **Start URL** |
+ | Identity provider single sign-on URL | **SSO URL** |
+
+1. Google Workspace displays a SHA256 fingerprint. To retrieve the SHA1 fingerprint
+ required by GitLab to [configure SAML](#configure-gitlab):
+ 1. Download the certificate.
+ 1. Run this command:
+
+ ```shell
+ openssl x509 -noout -fingerprint -sha1 -inform pem -in "GoogleIDPCertificate-domain.com.pem"
+ ```
+
+1. Set these values:
+ - For **Primary email**: `email`
+ - For **First name**: `first_name`
+ - For **Last name**: `last_name`
+ - For **Name ID format**: `EMAIL`
+ - For **NameID**: `Basic Information > Primary email`
+
+On the GitLab SAML SSO page, when you select **Verify SAML Configuration**, disregard
+the warning that recommends setting the **NameID** format to `persistent`.
+
+For details, see the [example configuration page](example_saml_config.md#google-workspace).
+
+### Set up Okta
+
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For a demo of the Okta SAML setup including SCIM, see [Demo: Okta Group SAML & SCIM setup](https://youtu.be/0ES9HsZq0AQ).
+
+1. [Set up a SAML application in Okta](https://developer.okta.com/docs/guides/build-sso-integration/saml2/main/).
+ The following GitLab settings correspond to the Okta fields.
+
+ | GitLab setting | Okta field |
+ | ------------------------------------ | ---------------------------------------------------------- |
+ | Identifier | **Audience URI** |
+ | Assertion consumer service URL | **Single sign-on URL** |
+ | GitLab single sign-on URL | **Login page URL** (under **Application Login Page** settings) |
+ | Identity provider single sign-on URL | **Identity Provider Single Sign-On URL** |
+
+1. Under the Okta **Single sign-on URL** field, select the **Use this for Recipient URL and Destination URL** checkbox.
+
+1. Set these values:
+ - For **Application username (NameID)**: **Custom** `user.getInternalProperty("id")`
+ - For **Name ID Format**: `Persistent`
+
+The Okta GitLab application available in the App Catalog only supports [SCIM](scim_setup.md). Support
+for SAML is proposed in [issue 216173](https://gitlab.com/gitlab-org/gitlab/-/issues/216173).
+
+### Set up OneLogin
+
+OneLogin supports its own [GitLab (SaaS) application](https://onelogin.service-now.com/support?id=kb_article&sys_id=92e4160adbf16cd0ca1c400e0b961923&kb_category=50984e84db738300d5505eea4b961913).
+
+1. If you use the OneLogin generic
+ [SAML Test Connector (Advanced)](https://onelogin.service-now.com/support?id=kb_article&sys_id=b2c19353dbde7b8024c780c74b9619fb&kb_category=93e869b0db185340d5505eea4b961934),
+ you should [use the OneLogin SAML Test Connector](https://onelogin.service-now.com/support?id=kb_article&sys_id=93f95543db109700d5505eea4b96198f). The following GitLab settings correspond
+ to the OneLogin fields:
+
+ | GitLab setting | OneLogin field |
+ | ------------------------------------------------ | -------------------------------- |
+ | Identifier | **Audience** |
+ | Assertion consumer service URL | **Recipient** |
+ | Assertion consumer service URL | **ACS (Consumer) URL** |
+ | Assertion consumer service URL (escaped version) | **ACS (Consumer) URL Validator** |
+ | GitLab single sign-on URL | **Login URL** |
+ | Identity provider single sign-on URL | **SAML 2.0 Endpoint** |
+
+1. For **NameID**, use `OneLogin ID`.
+
### NameID
GitLab.com uses the SAML NameID to identify users. The NameID element:
@@ -52,7 +165,7 @@ GitLab.com uses the SAML NameID to identify users. The NameID element:
guarantee it doesn't ever change, for example, when a person's name changes. Email addresses are
also case-insensitive, which can result in users being unable to sign in.
-The relevant field name and recommended value for supported providers are in the [provider specific notes](#providers).
+The relevant field name and recommended value for supported providers are in the [provider specific notes](#set-up-identity-provider).
WARNING:
Once users have signed into GitLab using the SSO SAML setup, changing the `NameID` breaks the configuration and potentially locks users out of the GitLab group.
@@ -201,121 +314,7 @@ immediately. If the user:
- Is signed out, they cannot access the group after being removed from the
identity provider.
-## Providers
-
-The SAML standard means that you can use a wide range of identity providers with GitLab. Your identity provider might have relevant documentation. It can be generic SAML documentation or specifically targeted for GitLab.
-
-When [configuring your identity provider](#configure-your-identity-provider), consider the notes below for specific providers to help avoid common issues and as a guide for terminology used.
-
-For providers not listed below, you can refer to the [instance SAML notes on configuring an identity provider](../../../integration/saml.md#configure-saml-on-your-idp)
-for additional guidance on information your identity provider may require.
-
-GitLab provides the following information for guidance only.
-If you have any questions on configuring the SAML app, contact your provider's support.
-
-### Set up Azure
-
-1. [Use Azure to configure SSO for an application](https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/add-application-portal-setup-sso). The following GitLab settings correspond to the Azure fields.
-
- | GitLab setting | Azure field |
- | ------------------------------------ | ------------------------------------------ |
- | Identifier | Identifier (Entity ID) |
- | Assertion consumer service URL | Reply URL (Assertion Consumer Service URL) |
- | GitLab single sign-on URL | Sign on URL |
- | Identity provider single sign-on URL | Login URL |
- | Certificate fingerprint | Thumbprint |
-
-1. Set the following attributes:
- - **Unique User Identifier (Name identifier)** to `user.objectID`.
- - **nameid-format** to persistent.
- - **Additional claims** to [supported attributes](#user-attributes).
-
-1. Optional. If you use [Group Sync](group_sync.md), customize the name of the group
- claim to match the required attribute.
-
-<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
-View a demo of [SCIM provisioning on Azure using SAML SSO for groups](https://youtu.be/24-ZxmTeEBU). The `objectID` mapping is outdated in this video. Follow the [SCIM documentation](scim_setup.md#configure-azure-active-directory) instead.
-
-View an [example configuration page](example_saml_config.md#azure-active-directory).
-
-### Set up Google Workspace
-
-1. [Set up SSO with Google as your identity provider](https://support.google.com/a/answer/6087519?hl=en).
- The following GitLab settings correspond to the Google Workspace fields.
-
- | GitLab setting | Google Workspace field |
- |:-------------------------------------|:-----------------------|
- | Identifier | **Entity ID** |
- | Assertion consumer service URL | **ACS URL** |
- | GitLab single sign-on URL | **Start URL** |
- | Identity provider single sign-on URL | **SSO URL** |
-
-1. Google Workspace displays a SHA256 fingerprint. To retrieve the SHA1 fingerprint
- required by GitLab to [configure SAML](#configure-gitlab):
- 1. Download the certificate.
- 1. Run this command:
-
- ```shell
- openssl x509 -noout -fingerprint -sha1 -inform pem -in "GoogleIDPCertificate-domain.com.pem"
- ```
-
-1. Set these values:
- - For **Primary email**: `email`
- - For **First name**: `first_name`
- - For **Last name**: `last_name`
- - For **Name ID format**: `EMAIL`
- - For **NameID**: `Basic Information > Primary email`
-
-On the GitLab SAML SSO page, when you select **Verify SAML Configuration**, disregard
-the warning that recommends setting the **NameID** format to `persistent`.
-
-For details, see the [example configuration page](example_saml_config.md#google-workspace).
-
-### Set up Okta
-
-<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
-For a demo of the Okta SAML setup including SCIM, see [Demo: Okta Group SAML & SCIM setup](https://youtu.be/0ES9HsZq0AQ).
-
-1. [Set up a SAML application in Okta](https://developer.okta.com/docs/guides/build-sso-integration/saml2/main/).
- The following GitLab settings correspond to the Okta fields.
-
- | GitLab setting | Okta field |
- | ------------------------------------ | ---------------------------------------------------------- |
- | Identifier | **Audience URI** |
- | Assertion consumer service URL | **Single sign-on URL** |
- | GitLab single sign-on URL | **Login page URL** (under **Application Login Page** settings) |
- | Identity provider single sign-on URL | **Identity Provider Single Sign-On URL** |
-
-1. Under the Okta **Single sign-on URL** field, select the **Use this for Recipient URL and Destination URL** checkbox.
-
-1. Set these values:
- - For **Application username (NameID)**: **Custom** `user.getInternalProperty("id")`
- - For **Name ID Format**: `Persistent`
-
-The Okta GitLab application available in the App Catalog only supports [SCIM](scim_setup.md). Support
-for SAML is proposed in [issue 216173](https://gitlab.com/gitlab-org/gitlab/-/issues/216173).
-
-### Set up OneLogin
-
-OneLogin supports its own [GitLab (SaaS) application](https://onelogin.service-now.com/support?id=kb_article&sys_id=92e4160adbf16cd0ca1c400e0b961923&kb_category=50984e84db738300d5505eea4b961913).
-
-1. If you use the OneLogin generic
- [SAML Test Connector (Advanced)](https://onelogin.service-now.com/support?id=kb_article&sys_id=b2c19353dbde7b8024c780c74b9619fb&kb_category=93e869b0db185340d5505eea4b961934),
- you should [use the OneLogin SAML Test Connector](https://onelogin.service-now.com/support?id=kb_article&sys_id=93f95543db109700d5505eea4b96198f). The following GitLab settings correspond
- to the OneLogin fields:
-
- | GitLab setting | OneLogin field |
- | ------------------------------------------------ | -------------------------------- |
- | Identifier | **Audience** |
- | Assertion consumer service URL | **Recipient** |
- | Assertion consumer service URL | **ACS (Consumer) URL** |
- | Assertion consumer service URL (escaped version) | **ACS (Consumer) URL Validator** |
- | GitLab single sign-on URL | **Login URL** |
- | Identity provider single sign-on URL | **SAML 2.0 Endpoint** |
-
-1. For **NameID**, use `OneLogin ID`.
-
-## Manage your identity provider
+### Change the SAML app
After you have configured your identity provider, you can:
@@ -482,7 +481,7 @@ To rescind a user's access to the group when only SAML SSO is configured, either
- Remove (in order) the user from:
1. The user data store on the identity provider or the list of users on the specific app.
1. The GitLab.com group.
-- Use Group Sync at the top-level of your group to [automatically remove the user](group_sync.md#automatic-member-removal).
+- Use [Group Sync](group_sync.md#automatic-member-removal) at the top-level of your group with the [default role](#role) set to [minimal access](../../permissions.md#users-with-minimal-access) to automatically block access to all resources within the group. Users may continue to [use a seat](../../permissions.md#minimal-access-users-take-license-seats).
To rescind a user's access to the group when also using SCIM, refer to [Remove access](scim_setup.md#remove-access).
@@ -516,6 +515,17 @@ For information on automatically managing GitLab group membership, see [SAML Gro
The [Generated passwords for users created through integrated authentication](../../../security/passwords_for_integrated_authentication_methods.md) guide provides an overview of how GitLab generates and sets passwords for users created via SAML SSO for Groups.
+## Related topics
+
+For more information on:
+
+- Setting up SAML on self-managed GitLab instances, see
+ [SAML SSO for self-managed GitLab instances](../../../integration/saml.md).
+- Commonly-used terms, see the
+ [glossary of common terms](../../../integration/saml.md#glossary-of-common-terms).
+- The differences between SaaS and self-managed authentication and authorization,
+ see the [SaaS vs. Self-Managed comparison](../../../administration/auth/index.md#saas-vs-self-managed-comparison).
+
## Troubleshooting
See our [troubleshooting SAML guide](troubleshooting.md).
diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md
index 9e6e9984c15..e6675b4eff2 100644
--- a/doc/user/profile/notifications.md
+++ b/doc/user/profile/notifications.md
@@ -286,7 +286,8 @@ To always receive notifications on your own issues, merge requests, and so on, t
## Notifications for unknown sign-ins
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27211) in GitLab 13.0.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27211) in GitLab 13.0.
+> - Listing the full name and username of the signed-in user [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/225183) in GitLab 15.10.
NOTE:
This feature is enabled by default for self-managed instances. Administrators may disable this feature
@@ -295,7 +296,12 @@ The feature is always enabled on GitLab.com.
When a user successfully signs in from a previously unknown IP address or device,
GitLab notifies the user by email. In this way, GitLab proactively alerts users of potentially
-malicious or unauthorized sign-ins.
+malicious or unauthorized sign-ins. This notification email includes the:
+
+- Hostname.
+- User's name and username.
+- IP address.
+- Date and time of sign-in.
GitLab uses several methods to identify a known sign-in. All methods must fail for a notification email to be sent.
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index 38a9856ca58..249301f308c 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -8,7 +8,7 @@ module API
helpers ::API::Helpers::MembersHelpers
- feature_category :authentication_and_authorization
+ feature_category :system_access
%w[group project].each do |source_type|
params do
diff --git a/lib/api/applications.rb b/lib/api/applications.rb
index 6fc9408a570..39f1638301b 100644
--- a/lib/api/applications.rb
+++ b/lib/api/applications.rb
@@ -5,7 +5,7 @@ module API
class Applications < ::API::Base
before { authenticated_as_admin! }
- feature_category :authentication_and_authorization
+ feature_category :system_access
resource :applications do
desc 'Create a new application' do
diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb
index 3f6e052f7b6..2a5ff257718 100644
--- a/lib/api/internal/base.rb
+++ b/lib/api/internal/base.rb
@@ -195,7 +195,7 @@ module API
#
# Discover user by ssh key, user id or username
#
- get '/discover', feature_category: :authentication_and_authorization do
+ get '/discover', feature_category: :system_access do
present actor.user, with: Entities::UserSafe
end
@@ -208,7 +208,7 @@ module API
}
end
- post '/two_factor_recovery_codes', feature_category: :authentication_and_authorization do
+ post '/two_factor_recovery_codes', feature_category: :system_access do
status 200
actor.update_last_used_at!
@@ -237,7 +237,7 @@ module API
{ success: true, recovery_codes: codes }
end
- post '/personal_access_token', feature_category: :authentication_and_authorization do
+ post '/personal_access_token', feature_category: :system_access do
status 200
actor.update_last_used_at!
@@ -308,7 +308,7 @@ module API
# decided to pursue a different approach, so it's currently not used.
# We might revive the PAM module though as it provides better user
# flow.
- post '/two_factor_config', feature_category: :authentication_and_authorization do
+ post '/two_factor_config', feature_category: :system_access do
status 200
break { success: false } unless Feature.enabled?(:two_factor_for_cli)
@@ -330,13 +330,13 @@ module API
end
end
- post '/two_factor_push_otp_check', feature_category: :authentication_and_authorization do
+ post '/two_factor_push_otp_check', feature_category: :system_access do
status 200
two_factor_push_otp_check
end
- post '/two_factor_manual_otp_check', feature_category: :authentication_and_authorization do
+ post '/two_factor_manual_otp_check', feature_category: :system_access do
status 200
two_factor_manual_otp_check
diff --git a/lib/api/keys.rb b/lib/api/keys.rb
index 77952bac01a..c711b3d9c19 100644
--- a/lib/api/keys.rb
+++ b/lib/api/keys.rb
@@ -5,7 +5,7 @@ module API
class Keys < ::API::Base
before { authenticate! }
- feature_category :authentication_and_authorization
+ feature_category :system_access
resource :keys do
desc 'Get single ssh key by id. Only available to admin users' do
diff --git a/lib/api/personal_access_tokens.rb b/lib/api/personal_access_tokens.rb
index 66930ecd797..e588eb17720 100644
--- a/lib/api/personal_access_tokens.rb
+++ b/lib/api/personal_access_tokens.rb
@@ -4,7 +4,7 @@ module API
class PersonalAccessTokens < ::API::Base
include ::API::PaginationParams
- feature_category :authentication_and_authorization
+ feature_category :system_access
before do
authenticate!
diff --git a/lib/api/personal_access_tokens/self_information.rb b/lib/api/personal_access_tokens/self_information.rb
index 5735fe49f33..4f17ca955ac 100644
--- a/lib/api/personal_access_tokens/self_information.rb
+++ b/lib/api/personal_access_tokens/self_information.rb
@@ -5,7 +5,7 @@ module API
class SelfInformation < ::API::Base
include APIGuard
- feature_category :authentication_and_authorization
+ feature_category :system_access
helpers ::API::Helpers::PersonalAccessTokensHelpers
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index fbe30aad120..b39444f642f 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -709,7 +709,7 @@ module API
requires :group_access, type: Integer, values: Gitlab::Access.values, as: :link_group_access, desc: 'The group access level'
optional :expires_at, type: Date, desc: 'Share expiration date'
end
- post ":id/share", feature_category: :authentication_and_authorization do
+ post ":id/share", feature_category: :system_access do
authorize! :admin_project, user_project
shared_with_group = Group.find_by_id(params[:group_id])
@@ -739,7 +739,7 @@ module API
requires :group_id, type: Integer, desc: 'The ID of the group'
end
# rubocop: disable CodeReuse/ActiveRecord
- delete ":id/share/:group_id", feature_category: :authentication_and_authorization do
+ delete ":id/share/:group_id", feature_category: :system_access do
authorize! :admin_project, user_project
link = user_project.project_group_links.find_by(group_id: params[:group_id])
@@ -830,7 +830,7 @@ module API
optional :skip_users, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Filter out users with the specified IDs'
use :pagination
end
- get ':id/users', urgency: :low, feature_category: :authentication_and_authorization do
+ get ':id/users', urgency: :low, feature_category: :system_access do
users = DeclarativePolicy.subject_scope { user_project.team.users }
users = users.search(params[:search]) if params[:search].present?
users = users.where_not_in(params[:skip_users]) if params[:skip_users].present?
diff --git a/lib/api/resource_access_tokens.rb b/lib/api/resource_access_tokens.rb
index 754dfadb5fc..2726e05cd44 100644
--- a/lib/api/resource_access_tokens.rb
+++ b/lib/api/resource_access_tokens.rb
@@ -8,7 +8,7 @@ module API
before { authenticate! }
- feature_category :authentication_and_authorization
+ feature_category :system_access
%w[project group].each do |source_type|
resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
diff --git a/lib/api/users.rb b/lib/api/users.rb
index cc7eb63798a..297306fa6d7 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -381,7 +381,7 @@ module API
params do
requires :id, type: Integer, desc: 'The ID of the user'
end
- patch ":id/disable_two_factor", feature_category: :authentication_and_authorization do
+ patch ":id/disable_two_factor", feature_category: :system_access do
authenticated_as_admin!
user = User.find_by_id(params[:id])
@@ -407,7 +407,7 @@ module API
requires :provider, type: String, desc: 'The external provider'
end
# rubocop: disable CodeReuse/ActiveRecord
- delete ":id/identities/:provider", feature_category: :authentication_and_authorization do
+ delete ":id/identities/:provider", feature_category: :system_access do
authenticated_as_admin!
user = User.find_by(id: params[:id])
@@ -456,7 +456,7 @@ module API
desc: 'Scope of usage for the SSH key'
end
# rubocop: disable CodeReuse/ActiveRecord
- post ":user_id/keys", feature_category: :authentication_and_authorization do
+ post ":user_id/keys", feature_category: :system_access do
authenticated_as_admin!
user = User.find_by(id: params.delete(:user_id))
@@ -479,7 +479,7 @@ module API
requires :user_id, type: String, desc: 'The ID or username of the user'
use :pagination
end
- get ':user_id/keys', requirements: API::USER_REQUIREMENTS, feature_category: :authentication_and_authorization do
+ get ':user_id/keys', requirements: API::USER_REQUIREMENTS, feature_category: :system_access do
user = find_user(params[:user_id])
not_found!('User') unless user && can?(current_user, :read_user, user)
@@ -494,7 +494,7 @@ module API
requires :id, type: Integer, desc: 'The ID of the user'
requires :key_id, type: Integer, desc: 'The ID of the SSH key'
end
- get ':id/keys/:key_id', requirements: API::USER_REQUIREMENTS, feature_category: :authentication_and_authorization do
+ get ':id/keys/:key_id', requirements: API::USER_REQUIREMENTS, feature_category: :system_access do
user = find_user(params[:id])
not_found!('User') unless user && can?(current_user, :read_user, user)
@@ -512,7 +512,7 @@ module API
requires :key_id, type: Integer, desc: 'The ID of the SSH key'
end
# rubocop: disable CodeReuse/ActiveRecord
- delete ':id/keys/:key_id', feature_category: :authentication_and_authorization do
+ delete ':id/keys/:key_id', feature_category: :system_access do
authenticated_as_admin!
user = User.find_by(id: params[:id])
@@ -537,7 +537,7 @@ module API
requires :key, type: String, desc: 'The new GPG key'
end
# rubocop: disable CodeReuse/ActiveRecord
- post ':id/gpg_keys', feature_category: :authentication_and_authorization do
+ post ':id/gpg_keys', feature_category: :system_access do
authenticated_as_admin!
user = User.find_by(id: params.delete(:id))
@@ -562,7 +562,7 @@ module API
use :pagination
end
# rubocop: disable CodeReuse/ActiveRecord
- get ':id/gpg_keys', feature_category: :authentication_and_authorization do
+ get ':id/gpg_keys', feature_category: :system_access do
user = User.find_by(id: params[:id])
not_found!('User') unless user
@@ -579,7 +579,7 @@ module API
requires :key_id, type: Integer, desc: 'The ID of the GPG key'
end
# rubocop: disable CodeReuse/ActiveRecord
- get ':id/gpg_keys/:key_id', feature_category: :authentication_and_authorization do
+ get ':id/gpg_keys/:key_id', feature_category: :system_access do
user = User.find_by(id: params[:id])
not_found!('User') unless user
@@ -598,7 +598,7 @@ module API
requires :key_id, type: Integer, desc: 'The ID of the GPG key'
end
# rubocop: disable CodeReuse/ActiveRecord
- delete ':id/gpg_keys/:key_id', feature_category: :authentication_and_authorization do
+ delete ':id/gpg_keys/:key_id', feature_category: :system_access do
authenticated_as_admin!
user = User.find_by(id: params[:id])
@@ -622,7 +622,7 @@ module API
requires :key_id, type: Integer, desc: 'The ID of the GPG key'
end
# rubocop: disable CodeReuse/ActiveRecord
- post ':id/gpg_keys/:key_id/revoke', feature_category: :authentication_and_authorization do
+ post ':id/gpg_keys/:key_id/revoke', feature_category: :system_access do
authenticated_as_admin!
user = User.find_by(id: params[:id])
@@ -726,7 +726,7 @@ module API
requires :id, type: Integer, desc: 'The ID of the user'
end
# rubocop: disable CodeReuse/ActiveRecord
- post ':id/activate', feature_category: :authentication_and_authorization do
+ post ':id/activate', feature_category: :system_access do
authenticated_as_admin!
user = User.find_by(id: params[:id])
@@ -740,7 +740,7 @@ module API
params do
requires :id, type: Integer, desc: 'The ID of the user'
end
- post ':id/approve', feature_category: :authentication_and_authorization do
+ post ':id/approve', feature_category: :system_access do
user = User.find_by(id: params[:id])
not_found!('User') unless can?(current_user, :read_user, user)
@@ -757,7 +757,7 @@ module API
params do
requires :id, type: Integer, desc: 'The ID of the user'
end
- post ':id/reject', feature_category: :authentication_and_authorization do
+ post ':id/reject', feature_category: :system_access do
user = find_user_by_id(params)
result = ::Users::RejectService.new(current_user).execute(user)
@@ -775,7 +775,7 @@ module API
requires :id, type: Integer, desc: 'The ID of the user'
end
# rubocop: disable CodeReuse/ActiveRecord
- post ':id/deactivate', feature_category: :authentication_and_authorization do
+ post ':id/deactivate', feature_category: :system_access do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
@@ -801,7 +801,7 @@ module API
requires :id, type: Integer, desc: 'The ID of the user'
end
# rubocop: disable CodeReuse/ActiveRecord
- post ':id/block', feature_category: :authentication_and_authorization do
+ post ':id/block', feature_category: :system_access do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
@@ -828,7 +828,7 @@ module API
requires :id, type: Integer, desc: 'The ID of the user'
end
# rubocop: disable CodeReuse/ActiveRecord
- post ':id/unblock', feature_category: :authentication_and_authorization do
+ post ':id/unblock', feature_category: :system_access do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
@@ -848,7 +848,7 @@ module API
params do
requires :id, type: Integer, desc: 'The ID of the user'
end
- post ':id/ban', feature_category: :authentication_and_authorization do
+ post ':id/ban', feature_category: :system_access do
authenticated_as_admin!
user = find_user_by_id(params)
@@ -864,7 +864,7 @@ module API
params do
requires :id, type: Integer, desc: 'The ID of the user'
end
- post ':id/unban', feature_category: :authentication_and_authorization do
+ post ':id/unban', feature_category: :system_access do
authenticated_as_admin!
user = find_user_by_id(params)
@@ -928,7 +928,7 @@ module API
use :pagination
optional :state, type: String, default: 'all', values: %w[all active inactive], desc: 'Filters (all|active|inactive) impersonation_tokens'
end
- get feature_category: :authentication_and_authorization do
+ get feature_category: :system_access do
present paginate(finder(declared_params(include_missing: false)).execute), with: Entities::ImpersonationToken
end
@@ -941,7 +941,7 @@ module API
optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the impersonation token'
optional :scopes, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The array of scopes of the impersonation token'
end
- post feature_category: :authentication_and_authorization do
+ post feature_category: :system_access do
impersonation_token = finder.build(declared_params(include_missing: false))
if impersonation_token.save
@@ -958,7 +958,7 @@ module API
params do
requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token'
end
- get ':impersonation_token_id', feature_category: :authentication_and_authorization do
+ get ':impersonation_token_id', feature_category: :system_access do
present find_impersonation_token, with: Entities::ImpersonationToken
end
@@ -968,7 +968,7 @@ module API
params do
requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token'
end
- delete ':impersonation_token_id', feature_category: :authentication_and_authorization do
+ delete ':impersonation_token_id', feature_category: :system_access do
token = find_impersonation_token
destroy_conditionally!(token) do
@@ -996,7 +996,7 @@ module API
desc: 'The array of scopes of the personal access token'
optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the personal access token'
end
- post feature_category: :authentication_and_authorization do
+ post feature_category: :system_access do
response = ::PersonalAccessTokens::CreateService.new(
current_user: current_user, target_user: target_user, params: declared_params(include_missing: false)
).execute
@@ -1060,7 +1060,7 @@ module API
params do
use :pagination
end
- get "keys", feature_category: :authentication_and_authorization do
+ get "keys", feature_category: :system_access do
keys = current_user.keys.preload_users
present paginate(keys), with: Entities::SSHKey
@@ -1073,7 +1073,7 @@ module API
requires :key_id, type: Integer, desc: 'The ID of the SSH key'
end
# rubocop: disable CodeReuse/ActiveRecord
- get "keys/:key_id", feature_category: :authentication_and_authorization do
+ get "keys/:key_id", feature_category: :system_access do
key = current_user.keys.find_by(id: params[:key_id])
not_found!('Key') unless key
@@ -1091,7 +1091,7 @@ module API
optional :usage_type, type: String, values: Key.usage_types.keys, default: 'auth_and_signing',
desc: 'Scope of usage for the SSH key'
end
- post "keys", feature_category: :authentication_and_authorization do
+ post "keys", feature_category: :system_access do
key = ::Keys::CreateService.new(current_user, declared_params(include_missing: false)).execute
if key.persisted?
@@ -1108,7 +1108,7 @@ module API
requires :key_id, type: Integer, desc: 'The ID of the SSH key'
end
# rubocop: disable CodeReuse/ActiveRecord
- delete "keys/:key_id", feature_category: :authentication_and_authorization do
+ delete "keys/:key_id", feature_category: :system_access do
key = current_user.keys.find_by(id: params[:key_id])
not_found!('Key') unless key
@@ -1126,7 +1126,7 @@ module API
params do
use :pagination
end
- get 'gpg_keys', feature_category: :authentication_and_authorization do
+ get 'gpg_keys', feature_category: :system_access do
present paginate(current_user.gpg_keys), with: Entities::GpgKey
end
@@ -1138,7 +1138,7 @@ module API
requires :key_id, type: Integer, desc: 'The ID of the GPG key'
end
# rubocop: disable CodeReuse/ActiveRecord
- get 'gpg_keys/:key_id', feature_category: :authentication_and_authorization do
+ get 'gpg_keys/:key_id', feature_category: :system_access do
key = current_user.gpg_keys.find_by(id: params[:key_id])
not_found!('GPG Key') unless key
@@ -1153,7 +1153,7 @@ module API
params do
requires :key, type: String, desc: 'The new GPG key'
end
- post 'gpg_keys', feature_category: :authentication_and_authorization do
+ post 'gpg_keys', feature_category: :system_access do
key = ::GpgKeys::CreateService.new(current_user, declared_params(include_missing: false)).execute
if key.persisted?
@@ -1170,7 +1170,7 @@ module API
requires :key_id, type: Integer, desc: 'The ID of the GPG key'
end
# rubocop: disable CodeReuse/ActiveRecord
- post 'gpg_keys/:key_id/revoke', feature_category: :authentication_and_authorization do
+ post 'gpg_keys/:key_id/revoke', feature_category: :system_access do
key = current_user.gpg_keys.find_by(id: params[:key_id])
not_found!('GPG Key') unless key
@@ -1186,7 +1186,7 @@ module API
requires :key_id, type: Integer, desc: 'The ID of the SSH key'
end
# rubocop: disable CodeReuse/ActiveRecord
- delete 'gpg_keys/:key_id', feature_category: :authentication_and_authorization do
+ delete 'gpg_keys/:key_id', feature_category: :system_access do
key = current_user.gpg_keys.find_by(id: params[:key_id])
not_found!('GPG Key') unless key
diff --git a/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens.rb b/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens.rb
index 82e607ac7a7..6f5ddec628d 100644
--- a/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens.rb
+++ b/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens.rb
@@ -12,7 +12,7 @@ module Gitlab
end
operation_name :update_all
- feature_category :authentication_and_authorization
+ feature_category :system_access
ADMIN_MODE_SCOPE = ['admin_mode'].freeze
diff --git a/lib/gitlab/background_migration/backfill_namespace_ldap_settings.rb b/lib/gitlab/background_migration/backfill_namespace_ldap_settings.rb
index 9283842046a..1a5ad1c14a6 100644
--- a/lib/gitlab/background_migration/backfill_namespace_ldap_settings.rb
+++ b/lib/gitlab/background_migration/backfill_namespace_ldap_settings.rb
@@ -5,7 +5,7 @@ module Gitlab
# Back-fill container_registry_size for project_statistics
class BackfillNamespaceLdapSettings < Gitlab::BackgroundMigration::BatchedMigrationJob
operation_name :backfill_namespace_ldap_settings
- feature_category :authentication_and_authorization
+ feature_category :system_access
def perform
# no-op in FOSS
diff --git a/lib/gitlab/github_import/clients/proxy.rb b/lib/gitlab/github_import/clients/proxy.rb
index b12df404640..afe313c5340 100644
--- a/lib/gitlab/github_import/clients/proxy.rb
+++ b/lib/gitlab/github_import/clients/proxy.rb
@@ -13,19 +13,11 @@ module Gitlab
def repos(search_text, options)
return { repos: filtered(client.repos, search_text) } if use_legacy?
- if use_graphql?
- fetch_repos_via_graphql(search_text, options)
- else
- fetch_repos_via_rest(search_text, options)
- end
+ fetch_repos_via_graphql(search_text, options)
end
private
- def fetch_repos_via_rest(search_text, options)
- { repos: client.search_repos_by_name(search_text, options)[:items] }
- end
-
def fetch_repos_via_graphql(search_text, options)
response = client.search_repos_by_name_graphql(search_text, options)
{
@@ -49,10 +41,6 @@ module Gitlab
def use_legacy?
Feature.disabled?(:remove_legacy_github_client)
end
-
- def use_graphql?
- Feature.enabled?(:github_client_fetch_repos_via_graphql)
- end
end
end
end
diff --git a/lib/gitlab/github_import/clients/search_repos.rb b/lib/gitlab/github_import/clients/search_repos.rb
index b72e5ac7751..3547173b4f8 100644
--- a/lib/gitlab/github_import/clients/search_repos.rb
+++ b/lib/gitlab/github_import/clients/search_repos.rb
@@ -13,14 +13,6 @@ module Gitlab
end
end
- def search_repos_by_name(name, options = {})
- search_query = search_repos_query(name, options)
-
- with_retry do
- octokit.search_repositories(search_query, options).to_h
- end
- end
-
private
def graphql_search_repos_body(name, options)
diff --git a/lib/gitlab/metrics/requests_rack_middleware.rb b/lib/gitlab/metrics/requests_rack_middleware.rb
index f635deabf76..b4baeba72e8 100644
--- a/lib/gitlab/metrics/requests_rack_middleware.rb
+++ b/lib/gitlab/metrics/requests_rack_middleware.rb
@@ -22,7 +22,7 @@ module Gitlab
# reasonable default. If we initialize every category we'll end up
# with an explosion in unused metric combinations, but we want the
# most common ones to be always present.
- FEATURE_CATEGORIES_TO_INITIALIZE = ['authentication_and_authorization',
+ FEATURE_CATEGORIES_TO_INITIALIZE = ['system_access',
'code_review_workflow', 'continuous_integration',
'not_owned', 'source_code_management',
FEATURE_CATEGORY_DEFAULT].freeze
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a4d914797cf..f86eaaa88a5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6781,6 +6781,9 @@ msgstr ""
msgid "Blame"
msgstr ""
+msgid "Blame could not be loaded as a single page."
+msgstr ""
+
msgid "BlobViewer|View on %{environmentName}"
msgstr ""
@@ -18006,9 +18009,6 @@ msgstr ""
msgid "For example, the application using the token or the purpose of the token. Do not give sensitive information for the name of the token, as it will be visible to all %{resource_type} members."
msgstr ""
-msgid "For faster browsing, not all history is shown."
-msgstr ""
-
msgid "For files larger than this limit, only index the file name. The file content is neither indexed nor searchable."
msgstr ""
@@ -25708,6 +25708,9 @@ msgstr ""
msgid "Loading files, directories, and submodules in the path %{path} for commit reference %{ref}"
msgstr ""
+msgid "Loading full blame..."
+msgstr ""
+
msgid "Loading more"
msgstr ""
@@ -40219,6 +40222,9 @@ msgstr ""
msgid "Show filters"
msgstr ""
+msgid "Show full blame"
+msgstr ""
+
msgid "Show group milestones"
msgstr ""
@@ -47461,6 +47467,9 @@ msgstr ""
msgid "View blame"
msgstr ""
+msgid "View blame as separate pages"
+msgstr ""
+
msgid "View blame prior to this change"
msgstr ""
@@ -47490,9 +47499,6 @@ msgstr ""
msgid "View eligible approvers"
msgstr ""
-msgid "View entire blame"
-msgstr ""
-
msgid "View exposed artifact"
msgid_plural "View %d exposed artifacts"
msgstr[0] ""
@@ -47695,6 +47701,21 @@ msgstr ""
msgid "VulnerabilityChart|Severity"
msgstr ""
+msgid "VulnerabilityDismissalReasons|Acceptable risk"
+msgstr ""
+
+msgid "VulnerabilityDismissalReasons|False positive"
+msgstr ""
+
+msgid "VulnerabilityDismissalReasons|Mitigating control"
+msgstr ""
+
+msgid "VulnerabilityDismissalReasons|Not applicable"
+msgstr ""
+
+msgid "VulnerabilityDismissalReasons|Used in tests"
+msgstr ""
+
msgid "VulnerabilityManagement|%{statusStart}Confirmed%{statusEnd} %{timeago} by %{user}"
msgstr ""
diff --git a/package.json b/package.json
index c5976b96ce3..cec754ca1a2 100644
--- a/package.json
+++ b/package.json
@@ -59,6 +59,7 @@
"@gitlab/ui": "56.2.0",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20230223005157",
+ "@mattiasbuelens/web-streams-adapter": "^0.1.0",
"@rails/actioncable": "6.1.4-7",
"@rails/ujs": "6.1.4-7",
"@sourcegraph/code-host-integration": "0.0.84",
@@ -197,6 +198,7 @@
"vue-virtual-scroll-list": "^1.4.7",
"vuedraggable": "^2.23.0",
"vuex": "^3.6.2",
+ "web-streams-polyfill": "^3.2.1",
"web-vitals": "^0.2.4",
"webpack": "^4.46.0",
"webpack-bundle-analyzer": "^4.6.1",
diff --git a/qa/qa/specs/features/api/1_manage/group_access_token_spec.rb b/qa/qa/specs/features/api/1_manage/group_access_token_spec.rb
index e0db758dde3..87218442c76 100644
--- a/qa/qa/specs/features/api/1_manage/group_access_token_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/group_access_token_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Manage' do
- describe 'Group access token', product_group: :authentication_and_authorization do
+ describe 'Group access token', product_group: :system_access do
let(:group_access_token) { QA::Resource::GroupAccessToken.fabricate_via_api! }
let(:api_client) { Runtime::API::Client.new(:gitlab, personal_access_token: group_access_token.token) }
let(:project) do
diff --git a/qa/qa/specs/features/api/1_manage/project_access_token_spec.rb b/qa/qa/specs/features/api/1_manage/project_access_token_spec.rb
index d693bbd43ff..a85d8fa3327 100644
--- a/qa/qa/specs/features/api/1_manage/project_access_token_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/project_access_token_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Manage' do
- describe 'Project access token', product_group: :authentication_and_authorization do
+ describe 'Project access token', product_group: :system_access do
before(:all) do
@project_access_token = QA::Resource::ProjectAccessToken.fabricate_via_api! do |pat|
pat.project = Resource::Project.fabricate_via_api! do |project|
diff --git a/qa/qa/specs/features/api/1_manage/user_access_termination_spec.rb b/qa/qa/specs/features/api/1_manage/user_access_termination_spec.rb
index b7d0d72297a..9903c7f1f01 100644
--- a/qa/qa/specs/features/api/1_manage/user_access_termination_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/user_access_termination_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Manage' do
- describe 'User', :requires_admin, :reliable, product_group: :authentication_and_authorization do
+ describe 'User', :requires_admin, :reliable, product_group: :system_access do
before(:all) do
admin_api_client = Runtime::API::Client.as_admin
diff --git a/qa/qa/specs/features/browser_ui/1_manage/group/group_access_token_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/group/group_access_token_spec.rb
index a35cde854a2..f095a902e11 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/group/group_access_token_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/group/group_access_token_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Manage' do
- describe 'Group access tokens', product_group: :authentication_and_authorization do
+ describe 'Group access tokens', product_group: :system_access do
let(:group_access_token) { QA::Resource::GroupAccessToken.fabricate_via_browser_ui! }
it(
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb
index 0f3d6a104a7..3b048ecbe24 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Manage', :requires_admin, :skip_live_env, :reliable do
- describe '2FA', product_group: :authentication_and_authorization do
+ describe '2FA', product_group: :system_access do
let(:owner_user) do
Resource::User.fabricate_via_api! do |usr|
usr.api_client = admin_api_client
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb
index 9484f15f35d..d124048cc89 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Manage', :reliable, :requires_admin, :skip_live_env, product_group: :authentication_and_authorization do
+ context 'Manage', :reliable, :requires_admin, :skip_live_env, product_group: :system_access do
describe '2FA' do
let!(:user) { Resource::User.fabricate_via_api! }
let!(:user_api_client) { Runtime::API::Client.new(:gitlab, user: user) }
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
index 7b91156d926..344cbefcea9 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Manage', :smoke, :mobile, product_group: :authentication_and_authorization do
+ RSpec.describe 'Manage', :smoke, :mobile, product_group: :system_access do
describe 'basic user login' do
it 'user logs in using basic credentials and logs out', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347880' do
Flow::Login.sign_in
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb
index cf9282c1149..e04d688fdee 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Manage', :requires_admin, :skip_live_env, product_group: :authentication_and_authorization do
+ RSpec.describe 'Manage', :requires_admin, :skip_live_env, product_group: :system_access do
describe '2FA' do
let(:admin_api_client) { Runtime::API::Client.as_admin }
let(:owner_api_client) { Runtime::API::Client.new(:gitlab, user: owner_user) }
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
index 3d2e8c13900..3db8fdc5885 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Manage', :orchestrated, :ldap_no_tls, :ldap_tls, product_group: :authentication_and_authorization do
+ RSpec.describe 'Manage', :orchestrated, :ldap_no_tls, :ldap_tls, product_group: :system_access do
describe 'LDAP login' do
it 'user logs into GitLab using LDAP credentials', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347892' do
Flow::Login.sign_in
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb
index 388c9f6b486..5132887f4dc 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Manage', :orchestrated, :mattermost, product_group: :authentication_and_authorization do
+ RSpec.describe 'Manage', :orchestrated, :mattermost, product_group: :system_access do
describe 'Mattermost login' do
it 'user logs into Mattermost using GitLab OAuth', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347891' do
Flow::Login.sign_in
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb
index ca10a3f3d65..4a67668337a 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Manage', :orchestrated, :instance_saml, product_group: :authentication_and_authorization do
+ RSpec.describe 'Manage', :orchestrated, :instance_saml, product_group: :system_access do
describe 'Instance wide SAML SSO' do
it(
'user logs in to gitlab with SAML SSO',
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb
index dd39b0c8835..20befbae773 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Manage', only: { subdomain: %i[staging staging-canary] }, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/344213', type: :stale } do
- describe 'basic user', product_group: :authentication_and_authorization do
+ describe 'basic user', product_group: :system_access do
it 'remains logged in when redirected from canary to non-canary node', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347626' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
index 3f5842d756e..22f4d007b01 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
@@ -15,7 +15,7 @@ module QA
end
end
- RSpec.describe 'Manage', :skip_signup_disabled, :requires_admin, product_group: :authentication_and_authorization do
+ RSpec.describe 'Manage', :skip_signup_disabled, :requires_admin, product_group: :system_access do
describe 'while LDAP is enabled', :orchestrated, :ldap_no_tls, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347934' do
before do
# When LDAP is enabled, a previous test might have created a token for the LDAP 'tanuki' user who is not an admin
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/project_access_token_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/project_access_token_spec.rb
index 55f63845acd..76ba541d052 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/project_access_token_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/project_access_token_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Manage' do
- describe 'Project access tokens', :reliable, product_group: :authentication_and_authorization do
+ describe 'Project access tokens', :reliable, product_group: :system_access do
let(:project_access_token) { QA::Resource::ProjectAccessToken.fabricate_via_browser_ui! }
it(
diff --git a/qa/qa/specs/features/browser_ui/1_manage/user/impersonation_token_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/user/impersonation_token_spec.rb
index ce5d9307769..2bd87cd9ea2 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/user/impersonation_token_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/user/impersonation_token_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Manage' do
- describe 'Impersonation tokens', :requires_admin, product_group: :authentication_and_authorization do
+ describe 'Impersonation tokens', :requires_admin, product_group: :system_access do
let(:admin_api_client) { Runtime::API::Client.as_admin }
let!(:user) do
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index 406a3604b23..b544c0615b3 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -161,9 +161,7 @@ RSpec.describe Import::GithubController, feature_category: :importers do
let(:provider_repos) { [] }
let(:expected_filter) { '' }
let(:expected_options) do
- pagination_params.merge(relation_params).merge(
- first: 25, page: 1, per_page: 25
- )
+ pagination_params.merge(relation_params).merge(first: 25)
end
before do
@@ -279,22 +277,12 @@ RSpec.describe Import::GithubController, feature_category: :importers do
it_behaves_like 'calls repos through Clients::Proxy with expected args'
end
-
- context 'when page is specified' do
- let(:pagination_params) { { before: nil, after: nil, page: 2 } }
- let(:params) { pagination_params }
- let(:expected_options) do
- pagination_params.merge(relation_params).merge(first: 25, page: 2, per_page: 25)
- end
-
- it_behaves_like 'calls repos through Clients::Proxy with expected args'
- end
end
context 'when relation type params present' do
let(:organization_login) { 'test-login' }
let(:params) { pagination_params.merge(relation_type: 'organization', organization_login: organization_login) }
- let(:pagination_defaults) { { first: 25, page: 1, per_page: 25 } }
+ let(:pagination_defaults) { { first: 25 } }
let(:expected_options) do
pagination_defaults.merge(pagination_params).merge(
relation_type: 'organization', organization_login: organization_login
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index bd9e9f1c4cf..69eae1c00db 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -10,7 +10,8 @@ RSpec.describe 'Database schema', feature_category: :database do
let(:columns_name_with_jsonb) { retrieve_columns_name_with_jsonb }
IGNORED_INDEXES_ON_FKS = {
- slack_integrations_scopes: %w[slack_api_scope_id]
+ slack_integrations_scopes: %w[slack_api_scope_id],
+ p_ci_builds_metadata: %w[partition_id] # composable FK, the columns are reversed in the index definition
}.with_indifferent_access.freeze
TABLE_PARTITIONS = %w[ci_builds_metadata].freeze
@@ -39,7 +40,7 @@ RSpec.describe 'Database schema', feature_category: :database do
ci_build_trace_metadata: %w[partition_id build_id],
ci_builds: %w[erased_by_id trigger_request_id partition_id],
ci_builds_runner_session: %w[partition_id build_id],
- p_ci_builds_metadata: %w[partition_id],
+ p_ci_builds_metadata: %w[partition_id build_id],
ci_job_artifacts: %w[partition_id job_id],
ci_job_variables: %w[partition_id job_id],
ci_namespace_monthly_usages: %w[namespace_id],
diff --git a/spec/features/merge_request/real_time_merge_widget_spec.rb b/spec/features/merge_request/real_time_merge_widget_spec.rb
new file mode 100644
index 00000000000..299651feb53
--- /dev/null
+++ b/spec/features/merge_request/real_time_merge_widget_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Merge request > Real-time merge widget', :js, feature_category: :code_review_workflow do
+ let_it_be_with_reload(:project) { create(:project, :public, :repository) }
+ let(:user) { project.creator }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project, author: user) }
+
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ context 'when merge status changes' do
+ let(:trigger_action) do
+ # There are different service classes that can change the merge_status
+ # so we simulate it here.
+ merge_request.mark_as_unchecked!
+ GraphqlTriggers.merge_request_merge_status_updated(merge_request)
+ end
+
+ let(:widget_text) { s_('mrWidget|Checking if merge request can be merged…') }
+
+ it_behaves_like 'updates merge widget in real-time'
+ end
+
+ context 'when MR gets closed' do
+ let(:trigger_action) do
+ MergeRequests::CloseService
+ .new(project: project, current_user: user)
+ .execute(merge_request)
+ end
+
+ let(:widget_text) { s_('mrWidget|Closed by') }
+
+ it_behaves_like 'updates merge widget in real-time'
+ end
+
+ context 'when MR gets marked as draft' do
+ let(:trigger_action) do
+ MergeRequests::UpdateService
+ .new(project: project, current_user: user, params: { title: 'Draft: title' })
+ .execute(merge_request)
+ end
+
+ let(:widget_text) { 'Merge blocked: Select Mark as ready to remove it from Draft status.' }
+
+ it_behaves_like 'updates merge widget in real-time'
+ end
+
+ context 'when MR gets approved' do
+ let(:trigger_action) do
+ MergeRequests::ApprovalService
+ .new(project: project, current_user: user)
+ .execute(merge_request)
+ end
+
+ let(:widget_text) { _('Ready to merge!') }
+
+ before do
+ merge_request.update!(approvals_before_merge: 1)
+ end
+
+ it_behaves_like 'updates merge widget in real-time'
+ end
+
+ context 'when a new discussion is started and all threads must be resolved before merge' do
+ let(:trigger_action) do
+ Notes::CreateService.new(project, user, {
+ merge_request_diff_head_sha: merge_request.diff_head_sha,
+ noteable_id: merge_request.id,
+ noteable_type: merge_request.class.name,
+ note: 'Unresolved discussion',
+ type: 'DiscussionNote'
+ }).execute
+ end
+
+ let(:widget_text) { s_('mrWidget|Merge blocked: all threads must be resolved.') }
+
+ before do
+ project.update!(only_allow_merge_if_all_discussions_are_resolved: true)
+ end
+
+ it_behaves_like 'updates merge widget in real-time'
+ end
+end
diff --git a/spec/features/projects/blobs/blame_spec.rb b/spec/features/projects/blobs/blame_spec.rb
index 27b7c6ef2d5..d3558af81b8 100644
--- a/spec/features/projects/blobs/blame_spec.rb
+++ b/spec/features/projects/blobs/blame_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe 'File blame', :js, feature_category: :projects do
within '[data-testid="blob-content-holder"]' do
expect(page).to have_css('.blame-commit')
expect(page).not_to have_css('.gl-pagination')
- expect(page).not_to have_link _('View entire blame')
+ expect(page).not_to have_link _('Show full blame')
end
end
@@ -53,7 +53,7 @@ RSpec.describe 'File blame', :js, feature_category: :projects do
within '[data-testid="blob-content-holder"]' do
expect(page).to have_css('.blame-commit')
expect(page).to have_css('.gl-pagination')
- expect(page).to have_link _('View entire blame')
+ expect(page).to have_link _('Show full blame')
expect(page).to have_css('#L1')
expect(page).not_to have_css('#L3')
@@ -85,19 +85,42 @@ RSpec.describe 'File blame', :js, feature_category: :projects do
end
end
- context 'when user clicks on View entire blame button' do
+ shared_examples 'a full blame page' do
+ context 'when user clicks on Show full blame button' do
+ before do
+ visit_blob_blame(path)
+ click_link _('Show full blame')
+ end
+
+ it 'displays the blame page without pagination' do
+ within '[data-testid="blob-content-holder"]' do
+ expect(page).to have_css('#L1')
+ expect(page).to have_css('#L667')
+ expect(page).not_to have_css('.gl-pagination')
+ end
+ end
+ end
+ end
+
+ context 'when streaming is disabled' do
before do
- visit_blob_blame(path)
+ stub_feature_flags(blame_page_streaming: false)
end
- it 'displays the blame page without pagination' do
- within '[data-testid="blob-content-holder"]' do
- click_link _('View entire blame')
+ it_behaves_like 'a full blame page'
+ end
- expect(page).to have_css('#L1')
- expect(page).to have_css('#L3')
- expect(page).not_to have_css('.gl-pagination')
- end
+ context 'when streaming is enabled' do
+ before do
+ stub_const('Projects::BlameService::STREAMING_PER_PAGE', 50)
+ end
+
+ it_behaves_like 'a full blame page'
+
+ it 'shows loading text' do
+ visit_blob_blame(path)
+ click_link _('Show full blame')
+ expect(page).to have_text('Loading full blame...')
end
end
@@ -112,7 +135,7 @@ RSpec.describe 'File blame', :js, feature_category: :projects do
within '[data-testid="blob-content-holder"]' do
expect(page).to have_css('.blame-commit')
expect(page).not_to have_css('.gl-pagination')
- expect(page).not_to have_link _('View entire blame')
+ expect(page).not_to have_link _('Show full blame')
end
end
end
diff --git a/spec/frontend/__helpers__/shared_test_setup.js b/spec/frontend/__helpers__/shared_test_setup.js
index 2fe9fe89a90..7fc81cf6548 100644
--- a/spec/frontend/__helpers__/shared_test_setup.js
+++ b/spec/frontend/__helpers__/shared_test_setup.js
@@ -1,4 +1,5 @@
/* Common setup for both unit and integration test environments */
+import { ReadableStream, WritableStream } from 'node:stream/web';
import * as jqueryMatchers from 'custom-jquery-matchers';
import Vue from 'vue';
import { enableAutoDestroy } from '@vue/test-utils';
@@ -13,6 +14,9 @@ import './dom_shims';
import './jquery';
import '~/commons/bootstrap';
+global.ReadableStream = ReadableStream;
+global.WritableStream = WritableStream;
+
enableAutoDestroy(afterEach);
// This module has some fairly decent visual test coverage in it's own repository.
diff --git a/spec/frontend/__mocks__/lodash/debounce.js b/spec/frontend/__mocks__/lodash/debounce.js
index d4fe2ce5406..15f806fc31a 100644
--- a/spec/frontend/__mocks__/lodash/debounce.js
+++ b/spec/frontend/__mocks__/lodash/debounce.js
@@ -9,9 +9,22 @@
// Further reference: https://github.com/facebook/jest/issues/3465
export default (fn) => {
- const debouncedFn = jest.fn().mockImplementation(fn);
- debouncedFn.cancel = jest.fn();
- debouncedFn.flush = jest.fn().mockImplementation(() => {
+ let id;
+ const debouncedFn = jest.fn(function run(...args) {
+ // this is calculated in runtime so beforeAll hook works in tests
+ const timeout = global.JEST_DEBOUNCE_THROTTLE_TIMEOUT;
+ if (timeout) {
+ id = setTimeout(() => {
+ fn.apply(this, args);
+ }, timeout);
+ } else {
+ fn.apply(this, args);
+ }
+ });
+ debouncedFn.cancel = jest.fn(() => {
+ clearTimeout(id);
+ });
+ debouncedFn.flush = jest.fn(() => {
const errorMessage =
"The .flush() method returned by lodash.debounce is not yet implemented/mocked by the mock in 'spec/frontend/__mocks__/lodash/debounce.js'.";
diff --git a/spec/frontend/__mocks__/lodash/throttle.js b/spec/frontend/__mocks__/lodash/throttle.js
index e8a82654c78..b1014662918 100644
--- a/spec/frontend/__mocks__/lodash/throttle.js
+++ b/spec/frontend/__mocks__/lodash/throttle.js
@@ -1,4 +1,4 @@
// Similar to `lodash/debounce`, `lodash/throttle` also causes flaky specs.
// See `./debounce.js` for more details.
-export default (fn) => fn;
+export { default } from './debounce';
diff --git a/spec/frontend/blame/streaming/index_spec.js b/spec/frontend/blame/streaming/index_spec.js
new file mode 100644
index 00000000000..a5069f8a7d8
--- /dev/null
+++ b/spec/frontend/blame/streaming/index_spec.js
@@ -0,0 +1,110 @@
+import waitForPromises from 'helpers/wait_for_promises';
+import { renderBlamePageStreams } from '~/blame/streaming';
+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 { toPolyfillReadable } from '~/streaming/polyfills';
+import { createAlert } from '~/flash';
+
+jest.mock('~/streaming/render_html_streams');
+jest.mock('~/streaming/rate_limit_stream_requests');
+jest.mock('~/streaming/handle_streamed_anchor_link');
+jest.mock('~/streaming/polyfills');
+jest.mock('~/sentry');
+jest.mock('~/flash');
+
+global.fetch = jest.fn();
+
+describe('renderBlamePageStreams', () => {
+ let stopAnchor;
+ const PAGES_URL = 'https://example.com/';
+ const findStreamContainer = () => document.querySelector('#blame-stream-container');
+ const findStreamLoadingIndicator = () => document.querySelector('#blame-stream-loading');
+
+ const setupHtml = (totalExtraPages = 0) => {
+ setHTMLFixture(`
+ <div id="blob-content-holder"
+ data-total-extra-pages="${totalExtraPages}"
+ data-pages-url="${PAGES_URL}"
+ ></div>
+ <div id="blame-stream-container"></div>
+ <div id="blame-stream-loading"></div>
+ `);
+ };
+
+ handleStreamedAnchorLink.mockImplementation(() => stopAnchor);
+ rateLimitStreamRequests.mockImplementation(({ factory, total }) => {
+ return Array.from({ length: total }, (_, i) => {
+ return Promise.resolve(factory(i));
+ });
+ });
+ toPolyfillReadable.mockImplementation((obj) => obj);
+
+ beforeEach(() => {
+ stopAnchor = jest.fn();
+ fetch.mockClear();
+ });
+
+ it('does nothing for an empty page', async () => {
+ await renderBlamePageStreams();
+
+ expect(handleStreamedAnchorLink).not.toHaveBeenCalled();
+ expect(renderHtmlStreams).not.toHaveBeenCalled();
+ });
+
+ it('renders a single stream', async () => {
+ let res;
+ const stream = new Promise((resolve) => {
+ res = resolve;
+ });
+ renderHtmlStreams.mockImplementationOnce(() => stream);
+ setupHtml();
+
+ renderBlamePageStreams(stream);
+
+ expect(handleStreamedAnchorLink).toHaveBeenCalledTimes(1);
+ expect(stopAnchor).toHaveBeenCalledTimes(0);
+ expect(renderHtmlStreams).toHaveBeenCalledWith([stream], findStreamContainer());
+ expect(findStreamLoadingIndicator()).not.toBe(null);
+
+ res();
+ await waitForPromises();
+
+ expect(stopAnchor).toHaveBeenCalledTimes(1);
+ expect(findStreamLoadingIndicator()).toBe(null);
+ });
+
+ it('renders rest of the streams', async () => {
+ const stream = Promise.resolve();
+ const stream2 = Promise.resolve({ body: null });
+ fetch.mockImplementationOnce(() => stream2);
+ setupHtml(1);
+
+ await renderBlamePageStreams(stream);
+
+ expect(fetch.mock.calls[0][0].toString()).toBe(`${PAGES_URL}?page=3`);
+ expect(renderHtmlStreams).toHaveBeenCalledWith([stream, stream2], findStreamContainer());
+ });
+
+ it('shows an error message when failed', async () => {
+ const stream = Promise.resolve();
+ const error = new Error();
+ renderHtmlStreams.mockImplementationOnce(() => Promise.reject(error));
+ setupHtml();
+
+ try {
+ await renderBlamePageStreams(stream);
+ } catch (err) {
+ expect(err).toBe(error);
+ }
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Blame could not be loaded as a single page.',
+ primaryButton: {
+ text: 'View blame as separate pages',
+ clickHandler: expect.any(Function),
+ },
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
index f6d5833edee..ce43e648b43 100644
--- a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
@@ -1,7 +1,9 @@
-import { mount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import EditorHeader from '~/ide/components/commit_sidebar/editor_header.vue';
+import { stubComponent } from 'helpers/stub_component';
import { createStore } from '~/ide/stores';
import { file } from '../../helpers';
@@ -12,9 +14,10 @@ const TEST_FILE_PATH = 'test/file/path';
describe('IDE commit editor header', () => {
let wrapper;
let store;
+ const showMock = jest.fn();
const createComponent = (fileProps = {}) => {
- wrapper = mount(EditorHeader, {
+ wrapper = shallowMount(EditorHeader, {
store,
propsData: {
activeFile: {
@@ -23,22 +26,17 @@ describe('IDE commit editor header', () => {
...fileProps,
},
},
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ methods: { show: showMock },
+ }),
+ },
});
};
const findDiscardModal = () => wrapper.findComponent({ ref: 'discardModal' });
const findDiscardButton = () => wrapper.findComponent({ ref: 'discardButton' });
- beforeEach(() => {
- store = createStore();
- jest.spyOn(store, 'dispatch').mockImplementation();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it.each`
fileProps | shouldExist
${{ staged: false, changed: false }} | ${false}
@@ -52,20 +50,19 @@ describe('IDE commit editor header', () => {
});
describe('discard button', () => {
- beforeEach(() => {
+ it('opens a dialog confirming discard', () => {
createComponent();
+ findDiscardButton().vm.$emit('click');
- const modal = findDiscardModal();
- jest.spyOn(modal.vm, 'show');
-
- findDiscardButton().trigger('click');
- });
-
- it('opens a dialog confirming discard', () => {
- expect(findDiscardModal().vm.show).toHaveBeenCalled();
+ expect(showMock).toHaveBeenCalled();
});
it('calls discardFileChanges if dialog result is confirmed', () => {
+ store = createStore();
+ jest.spyOn(store, 'dispatch').mockImplementation();
+
+ createComponent();
+
expect(store.dispatch).not.toHaveBeenCalled();
findDiscardModal().vm.$emit('primary');
diff --git a/spec/frontend/streaming/chunk_writer_spec.js b/spec/frontend/streaming/chunk_writer_spec.js
new file mode 100644
index 00000000000..2aadb332838
--- /dev/null
+++ b/spec/frontend/streaming/chunk_writer_spec.js
@@ -0,0 +1,214 @@
+import { ChunkWriter } from '~/streaming/chunk_writer';
+import { RenderBalancer } from '~/streaming/render_balancer';
+
+jest.mock('~/streaming/render_balancer');
+
+describe('ChunkWriter', () => {
+ let accumulator = '';
+ let write;
+ let close;
+ let abort;
+ let config;
+ let render;
+
+ const createChunk = (text) => {
+ const encoder = new TextEncoder();
+ return encoder.encode(text);
+ };
+
+ const createHtmlStream = () => {
+ write = jest.fn((part) => {
+ accumulator += part;
+ });
+ close = jest.fn();
+ abort = jest.fn();
+ return {
+ write,
+ close,
+ abort,
+ };
+ };
+
+ const createWriter = () => {
+ return new ChunkWriter(createHtmlStream(), config);
+ };
+
+ const pushChunks = (...chunks) => {
+ const writer = createWriter();
+ chunks.forEach((chunk) => {
+ writer.write(createChunk(chunk));
+ });
+ writer.close();
+ };
+
+ afterAll(() => {
+ global.JEST_DEBOUNCE_THROTTLE_TIMEOUT = undefined;
+ });
+
+ beforeEach(() => {
+ global.JEST_DEBOUNCE_THROTTLE_TIMEOUT = 100;
+ accumulator = '';
+ config = undefined;
+ render = jest.fn((cb) => {
+ while (cb()) {
+ // render until 'false'
+ }
+ });
+ RenderBalancer.mockImplementation(() => ({ render }));
+ });
+
+ describe('when chunk length must be "1"', () => {
+ beforeEach(() => {
+ config = { minChunkSize: 1, maxChunkSize: 1 };
+ });
+
+ it('splits big chunks into smaller ones', () => {
+ const text = 'foobar';
+ pushChunks(text);
+ expect(accumulator).toBe(text);
+ expect(write).toHaveBeenCalledTimes(text.length);
+ });
+
+ it('handles small emoji chunks', () => {
+ const text = 'foo👀bar👨‍👩‍👧baz👧👧🏻👧🏼👧🏽👧🏾👧🏿';
+ pushChunks(text);
+ expect(accumulator).toBe(text);
+ expect(write).toHaveBeenCalledTimes(createChunk(text).length);
+ });
+ });
+
+ describe('when chunk length must not be lower than "5" and exceed "10"', () => {
+ beforeEach(() => {
+ config = { minChunkSize: 5, maxChunkSize: 10 };
+ });
+
+ it('joins small chunks', () => {
+ const text = '12345';
+ pushChunks(...text.split(''));
+ expect(accumulator).toBe(text);
+ expect(write).toHaveBeenCalledTimes(1);
+ expect(close).toHaveBeenCalledTimes(1);
+ });
+
+ it('handles overflow with small chunks', () => {
+ const text = '123456789';
+ pushChunks(...text.split(''));
+ expect(accumulator).toBe(text);
+ expect(write).toHaveBeenCalledTimes(2);
+ expect(close).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls flush on small chunks', () => {
+ global.JEST_DEBOUNCE_THROTTLE_TIMEOUT = undefined;
+ const flushAccumulator = jest.spyOn(ChunkWriter.prototype, 'flushAccumulator');
+ const text = '1';
+ pushChunks(text);
+ expect(accumulator).toBe(text);
+ expect(flushAccumulator).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls flush on large chunks', () => {
+ const flushAccumulator = jest.spyOn(ChunkWriter.prototype, 'flushAccumulator');
+ const text = '1234567890123';
+ const writer = createWriter();
+ writer.write(createChunk(text));
+ jest.runAllTimers();
+ expect(accumulator).toBe(text);
+ expect(flushAccumulator).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('chunk balancing', () => {
+ let increase;
+ let decrease;
+ let renderOnce;
+
+ beforeEach(() => {
+ render = jest.fn((cb) => {
+ let next = true;
+ renderOnce = () => {
+ if (!next) return;
+ next = cb();
+ };
+ });
+ RenderBalancer.mockImplementation(({ increase: inc, decrease: dec }) => {
+ increase = jest.fn(inc);
+ decrease = jest.fn(dec);
+ return {
+ render,
+ };
+ });
+ });
+
+ describe('when frame time exceeds low limit', () => {
+ beforeEach(() => {
+ config = {
+ minChunkSize: 1,
+ maxChunkSize: 5,
+ balanceRate: 10,
+ };
+ });
+
+ it('increases chunk size', () => {
+ const text = '111222223';
+ const writer = createWriter();
+ const chunk = createChunk(text);
+
+ writer.write(chunk);
+
+ renderOnce();
+ increase();
+ renderOnce();
+ renderOnce();
+
+ writer.close();
+
+ expect(accumulator).toBe(text);
+ expect(write.mock.calls).toMatchObject([['111'], ['22222'], ['3']]);
+ expect(close).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('when frame time exceeds high limit', () => {
+ beforeEach(() => {
+ config = {
+ minChunkSize: 1,
+ maxChunkSize: 10,
+ balanceRate: 2,
+ };
+ });
+
+ it('decreases chunk size', () => {
+ const text = '1111112223345';
+ const writer = createWriter();
+ const chunk = createChunk(text);
+
+ writer.write(chunk);
+
+ renderOnce();
+ decrease();
+
+ renderOnce();
+ decrease();
+
+ renderOnce();
+ decrease();
+
+ renderOnce();
+ renderOnce();
+
+ writer.close();
+
+ expect(accumulator).toBe(text);
+ expect(write.mock.calls).toMatchObject([['111111'], ['222'], ['33'], ['4'], ['5']]);
+ expect(close).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
+ it('calls abort on htmlStream', () => {
+ const writer = createWriter();
+ writer.abort();
+ expect(abort).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/spec/frontend/streaming/handle_streamed_anchor_link_spec.js b/spec/frontend/streaming/handle_streamed_anchor_link_spec.js
new file mode 100644
index 00000000000..ef17957b2fc
--- /dev/null
+++ b/spec/frontend/streaming/handle_streamed_anchor_link_spec.js
@@ -0,0 +1,132 @@
+import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures';
+import waitForPromises from 'helpers/wait_for_promises';
+import { handleStreamedAnchorLink } from '~/streaming/handle_streamed_anchor_link';
+import { scrollToElement } from '~/lib/utils/common_utils';
+import LineHighlighter from '~/blob/line_highlighter';
+import { TEST_HOST } from 'spec/test_constants';
+
+jest.mock('~/lib/utils/common_utils');
+jest.mock('~/blob/line_highlighter');
+
+describe('handleStreamedAnchorLink', () => {
+ const ANCHOR_START = 'L100';
+ const ANCHOR_END = '300';
+ const findRoot = () => document.querySelector('#root');
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ describe('when single line anchor is given', () => {
+ beforeEach(() => {
+ delete window.location;
+ window.location = new URL(`${TEST_HOST}#${ANCHOR_START}`);
+ });
+
+ describe('when element is present', () => {
+ beforeEach(() => {
+ setHTMLFixture(`<div id="root"><div id="${ANCHOR_START}"></div></div>`);
+ handleStreamedAnchorLink(findRoot());
+ });
+
+ it('does nothing', async () => {
+ await waitForPromises();
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when element is streamed', () => {
+ let stop;
+ const insertElement = () => {
+ findRoot().insertAdjacentHTML('afterbegin', `<div id="${ANCHOR_START}"></div>`);
+ };
+
+ beforeEach(() => {
+ setHTMLFixture('<div id="root"></div>');
+ stop = handleStreamedAnchorLink(findRoot());
+ });
+
+ afterEach(() => {
+ stop = undefined;
+ });
+
+ it('scrolls to the anchor when inserted', async () => {
+ insertElement();
+ await waitForPromises();
+ expect(scrollToElement).toHaveBeenCalledTimes(1);
+ expect(LineHighlighter).toHaveBeenCalledTimes(1);
+ });
+
+ it("doesn't scroll to the anchor when destroyed", async () => {
+ stop();
+ insertElement();
+ await waitForPromises();
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when line range anchor is given', () => {
+ beforeEach(() => {
+ delete window.location;
+ window.location = new URL(`${TEST_HOST}#${ANCHOR_START}-${ANCHOR_END}`);
+ });
+
+ describe('when last element is present', () => {
+ beforeEach(() => {
+ setHTMLFixture(`<div id="root"><div id="L${ANCHOR_END}"></div></div>`);
+ handleStreamedAnchorLink(findRoot());
+ });
+
+ it('does nothing', async () => {
+ await waitForPromises();
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when last element is streamed', () => {
+ let stop;
+ const insertElement = () => {
+ findRoot().insertAdjacentHTML(
+ 'afterbegin',
+ `<div id="${ANCHOR_START}"></div><div id="L${ANCHOR_END}"></div>`,
+ );
+ };
+
+ beforeEach(() => {
+ setHTMLFixture('<div id="root"></div>');
+ stop = handleStreamedAnchorLink(findRoot());
+ });
+
+ afterEach(() => {
+ stop = undefined;
+ });
+
+ it('scrolls to the anchor when inserted', async () => {
+ insertElement();
+ await waitForPromises();
+ expect(scrollToElement).toHaveBeenCalledTimes(1);
+ expect(LineHighlighter).toHaveBeenCalledTimes(1);
+ });
+
+ it("doesn't scroll to the anchor when destroyed", async () => {
+ stop();
+ insertElement();
+ await waitForPromises();
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when anchor is not given', () => {
+ beforeEach(() => {
+ setHTMLFixture(`<div id="root"></div>`);
+ handleStreamedAnchorLink(findRoot());
+ });
+
+ it('does nothing', async () => {
+ await waitForPromises();
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/streaming/html_stream_spec.js b/spec/frontend/streaming/html_stream_spec.js
new file mode 100644
index 00000000000..115a9ddc803
--- /dev/null
+++ b/spec/frontend/streaming/html_stream_spec.js
@@ -0,0 +1,46 @@
+import { HtmlStream } from '~/streaming/html_stream';
+import { ChunkWriter } from '~/streaming/chunk_writer';
+
+jest.mock('~/streaming/chunk_writer');
+
+describe('HtmlStream', () => {
+ let write;
+ let close;
+ let streamingElement;
+
+ beforeEach(() => {
+ write = jest.fn();
+ close = jest.fn();
+ jest.spyOn(Document.prototype, 'write').mockImplementation(write);
+ jest.spyOn(Document.prototype, 'close').mockImplementation(close);
+ jest.spyOn(Document.prototype, 'querySelector').mockImplementation(() => {
+ streamingElement = document.createElement('div');
+ return streamingElement;
+ });
+ });
+
+ it('attaches to original document', () => {
+ // eslint-disable-next-line no-new
+ new HtmlStream(document.body);
+ expect(document.body.contains(streamingElement)).toBe(true);
+ });
+
+ it('can write to a document', () => {
+ const htmlStream = new HtmlStream(document.body);
+ htmlStream.write('foo');
+ htmlStream.close();
+ expect(write.mock.calls).toEqual([['<streaming-element>'], ['foo'], ['</streaming-element>']]);
+ expect(close).toHaveBeenCalledTimes(1);
+ });
+
+ it('returns chunked writer', () => {
+ const htmlStream = new HtmlStream(document.body).withChunkWriter();
+ expect(htmlStream).toBeInstanceOf(ChunkWriter);
+ });
+
+ it('closes on abort', () => {
+ const htmlStream = new HtmlStream(document.body);
+ htmlStream.abort();
+ expect(close).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/streaming/rate_limit_stream_requests_spec.js b/spec/frontend/streaming/rate_limit_stream_requests_spec.js
new file mode 100644
index 00000000000..02e3cf93014
--- /dev/null
+++ b/spec/frontend/streaming/rate_limit_stream_requests_spec.js
@@ -0,0 +1,155 @@
+import waitForPromises from 'helpers/wait_for_promises';
+import { rateLimitStreamRequests } from '~/streaming/rate_limit_stream_requests';
+
+describe('rateLimitStreamRequests', () => {
+ const encoder = new TextEncoder('utf-8');
+ const createStreamResponse = (content = 'foo') =>
+ new ReadableStream({
+ pull(controller) {
+ controller.enqueue(encoder.encode(content));
+ controller.close();
+ },
+ });
+
+ const createFactory = (content) => {
+ return jest.fn(() => {
+ return Promise.resolve(createStreamResponse(content));
+ });
+ };
+
+ it('does nothing for zero total requests', () => {
+ const factory = jest.fn();
+ const requests = rateLimitStreamRequests({
+ factory,
+ total: 0,
+ });
+ expect(factory).toHaveBeenCalledTimes(0);
+ expect(requests.length).toBe(0);
+ });
+
+ it('does not exceed total requests', () => {
+ const factory = createFactory();
+ const requests = rateLimitStreamRequests({
+ factory,
+ immediateCount: 100,
+ maxConcurrentRequests: 100,
+ total: 2,
+ });
+ expect(factory).toHaveBeenCalledTimes(2);
+ expect(requests.length).toBe(2);
+ });
+
+ it('creates immediate requests', () => {
+ const factory = createFactory();
+ const requests = rateLimitStreamRequests({
+ factory,
+ maxConcurrentRequests: 2,
+ total: 2,
+ });
+ expect(factory).toHaveBeenCalledTimes(2);
+ expect(requests.length).toBe(2);
+ });
+
+ it('returns correct values', async () => {
+ const fixture = 'foobar';
+ const factory = createFactory(fixture);
+ const requests = rateLimitStreamRequests({
+ factory,
+ maxConcurrentRequests: 2,
+ total: 2,
+ });
+
+ const decoder = new TextDecoder('utf-8');
+ let result = '';
+ for await (const stream of requests) {
+ await stream.pipeTo(
+ new WritableStream({
+ // eslint-disable-next-line no-loop-func
+ write(content) {
+ result += decoder.decode(content);
+ },
+ }),
+ );
+ }
+
+ expect(result).toBe(fixture + fixture);
+ });
+
+ it('delays rate limited requests', async () => {
+ const factory = createFactory();
+ const requests = rateLimitStreamRequests({
+ factory,
+ maxConcurrentRequests: 2,
+ total: 3,
+ });
+ expect(factory).toHaveBeenCalledTimes(2);
+ expect(requests.length).toBe(3);
+
+ await waitForPromises();
+
+ expect(factory).toHaveBeenCalledTimes(3);
+ });
+
+ it('runs next request after previous has been fulfilled', async () => {
+ let res;
+ const factory = jest
+ .fn()
+ .mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ res = resolve;
+ }),
+ )
+ .mockImplementationOnce(() => Promise.resolve(createStreamResponse()));
+ const requests = rateLimitStreamRequests({
+ factory,
+ maxConcurrentRequests: 1,
+ total: 2,
+ });
+ expect(factory).toHaveBeenCalledTimes(1);
+ expect(requests.length).toBe(2);
+
+ await waitForPromises();
+
+ expect(factory).toHaveBeenCalledTimes(1);
+
+ res(createStreamResponse());
+
+ await waitForPromises();
+
+ expect(factory).toHaveBeenCalledTimes(2);
+ });
+
+ it('uses timer to schedule next request', async () => {
+ let res;
+ const factory = jest
+ .fn()
+ .mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ res = resolve;
+ }),
+ )
+ .mockImplementationOnce(() => Promise.resolve(createStreamResponse()));
+ const requests = rateLimitStreamRequests({
+ factory,
+ immediateCount: 1,
+ maxConcurrentRequests: 2,
+ total: 2,
+ timeout: 9999,
+ });
+ expect(factory).toHaveBeenCalledTimes(1);
+ expect(requests.length).toBe(2);
+
+ await waitForPromises();
+
+ expect(factory).toHaveBeenCalledTimes(1);
+
+ jest.runAllTimers();
+
+ await waitForPromises();
+
+ expect(factory).toHaveBeenCalledTimes(2);
+ res(createStreamResponse());
+ });
+});
diff --git a/spec/frontend/streaming/render_balancer_spec.js b/spec/frontend/streaming/render_balancer_spec.js
new file mode 100644
index 00000000000..dae0c98d678
--- /dev/null
+++ b/spec/frontend/streaming/render_balancer_spec.js
@@ -0,0 +1,69 @@
+import { RenderBalancer } from '~/streaming/render_balancer';
+
+const HIGH_FRAME_TIME = 100;
+const LOW_FRAME_TIME = 10;
+
+describe('renderBalancer', () => {
+ let frameTime = 0;
+ let frameTimeDelta = 0;
+ let decrease;
+ let increase;
+
+ const createBalancer = () => {
+ decrease = jest.fn();
+ increase = jest.fn();
+ return new RenderBalancer({
+ highFrameTime: HIGH_FRAME_TIME,
+ lowFrameTime: LOW_FRAME_TIME,
+ increase,
+ decrease,
+ });
+ };
+
+ const renderTimes = (times) => {
+ const balancer = createBalancer();
+ return new Promise((resolve) => {
+ let counter = 0;
+ balancer.render(() => {
+ if (counter === times) {
+ resolve(counter);
+ return false;
+ }
+ counter += 1;
+ return true;
+ });
+ });
+ };
+
+ beforeEach(() => {
+ jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
+ frameTime += frameTimeDelta;
+ cb(frameTime);
+ });
+ });
+
+ afterEach(() => {
+ window.requestAnimationFrame.mockRestore();
+ frameTime = 0;
+ frameTimeDelta = 0;
+ });
+
+ it('renders in a loop', async () => {
+ const count = await renderTimes(5);
+ expect(count).toBe(5);
+ });
+
+ it('calls decrease', async () => {
+ frameTimeDelta = 200;
+ await renderTimes(5);
+ expect(decrease).toHaveBeenCalled();
+ expect(increase).not.toHaveBeenCalled();
+ });
+
+ it('calls increase', async () => {
+ frameTimeDelta = 1;
+ await renderTimes(5);
+ expect(increase).toHaveBeenCalled();
+ expect(decrease).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/streaming/render_html_streams_spec.js b/spec/frontend/streaming/render_html_streams_spec.js
new file mode 100644
index 00000000000..55cef0ea469
--- /dev/null
+++ b/spec/frontend/streaming/render_html_streams_spec.js
@@ -0,0 +1,96 @@
+import { ReadableStream } from 'node:stream/web';
+import { renderHtmlStreams } from '~/streaming/render_html_streams';
+import { HtmlStream } from '~/streaming/html_stream';
+import waitForPromises from 'helpers/wait_for_promises';
+
+jest.mock('~/streaming/html_stream');
+jest.mock('~/streaming/constants', () => {
+ return {
+ HIGH_FRAME_TIME: 0,
+ LOW_FRAME_TIME: 0,
+ MAX_CHUNK_SIZE: 1,
+ MIN_CHUNK_SIZE: 1,
+ };
+});
+
+const firstStreamContent = 'foobar';
+const secondStreamContent = 'bazqux';
+
+describe('renderHtmlStreams', () => {
+ let htmlWriter;
+ const encoder = new TextEncoder();
+ const createSingleChunkStream = (chunk) => {
+ const encoded = encoder.encode(chunk);
+ const stream = new ReadableStream({
+ pull(controller) {
+ controller.enqueue(encoded);
+ controller.close();
+ },
+ });
+ return [stream, encoded];
+ };
+
+ beforeEach(() => {
+ htmlWriter = {
+ write: jest.fn(),
+ close: jest.fn(),
+ abort: jest.fn(),
+ };
+ jest.spyOn(HtmlStream.prototype, 'withChunkWriter').mockReturnValue(htmlWriter);
+ });
+
+ it('renders a single stream', async () => {
+ const [stream, encoded] = createSingleChunkStream(firstStreamContent);
+
+ await renderHtmlStreams([Promise.resolve(stream)], document.body);
+
+ expect(htmlWriter.write).toHaveBeenCalledWith(encoded);
+ expect(htmlWriter.close).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders stream sequence', async () => {
+ const [stream1, encoded1] = createSingleChunkStream(firstStreamContent);
+ const [stream2, encoded2] = createSingleChunkStream(secondStreamContent);
+
+ await renderHtmlStreams([Promise.resolve(stream1), Promise.resolve(stream2)], document.body);
+
+ expect(htmlWriter.write.mock.calls).toMatchObject([[encoded1], [encoded2]]);
+ expect(htmlWriter.close).toHaveBeenCalledTimes(1);
+ });
+
+ it("doesn't wait for the whole sequence to resolve before streaming", async () => {
+ const [stream1, encoded1] = createSingleChunkStream(firstStreamContent);
+ const [stream2, encoded2] = createSingleChunkStream(secondStreamContent);
+
+ let res;
+ const delayedStream = new Promise((resolve) => {
+ res = resolve;
+ });
+
+ renderHtmlStreams([Promise.resolve(stream1), delayedStream], document.body);
+
+ await waitForPromises();
+
+ expect(htmlWriter.write.mock.calls).toMatchObject([[encoded1]]);
+ expect(htmlWriter.close).toHaveBeenCalledTimes(0);
+
+ res(stream2);
+ await waitForPromises();
+
+ expect(htmlWriter.write.mock.calls).toMatchObject([[encoded1], [encoded2]]);
+ expect(htmlWriter.close).toHaveBeenCalledTimes(1);
+ });
+
+ it('closes HtmlStream on error', async () => {
+ const [stream1] = createSingleChunkStream(firstStreamContent);
+ const error = new Error();
+
+ try {
+ await renderHtmlStreams([Promise.resolve(stream1), Promise.reject(error)], document.body);
+ } catch (err) {
+ expect(err).toBe(error);
+ }
+
+ expect(htmlWriter.abort).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/spec/helpers/device_registration_helper_spec.rb b/spec/helpers/device_registration_helper_spec.rb
index a8222cddca9..7556d037b3d 100644
--- a/spec/helpers/device_registration_helper_spec.rb
+++ b/spec/helpers/device_registration_helper_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe DeviceRegistrationHelper, feature_category: :authentication_and_authorization do
+RSpec.describe DeviceRegistrationHelper, feature_category: :system_access do
describe "#device_registration_data" do
it "returns a hash with device registration properties without initial error" do
device_registration_data = helper.device_registration_data(
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index e93d585bc3c..05c9c4bc70c 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -706,48 +706,6 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
end
end
end
-
- describe '#search_repos_by_name' do
- let(:expected_query) { 'test in:name is:public,private user:user repo:repo1 repo:repo2 org:org1 org:org2' }
-
- it 'searches for repositories based on name' do
- expect(client.octokit).to receive(:search_repositories).with(expected_query, {})
-
- client.search_repos_by_name('test')
- end
-
- context 'when pagination options present' do
- it 'searches for repositories via expected query' do
- expect(client.octokit).to receive(:search_repositories).with(
- expected_query, { page: 2, per_page: 25 }
- )
-
- client.search_repos_by_name('test', { page: 2, per_page: 25 })
- end
- end
-
- context 'when Faraday error received from octokit', :aggregate_failures do
- let(:error_class) { described_class::CLIENT_CONNECTION_ERROR }
- let(:info_params) { { 'error.class': error_class } }
-
- it 'retries on error and succeeds' do
- allow_retry(:search_repositories)
-
- expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once
-
- expect(client.search_repos_by_name('test')).to eq({})
- end
-
- it 'retries and does not succeed' do
- allow(client.octokit)
- .to receive(:search_repositories)
- .with(expected_query, {})
- .and_raise(error_class, 'execution expired')
-
- expect { client.search_repos_by_name('test') }.to raise_error(error_class, 'execution expired')
- end
- end
- end
end
def allow_retry(method = :pull_request)
diff --git a/spec/lib/gitlab/github_import/clients/proxy_spec.rb b/spec/lib/gitlab/github_import/clients/proxy_spec.rb
index 0baff7bafcb..5f785ae20b0 100644
--- a/spec/lib/gitlab/github_import/clients/proxy_spec.rb
+++ b/spec/lib/gitlab/github_import/clients/proxy_spec.rb
@@ -15,54 +15,30 @@ RSpec.describe Gitlab::GithubImport::Clients::Proxy, :manage, feature_category:
context 'when remove_legacy_github_client FF is enabled' do
let(:client_stub) { instance_double(Gitlab::GithubImport::Client) }
- context 'with github_client_fetch_repos_via_graphql FF enabled' do
- let(:client_response) do
- {
- data: {
- search: {
- nodes: [{ name: 'foo' }, { name: 'bar' }],
- pageInfo: { startCursor: 'foo', endCursor: 'bar' }
- }
+ let(:client_response) do
+ {
+ data: {
+ search: {
+ nodes: [{ name: 'foo' }, { name: 'bar' }],
+ pageInfo: { startCursor: 'foo', endCursor: 'bar' }
}
}
- end
-
- it 'fetches repos with Gitlab::GithubImport::Client (GraphQL API)' do
- expect(Gitlab::GithubImport::Client)
- .to receive(:new).with(access_token).and_return(client_stub)
- expect(client_stub)
- .to receive(:search_repos_by_name_graphql)
- .with(search_text, pagination_options).and_return(client_response)
-
- expect(client.repos(search_text, pagination_options)).to eq(
- {
- repos: [{ name: 'foo' }, { name: 'bar' }],
- page_info: { startCursor: 'foo', endCursor: 'bar' }
- }
- )
- end
+ }
end
- context 'with github_client_fetch_repos_via_graphql FF disabled' do
- let(:client_response) do
- { items: [{ name: 'foo' }, { name: 'bar' }] }
- end
+ it 'fetches repos with Gitlab::GithubImport::Client (GraphQL API)' do
+ expect(Gitlab::GithubImport::Client)
+ .to receive(:new).with(access_token).and_return(client_stub)
+ expect(client_stub)
+ .to receive(:search_repos_by_name_graphql)
+ .with(search_text, pagination_options).and_return(client_response)
- before do
- stub_feature_flags(github_client_fetch_repos_via_graphql: false)
- end
-
- it 'fetches repos with Gitlab::GithubImport::Client (REST API)' do
- expect(Gitlab::GithubImport::Client)
- .to receive(:new).with(access_token).and_return(client_stub)
- expect(client_stub)
- .to receive(:search_repos_by_name)
- .with(search_text, pagination_options).and_return(client_response)
-
- expect(client.repos(search_text, pagination_options)).to eq(
- { repos: [{ name: 'foo' }, { name: 'bar' }] }
- )
- end
+ expect(client.repos(search_text, pagination_options)).to eq(
+ {
+ repos: [{ name: 'foo' }, { name: 'bar' }],
+ page_info: { startCursor: 'foo', endCursor: 'bar' }
+ }
+ )
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
index f7cee6beb58..80c1af1b913 100644
--- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
@@ -331,7 +331,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
include_context 'server metrics call'
context 'when a worker has a feature category' do
- let(:worker_category) { 'authentication_and_authorization' }
+ let(:worker_category) { 'system_access' }
it 'uses that category for metrics' do
expect(completion_seconds_metric).to receive(:observe).with(a_hash_including(feature_category: worker_category), anything)
diff --git a/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb b/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
index 1b6cd7ac5fb..4fbc64a45d6 100644
--- a/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
@@ -123,7 +123,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
context 'when the feature category is already set in the surrounding block' do
it 'takes the feature category from the worker, not the caller' do
- Gitlab::ApplicationContext.with_context(feature_category: 'authentication_and_authorization') do
+ Gitlab::ApplicationContext.with_context(feature_category: 'system_access') do
TestWithContextWorker.bulk_perform_async_with_contexts(
%w(job1 job2),
arguments_proc: -> (name) { [name, 1, 2, 3] },
@@ -139,7 +139,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
end
it 'takes the feature category from the caller if the worker is not owned' do
- Gitlab::ApplicationContext.with_context(feature_category: 'authentication_and_authorization') do
+ Gitlab::ApplicationContext.with_context(feature_category: 'system_access') do
TestNotOwnedWithContextWorker.bulk_perform_async_with_contexts(
%w(job1 job2),
arguments_proc: -> (name) { [name, 1, 2, 3] },
@@ -150,8 +150,8 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
job1 = TestNotOwnedWithContextWorker.job_for_args(['job1', 1, 2, 3])
job2 = TestNotOwnedWithContextWorker.job_for_args(['job2', 1, 2, 3])
- expect(job1['meta.feature_category']).to eq('authentication_and_authorization')
- expect(job2['meta.feature_category']).to eq('authentication_and_authorization')
+ expect(job1['meta.feature_category']).to eq('system_access')
+ expect(job2['meta.feature_category']).to eq('system_access')
end
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb
index 2deab3064eb..eb077a0371c 100644
--- a/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb
@@ -69,7 +69,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Server do
context 'feature category' do
it 'takes the feature category from the worker' do
- Gitlab::ApplicationContext.with_context(feature_category: 'authentication_and_authorization') do
+ Gitlab::ApplicationContext.with_context(feature_category: 'system_access') do
TestWorker.perform_async('identifier', 1)
end
@@ -78,11 +78,11 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Server do
context 'when the worker is not owned' do
it 'takes the feature category from the surrounding context' do
- Gitlab::ApplicationContext.with_context(feature_category: 'authentication_and_authorization') do
+ Gitlab::ApplicationContext.with_context(feature_category: 'system_access') do
NotOwnedWorker.perform_async('identifier', 1)
end
- expect(NotOwnedWorker.contexts['identifier']).to include('meta.feature_category' => 'authentication_and_authorization')
+ expect(NotOwnedWorker.contexts['identifier']).to include('meta.feature_category' => 'system_access')
end
end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 65a407b0346..003ca2512dc 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -1297,9 +1297,9 @@ RSpec.describe GroupPolicy, feature_category: :system_access do
end
end
- context 'create_runner_workflow_for_admin flag enabled' do
+ context 'create_runner_workflow_for_namespace flag enabled' do
before do
- stub_feature_flags(create_runner_workflow_for_admin: true)
+ stub_feature_flags(create_runner_workflow_for_namespace: [group])
end
context 'admin' do
@@ -1380,11 +1380,13 @@ RSpec.describe GroupPolicy, feature_category: :system_access do
end
end
- context 'with create_runner_workflow_for_admin flag disabled' do
+ context 'with create_runner_workflow_for_namespace flag disabled' do
before do
- stub_feature_flags(create_runner_workflow_for_admin: false)
+ stub_feature_flags(create_runner_workflow_for_namespace: [other_group])
end
+ let_it_be(:other_group) { create(:group) }
+
context 'admin' do
let(:current_user) { admin }
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index bc58171f27b..e7b548b8f3b 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -2740,9 +2740,9 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
describe 'create_project_runners' do
- context 'create_runner_workflow_for_admin flag enabled' do
+ context 'create_runner_workflow_for_namespace flag enabled' do
before do
- stub_feature_flags(create_runner_workflow_for_admin: true)
+ stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
end
context 'admin' do
@@ -2810,9 +2810,9 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
- context 'create_runner_workflow_for_admin flag disabled' do
+ context 'create_runner_workflow_for_namespace flag disabled' do
before do
- stub_feature_flags(create_runner_workflow_for_admin: false)
+ stub_feature_flags(create_runner_workflow_for_namespace: [group])
end
context 'admin' do
diff --git a/spec/requests/api/admin/ci/variables_spec.rb b/spec/requests/api/admin/ci/variables_spec.rb
index 1ecd1edd99e..dd4171b257a 100644
--- a/spec/requests/api/admin/ci/variables_spec.rb
+++ b/spec/requests/api/admin/ci/variables_spec.rb
@@ -51,7 +51,9 @@ RSpec.describe ::API::Admin::Ci::Variables do
end
describe 'POST /admin/ci/variables' do
- it_behaves_like 'POST request permissions for admin mode', { key: 'KEY', value: 'VALUE' }
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { { key: 'KEY', value: 'VALUE' } }
+ end
context 'authorized user with proper permissions' do
it 'creates variable for admins', :aggregate_failures do
@@ -137,19 +139,21 @@ RSpec.describe ::API::Admin::Ci::Variables do
describe 'PUT /admin/ci/variables/:key' do
let_it_be(:path) { "/admin/ci/variables/#{variable.key}" }
+ let_it_be(:params) do
+ {
+ variable_type: 'file',
+ value: 'VALUE_1_UP',
+ protected: true,
+ masked: true,
+ raw: true
+ }
+ end
it_behaves_like 'PUT request permissions for admin mode'
context 'authorized user with proper permissions' do
it 'updates variable data', :aggregate_failures do
- put api(path, admin, admin_mode: true),
- params: {
- variable_type: 'file',
- value: 'VALUE_1_UP',
- protected: true,
- masked: true,
- raw: true
- }
+ put api(path, admin, admin_mode: true), params: params
expect(variable.reload.value).to eq('VALUE_1_UP')
expect(variable.reload).to be_protected
diff --git a/spec/requests/api/admin/instance_clusters_spec.rb b/spec/requests/api/admin/instance_clusters_spec.rb
index 84ebe68b732..0a72f404e89 100644
--- a/spec/requests/api/admin/instance_clusters_spec.rb
+++ b/spec/requests/api/admin/instance_clusters_spec.rb
@@ -181,20 +181,9 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
}
end
- it_behaves_like 'POST request permissions for admin mode',
- {
- name: 'test-instance-cluster',
- domain: 'domain.example.com',
- managed: false,
- enabled: false,
- namespace_per_environment: false,
- clusterable: Clusters::Instance.new,
- platform_kubernetes_attributes: {
- api_url: 'https://example.com',
- token: 'sample-token',
- authorization_type: 'rbac'
- }
- }
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { cluster_params }
+ end
include_examples ':certificate_based_clusters feature flag API responses' do
let(:subject) { post api(path, admin_user, admin_mode: true), params: cluster_params }
@@ -319,7 +308,9 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
let(:path) { "/admin/clusters/#{cluster.id}" }
- it_behaves_like 'PUT request permissions for admin mode'
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:params) { update_params }
+ end
include_examples ':certificate_based_clusters feature flag API responses' do
let(:subject) { put api(path, admin_user, admin_mode: true), params: update_params }
diff --git a/spec/requests/api/admin/plan_limits_spec.rb b/spec/requests/api/admin/plan_limits_spec.rb
index 4f170a1c86d..dffe062c031 100644
--- a/spec/requests/api/admin/plan_limits_spec.rb
+++ b/spec/requests/api/admin/plan_limits_spec.rb
@@ -84,7 +84,9 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :shared d
end
describe 'PUT /application/plan_limits' do
- it_behaves_like 'PUT request permissions for admin mode', { 'plan_name': 'default' }
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:params) { { 'plan_name': 'default' } }
+ end
context 'as an admin user' do
context 'correct params' do
diff --git a/spec/requests/api/appearance_spec.rb b/spec/requests/api/appearance_spec.rb
index c08ecae28e8..3550e51d585 100644
--- a/spec/requests/api/appearance_spec.rb
+++ b/spec/requests/api/appearance_spec.rb
@@ -36,7 +36,9 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do
end
describe "PUT /application/appearance" do
- it_behaves_like 'PUT request permissions for admin mode', { title: "Test" }
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:params) { { title: "Test" } }
+ end
context 'as an admin user' do
context "instance basics" do
diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb
index 933be87d680..5b07bded82c 100644
--- a/spec/requests/api/applications_spec.rb
+++ b/spec/requests/api/applications_spec.rb
@@ -10,7 +10,9 @@ RSpec.describe API::Applications, :api, feature_category: :system_access do
let!(:application) { create(:application, name: 'another_application', owner: nil, redirect_uri: 'http://other_application.url', scopes: scopes) }
describe 'POST /applications' do
- it_behaves_like 'POST request permissions for admin mode', { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'api' }
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'api' } }
+ end
context 'authenticated and authorized user' do
it 'creates and returns an OAuth application' do
diff --git a/spec/requests/api/graphql/project/flow_metrics_spec.rb b/spec/requests/api/graphql/project/flow_metrics_spec.rb
index 0bdf7bad8db..3b5758b3a2e 100644
--- a/spec/requests/api/graphql/project/flow_metrics_spec.rb
+++ b/spec/requests/api/graphql/project/flow_metrics_spec.rb
@@ -6,14 +6,18 @@ RSpec.describe 'getting project flow metrics', feature_category: :value_stream_m
include GraphqlHelpers
let_it_be(:group) { create(:group) }
- let_it_be(:project1) { create(:project, group: group) }
+ let_it_be(:project1) { create(:project, :repository, group: group) }
# This is done so we can use the same count expectations in the shared examples and
# reuse the shared example for the group-level test.
let_it_be(:project2) { project1 }
+ let_it_be(:production_environment1) { create(:environment, :production, project: project1) }
+ let_it_be(:production_environment2) { production_environment1 }
let_it_be(:current_user) { create(:user, maintainer_projects: [project1]) }
- it_behaves_like 'value stream analytics flow metrics issueCount examples' do
- let(:full_path) { project1.full_path }
- let(:context) { :project }
- end
+ let(:full_path) { project1.full_path }
+ let(:context) { :project }
+
+ it_behaves_like 'value stream analytics flow metrics issueCount examples'
+
+ it_behaves_like 'value stream analytics flow metrics deploymentCount examples'
end
diff --git a/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb
index 046036c40ba..fa78ddf206a 100644
--- a/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb
+++ b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb
@@ -122,3 +122,84 @@ RSpec.shared_examples 'value stream analytics flow metrics issueCount examples'
end
end
end
+
+RSpec.shared_examples 'value stream analytics flow metrics deploymentCount examples' do
+ let_it_be(:deployment1) do
+ create(:deployment, :success, environment: production_environment1, finished_at: 5.days.ago)
+ end
+
+ let_it_be(:deployment2) do
+ create(:deployment, :success, environment: production_environment2, finished_at: 10.days.ago)
+ end
+
+ let_it_be(:deployment3) do
+ create(:deployment, :success, environment: production_environment2, finished_at: 15.days.ago)
+ end
+
+ let(:variables) do
+ {
+ path: full_path,
+ from: 12.days.ago.iso8601,
+ to: 3.days.ago.iso8601
+ }
+ end
+
+ let(:query) do
+ <<~QUERY
+ query($path: ID!, $from: Time!, $to: Time!) {
+ #{context}(fullPath: $path) {
+ flowMetrics {
+ deploymentCount(from: $from, to: $to) {
+ value
+ unit
+ identifier
+ title
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ subject(:result) do
+ post_graphql(query, current_user: current_user, variables: variables)
+
+ graphql_data.dig(context.to_s, 'flowMetrics', 'deploymentCount')
+ end
+
+ it 'returns the correct count' do
+ expect(result).to eq({
+ 'identifier' => 'deploys',
+ 'unit' => nil,
+ 'value' => 2,
+ 'title' => n_('Deploy', 'Deploys', 2)
+ })
+ end
+
+ context 'when the user is not authorized' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nil' do
+ expect(result).to eq(nil)
+ end
+ end
+
+ context 'when outside of the date range' do
+ let(:variables) do
+ {
+ path: full_path,
+ from: 20.days.ago.iso8601,
+ to: 18.days.ago.iso8601
+ }
+ end
+
+ it 'returns 0 count' do
+ expect(result).to eq({
+ 'identifier' => 'deploys',
+ 'unit' => nil,
+ 'value' => 0,
+ 'title' => n_('Deploy', 'Deploys', 0)
+ })
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/real_time_merge_widget_shared_examples.rb b/spec/support/shared_examples/features/real_time_merge_widget_shared_examples.rb
new file mode 100644
index 00000000000..d76d0f74e98
--- /dev/null
+++ b/spec/support/shared_examples/features/real_time_merge_widget_shared_examples.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'updates merge widget in real-time' do
+ specify do
+ wait_for_requests
+
+ # Simulate a real-time update of merge widget
+ trigger_action
+
+ expect(find('.mr-state-widget')).to have_content(widget_text)
+ end
+end
diff --git a/spec/support/shared_examples/requests/admin_mode_shared_examples.rb b/spec/support/shared_examples/requests/admin_mode_shared_examples.rb
index 7e5ad751247..2225291661c 100644
--- a/spec/support/shared_examples/requests/admin_mode_shared_examples.rb
+++ b/spec/support/shared_examples/requests/admin_mode_shared_examples.rb
@@ -4,14 +4,14 @@ RSpec.shared_examples 'GET request permissions for admin mode' do
it_behaves_like 'GET request permissions for admin mode when admin'
end
-RSpec.shared_examples 'PUT request permissions for admin mode' do |params|
- it_behaves_like 'PUT request permissions for admin mode when user', params
- it_behaves_like 'PUT request permissions for admin mode when admin', params
+RSpec.shared_examples 'PUT request permissions for admin mode' do
+ it_behaves_like 'PUT request permissions for admin mode when user'
+ it_behaves_like 'PUT request permissions for admin mode when admin'
end
-RSpec.shared_examples 'POST request permissions for admin mode' do |params|
- it_behaves_like 'POST request permissions for admin mode when user', params
- it_behaves_like 'POST request permissions for admin mode when admin', params
+RSpec.shared_examples 'POST request permissions for admin mode' do
+ it_behaves_like 'POST request permissions for admin mode when user'
+ it_behaves_like 'POST request permissions for admin mode when admin'
end
RSpec.shared_examples 'DELETE request permissions for admin mode' do |success_status_code = :no_content|
@@ -37,7 +37,7 @@ RSpec.shared_examples 'GET request permissions for admin mode when admin' do
it_behaves_like 'admin mode on', false, :forbidden
end
-RSpec.shared_examples 'PUT request permissions for admin mode when user' do |params|
+RSpec.shared_examples 'PUT request permissions for admin mode when user' do
subject { put api(path, current_user, admin_mode: admin_mode), params: params }
let_it_be(:current_user) { create(:user) }
@@ -46,7 +46,7 @@ RSpec.shared_examples 'PUT request permissions for admin mode when user' do |par
it_behaves_like 'admin mode on', false, :forbidden
end
-RSpec.shared_examples 'PUT request permissions for admin mode when admin' do |params|
+RSpec.shared_examples 'PUT request permissions for admin mode when admin' do
subject { put api(path, current_user, admin_mode: admin_mode), params: params }
let_it_be(:current_user) { create(:admin) }
@@ -55,7 +55,7 @@ RSpec.shared_examples 'PUT request permissions for admin mode when admin' do |pa
it_behaves_like 'admin mode on', false, :forbidden
end
-RSpec.shared_examples 'POST request permissions for admin mode when user' do |params|
+RSpec.shared_examples 'POST request permissions for admin mode when user' do
subject { post api(path, current_user, admin_mode: admin_mode), params: params }
let_it_be(:current_user) { create(:user) }
@@ -64,7 +64,7 @@ RSpec.shared_examples 'POST request permissions for admin mode when user' do |pa
it_behaves_like 'admin mode on', false, :forbidden
end
-RSpec.shared_examples 'POST request permissions for admin mode when admin' do |params|
+RSpec.shared_examples 'POST request permissions for admin mode when admin' do
subject { post api(path, current_user, admin_mode: admin_mode), params: params }
let_it_be(:current_user) { create(:admin) }
diff --git a/spec/tasks/gitlab/feature_categories_rake_spec.rb b/spec/tasks/gitlab/feature_categories_rake_spec.rb
index 22f36309a7c..33f4bca4c85 100644
--- a/spec/tasks/gitlab/feature_categories_rake_spec.rb
+++ b/spec/tasks/gitlab/feature_categories_rake_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe 'gitlab:feature_categories:index', :silence_stdout, feature_categ
)
),
'api_endpoints' => a_hash_including(
- 'authentication_and_authorization' => a_collection_including(
+ 'system_access' => a_collection_including(
klass: 'API::AccessRequests',
action: '/groups/:id/access_requests',
source_location: [
diff --git a/yarn.lock b/yarn.lock
index 45ddeebeceb..2b810df506e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1832,6 +1832,11 @@
resolved "https://registry.yarnpkg.com/@linaria/core/-/core-3.0.0-beta.13.tgz#049c5be5faa67e341e413a0f6b641d5d78d91056"
integrity sha512-3zEi5plBCOsEzUneRVuQb+2SAx3qaC1dj0FfFAI6zIJQoDWu0dlSwKijMRack7oO9tUWrchfj3OkKQAd1LBdVg==
+"@mattiasbuelens/web-streams-adapter@^0.1.0":
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/@mattiasbuelens/web-streams-adapter/-/web-streams-adapter-0.1.0.tgz#607b5a25682f4ae2741da7ba6df39302505336b3"
+ integrity sha512-oV4PyZfwJNtmFWhvlJLqYIX1Nn22ML8FZpS16ZUKv0hg7414xV1fjsGqxQzLT2dyK92TKxsJSwMOd7VNHAtPmA==
+
"@miragejs/pretender-node-polyfill@^0.1.0":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@miragejs/pretender-node-polyfill/-/pretender-node-polyfill-0.1.2.tgz#d26b6b7483fb70cd62189d05c95d2f67153e43f2"