diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-06 18:12:21 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-06 18:12:21 +0300 |
commit | 74739c4b9e2ced14233eaeee221c5472589d26cd (patch) | |
tree | 4941af4c79d998d0f9b99de8e3b11c19765ee9a8 | |
parent | e08e64feb78ad8a490769b2aa59a13166bbdc073 (diff) |
Add latest changes from gitlab-org/gitlab@master
50 files changed, 589 insertions, 284 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 68408a5f640..5780c96e363 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -bda5c66c7a4207b1bfae779f367eba0776873cd5 +f6193a424bcc235e906bc1ae82737b4ed58ec8fd diff --git a/app/assets/javascripts/jobs/components/job/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job/job_log_controllers.vue index ffa06892653..78204103feb 100644 --- a/app/assets/javascripts/jobs/components/job/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job/job_log_controllers.vue @@ -215,7 +215,7 @@ export default { :aria-label="$options.i18n.showRawButtonLabel" :href="rawPath" data-testid="job-raw-link-controller" - icon="doc-text" + icon="doc-code" /> <!-- eo links --> diff --git a/app/assets/javascripts/jobs/components/table/cells/job_cell.vue b/app/assets/javascripts/jobs/components/table/cells/job_cell.vue index 27d286fc766..b435eb283fd 100644 --- a/app/assets/javascripts/jobs/components/table/cells/job_cell.vue +++ b/app/assets/javascripts/jobs/components/table/cells/job_cell.vue @@ -71,7 +71,7 @@ export default { <template> <div> - <div class="gl-text-truncate gl-mb-2"> + <div class="gl-text-truncate gl-p-3 gl-mt-n3 gl-mx-n3 gl-mb-n2"> <gl-link v-if="canReadJob" class="gl-text-blue-600!" @@ -96,7 +96,7 @@ export default { > <div v-if="jobRef" - class="gl-px-2 gl-rounded-base gl-bg-gray-50 gl-max-w-15 gl-text-truncate" + class="gl-p-2 gl-rounded-base gl-bg-gray-50 gl-max-w-15 gl-text-truncate" > <gl-icon v-if="createdByTag" @@ -114,7 +114,7 @@ export default { </div> <span v-else>{{ __('none') }}</span> - <div class="gl-ml-2 gl-rounded-base gl-px-2 gl-bg-gray-50"> + <div class="gl-ml-2 gl-p-2 gl-rounded-base gl-bg-gray-50"> <gl-icon class="gl-mx-2" name="commit" :size="$options.iconSize" /> <gl-link class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900" diff --git a/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue b/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue index c8f0fdd4439..18d68ee8a29 100644 --- a/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue +++ b/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue @@ -36,12 +36,16 @@ export default { <template> <div> - <div class="gl-text-truncate"> - <gl-link class="gl-text-gray-500!" :href="pipelinePath" data-testid="pipeline-id"> + <div class="gl-p-3 gl-mt-n3"> + <gl-link + class="gl-text-truncate gl-ml-n3 gl-text-gray-500!" + :href="pipelinePath" + data-testid="pipeline-id" + > {{ pipelineId }} </gl-link> </div> - <div class="gl-font-sm gl-text-secondary gl-mt-2"> + <div class="gl-font-sm gl-text-secondary gl-mt-n2"> <span>{{ __('created by') }}</span> <gl-link v-if="showAvatar" :href="userPath" data-testid="pipeline-user-link"> <gl-avatar :src="pipelineUserAvatar" :size="16" /> diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue index b64f3ac52b2..4cfc329f8b8 100644 --- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue +++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue @@ -18,14 +18,12 @@ import { sprintf } from '~/locale'; import { ARROW_DOWN_KEY, ARROW_UP_KEY, END_KEY, HOME_KEY, ESC_KEY } from '~/lib/utils/keys'; import { MIN_SEARCH_TERM, - SEARCH_GITLAB, SEARCH_DESCRIBED_BY_WITH_RESULTS, SEARCH_DESCRIBED_BY_DEFAULT, SEARCH_DESCRIBED_BY_UPDATED, SEARCH_RESULTS_LOADING, SEARCH_RESULTS_SCOPE, } from '~/vue_shared/global_search/constants'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { darkModeEnabled } from '~/lib/utils/color_utils'; import { SEARCH_INPUT_DESCRIPTION, @@ -52,10 +50,10 @@ export default { name: 'GlobalSearchModal', SEARCH_MODAL_ID, i18n: { - SEARCH_GITLAB, SEARCH_DESCRIBED_BY_WITH_RESULTS, SEARCH_DESCRIBED_BY_DEFAULT, SEARCH_DESCRIBED_BY_UPDATED, + SEARCH_OR_COMMAND_MODE_PLACEHOLDER, SEARCH_RESULTS_LOADING, SEARCH_RESULTS_SCOPE, MIN_SEARCH_TERM, @@ -72,7 +70,6 @@ export default { CommandPaletteItems, FakeSearchInput, }, - mixins: [glFeatureFlagMixin()], data() { return { nextFocusedItemIndex: null, @@ -89,9 +86,6 @@ export default { this.setSearch(value); }, }, - searchPlaceholder() { - return this.glFeatures?.commandPalette ? SEARCH_OR_COMMAND_MODE_PLACEHOLDER : SEARCH_GITLAB; - }, showDefaultItems() { return !this.searchText; }, @@ -146,9 +140,8 @@ export default { }, isCommandMode() { return ( - this.glFeatures?.commandPalette && - (COMMON_HANDLES.includes(this.searchTextFirstChar) || - (this.searchContext.project && this.searchTextFirstChar === PATH_HANDLE)) + COMMON_HANDLES.includes(this.searchTextFirstChar) || + (this.searchContext?.project && this.searchTextFirstChar === PATH_HANDLE) ); }, commandPaletteQuery() { @@ -294,7 +287,7 @@ export default { > <form role="search" - :aria-label="searchPlaceholder" + :aria-label="$options.i18n.SEARCH_OR_COMMAND_MODE_PLACEHOLDER" class="gl-relative gl-rounded-base gl-w-full gl-pb-0" > <div class="gl-relative gl-bg-white gl-border-b gl-mb-n1 gl-p-3"> @@ -305,7 +298,7 @@ export default { role="searchbox" data-testid="global-search-input" autocomplete="off" - :placeholder="searchPlaceholder" + :placeholder="$options.i18n.SEARCH_OR_COMMAND_MODE_PLACEHOLDER" :aria-describedby="$options.SEARCH_INPUT_DESCRIPTION" borderless @input="getAutocompleteOptions" diff --git a/app/finders/work_items/namespace_work_items_finder.rb b/app/finders/work_items/namespace_work_items_finder.rb index aad99d710b6..da6437e0907 100644 --- a/app/finders/work_items/namespace_work_items_finder.rb +++ b/app/finders/work_items/namespace_work_items_finder.rb @@ -2,19 +2,14 @@ module WorkItems class NamespaceWorkItemsFinder < WorkItemsFinder + FilterNotAvailableError = Class.new(ArgumentError) + def initialize(...) super self.parent_param = namespace end - def execute - items = init_collection - items = by_namespace(items) - - sort(items) - end - override :with_confidentiality_access_check def with_confidentiality_access_check return model_class.all if params.user_can_see_all_issuables? @@ -31,6 +26,12 @@ module WorkItems private + def filter_items(items) + items = super(items) + + by_namespace(items) + end + def by_namespace(items) if namespace.blank? || !Ability.allowed?(current_user, "read_#{namespace.to_ability_name}".to_sym, namespace) return klass.none @@ -39,11 +40,23 @@ module WorkItems items.in_namespaces(namespace) end + override :by_search + def by_search(items) + return items unless search + + raise FilterNotAvailableError, 'Searching is not available for work items at the namespace level yet' + end + def namespace return if params[:namespace_id].blank? params[:namespace_id].is_a?(Namespace) ? params[:namespace_id] : Namespace.find_by_id(params[:namespace_id]) end strong_memoize_attr :namespace + + override :by_project + def by_project(items) + items + end end end diff --git a/app/graphql/resolvers/ci/all_jobs_resolver.rb b/app/graphql/resolvers/ci/all_jobs_resolver.rb index 54dd435d617..85b0f8da877 100644 --- a/app/graphql/resolvers/ci/all_jobs_resolver.rb +++ b/app/graphql/resolvers/ci/all_jobs_resolver.rb @@ -40,6 +40,7 @@ params: { scope: statuses, runner_type: runner_types }).execute play_path: [{ project: { namespace: [:route] } }], web_path: [{ project: { namespace: [:route] } }], tags: [:tags], + ai_failure_analysis: [{ project: [:project_feature, :namespace] }], trace: [{ project: [:namespace] }, :job_artifacts_trace] } end diff --git a/app/graphql/resolvers/ci/runner_jobs_resolver.rb b/app/graphql/resolvers/ci/runner_jobs_resolver.rb index c0a5057cddd..b005702a71d 100644 --- a/app/graphql/resolvers/ci/runner_jobs_resolver.rb +++ b/app/graphql/resolvers/ci/runner_jobs_resolver.rb @@ -45,6 +45,7 @@ module Resolvers web_path: [{ project: { namespace: [:route] } }], short_sha: [:pipeline], tags: [:tags], + ai_failure_analysis: [{ project: [:project_feature, :namespace] }], trace: [{ project: [:namespace] }, :job_artifacts_trace] } end diff --git a/app/graphql/resolvers/namespaces/work_items_resolver.rb b/app/graphql/resolvers/namespaces/work_items_resolver.rb index 54bb8392071..6985a7a898a 100644 --- a/app/graphql/resolvers/namespaces/work_items_resolver.rb +++ b/app/graphql/resolvers/namespaces/work_items_resolver.rb @@ -2,33 +2,31 @@ module Resolvers module Namespaces - class WorkItemsResolver < BaseResolver - prepend ::WorkItems::LookAheadPreloads + # rubocop:disable Graphql/ResolverType (inherited from Resolvers::WorkItemsResolver) + class WorkItemsResolver < ::Resolvers::WorkItemsResolver + def ready?(**args) + return false if Feature.disabled?(:namespace_level_work_items, resource_parent) - type Types::WorkItemType.connection_type, null: true - - def resolve_with_lookahead(**args) - return unless Feature.enabled?(:namespace_level_work_items, resource_parent) - return WorkItem.none if resource_parent.nil? - - finder = ::WorkItems::NamespaceWorkItemsFinder.new(current_user, args.merge( - namespace_id: resource_parent - )) + super + end - Gitlab::Graphql::Loaders::IssuableLoader.new(resource_parent, finder).batching_find_all do |q| - apply_lookahead(q) - end + override :resolve_with_lookahead + def resolve_with_lookahead(...) + super + rescue ::WorkItems::NamespaceWorkItemsFinder::FilterNotAvailableError => e + raise Gitlab::Graphql::Errors::ArgumentError, e.message end private - def resource_parent - # The project could have been loaded in batch by `BatchLoader`. - # At this point we need the `id` of the project to query for work items, so - # make sure it's loaded and not `nil` before continuing. - object.respond_to?(:sync) ? object.sync : object + override :finder + def finder(args) + ::WorkItems::NamespaceWorkItemsFinder.new( + current_user, + args.merge(namespace_id: resource_parent) + ) end - strong_memoize_attr :resource_parent end + # rubocop:enable Graphql/ResolverType end end diff --git a/app/graphql/resolvers/work_items_resolver.rb b/app/graphql/resolvers/work_items_resolver.rb index d4f73361e05..995f54f35d8 100644 --- a/app/graphql/resolvers/work_items_resolver.rb +++ b/app/graphql/resolvers/work_items_resolver.rb @@ -21,13 +21,18 @@ module Resolvers def resolve_with_lookahead(**args) return WorkItem.none if resource_parent.nil? - finder = ::WorkItems::WorkItemsFinder.new(current_user, prepare_finder_params(args)) - - Gitlab::Graphql::Loaders::IssuableLoader.new(resource_parent, finder).batching_find_all { |q| apply_lookahead(q) } + Gitlab::Graphql::Loaders::IssuableLoader.new( + resource_parent, + finder(prepare_finder_params(args)) + ).batching_find_all { |q| apply_lookahead(q) } end private + def finder(args) + ::WorkItems::WorkItemsFinder.new(current_user, args) + end + def prepare_finder_params(args) params = super(args) params[:iids] ||= [params.delete(:iid)].compact if params[:iid] diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb index cc7f9cd2a57..17dbf8acbc3 100644 --- a/app/models/users/callout.rb +++ b/app/models/users/callout.rb @@ -73,7 +73,8 @@ module Users new_navigation_callout: 71, # 72 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129022 namespace_over_storage_users_combined_alert: 73, # EE-only - rich_text_editor: 74 + rich_text_editor: 74, + vsd_feedback_banner: 75 # EE-only } validates :feature_name, diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index f546bd5951a..27cfaef2db2 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -180,7 +180,18 @@ class IssuableBaseService < ::BaseContainerService new_label_ids |= add_label_ids if add_label_ids new_label_ids -= remove_label_ids if remove_label_ids - new_label_ids.uniq + filter_locked_labels(issuable, new_label_ids.uniq, existing_label_ids) + end + + # Filter out any locked labels that are attempting to be removed + def filter_locked_labels(issuable, ids, existing_label_ids) + return ids unless issuable.supports_lock_on_merge? + return ids unless existing_label_ids.present? + + removed_label_ids = existing_label_ids - ids + removed_locked_label_ids = labels_service.filter_locked_label_ids(removed_label_ids) + + ids + removed_locked_label_ids end def process_assignee_ids(attributes, existing_assignee_ids: nil, extra_assignee_ids: []) diff --git a/app/services/labels/available_labels_service.rb b/app/services/labels/available_labels_service.rb index 21f92eeaf09..7809f263eb6 100644 --- a/app/services/labels/available_labels_service.rb +++ b/app/services/labels/available_labels_service.rb @@ -40,19 +40,8 @@ module Labels ids.map(&:to_i) & existing_ids end - def filter_locked_labels_ids_in_param(key) - ids = Array.wrap(params[key]) - return [] if ids.empty? - - params = finder_params - params[:locked_labels] = true - existing_labels = LabelsFinder.new(current_user, params).execute - - # rubocop:disable CodeReuse/ActiveRecord - existing_ids = existing_labels.id_in(ids).pluck(:id) - # rubocop:enable CodeReuse/ActiveRecord - - ids.map(&:to_i) & existing_ids + def filter_locked_label_ids(ids) + available_labels.with_lock_on_merge.id_in(ids).pluck(:id) # rubocop:disable CodeReuse/ActiveRecord end def available_labels diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index b1894f7fded..8a2ed4d08e6 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -176,20 +176,10 @@ module MergeRequests params.delete(:allow_collaboration) end - filter_locked_labels(merge_request) filter_reviewer(merge_request) filter_suggested_reviewers end - # Filter out any locked labels that are requested to be removed. - # Only supported for merged MRs. - def filter_locked_labels(merge_request) - return unless params[:remove_label_ids].present? - return unless merge_request.supports_lock_on_merge? - - params[:remove_label_ids] -= labels_service.filter_locked_labels_ids_in_param(:remove_label_ids) - end - def filter_reviewer(merge_request) return if params[:reviewer_ids].blank? diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index 98bd9ad93ef..215fbc3b262 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -25,7 +25,7 @@ .form-group = f.label :max_import_size, _('Maximum import size (MiB)'), class: 'label-light' = f.number_field :max_import_size, class: 'form-control gl-form-input', title: _('Maximum size of import files.'), data: { toggle: 'tooltip', container: 'body' } - %span.form-text.text-muted= _('Only effective when remote storage is enabled. Set to 0 for no size limit.') + %span.form-text.text-muted= _('Set to 0 for no size limit.') .form-group = f.label :max_import_remote_file_size, s_('Import|Maximum import remote file size (MB)'), class: 'label-light' = f.number_field :max_import_remote_file_size, class: 'form-control gl-form-input', title: s_('Import|Maximum remote file size for imports from external object storages. For example, AWS S3.'), data: { toggle: 'tooltip', container: 'body' } diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 345a1cc0225..e6551adffde 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -1,6 +1,6 @@ = gitlab_ui_form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'gl-p-5 gl-show-field-errors js-arkose-labs-form', aria: { live: 'assertive' }, data: { testid: 'sign-in-form' }}) do |f| .form-group - = f.label :login, _('Username or email') + = f.label :login, _('Username or primary email') = f.text_field :login, value: @invite_email, class: 'form-control gl-form-input js-username-field', autocomplete: 'username', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', required: true, title: _('This field is required.'), data: { qa_selector: 'login_field', testid: 'username-field' } .form-group = f.label :password, _('Password') diff --git a/app/views/projects/settings/merge_requests/show.html.haml b/app/views/projects/settings/merge_requests/show.html.haml index f9f26d04e53..01af028f30c 100644 --- a/app/views/projects/settings/merge_requests/show.html.haml +++ b/app/views/projects/settings/merge_requests/show.html.haml @@ -17,3 +17,4 @@ = render_if_exists 'projects/settings/merge_requests/merge_request_approvals_settings', expanded: true = render_if_exists 'projects/settings/merge_requests/suggested_reviewers_settings', expanded: true += render_if_exists 'projects/settings/merge_requests/target_branch_rules_settings' diff --git a/config/feature_flags/development/command_palette.yml b/config/feature_flags/development/command_palette.yml deleted file mode 100644 index 3a7935e6bf5..00000000000 --- a/config/feature_flags/development/command_palette.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: command_palette -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121932 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/413060 -milestone: '16.1' -type: development -group: group::foundations -default_enabled: true diff --git a/db/docs/audit_events_instance_google_cloud_logging_configurations.yml b/db/docs/audit_events_instance_google_cloud_logging_configurations.yml new file mode 100644 index 00000000000..059ab59d860 --- /dev/null +++ b/db/docs/audit_events_instance_google_cloud_logging_configurations.yml @@ -0,0 +1,10 @@ +--- +table_name: audit_events_instance_google_cloud_logging_configurations +classes: + - AuditEvents::Instance::GoogleCloudLoggingConfiguration +feature_categories: + - audit_events +description: Stores Instance level Google Cloud Logging configurations associated with IAM service accounts, used for generating access tokens. +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/423036 +milestone: '16.4' +gitlab_schema: gitlab_main diff --git a/db/migrate/20230823132142_create_instance_google_cloud_logging_configurations.rb b/db/migrate/20230823132142_create_instance_google_cloud_logging_configurations.rb new file mode 100644 index 00000000000..c659264038d --- /dev/null +++ b/db/migrate/20230823132142_create_instance_google_cloud_logging_configurations.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class CreateInstanceGoogleCloudLoggingConfigurations < Gitlab::Database::Migration[2.1] + enable_lock_retries! + + UNIQUE_INDEX_NAME = "unique_instance_google_cloud_logging_configurations" + UNIQUE_CONFIG_NAME_INDEX = "unique_instance_google_cloud_logging_configurations_name" + + def change + create_table :audit_events_instance_google_cloud_logging_configurations do |t| + t.timestamps_with_timezone null: false + t.text :google_project_id_name, null: false, limit: 30 + t.text :client_email, null: false, limit: 254 + t.text :log_id_name, default: "audit_events", limit: 511 + t.text :name, null: false, limit: 72 + t.binary :encrypted_private_key, null: false + t.binary :encrypted_private_key_iv, null: false + + t.index [:google_project_id_name, :log_id_name], unique: true, name: UNIQUE_INDEX_NAME + t.index :name, unique: true, name: UNIQUE_CONFIG_NAME_INDEX + end + end +end diff --git a/db/schema_migrations/20230823132142 b/db/schema_migrations/20230823132142 new file mode 100644 index 00000000000..2ea42285c0e --- /dev/null +++ b/db/schema_migrations/20230823132142 @@ -0,0 +1 @@ +438ce9b705e575c139724a2ad81e799bdda63b9428b4cf3bd21b9e47df36ece7
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index c68fc40f247..0ab32dec106 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -12379,6 +12379,31 @@ CREATE SEQUENCE audit_events_instance_external_audit_event_destinations_id_seq ALTER SEQUENCE audit_events_instance_external_audit_event_destinations_id_seq OWNED BY audit_events_instance_external_audit_event_destinations.id; +CREATE TABLE audit_events_instance_google_cloud_logging_configurations ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + google_project_id_name text NOT NULL, + client_email text NOT NULL, + log_id_name text DEFAULT 'audit_events'::text, + name text NOT NULL, + encrypted_private_key bytea NOT NULL, + encrypted_private_key_iv bytea NOT NULL, + CONSTRAINT check_0da5c76c49 CHECK ((char_length(client_email) <= 254)), + CONSTRAINT check_74fd943192 CHECK ((char_length(log_id_name) <= 511)), + CONSTRAINT check_ab65f57721 CHECK ((char_length(google_project_id_name) <= 30)), + CONSTRAINT check_ac42ad3ca2 CHECK ((char_length(name) <= 72)) +); + +CREATE SEQUENCE audit_events_instance_google_cloud_logging_configuration_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE audit_events_instance_google_cloud_logging_configuration_id_seq OWNED BY audit_events_instance_google_cloud_logging_configurations.id; + CREATE TABLE audit_events_streaming_event_type_filters ( id bigint NOT NULL, created_at timestamp with time zone NOT NULL, @@ -25422,6 +25447,8 @@ ALTER TABLE ONLY audit_events_google_cloud_logging_configurations ALTER COLUMN i ALTER TABLE ONLY audit_events_instance_external_audit_event_destinations ALTER COLUMN id SET DEFAULT nextval('audit_events_instance_external_audit_event_destinations_id_seq'::regclass); +ALTER TABLE ONLY audit_events_instance_google_cloud_logging_configurations ALTER COLUMN id SET DEFAULT nextval('audit_events_instance_google_cloud_logging_configuration_id_seq'::regclass); + ALTER TABLE ONLY audit_events_streaming_event_type_filters ALTER COLUMN id SET DEFAULT nextval('audit_events_streaming_event_type_filters_id_seq'::regclass); ALTER TABLE ONLY audit_events_streaming_headers ALTER COLUMN id SET DEFAULT nextval('audit_events_streaming_headers_id_seq'::regclass); @@ -27215,6 +27242,9 @@ ALTER TABLE ONLY audit_events_google_cloud_logging_configurations ALTER TABLE ONLY audit_events_instance_external_audit_event_destinations ADD CONSTRAINT audit_events_instance_external_audit_event_destinations_pkey PRIMARY KEY (id); +ALTER TABLE ONLY audit_events_instance_google_cloud_logging_configurations + ADD CONSTRAINT audit_events_instance_google_cloud_logging_configurations_pkey PRIMARY KEY (id); + ALTER TABLE ONLY audit_events ADD CONSTRAINT audit_events_pkey PRIMARY KEY (id, created_at); @@ -34328,6 +34358,10 @@ CREATE UNIQUE INDEX unique_index_sysaccess_ms_access_tokens_on_sysaccess_ms_app_ CREATE UNIQUE INDEX unique_instance_audit_event_destination_name ON audit_events_instance_external_audit_event_destinations USING btree (name); +CREATE UNIQUE INDEX unique_instance_google_cloud_logging_configurations ON audit_events_instance_google_cloud_logging_configurations USING btree (google_project_id_name, log_id_name); + +CREATE UNIQUE INDEX unique_instance_google_cloud_logging_configurations_name ON audit_events_instance_google_cloud_logging_configurations USING btree (name); + CREATE UNIQUE INDEX unique_merge_request_diff_llm_summaries_on_mr_diff_id ON merge_request_diff_llm_summaries USING btree (merge_request_diff_id); CREATE UNIQUE INDEX unique_merge_request_metrics_by_merge_request_id ON merge_request_metrics USING btree (merge_request_id); diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index f307792901b..ffeeae09611 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -16867,7 +16867,6 @@ GPG signature for a signed commit. | <a id="groupvisibility"></a>`visibility` | [`String`](#string) | Visibility of the namespace. | | <a id="groupvulnerabilityscanners"></a>`vulnerabilityScanners` | [`VulnerabilityScannerConnection`](#vulnerabilityscannerconnection) | Vulnerability scanners reported on the project vulnerabilities of the group and its subgroups. (see [Connections](#connections)) | | <a id="groupweburl"></a>`webUrl` | [`String!`](#string) | Web URL of the group. | -| <a id="groupworkitems"></a>`workItems` **{warning-solid}** | [`WorkItemConnection`](#workitemconnection) | **Introduced** in 16.3. This feature is an Experiment. It can be changed or removed at any time. Work items that belong to the namespace. | #### Fields with arguments @@ -17760,6 +17759,35 @@ four standard [pagination arguments](#connection-pagination-arguments): | ---- | ---- | ----------- | | <a id="groupworkitemtypestaskable"></a>`taskable` | [`Boolean`](#boolean) | If `true`, only taskable work item types will be returned. Argument is experimental and can be removed in the future without notice. | +##### `Group.workItems` + +Work items that belong to the namespace. + +WARNING: +**Introduced** in 16.3. +This feature is an Experiment. It can be changed or removed at any time. + +Returns [`WorkItemConnection`](#workitemconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#connection-pagination-arguments): +`before: String`, `after: String`, `first: Int`, `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="groupworkitemsauthorusername"></a>`authorUsername` **{warning-solid}** | [`String`](#string) | **Introduced** in 15.9. This feature is an Experiment. It can be changed or removed at any time. Filter work items by author username. | +| <a id="groupworkitemsiid"></a>`iid` | [`String`](#string) | IID of the work item. For example, "1". | +| <a id="groupworkitemsiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of work items. For example, `["1", "2"]`. | +| <a id="groupworkitemsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. | +| <a id="groupworkitemsrequirementlegacywidget"></a>`requirementLegacyWidget` **{warning-solid}** | [`RequirementLegacyFilterInput`](#requirementlegacyfilterinput) | **Deprecated** in 15.9. Use work item IID filter instead. | +| <a id="groupworkitemssearch"></a>`search` | [`String`](#string) | Search query for title or description. | +| <a id="groupworkitemssort"></a>`sort` | [`WorkItemSort`](#workitemsort) | Sort work items by criteria. | +| <a id="groupworkitemsstate"></a>`state` | [`IssuableState`](#issuablestate) | Current state of the work item. | +| <a id="groupworkitemsstatuswidget"></a>`statusWidget` | [`StatusFilterInput`](#statusfilterinput) | Input for status widget filter. Ignored if `work_items_mvc_2` is disabled. | +| <a id="groupworkitemstypes"></a>`types` | [`[IssueType!]`](#issuetype) | Filter work items by the given work item types. | + ### `GroupDataTransfer` #### Fields @@ -28195,6 +28223,7 @@ Name of the feature that the callout is for. | <a id="usercalloutfeaturenameenumunfinished_tag_cleanup_callout"></a>`UNFINISHED_TAG_CLEANUP_CALLOUT` | Callout feature name for unfinished_tag_cleanup_callout. | | <a id="usercalloutfeaturenameenumuser_reached_limit_free_plan_alert"></a>`USER_REACHED_LIMIT_FREE_PLAN_ALERT` | Callout feature name for user_reached_limit_free_plan_alert. | | <a id="usercalloutfeaturenameenumverification_reminder"></a>`VERIFICATION_REMINDER` | Callout feature name for verification_reminder. | +| <a id="usercalloutfeaturenameenumvsd_feedback_banner"></a>`VSD_FEEDBACK_BANNER` | Callout feature name for vsd_feedback_banner. | | <a id="usercalloutfeaturenameenumweb_ide_alert_dismissed"></a>`WEB_IDE_ALERT_DISMISSED` | Callout feature name for web_ide_alert_dismissed. | | <a id="usercalloutfeaturenameenumweb_ide_ci_environments_guidance"></a>`WEB_IDE_CI_ENVIRONMENTS_GUIDANCE` | Callout feature name for web_ide_ci_environments_guidance. | diff --git a/doc/ci/pipelines/merge_request_pipelines.md b/doc/ci/pipelines/merge_request_pipelines.md index 356b97aacc0..7b8a2a16734 100644 --- a/doc/ci/pipelines/merge_request_pipelines.md +++ b/doc/ci/pipelines/merge_request_pipelines.md @@ -169,8 +169,13 @@ To use the UI to run a pipeline in the parent project for a merge request from a 1. Select **Run pipeline**. You must read and accept the warning, or the pipeline does not run. You can disable this feature by using [the projects API](../../api/projects.md#edit-project) -to disable the `ci_allow_fork_pipelines_to_run_in_parent_project` setting. -The setting is `enabled` by default. +to disable the `ci_allow_fork_pipelines_to_run_in_parent_project` setting (enabled by default). +When you disable this setting, new pipelines from forks in the parent project are prevented. + +WARNING: +Older pipelines created before the setting was disabled are not affected and continue to run. +If you rerun a job in an older pipeline, the job uses the same context as when the +pipeline was originally created. ## Available predefined variables diff --git a/doc/user/project/codeowners/index.md b/doc/user/project/codeowners/index.md index 8a450fdb64f..c77efaab2ea 100644 --- a/doc/user/project/codeowners/index.md +++ b/doc/user/project/codeowners/index.md @@ -77,6 +77,14 @@ all others are ignored: For more information, see [Code Owners syntax and error handling](reference.md). +#### When user or group change names + +When a user or group change their names, the `CODEOWNERS` isn't automatically updated with the new names. +To enter the new names, you must edit the file. + +Organizations using SAML SSO can [set usernames](../../../integration/saml.md#set-a-username) to +prevent users from being able to change their usernames. + ### Add a group as a Code Owner To set the members of a group or subgroup as a Code Owner: diff --git a/doc/user/project/issues/crosslinking_issues.md b/doc/user/project/issues/crosslinking_issues.md index b3fe7da4280..882e4d97938 100644 --- a/doc/user/project/issues/crosslinking_issues.md +++ b/doc/user/project/issues/crosslinking_issues.md @@ -76,3 +76,10 @@ you can also [set an issue to close automatically](managing_issues.md#closing-is as soon as the merge request is merged. ![issue mentioned in MR](img/mention_in_merge_request.png) + +## From branch names + +When you create a branch in the same project as an issue and start the branch name with the issue +number, followed by a hyphen, the issue and MR you create are linked. +For more information, see +[Prefix branch names with issue numbers](../repository/branches/index.md#prefix-branch-names-with-issue-numbers). diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md index 3d62d6614c7..f33d443cd7f 100644 --- a/doc/user/project/repository/branches/index.md +++ b/doc/user/project/repository/branches/index.md @@ -219,8 +219,13 @@ To change the default pattern for branches created from issues: ### Prefix branch names with issue numbers -To streamline the creation of merge requests, start your branch name with an -issue number. GitLab uses the issue number to import data into the merge request: +To streamline the creation of merge requests, start your Git branch name with the +issue number, followed by a hyphen. +For example, to link a branch to issue `#123`, start the branch name with `123-`. + +The issue and the branch must be in the same project. + +GitLab uses the issue number to import data into the merge request: - The issue is marked as related to the merge request. The issue and merge request display links to each other. diff --git a/doc/user/search/command_palette.md b/doc/user/search/command_palette.md index 8876b61e4d2..0f42e727eda 100644 --- a/doc/user/search/command_palette.md +++ b/doc/user/search/command_palette.md @@ -7,16 +7,12 @@ type: reference # Command palette **(FREE ALL)** -> Introduced in GitLab 16.2 [with a flag](../../administration/feature_flags.md) named `command_palette`. Enabled by default. +> - Introduced in GitLab 16.2 [with a flag](../../administration/feature_flags.md) named `command_palette`. Enabled by default. +> - Feature flag `command_palette` removed in GitLab 16.4. You can use command palette to narrow down the scope of your search or to find an object more quickly. -FLAG: -On self-managed GitLab, by default this feature is available. -To hide the feature, an administrator can [disable the feature flag](../../administration/feature_flags.md) named `command_palette`. -On GitLab.com, this feature is available. - ## Open the command palette To open the command palette: diff --git a/lib/gitlab/database/load_balancing/service_discovery.rb b/lib/gitlab/database/load_balancing/service_discovery.rb index 61e44458613..bc38bbcbb51 100644 --- a/lib/gitlab/database/load_balancing/service_discovery.rb +++ b/lib/gitlab/database/load_balancing/service_discovery.rb @@ -118,7 +118,7 @@ module Gitlab # The return value is the amount of time (in seconds) to wait before # checking the DNS record for any changes. def refresh_if_necessary - interval, from_dns = addresses_from_dns + wait_time, from_dns = addresses_from_dns current = addresses_from_load_balancer @@ -132,7 +132,7 @@ module Gitlab replace_hosts(from_dns) end - interval + wait_time end # Replaces all the hosts in the load balancer with the new ones, diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 2b24d1e5716..4e6e18825f5 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -74,7 +74,6 @@ module Gitlab push_frontend_feature_flag(:source_editor_toolbar) push_frontend_feature_flag(:vscode_web_ide, current_user) push_frontend_feature_flag(:unbatch_graphql_queries, current_user) - push_frontend_feature_flag(:command_palette, current_user) # To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/399248 push_frontend_feature_flag(:remove_monitor_metrics) push_frontend_feature_flag(:gitlab_duo, current_user) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e23b2dfb135..317d93371bb 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2968,6 +2968,9 @@ msgstr "" msgid "Add tag" msgstr "" +msgid "Add target branch rule" +msgstr "" + msgid "Add text to the sign-in page. Markdown enabled." msgstr "" @@ -13799,6 +13802,9 @@ msgstr "" msgid "Create requirement" msgstr "" +msgid "Create rules for target branches in merge requests." +msgstr "" + msgid "Create service account" msgstr "" @@ -14783,6 +14789,9 @@ msgstr "" msgid "DORA4Metrics|To help us improve the Show forecast feature, please share feedback about your experience in %{linkStart}this issue%{linkEnd}." msgstr "" +msgid "DORA4Metrics|To help us improve the Value Stream Management Dashboard, please share feedback about your experience in this %{linkStart}survey%{linkEnd}." +msgstr "" + msgid "DORA4Metrics|Took 1 day or less to restore service when a service incident or a defect that impacts users occurs." msgstr "" @@ -32831,9 +32840,6 @@ msgstr "" msgid "Only active projects show up in the search and on the dashboard." msgstr "" -msgid "Only effective when remote storage is enabled. Set to 0 for no size limit." -msgstr "" - msgid "Only include features new to your current subscription tier." msgstr "" @@ -40286,6 +40292,9 @@ msgstr "" msgid "Ruby" msgstr "" +msgid "Rule name" +msgstr "" + msgid "Rule name is already taken." msgstr "" @@ -41651,9 +41660,18 @@ msgstr "" msgid "ScanResultPolicy|When %{scanType} in an open merge request targeting %{branches} %{branchExceptions} and the licenses match all of the following criteria:" msgstr "" +msgid "ScanResultPolicy|When %{scanType} in an open that targets %{branches} with %{commitType}" +msgstr "" + msgid "ScanResultPolicy|When %{scanners} find scanner specified conditions in an open merge request targeting the %{branches} %{branchExceptions} and match %{boldDescription} of the following criteria" msgstr "" +msgid "ScanResultPolicy|any commits" +msgstr "" + +msgid "ScanResultPolicy|any unsigned commits" +msgstr "" + msgid "ScanResultPolicy|license status" msgstr "" @@ -42297,6 +42315,9 @@ msgstr "" msgid "SecurityOrchestration|And scans to be performed:" msgstr "" +msgid "SecurityOrchestration|Any merge request" +msgstr "" + msgid "SecurityOrchestration|Are you sure you want to delete this policy? This action cannot be undone." msgstr "" @@ -46662,6 +46683,9 @@ msgstr "" msgid "TanukiBot|What is a fork?" msgstr "" +msgid "Targe branch" +msgstr "" + msgid "Target" msgstr "" @@ -46674,6 +46698,12 @@ msgstr "" msgid "Target branch" msgstr "" +msgid "Target branch rule" +msgstr "" + +msgid "Target branch rules" +msgstr "" + msgid "Target branch: %{target_branch}" msgstr "" @@ -47678,6 +47708,9 @@ msgstr "" msgid "There are currently no mirrored repositories." msgstr "" +msgid "There are currently no target branch rules" +msgstr "" + msgid "There are merge conflicts" msgstr "" @@ -51542,6 +51575,9 @@ msgstr "" msgid "Username or email" msgstr "" +msgid "Username or primary email" +msgstr "" + msgid "Username:" msgstr "" @@ -53795,7 +53831,7 @@ msgstr "" msgid "Workspaces" msgstr "" -msgid "Workspaces|A workspace is a virtual sandbox environment for your code in GitLab. You can create a workspace for a public project." +msgid "Workspaces|A workspace is a virtual sandbox environment for your code in GitLab." msgstr "" msgid "Workspaces|Cancel" @@ -53894,9 +53930,6 @@ msgstr "" msgid "Workspaces|Workspaces" msgstr "" -msgid "Workspaces|You can create a workspace for public projects only." -msgstr "" - msgid "Workspaces|You can't create a workspace for this project" msgstr "" @@ -55881,6 +55914,9 @@ msgstr "" msgid "eg party_tanuki" msgstr "" +msgid "eg. dev/*" +msgstr "" + msgid "element is not a hierarchy" msgstr "" @@ -55967,9 +56003,6 @@ msgstr[1] "" msgid "finding is not found or is already attached to a vulnerability" msgstr "" -msgid "for Workspace is required to be public" -msgstr "" - msgid "for Workspace must have an associated RemoteDevelopmentAgentConfig" msgstr "" @@ -56753,6 +56786,9 @@ msgstr "" msgid "must not contain commonly used combinations of words and letters" msgstr "" +msgid "must only contain letters, digits, forward-slash, underscore, hyphen or period" +msgstr "" + msgid "my-awesome-group" msgstr "" diff --git a/package.json b/package.json index 45244dc324a..b636f313db2 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,7 @@ "gettext-parser": "^6.0.0", "graphql": "^15.7.2", "graphql-tag": "^2.11.0", - "gridstack": "^8.4.0", + "gridstack": "^9.1.0", "highlight.js": "^11.8.0", "immer": "^9.0.15", "ipaddr.js": "^1.9.1", diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb index b3bd358170d..785a34e0b9b 100644 --- a/spec/features/invites_spec.rb +++ b/spec/features/invites_spec.rb @@ -64,10 +64,10 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate expect(page).to have_content('To accept this invitation, create an account or sign in') end - it 'pre-fills the "Username or email" field on the sign in box with the invite_email from the invite' do + it 'pre-fills the "Username or primary email" field on the sign in box with the invite_email from the invite' do click_link 'Sign in' - expect(find_field('Username or email').value).to eq(group_invite.invite_email) + expect(find_field('Username or primary email').value).to eq(group_invite.invite_email) end it 'pre-fills the Email field on the sign up box with the invite_email from the invite' do diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb index d6b6806d63d..111c0cce1b1 100644 --- a/spec/features/users/signup_spec.rb +++ b/spec/features/users/signup_spec.rb @@ -234,7 +234,7 @@ RSpec.describe 'Signup', :js, feature_category: :user_profile do confirm_email - expect(find_field('Username or email').value).to eq(new_user.email) + expect(find_field('Username or primary email').value).to eq(new_user.email) end end diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js index f9a6690a391..038c7a96adc 100644 --- a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js +++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js @@ -23,7 +23,6 @@ import { ICON_SUBGROUP, SCOPE_TOKEN_MAX_LENGTH, } from '~/super_sidebar/components/global_search/constants'; -import { SEARCH_GITLAB } from '~/vue_shared/global_search/constants'; import { truncate } from '~/lib/utils/text_utility'; import { visitUrl } from '~/lib/utils/url_utility'; import { ENTER_KEY } from '~/lib/utils/keys'; @@ -52,7 +51,7 @@ describe('GlobalSearchModal', () => { clearAutocomplete: jest.fn(), }; - const deafaultMockState = { + const defaultMockState = { searchContext: { project: MOCK_PROJECT, group: MOCK_GROUP, @@ -66,15 +65,14 @@ describe('GlobalSearchModal', () => { }; const createComponent = ({ - initialState = deafaultMockState, + initialState = defaultMockState, mockGetters = defaultMockGetters, stubs, - glFeatures = { commandPalette: false }, ...mountOptions } = {}) => { const store = new Vuex.Store({ state: { - ...deafaultMockState, + ...defaultMockState, ...initialState, }, actions: actionSpies, @@ -89,7 +87,6 @@ describe('GlobalSearchModal', () => { wrapper = shallowMountExtended(GlobalSearchModal, { store, stubs, - provide: { glFeatures }, ...mountOptions, }); }; @@ -271,49 +268,28 @@ describe('GlobalSearchModal', () => { }); describe('Command palette', () => { - describe('when FF `command_palette` is disabled', () => { + describe.each([...COMMON_HANDLES, PATH_HANDLE])('when search handle is %s', (handle) => { beforeEach(() => { - createComponent(); + createComponent({ + initialState: { search: handle }, + }); }); - it('should not render command mode components', () => { - expect(findCommandPaletteItems().exists()).toBe(false); - expect(findFakeSearchInput().exists()).toBe(false); + it('should render command mode components', () => { + expect(findCommandPaletteItems().exists()).toBe(true); + expect(findFakeSearchInput().exists()).toBe(true); }); - it('should provide default placeholder to the search input', () => { - expect(findGlobalSearchInput().attributes('placeholder')).toBe(SEARCH_GITLAB); + it('should provide an alternative placeholder to the search input', () => { + expect(findGlobalSearchInput().attributes('placeholder')).toBe( + SEARCH_OR_COMMAND_MODE_PLACEHOLDER, + ); }); - }); - - describe.each([...COMMON_HANDLES, PATH_HANDLE])( - 'when FF `command_palette` is enabled and search handle is %s', - (handle) => { - beforeEach(() => { - createComponent({ - initialState: { search: handle }, - glFeatures: { - commandPalette: true, - }, - }); - }); - it('should render command mode components', () => { - expect(findCommandPaletteItems().exists()).toBe(true); - expect(findFakeSearchInput().exists()).toBe(true); - }); - - it('should provide an alternative placeholder to the search input', () => { - expect(findGlobalSearchInput().attributes('placeholder')).toBe( - SEARCH_OR_COMMAND_MODE_PLACEHOLDER, - ); - }); - - it('should not render the scope token', () => { - expect(findScopeToken().exists()).toBe(false); - }); - }, - ); + it('should not render the scope token', () => { + expect(findScopeToken().exists()).toBe(false); + }); + }); }); }); @@ -373,9 +349,6 @@ describe('GlobalSearchModal', () => { beforeEach(() => { createComponent({ initialState: { search: '>' }, - glFeatures: { - commandPalette: true, - }, }); submitSearch(); }); diff --git a/spec/graphql/resolvers/work_items_resolver_spec.rb b/spec/graphql/resolvers/work_items_resolver_spec.rb index 6da62e3adb7..c856f990e7a 100644 --- a/spec/graphql/resolvers/work_items_resolver_spec.rb +++ b/spec/graphql/resolvers/work_items_resolver_spec.rb @@ -46,11 +46,6 @@ RSpec.describe Resolvers::WorkItemsResolver do expect(resolve_items).to contain_exactly(item1, item2) end - it 'filters by state' do - expect(resolve_items(state: 'opened')).to contain_exactly(item1) - expect(resolve_items(state: 'closed')).to contain_exactly(item2) - end - context 'when searching items' do it_behaves_like 'graphql query for searching issuables' do let_it_be(:parent) { project } diff --git a/spec/lib/gitlab/background_migration/remove_project_group_link_with_missing_groups_spec.rb b/spec/lib/gitlab/background_migration/remove_project_group_link_with_missing_groups_spec.rb index 64fba6e89a8..126e928fa77 100644 --- a/spec/lib/gitlab/background_migration/remove_project_group_link_with_missing_groups_spec.rb +++ b/spec/lib/gitlab/background_migration/remove_project_group_link_with_missing_groups_spec.rb @@ -104,8 +104,7 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveProjectGroupLinkWithMissingGro ).perform end - it 'removes the `project_group_links` records whose associated group does not exist anymore', - quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424274' do + it 'removes the `project_group_links` records whose associated group does not exist anymore' do group_2.delete # Schema is fixed to `20230206172702` on this spec. diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index 388c284d83c..6f1eb77fa9b 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -5,6 +5,8 @@ require 'spec_helper' RSpec.describe 'Query.runner(id)', :freeze_time, feature_category: :runner_fleet do include GraphqlHelpers + using RSpec::Parameterized::TableSyntax + let_it_be(:user) { create(:user, :admin) } let_it_be(:another_admin) { create(:user, :admin) } let_it_be_with_reload(:group) { create(:group) } @@ -144,23 +146,6 @@ RSpec.describe 'Query.runner(id)', :freeze_time, feature_category: :runner_fleet ) expect(runner_data['tagList']).to match_array runner.tag_list end - - it 'does not execute more queries per runner', :use_sql_query_cache, :aggregate_failures do - # warm-up license cache and so on: - personal_access_token = create(:personal_access_token, user: user) - args = { current_user: user, token: { personal_access_token: personal_access_token } } - post_graphql(query, **args) - expect(graphql_data_at(:runner)).not_to be_nil - - personal_access_token = create(:personal_access_token, user: another_admin) - args = { current_user: another_admin, token: { personal_access_token: personal_access_token } } - control = ActiveRecord::QueryRecorder.new(skip_cached: false) { post_graphql(query, **args) } - - create(:ci_runner, :instance, version: '14.0.0', tag_list: %w[tag5 tag6], creator: another_admin) - create(:ci_runner, :project, version: '14.0.1', projects: [project1], tag_list: %w[tag3 tag8], creator: another_admin) - - expect { post_graphql(query, **args) }.not_to exceed_all_query_limit(control) - end end shared_examples 'retrieval with no admin url' do @@ -248,23 +233,25 @@ RSpec.describe 'Query.runner(id)', :freeze_time, feature_category: :runner_fleet end describe 'for project runner' do - describe 'locked' do - using RSpec::Parameterized::TableSyntax + let_it_be_with_refind(:project_runner) do + create(:ci_runner, :project, + description: 'Runner 3', + contacted_at: 1.day.ago, + active: false, + locked: false, + version: 'adfe157', + revision: 'b', + ip_address: '10.10.10.10', + access_level: 1, + run_untagged: true) + end + describe 'locked' do where(is_locked: [true, false]) with_them do - let(:project_runner) do - create(:ci_runner, :project, - description: 'Runner 3', - contacted_at: 1.day.ago, - active: false, - locked: is_locked, - version: 'adfe157', - revision: 'b', - ip_address: '10.10.10.10', - access_level: 1, - run_untagged: true) + before do + project_runner.update!(locked: is_locked) end let(:query) do @@ -428,6 +415,38 @@ RSpec.describe 'Query.runner(id)', :freeze_time, feature_category: :runner_fleet end end end + + describe 'a query fetching all fields' do + let(:query) do + wrap_fields(query_graphql_path(query_path, all_graphql_fields_for('CiRunner'))) + end + + let(:query_path) do + [ + [:runner, { id: project_runner.to_global_id.to_s }] + ] + end + + it 'does not execute more queries per runner', :use_sql_query_cache, :aggregate_failures do + create(:ci_build, :failed, runner: project_runner) + create(:ci_runner_machine, runner: project_runner, version: '16.4.0') + + # warm-up license cache and so on: + personal_access_token = create(:personal_access_token, user: user) + args = { current_user: user, token: { personal_access_token: personal_access_token } } + post_graphql(query, **args) + expect(graphql_data_at(:runner)).not_to be_nil + + personal_access_token = create(:personal_access_token, user: another_admin) + args = { current_user: another_admin, token: { personal_access_token: personal_access_token } } + control = ActiveRecord::QueryRecorder.new(skip_cached: false) { post_graphql(query, **args) } + + create(:ci_build, :failed, runner: project_runner) + create(:ci_runner_machine, runner: project_runner, version: '16.4.1') + + expect { post_graphql(query, **args) }.not_to exceed_all_query_limit(control) + end + end end describe 'for inactive runner' do @@ -995,8 +1014,6 @@ RSpec.describe 'Query.runner(id)', :freeze_time, feature_category: :runner_fleet end context 'when requesting individual fields' do - using RSpec::Parameterized::TableSyntax - where(:field) do [ 'detailedStatus { id detailsPath group icon text }', diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb index 3f6d39435fd..c5571086700 100644 --- a/spec/requests/api/graphql/ci/runners_spec.rb +++ b/spec/requests/api/graphql/ci/runners_spec.rb @@ -72,10 +72,16 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do args = { current_user: admin2, token: { personal_access_token: personal_access_token } } control = ActiveRecord::QueryRecorder.new { post_graphql(query, **args) } - create(:ci_runner, :instance, version: '14.0.0', tag_list: %w[tag5 tag6], creator: admin2) - create(:ci_runner, :project, version: '14.0.1', projects: [project], tag_list: %w[tag3 tag8], + runner2 = create(:ci_runner, :instance, version: '14.0.0', tag_list: %w[tag5 tag6], creator: admin2) + runner3 = create(:ci_runner, :project, version: '14.0.1', projects: [project], tag_list: %w[tag3 tag8], creator: current_user) + create(:ci_build, :failed, runner: runner2) + create(:ci_runner_machine, runner: runner2, version: '16.4.1') + + create(:ci_build, :failed, runner: runner3) + create(:ci_runner_machine, runner: runner3, version: '16.4.0') + expect { post_graphql(query, **args) }.not_to exceed_query_limit(control) end end diff --git a/spec/requests/api/graphql/group/work_items_spec.rb b/spec/requests/api/graphql/group/work_items_spec.rb index f6dad577b5e..ef96ef4754a 100644 --- a/spec/requests/api/graphql/group/work_items_spec.rb +++ b/spec/requests/api/graphql/group/work_items_spec.rb @@ -35,7 +35,7 @@ RSpec.describe 'getting a work_item list for a group', feature_category: :team_p let_it_be(:other_work_item) { create(:work_item) } let(:work_items_data) { graphql_data['group']['workItems']['nodes'] } - let(:work_item_filter_params) { {} } + let(:item_filter_params) { {} } let(:current_user) { user } let(:query_group) { group } @@ -47,6 +47,28 @@ RSpec.describe 'getting a work_item list for a group', feature_category: :team_p QUERY end + it_behaves_like 'graphql work item list request spec' do + let_it_be(:container_build_params) { { namespace: group } } + let(:work_item_node_path) { %w[group workItems nodes] } + + def post_query(request_user = current_user) + post_graphql(query, current_user: request_user) + end + end + + context 'when filtering by search' do + let(:item_filter_params) { { search: 'search_term', in: [:DESCRIPTION] } } + + # TODO: https://gitlab.com/gitlab-org/gitlab/-/work_items/393126 + it 'returns an error since search is not implemented at the group level yet' do + post_graphql(query, current_user: current_user) + + expect(graphql_errors).to contain_exactly( + hash_including('message' => 'Searching is not available for work items at the namespace level yet') + ) + end + end + context 'when the user can not see confidential work_items' do it_behaves_like 'a working graphql query' do before do @@ -66,12 +88,6 @@ RSpec.describe 'getting a work_item list for a group', feature_category: :team_p context 'when the user can see confidential work_items' do let(:current_user) { reporter } - it_behaves_like 'a working graphql query' do - before do - post_graphql(query, current_user: current_user) - end - end - it 'returns also confidential work_items' do post_graphql(query, current_user: current_user) @@ -96,7 +112,7 @@ RSpec.describe 'getting a work_item list for a group', feature_category: :team_p graphql_dig_at(work_items_data, :id) end - def query(params = work_item_filter_params) + def query(params = item_filter_params) graphql_query_for( 'group', { 'fullPath' => query_group.full_path }, diff --git a/spec/requests/api/graphql/jobs_query_spec.rb b/spec/requests/api/graphql/jobs_query_spec.rb index 4248a03fa74..7607aeac6e0 100644 --- a/spec/requests/api/graphql/jobs_query_spec.rb +++ b/spec/requests/api/graphql/jobs_query_spec.rb @@ -10,7 +10,7 @@ RSpec.describe 'getting job information', feature_category: :continuous_integrat :jobs, {}, %( count nodes { - #{all_graphql_fields_for(::Types::Ci::JobType, max_depth: 1, excluded: %w[aiFailureAnalysis])} + #{all_graphql_fields_for(::Types::Ci::JobType, max_depth: 1)} }) ) end diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb index 4aba83dae92..d5d3d6c578f 100644 --- a/spec/requests/api/graphql/project/work_items_spec.rb +++ b/spec/requests/api/graphql/project/work_items_spec.rb @@ -30,16 +30,14 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team let_it_be(:confidential_item) { create(:work_item, confidential: true, project: project, title: 'item3') } let_it_be(:other_item) { create(:work_item) } - let(:items_data) { graphql_data['project']['workItems']['edges'] } + let(:items_data) { graphql_data['project']['workItems']['nodes'] } let(:item_filter_params) { {} } let(:fields) do <<~QUERY - edges { - node { + nodes { #{all_graphql_fields_for('workItems'.classify, max_depth: 2)} } - } QUERY end @@ -69,6 +67,15 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team end end + it_behaves_like 'graphql work item list request spec' do + let_it_be(:container_build_params) { { project: project } } + let(:work_item_node_path) { %w[project workItems nodes] } + + def post_query(request_user = current_user) + post_graphql(query, current_user: request_user) + end + end + describe 'N + 1 queries' do context 'when querying root fields' do it_behaves_like 'work items resolver without N + 1 queries' @@ -199,12 +206,6 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team end end - it_behaves_like 'a working graphql query' do - before do - post_graphql(query, current_user: current_user) - end - end - context 'when the user does not have access to the item' do before do project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) @@ -237,25 +238,12 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team context 'when filtering by search' do it_behaves_like 'query with a search term' do - let(:issuable_data) { items_data } + let(:ids) { item_ids } let(:user) { current_user } let_it_be(:issuable) { create(:work_item, project: project, description: 'bar') } end end - context 'when filtering by author username' do - let_it_be(:author) { create(:author) } - let_it_be(:item_3) { create(:work_item, project: project, author: author) } - - let(:item_filter_params) { { author_username: item_3.author.username } } - - it 'returns correct results' do - post_graphql(query, current_user: current_user) - - expect(item_ids).to match_array([item_3.to_global_id.to_s]) - end - end - describe 'sorting and pagination' do let(:data_path) { [:project, :work_items] } @@ -415,7 +403,7 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team end def item_ids - graphql_dig_at(items_data, :node, :id) + graphql_dig_at(items_data, :id) end def query(params = item_filter_params) diff --git a/spec/services/labels/available_labels_service_spec.rb b/spec/services/labels/available_labels_service_spec.rb index c9f75283c75..2b398210034 100644 --- a/spec/services/labels/available_labels_service_spec.rb +++ b/spec/services/labels/available_labels_service_spec.rb @@ -108,36 +108,24 @@ RSpec.describe Labels::AvailableLabelsService, feature_category: :team_planning end end - describe '#filter_locked_labels_ids_in_param' do - let(:label_ids) { labels.map(&:id).push(non_existing_record_id) } + describe '#filter_locked_label_ids' do + let(:label_ids) { labels.map(&:id) } context 'when parent is a project' do - it 'returns only locked label ids' do - result = described_class.new(user, project, ids: label_ids).filter_locked_labels_ids_in_param(:ids) + it 'returns only relevant label ids' do + result = described_class.new(user, project, ids: label_ids).filter_locked_label_ids(label_ids) expect(result).to match_array([project_label_locked.id, group_label_locked.id]) end - - it 'returns labels in preserved order' do - result = described_class.new(user, project, ids: label_ids.reverse).filter_locked_labels_ids_in_param(:ids) - - expect(result).to eq([group_label_locked.id, project_label_locked.id]) - end end context 'when parent is a group' do - it 'returns only locked label ids' do - result = described_class.new(user, group, ids: label_ids).filter_locked_labels_ids_in_param(:ids) + it 'returns only relevant label ids' do + result = described_class.new(user, group, ids: label_ids).filter_locked_label_ids(label_ids) expect(result).to match_array([group_label_locked.id]) end end - - it 'accepts a single id parameter' do - result = described_class.new(user, project, label_id: project_label_locked.id).filter_locked_labels_ids_in_param(:label_id) - - expect(result).to match_array([project_label_locked.id]) - end end describe '#available_labels' do diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 2f6db13a041..72e41f7b814 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -1301,41 +1301,39 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re end context 'updating labels' do - let(:label_a) { label } - let(:label_b) { create(:label, title: 'b', project: project) } - let(:label_c) { create(:label, title: 'c', project: project) } - let(:label_locked) { create(:label, title: 'locked', project: project, lock_on_merge: true) } - let(:issuable) { merge_request } + context 'when merge request is not merged' do + let(:label_a) { label } + let(:label_b) { create(:label, title: 'b', project: project) } + let(:label_c) { create(:label, title: 'c', project: project) } + let(:label_locked) { create(:label, title: 'locked', project: project, lock_on_merge: true) } + let(:issuable) { merge_request } - it_behaves_like 'updating issuable labels' - it_behaves_like 'keeps issuable labels sorted after update' - it_behaves_like 'broadcasting issuable labels updates' + it_behaves_like 'updating issuable labels' + it_behaves_like 'keeps issuable labels sorted after update' + it_behaves_like 'broadcasting issuable labels updates' + end context 'when merge request has been merged' do - context 'when remove_label_ids contains a locked label' do - let(:params) { { remove_label_ids: [label_locked.id] } } + let(:label_a) { create(:label, title: 'a', project: project, lock_on_merge: true) } + let(:label_b) { create(:label, title: 'b', project: project, lock_on_merge: true) } + let(:label_c) { create(:label, title: 'c', project: project, lock_on_merge: true) } + let(:label_unlocked) { create(:label, title: 'unlocked', project: project) } + let(:issuable) { merge_request } - context 'when feature flag is disabled' do - before do - stub_feature_flags(enforce_locked_labels_on_merge: false) - end + before do + merge_request.update!(state: 'merged') + end - it 'removes locked labels' do - merge_request.update!(state: 'merged', labels: [label_a, label_locked]) - update_issuable(params) + it_behaves_like 'updating merged MR with locked labels' - expect(merge_request.label_ids).to contain_exactly(label_a.id) - end - end - - context 'when feature flag is enabled' do - it 'does not remove locked labels' do - merge_request.update!(state: 'merged', labels: [label_a, label_locked]) - update_issuable(params) + context 'when feature flag is disabled' do + let(:label_locked) { create(:label, title: 'locked', project: project, lock_on_merge: true) } - expect(merge_request.label_ids).to contain_exactly(label_a.id, label_locked.id) - end + before do + stub_feature_flags(enforce_locked_labels_on_merge: false) end + + it_behaves_like 'updating issuable labels' end end diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index 17b4270fa20..3131a22a20b 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -73,7 +73,10 @@ module DbCleaner end end + disable_ddl_was = Feature.enabled?(:disallow_database_ddl_feature_flags, type: :ops) + stub_feature_flags(disallow_database_ddl_feature_flags: false) Gitlab::Database::Partitioning.sync_partitions_ignore_db_error + stub_feature_flags(disallow_database_ddl_feature_flags: disable_ddl_was) puts "Databases re-creation done in #{Gitlab::Metrics::System.monotonic_time - start}" end diff --git a/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb index d654a30d54a..c2d144bef3b 100644 --- a/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb +++ b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb @@ -47,7 +47,8 @@ RSpec.shared_examples 'labels sidebar widget' do end end - it 'adds first label by pressing enter when search' do + it 'adds first label by pressing enter when search', + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/414877' do within(labels_widget) do page.within('[data-testid="value-wrapper"]') do expect(page).not_to have_content(development.name) diff --git a/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb new file mode 100644 index 00000000000..a9c422c8f2d --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'graphql work item list request spec' do + let(:work_item_ids) { graphql_dig_at(work_item_data, :id) } + + it_behaves_like 'a working graphql query' do + before do + post_query + end + end + + describe 'filters' do + before do + post_query + end + + context 'when filtering by author username' do + let(:author) { create(:author) } + let(:authored_work_item) { create(:work_item, author: author, **container_build_params) } + + let(:item_filter_params) { { author_username: authored_work_item.author.username } } + + it 'returns correct results' do + expect(work_item_ids).to contain_exactly(authored_work_item.to_global_id.to_s) + end + end + + context 'when filtering by state' do + let_it_be(:opened_work_item) { create(:work_item, :opened, **container_build_params) } + let_it_be(:closed_work_item) { create(:work_item, :closed, **container_build_params) } + + context 'when filtering by state opened' do + let(:item_filter_params) { { state: :opened } } + + it 'filters by state' do + expect(work_item_ids).to include(opened_work_item.to_global_id.to_s) + expect(work_item_ids).not_to include(closed_work_item.to_global_id.to_s) + end + end + + context 'when filtering by state closed' do + let(:item_filter_params) { { state: :closed } } + + it 'filters by state' do + expect(work_item_ids).not_to include(opened_work_item.to_global_id.to_s) + expect(work_item_ids).to include(closed_work_item.to_global_id.to_s) + end + end + end + + context 'when filtering by type' do + let_it_be(:issue_work_item) { create(:work_item, :issue, **container_build_params) } + let_it_be(:task_work_item) { create(:work_item, :task, **container_build_params) } + + context 'when filtering by issue type' do + let(:item_filter_params) { { types: [:ISSUE] } } + + it 'filters by type' do + expect(work_item_ids).to include(issue_work_item.to_global_id.to_s) + expect(work_item_ids).not_to include(task_work_item.to_global_id.to_s) + end + end + + context 'when filtering by task type' do + let(:item_filter_params) { { types: [:TASK] } } + + it 'filters by type' do + expect(work_item_ids).not_to include(issue_work_item.to_global_id.to_s) + expect(work_item_ids).to include(task_work_item.to_global_id.to_s) + end + end + end + + context 'when filtering by iid' do + let_it_be(:work_item_by_iid) { create(:work_item, **container_build_params) } + + context 'when using the iid filter' do + let(:item_filter_params) { { iid: work_item_by_iid.iid.to_s } } + + it 'returns only items by the given iid' do + expect(work_item_ids).to contain_exactly(work_item_by_iid.to_global_id.to_s) + end + end + + context 'when using the iids filter' do + let(:item_filter_params) { { iids: [work_item_by_iid.iid.to_s] } } + + it 'returns only items by the given iid' do + expect(work_item_ids).to contain_exactly(work_item_by_iid.to_global_id.to_s) + end + end + end + end + + def work_item_data + graphql_data.dig(*work_item_node_path) + end +end diff --git a/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb index 3f95d6060ea..9624f7a4450 100644 --- a/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb +++ b/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb @@ -145,6 +145,77 @@ RSpec.shared_examples 'updating issuable labels' do end end +RSpec.shared_examples 'updating merged MR with locked labels' do + context 'when add_label_ids and label_ids are passed' do + let(:params) { { label_ids: [label_a.id], add_label_ids: [label_c.id] } } + + it 'replaces unlocked labels with the ones in label_ids and adds those in add_label_ids' do + issuable.update!(labels: [label_b, label_unlocked]) + update_issuable(params) + + expect(issuable.label_ids).to contain_exactly(label_a.id, label_b.id, label_c.id) + end + end + + context 'when remove_label_ids and label_ids are passed' do + let(:params) { { label_ids: [label_a.id, label_b.id, label_c.id], remove_label_ids: [label_a.id] } } + + it 'replaces unlocked labels with the ones in label_ids and does not remove locked label in remove_label_ids' do + issuable.update!(labels: [label_a, label_c, label_unlocked]) + update_issuable(params) + + expect(issuable.label_ids).to contain_exactly(label_a.id, label_b.id, label_c.id) + end + end + + context 'when add_label_ids and remove_label_ids are passed' do + let(:params) { { add_label_ids: [label_c.id], remove_label_ids: [label_a.id, label_unlocked.id] } } + + before do + issuable.update!(labels: [label_a, label_unlocked]) + update_issuable(params) + end + + it 'adds the passed labels' do + expect(issuable.label_ids).to include(label_c.id) + end + + it 'removes the passed unlocked labels' do + expect(issuable.label_ids).to include(label_a.id) + expect(issuable.label_ids).not_to include(label_unlocked.id) + end + end + + context 'when same id is passed as add_label_ids and remove_label_ids' do + let(:params) { { add_label_ids: [label_a.id], remove_label_ids: [label_a.id] } } + + context 'for a label assigned to an issue' do + it 'does not remove the label' do + issuable.update!(labels: [label_a]) + update_issuable(params) + + expect(issuable.label_ids).to contain_exactly(label_a.id) + end + end + + context 'for a label not assigned to an issue' do + it 'does not add the label' do + expect(issuable.label_ids).to be_empty + end + end + end + + context 'when duplicate label titles are given' do + let(:params) { { labels: [label_c.title, label_c.title] } } + + it 'assigns the label once' do + update_issuable(params) + + expect(issuable.labels).to contain_exactly(label_c) + end + end +end + RSpec.shared_examples 'keeps issuable labels sorted after update' do before do update_issuable(label_ids: [label_b.id]) diff --git a/spec/views/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb index e2aa0bb9870..24937cfdd4a 100644 --- a/spec/views/devise/shared/_signin_box.html.haml_spec.rb +++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb @@ -38,7 +38,7 @@ RSpec.describe 'devise/shared/_signin_box' do it 'renders user_login label' do render - expect(rendered).to have_content(_('Username or email')) + expect(rendered).to have_content(_('Username or primary email')) end end diff --git a/yarn.lock b/yarn.lock index 48001a5da3c..10c43095994 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7171,10 +7171,10 @@ graphql@^15.7.2: resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.7.2.tgz#85ab0eeb83722977151b3feb4d631b5f2ab287ef" integrity sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A== -gridstack@^8.4.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-8.4.0.tgz#7af49159f9dc144c89a2c56246e1710406f75fcf" - integrity sha512-qLJuJrBy9bbG3hI+h2cEhiuZ51J3MyEMmv5AXg7MCFiBeG8A4HyIUytueqtD/oZcA3Pccq2Xoj7GrwpmKOS3ig== +gridstack@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-9.1.0.tgz#9c82808cc87ff4491f5bfcd517dc7ead922aa62e" + integrity sha512-ViUwP6Q9WzYUd/ulKZ7/bmX3JZeQciVgZa4P5ZChXhwMsNJp7ctb6RnxxatcAkG+X0oln7udPI8AsW0SMsPUYQ== gzip-size@^6.0.0: version "6.0.0" |