diff options
Diffstat (limited to 'app')
31 files changed, 191 insertions, 412 deletions
diff --git a/app/assets/javascripts/constants.js b/app/assets/javascripts/constants.js index 631968ff531..f43a2d5d8ff 100644 --- a/app/assets/javascripts/constants.js +++ b/app/assets/javascripts/constants.js @@ -3,5 +3,3 @@ export const getModifierKey = (removeSuffix = false) => { const winKey = `Ctrl${removeSuffix ? '' : '+'}`; return window.gl?.client?.isMac ? '⌘' : winKey; }; - -export const PRELOAD_THROTTLE_TIMEOUT_MS = 4000; diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 776f27a8583..f4008fe3cc9 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -5,7 +5,6 @@ import GfmAutoComplete, { defaultAutocompleteConfig } from 'ee_else_ce/gfm_auto_ import { disableButtonIfEmptyField } from '~/lib/utils/common_utils'; import dropzoneInput from './dropzone_input'; import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown'; -import { PRELOAD_THROTTLE_TIMEOUT_MS } from './constants'; export default class GLForm { /** @@ -69,21 +68,6 @@ export default class GLForm { ); this.autoComplete = new GfmAutoComplete(dataSources); this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM); - - if (this.preloadMembers && dataSources?.members) { - // for now the preload is only implemented for the members - // timeout helping to trottle the preloads in the case content_editor - // is set as main comment editor and support for rspec tests - // https://gitlab.com/gitlab-org/gitlab/-/issues/427437 - - requestIdleCallback(() => - setTimeout( - () => this.autoComplete?.fetchData($('.js-gfm-input'), '@'), - PRELOAD_THROTTLE_TIMEOUT_MS, - ), - ); - } - this.formDropzone = dropzoneInput(this.form, { parallelUploads: 1 }); if (this.form.is(':not(.js-no-autosize)')) { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js index 24bc7017e06..88efcfa46e7 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js @@ -1,3 +1,5 @@ +import { __ } from '~/locale'; + export const COMPONENTS = { conflict: () => import('./conflicts.vue'), discussions_not_resolved: () => import('./unresolved_discussions.vue'), @@ -5,3 +7,18 @@ export const COMPONENTS = { need_rebase: () => import('./rebase.vue'), default: () => import('./message.vue'), }; + +export const FAILURE_REASONS = { + broken_status: __('Cannot merge the source into the target branch, due to a conflict.'), + ci_must_pass: __('Pipeline must succeed.'), + conflict: __('Merge conflicts must be resolved.'), + discussions_not_resolved: __('Unresolved discussions must be resolved.'), + draft_status: __('Merge request must not be draft.'), + not_open: __('Merge request must be open.'), + need_rebase: __('Merge request must be rebased, because a fast-forward merge is not possible.'), + not_approved: __('All required approvals must be given.'), + policies_denied: __('Denied licenses must be removed or approved.'), + merge_request_blocked: __('Merge request is blocked by another merge request.'), + status_checks_must_pass: __('Status checks must pass.'), + jira_association_missing: __('Either the title or description must reference a Jira issue.'), +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue index 7f21445559a..da3cb1397dd 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue @@ -1,6 +1,6 @@ <script> -import { __ } from '~/locale'; import StatusIcon from '../widget/status_icon.vue'; +import { FAILURE_REASONS } from './constants'; const ICON_NAMES = { failed: 'failed', @@ -8,21 +8,6 @@ const ICON_NAMES = { success: 'success', }; -export const FAILURE_REASONS = { - broken_status: __('Cannot merge the source into the target branch, due to a conflict.'), - ci_must_pass: __('Pipeline must succeed.'), - conflict: __('Merge conflicts must be resolved.'), - discussions_not_resolved: __('Unresolved discussions must be resolved.'), - draft_status: __('Merge request must not be draft.'), - not_open: __('Merge request must be open.'), - need_rebase: __('Merge request must be rebased, because a fast-forward merge is not possible.'), - not_approved: __('All required approvals must be given.'), - policies_denied: __('Denied licenses must be removed or approved.'), - merge_request_blocked: __('Merge request is blocked by another merge request.'), - status_checks_must_pass: __('Status checks must pass.'), - jira_association_missing: __('Either the title or description must reference a Jira issue.'), -}; - export default { name: 'MergeChecksMessage', components: { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue index 9afed170097..016278db4ca 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue @@ -3,7 +3,10 @@ import { GlSkeletonLoader } from '@gitlab/ui'; import { __, n__, sprintf } from '~/locale'; import { TYPENAME_MERGE_REQUEST } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; -import { COMPONENTS } from '~/vue_merge_request_widget/components/checks/constants'; +import { + COMPONENTS, + FAILURE_REASONS, +} from '~/vue_merge_request_widget/components/checks/constants'; import mergeRequestQueryVariablesMixin from '../mixins/merge_request_query_variables'; import mergeChecksQuery from '../queries/merge_checks.query.graphql'; import mergeChecksSubscription from '../queries/merge_checks.subscription.graphql'; @@ -102,7 +105,7 @@ export default { const order = ['FAILED', 'SUCCESS']; return [...this.checks] - .filter((s) => s.status !== 'INACTIVE') + .filter((s) => s.status !== 'INACTIVE' && FAILURE_REASONS[s.identifier.toLowerCase()]) .sort((a, b) => order.indexOf(a.status) - order.indexOf(b.status)); }, failedChecks() { diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue index 5d72ac34e73..8ea97ad73b4 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue @@ -78,16 +78,12 @@ export default { required: false, default: undefined, }, - multiSelectValues: { - type: Array, - required: false, - default: () => [], - }, }, data() { return { hasFetched: false, // use this to avoid flash of `No suggestions found` before fetching searchKey: '', + selectedTokens: [], recentSuggestions: this.config.recentSuggestionsStorageKey ? getRecentlyUsedSuggestions(this.config.recentSuggestionsStorageKey) ?? [] : [], @@ -197,6 +193,30 @@ export default { } }, }, + value: { + deep: true, + immediate: true, + handler(newValue) { + const { data } = newValue; + + if (!this.multiSelectEnabled) { + return; + } + + // don't add empty values to selectedUsernames + if (!data) { + return; + } + + if (Array.isArray(data)) { + this.selectedTokens = data; + // !active so we don't add strings while searching, e.g. r, ro, roo + // !includes so we don't add the same usernames (if @input is emitted twice) + } else if (!this.active && !this.selectedTokens.includes(data)) { + this.selectedTokens = this.selectedTokens.concat(data); + } + }, + }, }, methods: { handleInput: debounce(function debouncedSearch({ data, operator }) { @@ -222,7 +242,15 @@ export default { }, DEBOUNCE_DELAY), handleTokenValueSelected(selectedValue) { if (this.multiSelectEnabled) { - this.$emit('token-selected', selectedValue); + const index = this.selectedTokens.indexOf(selectedValue); + if (index > -1) { + this.selectedTokens.splice(index, 1); + } else { + this.selectedTokens.push(selectedValue); + } + + // need to clear search + this.$emit('input', { ...this.value, data: '' }); } const activeTokenValue = this.getActiveTokenValue(this.suggestions, selectedValue); @@ -253,7 +281,7 @@ export default { :config="validatedConfig" :value="value" :active="active" - :multi-select-values="multiSelectValues" + :multi-select-values="selectedTokens" v-bind="$attrs" v-on="$listeners" @input="handleInput" @@ -265,6 +293,7 @@ export default { :view-token-props="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ { ...viewTokenProps, activeTokenValue, + selectedTokens, } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */" ></slot> </template> @@ -274,6 +303,7 @@ export default { :view-token-props="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ { ...viewTokenProps, activeTokenValue, + selectedTokens, } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */" ></slot> </template> @@ -290,17 +320,26 @@ export default { </template> <template v-if="showRecentSuggestions"> <gl-dropdown-section-header>{{ __('Recently used') }}</gl-dropdown-section-header> - <slot name="suggestions-list" :suggestions="recentSuggestions"></slot> + <slot + name="suggestions-list" + :suggestions="recentSuggestions" + :selections="selectedTokens" + ></slot> <gl-dropdown-divider /> </template> <slot v-if="showPreloadedSuggestions" name="suggestions-list" :suggestions="preloadedSuggestions" + :selections="selectedTokens" ></slot> <gl-loading-icon v-if="suggestionsLoading" size="sm" /> <template v-else-if="showAvailableSuggestions"> - <slot name="suggestions-list" :suggestions="availableSuggestions"></slot> + <slot + name="suggestions-list" + :suggestions="availableSuggestions" + :selections="selectedTokens" + ></slot> </template> <gl-dropdown-text v-else-if="showNoMatchesText"> {{ __('No matches found') }} diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue index 87e295d00dd..8cf4759d419 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue @@ -6,8 +6,7 @@ import { __ } from '~/locale'; import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { OPERATORS_TO_GROUP, OPTIONS_NONE_ANY } from '../constants'; +import { OPTIONS_NONE_ANY } from '../constants'; import BaseToken from './base_token.vue'; @@ -19,7 +18,6 @@ export default { GlIntersperse, GlFilteredSearchSuggestion, }, - mixins: [glFeatureFlagMixin()], props: { config: { type: Object, @@ -40,7 +38,6 @@ export default { users: this.config.initialUsers || [], allUsers: this.config.initialUsers || [], loading: false, - selectedUsernames: [], }; }, computed: { @@ -56,39 +53,6 @@ export default { fetchUsersQuery() { return this.config.fetchUsers ? this.config.fetchUsers : this.fetchUsersBySearchTerm; }, - multiSelectEnabled() { - return ( - this.config.multiSelect && - this.glFeatures.groupMultiSelectTokens && - OPERATORS_TO_GROUP.includes(this.value.operator) - ); - }, - }, - watch: { - value: { - deep: true, - immediate: true, - handler(newValue) { - const { data } = newValue; - - if (!this.multiSelectEnabled) { - return; - } - - // don't add empty values to selectedUsernames - if (!data) { - return; - } - - if (Array.isArray(data)) { - this.selectedUsernames = data; - // !active so we don't add strings while searching, e.g. r, ro, roo - // !includes so we don't add the same usernames (if @input is emitted twice) - } else if (!this.active && !this.selectedUsernames.includes(data)) { - this.selectedUsernames = this.selectedUsernames.concat(data); - } - }, - }, }, methods: { getActiveUser(users, data) { @@ -104,26 +68,6 @@ export default { const user = this.getActiveUser(this.allUsers, username); return this.getAvatarUrl(user); }, - addCheckIcon(username) { - return this.multiSelectEnabled && this.selectedUsernames.includes(username); - }, - addPadding(username) { - return this.multiSelectEnabled && !this.selectedUsernames.includes(username); - }, - handleSelected(username) { - if (!this.multiSelectEnabled) { - return; - } - - const index = this.selectedUsernames.indexOf(username); - if (index > -1) { - this.selectedUsernames.splice(index, 1); - } else { - this.selectedUsernames.push(username); - } - - this.$emit('input', { ...this.value, data: '' }); - }, fetchUsersBySearchTerm(search) { return this.$apollo .query({ @@ -171,16 +115,14 @@ export default { :get-active-token-value="getActiveUser" :default-suggestions="defaultUsers" :preloaded-suggestions="preloadedUsers" - :multi-select-values="selectedUsernames" v-bind="$attrs" @fetch-suggestions="fetchUsers" - @token-selected="handleSelected" v-on="$listeners" > - <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }"> - <gl-intersperse v-if="multiSelectEnabled" separator=","> + <template #view="{ viewTokenProps: { inputValue, activeTokenValue, selectedTokens } }"> + <gl-intersperse v-if="selectedTokens.length > 0" separator=","> <span - v-for="(username, index) in selectedUsernames" + v-for="(username, index) in selectedTokens" :key="username" :class="{ 'gl-ml-2': index > 0 }" ><gl-avatar :size="16" :src="avatarFor(username)" class="gl-mr-1" />{{ @@ -198,7 +140,7 @@ export default { {{ activeTokenValue ? activeTokenValue.name : inputValue }} </template> </template> - <template #suggestions-list="{ suggestions }"> + <template #suggestions-list="{ suggestions, selections = [] }"> <gl-filtered-search-suggestion v-for="user in suggestions" :key="user.username" @@ -206,10 +148,10 @@ export default { > <div class="gl-display-flex gl-align-items-center" - :class="{ 'gl-pl-6': addPadding(user.username) }" + :class="{ 'gl-pl-6': !selections.includes(user.username) }" > <gl-icon - v-if="addCheckIcon(user.username)" + v-if="selections.includes(user.username)" name="check" class="gl-mr-3 gl-text-secondary gl-flex-shrink-0" /> diff --git a/app/controllers/projects/gcp/artifact_registry/base_controller.rb b/app/controllers/projects/gcp/artifact_registry/base_controller.rb deleted file mode 100644 index 4084427f3e5..00000000000 --- a/app/controllers/projects/gcp/artifact_registry/base_controller.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -module Projects - module Gcp - module ArtifactRegistry - class BaseController < ::Projects::ApplicationController - before_action :ensure_feature_flag - before_action :ensure_saas - before_action :authorize_read_container_image! - before_action :ensure_private_project - - feature_category :container_registry - urgency :low - - private - - def ensure_feature_flag - return if Feature.enabled?(:gcp_technical_demo, project) - - @error = 'Feature flag disabled' - - render - end - - def ensure_saas - return if Gitlab.com_except_jh? # rubocop: disable Gitlab/AvoidGitlabInstanceChecks -- demo requirement - - @error = "Can't run here" - - render - end - - def ensure_private_project - return if project.private? - - @error = 'Can only run on private projects' - - render - end - end - end - end -end diff --git a/app/controllers/projects/gcp/artifact_registry/docker_images_controller.rb b/app/controllers/projects/gcp/artifact_registry/docker_images_controller.rb deleted file mode 100644 index 60adbbe6e5d..00000000000 --- a/app/controllers/projects/gcp/artifact_registry/docker_images_controller.rb +++ /dev/null @@ -1,135 +0,0 @@ -# frozen_string_literal: true - -module Projects - module Gcp - module ArtifactRegistry - class DockerImagesController < Projects::Gcp::ArtifactRegistry::BaseController - before_action :require_gcp_params - before_action :handle_pagination - - REPO_NAME_REGEX = %r{/repositories/(.*)/dockerImages/} - - def index - result = service.execute(page_token: params[:page_token]) - - if result.success? - @docker_images = process_docker_images(result.payload[:images] || []) - @next_page_token = result.payload[:next_page_token] - @artifact_repository_name = artifact_repository_name - @error = @docker_images.blank? ? 'No docker images' : false - else - @error = result.message - end - end - - private - - def service - ::GoogleCloudPlatform::ArtifactRegistry::ListDockerImagesService.new( - project: @project, - current_user: current_user, - params: { - gcp_project_id: gcp_project_id, - gcp_location: gcp_location, - gcp_repository: gcp_ar_repository, - gcp_wlif: gcp_wlif_url - } - ) - end - - def process_docker_images(raw_images) - raw_images.map { |r| process_docker_image(r) } - end - - def process_docker_image(raw_image) - DockerImage.new( - name: raw_image[:name], - uri: raw_image[:uri], - tags: raw_image[:tags], - image_size_bytes: raw_image[:size_bytes], - media_type: raw_image[:media_type], - upload_time: raw_image[:uploaded_at], - build_time: raw_image[:built_at], - update_time: raw_image[:updated_at] - ) - end - - def artifact_repository_name - return unless @docker_images.present? - - (@docker_images.first.name || '')[REPO_NAME_REGEX, 1] - end - - def handle_pagination - @page = Integer(params[:page] || 1) - @page_tokens = {} - @previous_page_token = nil - - if params[:page_tokens] - @page_tokens = ::Gitlab::Json.parse(Base64.decode64(params[:page_tokens])) - @previous_page_token = @page_tokens[(@page - 1).to_s] - end - - @page_tokens[@page.to_s] = params[:page_token] - @page_tokens = Base64.encode64(::Gitlab::Json.dump(@page_tokens.compact)) - end - - def require_gcp_params - return unless gcp_project_id.blank? || gcp_location.blank? || gcp_ar_repository.blank? || gcp_wlif_url.blank? - - redirect_to new_namespace_project_gcp_artifact_registry_setup_path - end - - def gcp_project_id - params[:gcp_project_id] - end - - def gcp_location - params[:gcp_location] - end - - def gcp_ar_repository - params[:gcp_ar_repository] - end - - def gcp_wlif_url - params[:gcp_wlif_url] - end - - class DockerImage - include ActiveModel::API - - attr_accessor :name, :uri, :tags, :image_size_bytes, :upload_time, :media_type, :build_time, :update_time - - SHORT_NAME_REGEX = %r{dockerImages/(.*)$} - - def short_name - (name || '')[SHORT_NAME_REGEX, 1] - end - - def updated_at - return unless update_time - - Time.zone.parse(update_time) - end - - def built_at - return unless build_time - - Time.zone.parse(build_time) - end - - def uploaded_at - return unless upload_time - - Time.zone.parse(upload_time) - end - - def details_url - "https://#{uri}" - end - end - end - end - end -end diff --git a/app/controllers/projects/gcp/artifact_registry/setup_controller.rb b/app/controllers/projects/gcp/artifact_registry/setup_controller.rb deleted file mode 100644 index e90304ce593..00000000000 --- a/app/controllers/projects/gcp/artifact_registry/setup_controller.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module Projects - module Gcp - module ArtifactRegistry - class SetupController < ::Projects::Gcp::ArtifactRegistry::BaseController - def new; end - end - end - end -end diff --git a/app/graphql/resolvers/namespace_projects_resolver.rb b/app/graphql/resolvers/namespace_projects_resolver.rb index f0781058bea..1e8a7365fc0 100644 --- a/app/graphql/resolvers/namespace_projects_resolver.rb +++ b/app/graphql/resolvers/namespace_projects_resolver.rb @@ -7,6 +7,11 @@ module Resolvers default_value: false, description: 'Include also subgroup projects.' + argument :include_archived, GraphQL::Types::Boolean, + required: false, + default_value: true, + description: 'Include also archived projects.' + argument :not_aimed_for_deletion, GraphQL::Types::Boolean, required: false, default_value: false, @@ -65,6 +70,7 @@ module Resolvers def finder_params(args) { include_subgroups: args.dig(:include_subgroups), + include_archived: args.dig(:include_archived), not_aimed_for_deletion: args.dig(:not_aimed_for_deletion), sort: args.dig(:sort), search: args.dig(:search), diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index 3756584e3b3..89f6d61ef44 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -33,6 +33,10 @@ module DashboardHelper end end end + + def user_groups_requiring_reauth + [] + end end DashboardHelper.prepend_mod_with('DashboardHelper') diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index fc4d69dcdbc..7d29cd7a877 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -254,6 +254,10 @@ module TodosHelper !todo.build_failed? && !todo.unmergeable? end + def todo_groups_requiring_saml_reauth(_todos) + [] + end + private def todos_design_path(todo, path_options) diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index c7d6f2843de..1b083c70bba 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -381,7 +381,7 @@ class NotifyPreview < ActionMailer::Preview def custom_email_credential @custom_email_credential ||= project.service_desk_custom_email_credential || ServiceDesk::CustomEmailCredential.create!( project: project, - smtp_address: 'smtp.gmail.com', # Use gmail, because Gitlab::UrlBlocker resolves DNS + smtp_address: 'smtp.gmail.com', # Use gmail, because Gitlab::HTTP_V2::UrlBlocker resolves DNS smtp_port: 587, smtp_username: 'user@gmail.com', smtp_password: 'supersecret' diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 238556f0cf0..9020f90fd3c 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -472,12 +472,8 @@ class Namespace < ApplicationRecord false end - def all_project_ids - all_projects.pluck(:id) - end - def all_project_ids_except(ids) - all_projects.where.not(id: ids).pluck(:id) + all_project_ids.where.not(id: ids) end # Deprecated, use #licensed_feature_available? instead. Remove once Namespace#feature_available? isn't used anymore. diff --git a/app/models/namespaces/traversal/cached.rb b/app/models/namespaces/traversal/cached.rb index 55eaaa4667e..b962038d039 100644 --- a/app/models/namespaces/traversal/cached.rb +++ b/app/models/namespaces/traversal/cached.rb @@ -10,8 +10,62 @@ module Namespaces after_destroy :invalidate_descendants_cache end + override :self_and_descendant_ids + def self_and_descendant_ids + return super unless attempt_to_use_cached_data? + + scope_with_cached_ids( + super, + self.class, + Namespaces::Descendants.arel_table[:self_and_descendant_group_ids] + ) + end + + override :all_project_ids + def all_project_ids + return super unless attempt_to_use_cached_data? + + scope_with_cached_ids( + all_projects.select(:id), + Project, + Namespaces::Descendants.arel_table[:all_project_ids] + ) + end + private + # This method implements an OR based cache lookup using COALESCE, similar what you would do in Ruby: + # return cheap_cached_data || expensive_uncached_data + def scope_with_cached_ids(consistent_ids_scope, model, cached_ids_column) + # Look up the cached ids and unnest them into rows if the cache is up to date. + cache_lookup_query = Namespaces::Descendants + .where(outdated_at: nil, namespace_id: id) + .select(cached_ids_column.as('ids')) + + # Invoke the consistent lookup query and collect the ids as a single array value + consistent_descendant_ids_scope = model + .from(consistent_ids_scope.arel.as(model.table_name)) + .reselect(Arel::Nodes::NamedFunction.new('ARRAY_AGG', [model.arel_table[:id]]).as('ids')) + .unscope(where: :type) + + from = <<~SQL + UNNEST( + COALESCE( + (SELECT ids FROM (#{cache_lookup_query.to_sql}) cached_query), + (SELECT ids FROM (#{consistent_descendant_ids_scope.to_sql}) consistent_query)) + ) AS #{model.table_name}(id) + SQL + + model + .from(from) + .unscope(where: :type) + .select(:id) + end + + def attempt_to_use_cached_data? + Feature.enabled?(:group_hierarchy_optimization, self, type: :beta) + end + override :sync_traversal_ids def sync_traversal_ids super diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb index c3348c49ea1..5779b777fd7 100644 --- a/app/models/namespaces/traversal/linear.rb +++ b/app/models/namespaces/traversal/linear.rb @@ -106,6 +106,10 @@ module Namespaces end end + def all_project_ids + all_projects.select(:id) + end + def self_and_descendants return super unless use_traversal_ids? diff --git a/app/models/namespaces/traversal/recursive.rb b/app/models/namespaces/traversal/recursive.rb index 1c5d395cb3c..3d551243cfb 100644 --- a/app/models/namespaces/traversal/recursive.rb +++ b/app/models/namespaces/traversal/recursive.rb @@ -19,6 +19,12 @@ module Namespaces end alias_method :recursive_root_ancestor, :root_ancestor + def all_project_ids + namespace = user_namespace? ? self : recursive_self_and_descendant_ids + Project.where(namespace: namespace).select(:id) + end + alias_method :recursive_all_project_ids, :all_project_ids + # Returns all ancestors, self, and descendants of the current namespace. def self_and_hierarchy object_hierarchy(self.class.where(id: id)) diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb index bbb08ed5774..e6dc99d114b 100644 --- a/app/models/user_detail.rb +++ b/app/models/user_detail.rb @@ -39,6 +39,7 @@ class UserDetail < MainClusterwide::ApplicationRecord validates :skype, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true validates :twitter, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true validates :website_url, length: { maximum: DEFAULT_FIELD_LENGTH }, url: true, allow_blank: true, if: :website_url_changed? + validates :onboarding_status, json_schema: { filename: 'user_detail_onboarding_status' } before_validation :sanitize_attrs before_save :prevent_nil_fields diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index a6ef8c8743b..bdf943091e9 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -61,7 +61,8 @@ module Groups params[:namespace_descendants_attributes] = { traversal_ids: group.traversal_ids, all_project_ids: [], - self_and_descendant_group_ids: [] + self_and_descendant_group_ids: [], + outdated_at: Time.current } else return unless group.namespace_descendants diff --git a/app/validators/json_schemas/user_detail_onboarding_status.json b/app/validators/json_schemas/user_detail_onboarding_status.json new file mode 100644 index 00000000000..548e81f1955 --- /dev/null +++ b/app/validators/json_schemas/user_detail_onboarding_status.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Onboarding Status", + "description": "Onboarding Status items recorded during onboarding/registration", + "type": "object", + "properties": { + "step_url": { + "description": "Onboarding step the user is currently on or last step before finishing", + "type": "string" + }, + "email_opt_in": { + "description": "Setting to guide marketing email opt-ins outside of the product. See https://gitlab.com/gitlab-org/gitlab/-/issues/435741", + "type": "boolean" + } + }, + "additionalProperties": false +} diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 78c3270114e..f7b2ba59549 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -6,6 +6,8 @@ = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{current_user.name} issues") = render_dashboard_ultimate_trial(current_user) += render_if_exists 'shared/dashboard/saml_reauth_notice', + groups_requiring_saml_reauth: user_groups_requiring_reauth .page-title-holder.gl-display-flex.gl-align-items-center %h1.page-title.gl-font-size-h-display= _('Issues') diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index 91cec50226b..d29cb56db07 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -11,6 +11,8 @@ add_page_specific_style 'page_bundles/issuable_list' = render_dashboard_ultimate_trial(current_user) += render_if_exists 'shared/dashboard/saml_reauth_notice', + groups_requiring_saml_reauth: user_groups_requiring_reauth .page-title-holder.d-flex.align-items-start.flex-column.flex-sm-row.align-items-sm-center %h1.page-title.gl-font-size-h-display= title diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 1b0bd10db77..0587ba61db4 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -2,7 +2,10 @@ = render_two_factor_auth_recovery_settings_check = render_dashboard_ultimate_trial(current_user) -= render_if_exists 'dashboard/todos/saml_reauth_notice' + += render_if_exists 'shared/dashboard/saml_reauth_notice', + groups_requiring_saml_reauth: todo_groups_requiring_saml_reauth(@todos) + - add_page_specific_style 'page_bundles/todos' - add_page_specific_style 'page_bundles/issuable' - filter_by_done = params[:state] == 'done' diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 0ae2e5337f5..a7e078b074a 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -2,7 +2,7 @@ - custom_text = custom_sign_in_description !!! 5 %html.html-devise-layout{ class: user_application_theme, lang: I18n.locale } - = render "layouts/head", { startup_filename: 'signin' } + = render "layouts/head" %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{client_class_list}", data: { page: body_data_page, testid: 'login-page' } } = header_message = render "layouts/init_client_detection_flags" @@ -15,12 +15,12 @@ .row.gl-mt-5.gl-row-gap-5 .col-md.order-12.sm-bg-gray .col-sm-12 - %h1.mb-3.gl-font-size-h2 + %h1.gl-mb-5.gl-font-size-h2 = brand_title = custom_text .col-md.order-md-12 .col-sm-12.bar - .gl-text-center + .gl-text-center.gl-mb-5 = brand_image = yield - else diff --git a/app/views/projects/gcp/artifact_registry/docker_images/_docker_image.html.haml b/app/views/projects/gcp/artifact_registry/docker_images/_docker_image.html.haml deleted file mode 100644 index 750dea9896f..00000000000 --- a/app/views/projects/gcp/artifact_registry/docker_images/_docker_image.html.haml +++ /dev/null @@ -1,33 +0,0 @@ -.gl-display-flex.gl-flex-direction-column - .gl-display-flex.gl-flex-direction-column.gl-border-b-solid.gl-border-t-solid.gl-border-t-1.gl-border-b-1.gl-border-t-transparent.gl-border-b-gray-100 - .gl-display-flex.gl-align-items-center.gl-py-3 - .gl-display-flex.gl-flex-direction-column.gl-sm-flex-direction-row.gl-justify-content-space-between.gl-align-items-stretch.gl-flex-grow-1 - .gl-display-flex.gl-flex-direction-column.gl-mb-3.gl-sm-mb-0.gl-min-w-0.gl-flex-grow-1 - .gl-display-flex.gl-align-items-center.gl-text-body.gl-font-weight-bold.gl-font-size-h2 - %span.gl-text-body.gl-font-weight-bold= docker_image.short_name - .gl-bg-gray-50.gl-inset-border-1-gray-100.gl-rounded-base.gl-pt-6 - .gl-display-flex.gl-align-items-top.gl-font-monospace.gl-font-sm.gl-word-break-all.gl-p-4.gl-border-b-solid.gl-border-gray-100.gl-border-b-1 - = sprite_icon('information-o', css_class: 'gl-text-gray-500 gl-mr-3 gl-icon s16') - Full name: #{docker_image.name} - .gl-display-flex.gl-align-items-top.gl-font-monospace.gl-font-sm.gl-word-break-all.gl-p-4.gl-border-b-solid.gl-border-gray-100.gl-border-b-1 - = sprite_icon('earth', css_class: 'gl-text-gray-500 gl-mr-3 gl-icon s16') - %a{ href: docker_image.details_url, target: 'blank', rel: 'noopener noreferrer' } - Artifact Registry details page - .gl-display-flex.gl-align-items-top.gl-font-monospace.gl-font-sm.gl-word-break-all.gl-p-4.gl-border-b-solid.gl-border-gray-100.gl-border-b-1 - = sprite_icon('doc-code', css_class: 'gl-text-gray-500 gl-mr-3 gl-icon s16') - Media Type: #{docker_image.media_type} - .gl-display-flex.gl-align-items-top.gl-font-monospace.gl-font-sm.gl-word-break-all.gl-p-4.gl-border-b-solid.gl-border-gray-100.gl-border-b-1 - = sprite_icon('archive', css_class: 'gl-text-gray-500 gl-mr-3 gl-icon s16') - Size: #{number_to_human_size(docker_image.image_size_bytes)} - .gl-display-flex.gl-align-items-top.gl-font-monospace.gl-font-sm.gl-word-break-all.gl-p-4.gl-border-b-solid.gl-border-gray-100.gl-border-b-1 - = sprite_icon('calendar', css_class: 'gl-text-gray-500 gl-mr-3 gl-icon s16') - Built at: #{docker_image.built_at&.to_fs} - .gl-display-flex.gl-align-items-top.gl-font-monospace.gl-font-sm.gl-word-break-all.gl-p-4.gl-border-b-solid.gl-border-gray-100.gl-border-b-1 - = sprite_icon('calendar', css_class: 'gl-text-gray-500 gl-mr-3 gl-icon s16') - Uploaded at: #{docker_image.uploaded_at&.to_fs} - .gl-display-flex.gl-align-items-top.gl-font-monospace.gl-font-sm.gl-word-break-all.gl-p-4.gl-border-b-solid.gl-border-gray-100.gl-border-b-1 - = sprite_icon('calendar', css_class: 'gl-text-gray-500 gl-mr-3 gl-icon s16') - Updated at: #{docker_image.updated_at&.to_fs} - - if docker_image.tags.present? - .gl-display-flex.gl-align-items-center.gl-text-gray-500.gl-min-h-6.gl-min-w-0.gl-flex-grow-1.gl-pt-4 - = render partial: 'docker_image_tag', collection: docker_image.tags diff --git a/app/views/projects/gcp/artifact_registry/docker_images/_docker_image_tag.html.haml b/app/views/projects/gcp/artifact_registry/docker_images/_docker_image_tag.html.haml deleted file mode 100644 index a030cd7d634..00000000000 --- a/app/views/projects/gcp/artifact_registry/docker_images/_docker_image_tag.html.haml +++ /dev/null @@ -1 +0,0 @@ -%a.gl-button.btn.btn-md.btn-default.gl-mr-3!= docker_image_tag diff --git a/app/views/projects/gcp/artifact_registry/docker_images/_pagination.html.haml b/app/views/projects/gcp/artifact_registry/docker_images/_pagination.html.haml deleted file mode 100644 index df98ba8d68e..00000000000 --- a/app/views/projects/gcp/artifact_registry/docker_images/_pagination.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -.gl-display-flex.gl-justify-content-center - %nav.gl-pagination.gl-mt-3 - .gl-keyset-pagination.btn-group - - if @page > 1 - = link_to 'Prev', namespace_project_gcp_artifact_registry_docker_images_path(params[:namespace_id], params[:project_id], page_token: @previous_page_token, page_tokens: @page_tokens, page: @page - 1, gcp_project_id: params[:gcp_project_id], gcp_location: params[:gcp_location], gcp_ar_repository: params[:gcp_ar_repository], gcp_wlif_url: params[:gcp_wlif_url]), class: 'btn btn-default btn-md gl-button' - - else - %span.btn.btn-default.btn-md.gl-button.disabled= 'Prev' - - if @next_page_token.present? - = link_to 'Next', namespace_project_gcp_artifact_registry_docker_images_path(params[:namespace_id], params[:project_id], page_token: @next_page_token, page_tokens: @page_tokens, page: @page + 1, gcp_project_id: params[:gcp_project_id], gcp_location: params[:gcp_location], gcp_ar_repository: params[:gcp_ar_repository], gcp_wlif_url: params[:gcp_wlif_url]), class: 'btn btn-default btn-md gl-button' - - else - %span.btn.btn-default.btn-md.gl-button.disabled= 'Next' - - diff --git a/app/views/projects/gcp/artifact_registry/docker_images/index.html.haml b/app/views/projects/gcp/artifact_registry/docker_images/index.html.haml deleted file mode 100644 index b487a175691..00000000000 --- a/app/views/projects/gcp/artifact_registry/docker_images/index.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -- page_title 'Artifact Registry Docker Images' - -- unless @error - .gl-display-flex.gl-flex-direction-column - .gl-display-flex.gl-justify-content-space-between.gl-py-3 - .gl-flex-direction-column.gl-flex-grow-1 - .gl-display-flex - .gl-display-flex.gl-flex-direction-column - %h2.gl-font-size-h1.gl-mt-3.gl-mb-0 Docker Images of #{@artifact_repository_name} - = render partial: 'pagination' - = render partial: 'docker_image', collection: @docker_images - = render partial: 'pagination' -- else - .flash-container.flash-container-page.sticky - .gl-alert.flash-notice.gl-alert-info - .gl-alert-icon-container - = sprite_icon('information-o', css_class: 's16 gl-alert-icon gl-alert-icon-no-title') - .gl-alert-content - .gl-alert-body - - if @error - = @error - - else - Nothing to show here. diff --git a/app/views/projects/gcp/artifact_registry/setup/new.html.haml b/app/views/projects/gcp/artifact_registry/setup/new.html.haml deleted file mode 100644 index 39ce0093372..00000000000 --- a/app/views/projects/gcp/artifact_registry/setup/new.html.haml +++ /dev/null @@ -1,31 +0,0 @@ -- page_title 'Artifact Registry Setup' - -- if @error.present? - .flash-container.flash-container-page.sticky - .gl-alert.flash-notice.gl-alert-info - .gl-alert-icon-container - = sprite_icon('information-o', css_class: 's16 gl-alert-icon gl-alert-icon-no-title') - .gl-alert-content - .gl-alert-body= @error -- else - %p - - = form_tag namespace_project_gcp_artifact_registry_docker_images_path , method: :get do - .form-group.row - = label_tag :gcp_project_id, 'Google Project ID', class: 'col-form-label col-md-2' - .col-md-4 - = text_field_tag :gcp_project_id, nil, class: 'form-control gl-form-input gl-mr-3' - .form-group.row - = label_tag :gcp_location, 'Google Project Location', class: 'col-form-label col-md-2' - .col-md-4 - = text_field_tag :gcp_location, nil, class: 'form-control gl-form-input gl-mr-3' - .form-group.row - = label_tag :gcp_ar_repository, 'Artifact Registry Repository Name', class: 'col-form-label col-md-2' - .col-md-4 - = text_field_tag :gcp_ar_repository, nil, class: 'form-control gl-form-input gl-mr-3' - .form-group.row - = label_tag :gcp_wlif_url, 'Worflow Identity Federation url', class: 'col-form-label col-md-2' - .col-md-4 - = text_field_tag :gcp_wlif_url, nil, class: 'form-control gl-form-input gl-mr-3' - .form-actions - = submit_tag 'Setup', class: 'gl-button btn btn-confirm' diff --git a/app/views/shared/wikis/edit.html.haml b/app/views/shared/wikis/edit.html.haml index ce8c7782c7f..ffe479329b4 100644 --- a/app/views/shared/wikis/edit.html.haml +++ b/app/views/shared/wikis/edit.html.haml @@ -1,3 +1,4 @@ +- breadcrumb_title(s_("Wiki|New Page")) unless @page.persisted? - wiki_page_title @page, @page.persisted? ? _('Edit') : _('New') - add_page_specific_style 'page_bundles/wiki' - @gfm_form = true @@ -16,7 +17,7 @@ · = s_("Wiki|Edit Page") - else - = s_("Wiki|Create New Page") + = s_("Wiki|New Page") .nav-controls.pb-md-3.pb-lg-0 - if @page.persisted? |