diff options
158 files changed, 1659 insertions, 493 deletions
diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml index 7f373769ded..68c71b359c2 100644 --- a/.gitlab/ci/reports.gitlab-ci.yml +++ b/.gitlab/ci/reports.gitlab-ci.yml @@ -91,7 +91,7 @@ gemnasium-python-dependency_scanning: yarn-audit-dependency_scanning: extends: .ds-analyzer - image: "${REGISTRY_HOST}/${REGISTRY_GROUP}/security-products/analyzers/npm-audit:1.4.1" + image: "${REGISTRY_HOST}/${REGISTRY_GROUP}/security-products/analyzers/npm-audit:1" variables: TOOL: yarn rules: !reference [".reports:rules:yarn-audit-dependency_scanning", rules] diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 3a8a8677706..ba64a1d91b1 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -184,6 +184,17 @@ - "*.yml" - "**/*.yml" +.lint-pipeline-yaml-patterns: &lint-pipeline-yaml-patterns + - ".gitlab-ci.yml" + - ".gitlab/ci/**/*.yml" + - "lib/gitlab/ci/templates/**/*.yml" + - "data/deprecations/**/*.yml" + - "data/removals/**/*.yml" + - "data/whats_new/**/*.yml" + +.lint-metrics-yaml-patterns: &lint-metrics-yaml-patterns + - "config/metrics/**/*.yml" + .docs-patterns: &docs-patterns - ".gitlab/route-map.yml" - "doc/**/*" @@ -1801,3 +1812,13 @@ rules: - <<: *if-default-refs changes: *yaml-lint-patterns + +.lint-pipeline-yaml:rules: + rules: + - <<: *if-default-refs + changes: *lint-pipeline-yaml-patterns + +.lint-metrics-yaml:rules: + rules: + - <<: *if-default-refs + changes: *lint-metrics-yaml-patterns diff --git a/.gitlab/ci/yaml.gitlab-ci.yml b/.gitlab/ci/yaml.gitlab-ci.yml index a0665d712dc..0420f158bbb 100644 --- a/.gitlab/ci/yaml.gitlab-ci.yml +++ b/.gitlab/ci/yaml.gitlab-ci.yml @@ -18,7 +18,7 @@ lint-yaml: lint-pipeline-yaml: extends: - .default-retry - - .yaml-lint:rules + - .lint-pipeline-yaml:rules image: pipelinecomponents/yamllint:latest stage: lint needs: [] @@ -30,7 +30,7 @@ lint-pipeline-yaml: lint-metrics-yaml: extends: - .default-retry - - .yaml-lint:rules + - .lint-metrics-yaml:rules image: pipelinecomponents/yamllint:latest stage: lint needs: [] diff --git a/.rubocop_todo/cop/user_admin.yml b/.rubocop_todo/cop/user_admin.yml index 5f0f7213950..ab5f76a002d 100644 --- a/.rubocop_todo/cop/user_admin.yml +++ b/.rubocop_todo/cop/user_admin.yml @@ -26,7 +26,6 @@ Cop/UserAdmin: - app/models/protected_branch.rb - app/models/user.rb - app/policies/note_policy.rb - - app/serializers/deploy_key_entity.rb - app/services/auth/container_registry_authentication_service.rb - app/services/emails/create_service.rb - app/services/projects/enable_deploy_key_service.rb diff --git a/.rubocop_todo/gitlab/feature_available_usage.yml b/.rubocop_todo/gitlab/feature_available_usage.yml index 2a0975a6e9b..168edd94f90 100644 --- a/.rubocop_todo/gitlab/feature_available_usage.yml +++ b/.rubocop_todo/gitlab/feature_available_usage.yml @@ -25,7 +25,6 @@ Gitlab/FeatureAvailableUsage: - ee/app/controllers/projects/subscriptions_controller.rb - ee/app/finders/autocomplete/vulnerabilities_autocomplete_finder.rb - ee/app/finders/clusters/agents_finder.rb - - ee/app/finders/ee/alert_management/alerts_finder.rb - ee/app/finders/ee/alert_management/http_integrations_finder.rb - ee/app/graphql/ee/types/group_type.rb - ee/app/graphql/mutations/dast/profiles/create.rb diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml index 0125456954a..23ff86ba289 100644 --- a/.rubocop_todo/gitlab/namespaced_class.yml +++ b/.rubocop_todo/gitlab/namespaced_class.yml @@ -500,8 +500,6 @@ Gitlab/NamespacedClass: - 'app/serializers/current_board_entity.rb' - 'app/serializers/current_board_serializer.rb' - 'app/serializers/current_user_entity.rb' - - 'app/serializers/deploy_key_entity.rb' - - 'app/serializers/deploy_key_serializer.rb' - 'app/serializers/deploy_keys_project_entity.rb' - 'app/serializers/deployment_cluster_entity.rb' - 'app/serializers/deployment_entity.rb' @@ -770,7 +768,6 @@ Gitlab/NamespacedClass: - 'app/workers/error_tracking_issue_link_worker.rb' - 'app/workers/expire_build_artifacts_worker.rb' - 'app/workers/expire_job_cache_worker.rb' - - 'app/workers/expire_pipeline_cache_worker.rb' - 'app/workers/export_csv_worker.rb' - 'app/workers/external_service_reactive_caching_worker.rb' - 'app/workers/file_hook_worker.rb' @@ -812,7 +809,6 @@ Gitlab/NamespacedClass: - 'app/workers/post_receive.rb' - 'app/workers/process_commit_worker.rb' - 'app/workers/project_cache_worker.rb' - - 'app/workers/project_daily_statistics_worker.rb' - 'app/workers/project_destroy_worker.rb' - 'app/workers/project_export_worker.rb' - 'app/workers/propagate_integration_group_worker.rb' @@ -1017,7 +1013,6 @@ Gitlab/NamespacedClass: - 'ee/app/serializers/group_analytics_serializer.rb' - 'ee/app/serializers/group_vulnerability_autocomplete_entity.rb' - 'ee/app/serializers/group_vulnerability_autocomplete_serializer.rb' - - 'ee/app/serializers/invited_group_entity.rb' - 'ee/app/serializers/invited_group_serializer.rb' - 'ee/app/serializers/iteration_serializer.rb' - 'ee/app/serializers/license_entity.rb' @@ -1216,7 +1211,6 @@ Gitlab/NamespacedClass: - 'lib/gitlab/object_hierarchy.rb' - 'lib/gitlab/omniauth_initializer.rb' - 'lib/gitlab/otp_key_rotator.rb' - - 'lib/gitlab/pages_transfer.rb' - 'lib/gitlab/pipeline_scope_counts.rb' - 'lib/gitlab/polling_interval.rb' - 'lib/gitlab/process_memory_cache.rb' @@ -1270,7 +1264,6 @@ Gitlab/NamespacedClass: - 'lib/gitlab/unicode.rb' - 'lib/gitlab/untrusted_regexp.rb' - 'lib/gitlab/untrusted_regexp/ruby_syntax.rb' - - 'lib/gitlab/updated_notes_paginator.rb' - 'lib/gitlab/uploads_transfer.rb' - 'lib/gitlab/url_blocker.rb' - 'lib/gitlab/url_builder.rb' diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml index 6cbc062aae6..e889a05ad36 100644 --- a/.rubocop_todo/layout/argument_alignment.yml +++ b/.rubocop_todo/layout/argument_alignment.yml @@ -293,7 +293,6 @@ Layout/ArgumentAlignment: - 'ee/app/controllers/subscriptions_controller.rb' - 'ee/app/finders/geo/registry_finder.rb' - 'ee/app/graphql/ee/mutations/boards/issues/issue_move_list.rb' - - 'ee/app/graphql/ee/mutations/ci/ci_cd_settings_update.rb' - 'ee/app/graphql/ee/types/ci/pipeline_type.rb' - 'ee/app/graphql/ee/types/deprecated_mutations.rb' - 'ee/app/graphql/mutations/app_sec/fuzzing/api/ci_configuration/create.rb' @@ -395,7 +394,6 @@ Layout/ArgumentAlignment: - 'ee/spec/features/uncompleted_learn_gitlab_link_spec.rb' - 'ee/spec/finders/security/pipeline_vulnerabilities_finder_spec.rb' - 'ee/spec/frontend/fixtures/search.rb' - - 'ee/spec/graphql/mutations/incident_management/timeline_event/create_spec.rb' - 'ee/spec/graphql/mutations/requirements_management/export_requirements_spec.rb' - 'ee/spec/helpers/billing_plans_helper_spec.rb' - 'ee/spec/helpers/ee/users/callouts_helper_spec.rb' diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index 4c4e13b0436..16d22183345 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -383,7 +383,6 @@ Layout/LineLength: - 'app/models/concerns/redis_cacheable.rb' - 'app/models/concerns/restricted_signup.rb' - 'app/models/concerns/routable.rb' - - 'app/models/concerns/sha256_attribute.rb' - 'app/models/concerns/shardable.rb' - 'app/models/concerns/sortable.rb' - 'app/models/concerns/storage/legacy_namespace.rb' @@ -529,7 +528,6 @@ Layout/LineLength: - 'app/serializers/base_discussion_entity.rb' - 'app/serializers/build_details_entity.rb' - 'app/serializers/ci/pipeline_entity.rb' - - 'app/serializers/deploy_key_entity.rb' - 'app/serializers/deployment_cluster_entity.rb' - 'app/serializers/deployment_entity.rb' - 'app/serializers/diff_file_base_entity.rb' @@ -649,7 +647,6 @@ Layout/LineLength: - 'app/services/members/approve_access_request_service.rb' - 'app/services/members/destroy_service.rb' - 'app/services/members/invitation_reminder_email_service.rb' - - 'app/services/members/mailgun/process_webhook_service.rb' - 'app/services/members/update_service.rb' - 'app/services/merge_requests/add_context_service.rb' - 'app/services/merge_requests/assign_issues_service.rb' @@ -795,7 +792,6 @@ Layout/LineLength: - 'app/workers/packages/maven/metadata/sync_worker.rb' - 'app/workers/personal_access_tokens/expired_notification_worker.rb' - 'app/workers/pipeline_metrics_worker.rb' - - 'app/workers/quality/test_data_cleanup_worker.rb' - 'app/workers/repository_fork_worker.rb' - 'app/workers/repository_import_worker.rb' - 'app/workers/ssh_keys/expired_notification_worker.rb' @@ -831,7 +827,6 @@ Layout/LineLength: - 'config/initializers/wikicloth_redos_patch.rb' - 'config/initializers/zz_metrics.rb' - 'config/object_store_settings.rb' - - 'config/puma.rb' - 'config/routes.rb' - 'config/routes/admin.rb' - 'config/routes/api.rb' @@ -1176,7 +1171,6 @@ Layout/LineLength: - 'ee/app/controllers/projects/requirements_management/requirements_controller.rb' - 'ee/app/controllers/projects/security/policies_controller.rb' - 'ee/app/controllers/projects/security/vulnerabilities/notes_controller.rb' - - 'ee/app/controllers/projects/threat_monitoring_controller.rb' - 'ee/app/controllers/registrations/groups_controller.rb' - 'ee/app/controllers/registrations/groups_projects_controller.rb' - 'ee/app/controllers/subscriptions_controller.rb' @@ -1324,7 +1318,6 @@ Layout/LineLength: - 'ee/app/helpers/projects/security/dast_configuration_helper.rb' - 'ee/app/helpers/projects/security/dast_profiles_helper.rb' - 'ee/app/helpers/projects/security/discover_helper.rb' - - 'ee/app/helpers/projects/security/policies_helper.rb' - 'ee/app/helpers/push_rules_helper.rb' - 'ee/app/helpers/seats_count_alert_helper.rb' - 'ee/app/helpers/security_helper.rb' @@ -1524,7 +1517,6 @@ Layout/LineLength: - 'ee/app/services/ee/users/update_service.rb' - 'ee/app/services/elastic/cluster_reindexing_service.rb' - 'ee/app/services/elastic/indexing_control_service.rb' - - 'ee/app/services/epic_links/create_service.rb' - 'ee/app/services/epic_links/list_service.rb' - 'ee/app/services/epics/issue_promote_service.rb' - 'ee/app/services/epics/update_service.rb' @@ -1818,7 +1810,6 @@ Layout/LineLength: - 'ee/lib/gitlab/geo/jwt_request_decoder.rb' - 'ee/lib/gitlab/geo/log_cursor/events/repository_created_event.rb' - 'ee/lib/gitlab/geo/registry_batcher.rb' - - 'ee/lib/gitlab/geo/replication/base_downloader.rb' - 'ee/lib/gitlab/geo/replication/base_transfer.rb' - 'ee/lib/gitlab/geo/replication/blob_downloader.rb' - 'ee/lib/gitlab/geo/replicator.rb' diff --git a/.rubocop_todo/rspec/verified_doubles.yml b/.rubocop_todo/rspec/verified_doubles.yml index 0c920287bfe..a69ab9198b1 100644 --- a/.rubocop_todo/rspec/verified_doubles.yml +++ b/.rubocop_todo/rspec/verified_doubles.yml @@ -79,8 +79,6 @@ RSpec/VerifiedDoubles: - ee/spec/lib/gitlab/geo/log_cursor/lease_spec.rb - ee/spec/lib/gitlab/geo/oauth/logout_token_spec.rb - ee/spec/lib/gitlab/geo/oauth/session_spec.rb - - ee/spec/lib/gitlab/geo/replication/job_artifact_retriever_spec.rb - - ee/spec/lib/gitlab/geo/replication/job_artifact_transfer_spec.rb - ee/spec/lib/gitlab/geo/replicator_spec.rb - ee/spec/lib/gitlab/geo_spec.rb - ee/spec/lib/gitlab/git_access_spec.rb @@ -91,7 +89,6 @@ RSpec/VerifiedDoubles: - ee/spec/lib/gitlab/prometheus/queries/cluster_query_spec.rb - ee/spec/lib/gitlab/subscription_portal/clients/rest_spec.rb - ee/spec/lib/sidebars/groups/menus/analytics_menu_spec.rb - - ee/spec/lib/system_check/app/elasticsearch_check_spec.rb - ee/spec/lib/system_check/geo/geo_database_configured_check_spec.rb - ee/spec/models/app_sec/fuzzing/api/ci_configuration_spec.rb - ee/spec/models/approvable_spec.rb @@ -111,7 +108,6 @@ RSpec/VerifiedDoubles: - ee/spec/presenters/group_member_presenter_spec.rb - ee/spec/presenters/merge_request_approver_presenter_spec.rb - ee/spec/presenters/project_member_presenter_spec.rb - - ee/spec/requests/api/alert_management_alerts_spec.rb - ee/spec/requests/api/geo_spec.rb - ee/spec/requests/api/graphql/mutations/dast_site_profiles/delete_spec.rb - ee/spec/requests/api/internal/base_spec.rb @@ -142,7 +138,6 @@ RSpec/VerifiedDoubles: - ee/spec/serializers/issuable_sidebar_extras_entity_spec.rb - ee/spec/serializers/issues/linked_issue_feature_flag_entity_spec.rb - ee/spec/serializers/linked_feature_flag_issue_entity_spec.rb - - ee/spec/serializers/merge_request_compliance_entity_spec.rb - ee/spec/serializers/merge_request_poll_widget_entity_spec.rb - ee/spec/serializers/merge_request_sidebar_basic_entity_spec.rb - ee/spec/serializers/merge_request_widget_entity_spec.rb @@ -173,9 +168,6 @@ RSpec/VerifiedDoubles: - ee/spec/services/ee/merge_requests/refresh_service_spec.rb - ee/spec/services/ee/notification_service_spec.rb - ee/spec/services/ee/post_receive_service_spec.rb - - ee/spec/services/ee/service_ping/build_payload_service_spec.rb - - ee/spec/services/ee/service_ping/permit_data_categories_service_spec.rb - - ee/spec/services/ee/service_ping/service_ping_settings_spec.rb - ee/spec/services/geo/blob_download_service_spec.rb - ee/spec/services/geo/graphql_request_service_spec.rb - ee/spec/services/geo/node_status_request_service_spec.rb @@ -1040,8 +1032,6 @@ RSpec/VerifiedDoubles: - spec/services/repositories/changelog_service_spec.rb - spec/services/search_service_spec.rb - spec/services/service_ping/build_payload_service_spec.rb - - spec/services/service_ping/permit_data_categories_service_spec.rb - - spec/services/service_ping/service_ping_settings_spec.rb - spec/services/service_ping/submit_service_ping_service_spec.rb - spec/services/snippets/update_repository_storage_service_spec.rb - spec/services/spam/akismet_mark_as_spam_service_spec.rb @@ -1155,7 +1145,6 @@ RSpec/VerifiedDoubles: - spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb - spec/workers/gitlab_performance_bar_stats_worker_spec.rb - spec/workers/invalid_gpg_signature_update_worker_spec.rb - - spec/workers/issue_rebalancing_worker_spec.rb - spec/workers/issues/rebalancing_worker_spec.rb - spec/workers/merge_request_mergeability_check_worker_spec.rb - spec/workers/new_issue_worker_spec.rb diff --git a/.rubocop_todo/style/bare_percent_literals.yml b/.rubocop_todo/style/bare_percent_literals.yml index 658c6c22baa..104ead817d5 100644 --- a/.rubocop_todo/style/bare_percent_literals.yml +++ b/.rubocop_todo/style/bare_percent_literals.yml @@ -10,7 +10,6 @@ Style/BarePercentLiterals: - 'app/models/integrations/datadog.rb' - 'app/services/feature_flags/base_service.rb' - 'app/services/repositories/base_service.rb' - - 'app/services/repositories/destroy_rollback_service.rb' - 'app/services/repositories/destroy_service.rb' - 'ee/app/services/jira/jql_builder_service.rb' - 'ee/lib/ee/gitlab/checks/push_rules/file_size_check.rb' diff --git a/.rubocop_todo/style/class_and_module_children.yml b/.rubocop_todo/style/class_and_module_children.yml index 14f6ccad44f..fab05667adb 100644 --- a/.rubocop_todo/style/class_and_module_children.yml +++ b/.rubocop_todo/style/class_and_module_children.yml @@ -351,9 +351,7 @@ Style/ClassAndModuleChildren: - 'app/serializers/merge_requests/pipeline_entity.rb' - 'app/services/projects/branches_by_mode_service.rb' - 'app/services/repositories/base_service.rb' - - 'app/services/repositories/destroy_rollback_service.rb' - 'app/services/repositories/destroy_service.rb' - - 'app/services/repositories/shell_destroy_service.rb' - 'app/uploaders/dependency_proxy/file_uploader.rb' - 'app/uploaders/packages/composer/cache_uploader.rb' - 'app/uploaders/packages/debian/component_file_uploader.rb' diff --git a/.rubocop_todo/style/format_string.yml b/.rubocop_todo/style/format_string.yml index 13e118d6edc..a9065bb5932 100644 --- a/.rubocop_todo/style/format_string.yml +++ b/.rubocop_todo/style/format_string.yml @@ -182,7 +182,6 @@ Style/FormatString: - 'config/initializers/rack_lineprof.rb' - 'danger/roulette/Dangerfile' - 'ee/app/components/billing/plan_component.rb' - - 'ee/app/components/namespaces/preview_free_user_cap_alert_component.rb' - 'ee/app/controllers/admin/elasticsearch_controller.rb' - 'ee/app/controllers/admin/geo/application_controller.rb' - 'ee/app/controllers/admin/geo/projects_controller.rb' @@ -235,7 +234,6 @@ Style/FormatString: - 'ee/app/services/merge_requests/create_from_vulnerability_data_service.rb' - 'ee/app/services/namespaces/check_excess_storage_size_service.rb' - 'ee/app/services/namespaces/check_storage_size_service.rb' - - 'ee/app/services/network_policies/responses.rb' - 'ee/app/services/security/security_orchestration_policies/policy_configuration_validation_service.rb' - 'ee/app/services/security/security_orchestration_policies/validate_policy_service.rb' - 'ee/app/services/timebox_report_service.rb' diff --git a/.rubocop_todo/style/guard_clause.yml b/.rubocop_todo/style/guard_clause.yml index 008ccb971c4..c7109639677 100644 --- a/.rubocop_todo/style/guard_clause.yml +++ b/.rubocop_todo/style/guard_clause.yml @@ -60,7 +60,6 @@ Style/GuardClause: - 'app/finders/ci/runners_finder.rb' - 'app/finders/deployments_finder.rb' - 'app/finders/group_members_finder.rb' - - 'app/finders/projects/serverless/functions_finder.rb' - 'app/finders/snippets_finder.rb' - 'app/graphql/mutations/concerns/mutations/spam_protection.rb' - 'app/graphql/mutations/design_management/delete.rb' @@ -73,7 +72,6 @@ Style/GuardClause: - 'app/graphql/resolvers/concerns/time_frame_arguments.rb' - 'app/graphql/resolvers/projects/jira_projects_resolver.rb' - 'app/graphql/types/ci/job_type.rb' - - 'app/graphql/types/concerns/find_closest.rb' - 'app/helpers/admin/user_actions_helper.rb' - 'app/helpers/appearances_helper.rb' - 'app/helpers/application_helper.rb' @@ -235,7 +233,6 @@ Style/GuardClause: - 'app/services/post_receive_service.rb' - 'app/services/projects/after_rename_service.rb' - 'app/services/projects/create_service.rb' - - 'app/services/projects/destroy_rollback_service.rb' - 'app/services/projects/destroy_service.rb' - 'app/services/projects/hashed_storage/rollback_service.rb' - 'app/services/projects/import_export/export_service.rb' @@ -463,7 +460,6 @@ Style/GuardClause: - 'ee/lib/gitlab/insights/serializers/chartjs/multi_series_serializer.rb' - 'ee/lib/gitlab/insights/validators/params_validator.rb' - 'ee/lib/omni_auth/strategies/group_saml.rb' - - 'ee/lib/pseudonymizer/dumper.rb' - 'ee/spec/features/billings/billing_plans_spec.rb' - 'ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_spec.rb' - 'ee/spec/workers/update_max_seats_used_for_gitlab_com_subscriptions_worker_spec.rb' @@ -545,7 +541,6 @@ Style/GuardClause: - 'lib/gitlab/database/partitioning/sliding_list_strategy.rb' - 'lib/gitlab/database/postgresql_adapter/force_disconnectable_mixin.rb' - 'lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb' - - 'lib/gitlab/diff/custom_diff.rb' - 'lib/gitlab/diff/file.rb' - 'lib/gitlab/diff/highlight.rb' - 'lib/gitlab/diff/highlight_cache.rb' @@ -580,7 +575,6 @@ Style/GuardClause: - 'lib/gitlab/graphql/pagination/keyset/conditions/null_condition.rb' - 'lib/gitlab/graphql/pagination/keyset/order_info.rb' - 'lib/gitlab/graphql/pagination/keyset/query_builder.rb' - - 'lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb' - 'lib/gitlab/i18n/metadata_entry.rb' - 'lib/gitlab/i18n/po_linter.rb' - 'lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb' diff --git a/.rubocop_todo/style/if_inside_else.yml b/.rubocop_todo/style/if_inside_else.yml index fcddbae74fc..3ce6e5ec704 100644 --- a/.rubocop_todo/style/if_inside_else.yml +++ b/.rubocop_todo/style/if_inside_else.yml @@ -14,7 +14,6 @@ Style/IfInsideElse: - 'app/models/ci/build.rb' - 'app/models/namespace.rb' - 'app/presenters/project_presenter.rb' - - 'app/services/service_ping/build_payload_service.rb' - 'app/services/system_notes/commit_service.rb' - 'app/services/task_list_toggle_service.rb' - 'app/services/user_project_access_changed_service.rb' diff --git a/.rubocop_todo/style/if_unless_modifier.yml b/.rubocop_todo/style/if_unless_modifier.yml index ddc6a477b85..645fc5f764c 100644 --- a/.rubocop_todo/style/if_unless_modifier.yml +++ b/.rubocop_todo/style/if_unless_modifier.yml @@ -8,7 +8,6 @@ Style/IfUnlessModifier: - 'app/channels/graphql_channel.rb' - 'app/controllers/admin/application_settings_controller.rb' - 'app/controllers/admin/projects_controller.rb' - - 'app/controllers/admin/requests_profiles_controller.rb' - 'app/controllers/admin/runners_controller.rb' - 'app/controllers/admin/users_controller.rb' - 'app/controllers/application_controller.rb' @@ -379,7 +378,6 @@ Style/IfUnlessModifier: - 'app/services/projects/update_repository_storage_service.rb' - 'app/services/resource_access_tokens/create_service.rb' - 'app/services/resource_events/change_labels_service.rb' - - 'app/services/service_ping/devops_report_service.rb' - 'app/services/snippets/create_service.rb' - 'app/services/snippets/destroy_service.rb' - 'app/services/snippets/repository_validation_service.rb' @@ -420,7 +418,6 @@ Style/IfUnlessModifier: - 'app/workers/concerns/worker_attributes.rb' - 'app/workers/database/batched_background_migration/single_database_worker.rb' - 'app/workers/file_hook_worker.rb' - - 'app/workers/issue_rebalancing_worker.rb' - 'app/workers/issues/rebalancing_worker.rb' - 'app/workers/merge_request_mergeability_check_worker.rb' - 'app/workers/object_storage/migrate_uploads_worker.rb' @@ -497,7 +494,6 @@ Style/IfUnlessModifier: - 'ee/app/controllers/projects/push_rules_controller.rb' - 'ee/app/controllers/projects/settings/slacks_controller.rb' - 'ee/app/controllers/trials_controller.rb' - - 'ee/app/finders/ee/alert_management/alerts_finder.rb' - 'ee/app/finders/iterations_finder.rb' - 'ee/app/finders/merge_trains_finder.rb' - 'ee/app/finders/security/pipeline_vulnerabilities_finder.rb' @@ -610,8 +606,6 @@ Style/IfUnlessModifier: - 'ee/app/services/ee/users/update_service.rb' - 'ee/app/services/elastic/indexing_control_service.rb' - 'ee/app/services/elastic/process_bookkeeping_service.rb' - - 'ee/app/services/epic_links/create_service.rb' - - 'ee/app/services/epic_links/update_service.rb' - 'ee/app/services/epics/base_service.rb' - 'ee/app/services/epics/create_service.rb' - 'ee/app/services/epics/update_dates_service.rb' @@ -719,7 +713,6 @@ Style/IfUnlessModifier: - 'ee/lib/gitlab/geo/log_cursor/daemon.rb' - 'ee/lib/gitlab/geo/log_cursor/events/repository_deleted_event.rb' - 'ee/lib/gitlab/geo/oauth/logout_token.rb' - - 'ee/lib/gitlab/geo/replication/base_transfer.rb' - 'ee/lib/gitlab/geo/replication/blob_downloader.rb' - 'ee/lib/gitlab/geo/replicator.rb' - 'ee/lib/gitlab/graphql/aggregations/epics/lazy_epic_aggregate.rb' @@ -735,13 +728,10 @@ Style/IfUnlessModifier: - 'ee/lib/gitlab/usage/metrics/instrumentations/count_users_creating_ci_builds_metric.rb' - 'ee/lib/gitlab/usage/metrics/instrumentations/license_metric.rb' - 'ee/lib/omni_auth/strategies/group_saml.rb' - - 'ee/lib/pseudonymizer/dumper.rb' - - 'ee/lib/pseudonymizer/uploader.rb' - 'ee/lib/sidebars/groups/menus/administration_menu.rb' - 'ee/lib/sidebars/groups/menus/analytics_menu.rb' - 'ee/lib/sidebars/groups/menus/security_compliance_menu.rb' - 'ee/lib/tasks/geo.rake' - - 'ee/lib/tasks/gitlab/db.rake' - 'ee/lib/tasks/gitlab/geo.rake' - 'ee/lib/tasks/gitlab/seed/insights.rake' - 'ee/spec/controllers/subscriptions_controller_spec.rb' @@ -945,7 +935,6 @@ Style/IfUnlessModifier: - 'lib/gitlab/database/reindexing/reindex_concurrently.rb' - 'lib/gitlab/database/transaction/observer.rb' - 'lib/gitlab/database/with_lock_retries.rb' - - 'lib/gitlab/diff/custom_diff.rb' - 'lib/gitlab/diff/formatters/base_formatter.rb' - 'lib/gitlab/diff/rendered/notebook/diff_file.rb' - 'lib/gitlab/elasticsearch/logs/lines.rb' @@ -1154,7 +1143,6 @@ Style/IfUnlessModifier: - 'rubocop/cop/ignored_columns.rb' - 'rubocop/cop/migration/add_limit_to_text_columns.rb' - 'rubocop/cop/migration/add_reference.rb' - - 'rubocop/cop/migration/hash_index.rb' - 'rubocop/cop/migration/remove_column.rb' - 'rubocop/cop/migration/sidekiq_queue_migrate.rb' - 'rubocop/cop/performance/ar_exists_and_present_blank.rb' diff --git a/.rubocop_todo/style/next.yml b/.rubocop_todo/style/next.yml index e1f9b927db6..ba2bd293696 100644 --- a/.rubocop_todo/style/next.yml +++ b/.rubocop_todo/style/next.yml @@ -5,7 +5,6 @@ Style/Next: # Temporarily disabled due to too many offenses Enabled: false Exclude: - - 'app/finders/projects/serverless/functions_finder.rb' - 'app/models/preloaders/environments/deployment_preloader.rb' - 'app/models/route.rb' - 'app/services/authorized_project_update/find_records_due_for_refresh_service.rb' diff --git a/.rubocop_todo/style/numeric_literal_prefix.yml b/.rubocop_todo/style/numeric_literal_prefix.yml index b469a68ff5a..ea0b028d11c 100644 --- a/.rubocop_todo/style/numeric_literal_prefix.yml +++ b/.rubocop_todo/style/numeric_literal_prefix.yml @@ -10,16 +10,12 @@ Style/NumericLiteralPrefix: - 'config/initializers/01_secret_token.rb' - 'config/initializers/1_settings.rb' - 'db/post_migrate/20220131000001_schedule_trace_expiry_removal.rb' - - 'ee/lib/gitlab/geo/replication/base_transfer.rb' - 'ee/lib/gitlab/geo/replication/blob_downloader.rb' - 'ee/spec/lib/bulk_imports/groups/pipelines/iterations_pipeline_spec.rb' - 'ee/spec/lib/gitlab/background_migration/remove_all_trace_expiration_dates_spec.rb' - 'ee/spec/lib/gitlab/geo/replication/blob_downloader_spec.rb' - - 'ee/spec/lib/gitlab/geo/replication/file_transfer_spec.rb' - - 'ee/spec/lib/gitlab/geo/replication/job_artifact_transfer_spec.rb' - 'ee/spec/migrations/schedule_trace_expiry_removal_spec.rb' - 'ee/spec/models/analytics/devops_adoption/snapshot_spec.rb' - - 'ee/spec/models/ci/minutes/quota_spec.rb' - 'ee/spec/models/ee/group_spec.rb' - 'ee/spec/models/gitlab/seat_link_data_spec.rb' - 'ee/spec/services/incident_management/oncall_schedules/update_service_spec.rb' diff --git a/.rubocop_todo/style/percent_literal_delimiters.yml b/.rubocop_todo/style/percent_literal_delimiters.yml index 8611181f430..487e0e7cd20 100644 --- a/.rubocop_todo/style/percent_literal_delimiters.yml +++ b/.rubocop_todo/style/percent_literal_delimiters.yml @@ -177,7 +177,6 @@ Style/PercentLiteralDelimiters: - 'app/services/projects/update_service.rb' - 'app/services/prometheus/proxy_service.rb' - 'app/services/repositories/base_service.rb' - - 'app/services/repositories/destroy_rollback_service.rb' - 'app/services/repositories/destroy_service.rb' - 'app/services/repository_archive_clean_up_service.rb' - 'app/services/resource_access_tokens/create_service.rb' @@ -293,7 +292,6 @@ Style/PercentLiteralDelimiters: - 'ee/lib/api/managed_licenses.rb' - 'ee/lib/api/status_checks.rb' - 'ee/lib/api/visual_review_discussions.rb' - - 'ee/lib/banzai/pipeline/incident_management/timeline_event_pipeline.rb' - 'ee/lib/ee/api/helpers/members_helpers.rb' - 'ee/lib/ee/api/helpers/projects_helpers.rb' - 'ee/lib/ee/api/search.rb' @@ -305,7 +303,6 @@ Style/PercentLiteralDelimiters: - 'ee/lib/ee/gitlab/checks/push_rules/file_size_check.rb' - 'ee/lib/ee/gitlab/ci/parsers/security/validators/schema_validator.rb' - 'ee/lib/ee/gitlab/etag_caching/router/rails.rb' - - 'ee/lib/ee/gitlab/integrations/sti_type.rb' - 'ee/lib/ee/gitlab/middleware/read_only/controller.rb' - 'ee/lib/ee/gitlab/path_regex.rb' - 'ee/lib/ee/gitlab/uploads/migration_helper.rb' @@ -316,7 +313,6 @@ Style/PercentLiteralDelimiters: - 'ee/lib/elastic/latest/git_class_proxy.rb' - 'ee/lib/elastic/latest/project_instance_proxy.rb' - 'ee/lib/elastic/latest/snippet_class_proxy.rb' - - 'ee/lib/gitlab/alert_management/payload/cilium.rb' - 'ee/lib/gitlab/auth/group_saml/auth_hash.rb' - 'ee/lib/gitlab/ci/parsers/security/formatters/dast.rb' - 'ee/lib/gitlab/geo.rb' @@ -368,7 +364,6 @@ Style/PercentLiteralDelimiters: - 'ee/spec/lib/banzai/filter/references/epic_reference_filter_spec.rb' - 'ee/spec/lib/banzai/filter/references/iteration_reference_filter_spec.rb' - 'ee/spec/lib/banzai/filter/references/vulnerability_reference_filters_spec.rb' - - 'ee/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb' - 'ee/spec/lib/ee/gitlab/alert_management/payload/generic_spec.rb' - 'ee/spec/lib/ee/gitlab/auth/ldap/access_levels_spec.rb' - 'ee/spec/lib/ee/gitlab/auth/ldap/config_spec.rb' @@ -558,7 +553,6 @@ Style/PercentLiteralDelimiters: - 'lib/gitlab/gitaly_client/diff.rb' - 'lib/gitlab/gitaly_client/wiki_page.rb' - 'lib/gitlab/graphql/pagination/keyset/order_info.rb' - - 'lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb' - 'lib/gitlab/hotlinking_detector.rb' - 'lib/gitlab/import_export/command_line_util.rb' - 'lib/gitlab/import_export/file_importer.rb' @@ -645,10 +639,6 @@ Style/PercentLiteralDelimiters: - 'rubocop/migration_helpers.rb' - 'scripts/qa/testcases-check' - 'scripts/regenerate-schema' - - 'shared/packages/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b/packages/57/files/67/package.gemspec' - - 'shared/packages/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b/packages/6/files/5/package.gemspec' - - 'shared/packages/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b/packages/7/files/7/package.gemspec' - - 'shared/packages/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b/packages/88/files/91/package.gemspec' - 'sidekiq_cluster/cli.rb' - 'spec/benchmarks/banzai_benchmark.rb' - 'spec/commands/sidekiq_cluster/cli_spec.rb' @@ -894,7 +884,6 @@ Style/PercentLiteralDelimiters: - 'spec/lib/gitlab/jira_import/issue_serializer_spec.rb' - 'spec/lib/gitlab/jira_import/labels_importer_spec.rb' - 'spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb' - - 'spec/lib/gitlab/kubernetes/network_policy_spec.rb' - 'spec/lib/gitlab/kubernetes/role_spec.rb' - 'spec/lib/gitlab/language_data_spec.rb' - 'spec/lib/gitlab/markup_helper_spec.rb' diff --git a/.rubocop_todo/style/redundant_interpolation.yml b/.rubocop_todo/style/redundant_interpolation.yml index fb190ff2100..0d3651eb7b2 100644 --- a/.rubocop_todo/style/redundant_interpolation.yml +++ b/.rubocop_todo/style/redundant_interpolation.yml @@ -77,7 +77,6 @@ Style/RedundantInterpolation: - 'qa/qa/resource/events/base.rb' - 'qa/qa/service/praefect_manager.rb' - 'qa/qa/specs/features/browser_ui/4_verify/pipeline/include_multiple_files_from_a_project_spec.rb' - - 'qa/qa/specs/features/browser_ui/4_verify/pipeline/parent_child_pipelines_dependent_relationship_spec.rb' - 'qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb' - 'qa/qa/specs/features/ee/browser_ui/1_manage/project/project_templates_spec.rb' - 'qa/qa/tools/generate_perf_testdata.rb' diff --git a/.rubocop_todo/style/redundant_regexp_escape.yml b/.rubocop_todo/style/redundant_regexp_escape.yml index 1a88f62ab64..969a23601ca 100644 --- a/.rubocop_todo/style/redundant_regexp_escape.yml +++ b/.rubocop_todo/style/redundant_regexp_escape.yml @@ -35,7 +35,6 @@ Style/RedundantRegexpEscape: - 'ee/lib/gitlab/geo/git_ssh_proxy.rb' - 'ee/lib/gitlab/return_to_location.rb' - 'ee/spec/features/read_only_spec.rb' - - 'ee/spec/helpers/seats_count_alert_helper_spec.rb' - 'ee/spec/helpers/vulnerabilities_helper_spec.rb' - 'ee/spec/lib/ee/gitlab/usage_data_counters/hll_redis_counter_spec.rb' - 'ee/spec/mailers/notify_spec.rb' diff --git a/.rubocop_todo/style/redundant_self.yml b/.rubocop_todo/style/redundant_self.yml index e363f83761b..1aba23c90ae 100644 --- a/.rubocop_todo/style/redundant_self.yml +++ b/.rubocop_todo/style/redundant_self.yml @@ -260,7 +260,6 @@ Style/RedundantSelf: - 'ee/lib/elastic/latest/note_config.rb' - 'ee/lib/elastic/migration.rb' - 'ee/lib/gem_extensions/elasticsearch/model/indexing/instance_methods.rb' - - 'ee/lib/gitlab/alert_management/payload/cilium.rb' - 'ee/lib/gitlab/analytics/cycle_analytics/summary/base_time.rb' - 'ee/lib/gitlab/auth/group_saml/response_check.rb' - 'ee/lib/gitlab/auth/group_saml/user.rb' @@ -277,7 +276,6 @@ Style/RedundantSelf: - 'ee/lib/gitlab/geo/oauth/logout_state.rb' - 'ee/lib/gitlab/geo/replicator.rb' - 'ee/lib/gitlab/template/custom_template.rb' - - 'ee/lib/system_check/app/elasticsearch_check.rb' - 'ee/spec/helpers/ee/feature_flags_helper_spec.rb' - 'ee/spec/helpers/ee/graph_helper_spec.rb' - 'ee/spec/models/geo/deleted_project_spec.rb' @@ -365,8 +363,6 @@ Style/RedundantSelf: - 'lib/gitlab/instrumentation/redis_base.rb' - 'lib/gitlab/jira_import.rb' - 'lib/gitlab/jwt_token.rb' - - 'lib/gitlab/kubernetes/cilium_network_policy.rb' - - 'lib/gitlab/kubernetes/network_policy.rb' - 'lib/gitlab/logger.rb' - 'lib/gitlab/marker_range.rb' - 'lib/gitlab/memory/instrumentation.rb' diff --git a/.rubocop_todo/style/string_concatenation.yml b/.rubocop_todo/style/string_concatenation.yml index e21fecfb5e3..8c8dbf86b1c 100644 --- a/.rubocop_todo/style/string_concatenation.yml +++ b/.rubocop_todo/style/string_concatenation.yml @@ -301,11 +301,9 @@ Style/StringConcatenation: - 'spec/services/packages/helm/extract_file_metadata_service_spec.rb' - 'spec/services/packages/helm/process_file_service_spec.rb' - 'spec/services/projects/create_service_spec.rb' - - 'spec/services/projects/destroy_rollback_service_spec.rb' - 'spec/services/projects/destroy_service_spec.rb' - 'spec/services/projects/download_service_spec.rb' - 'spec/services/push_event_payload_service_spec.rb' - - 'spec/services/repositories/destroy_rollback_service_spec.rb' - 'spec/services/repositories/destroy_service_spec.rb' - 'spec/services/snippets/bulk_destroy_service_spec.rb' - 'spec/services/snippets/update_service_spec.rb' diff --git a/.rubocop_todo/style/string_literals_in_interpolation.yml b/.rubocop_todo/style/string_literals_in_interpolation.yml index 3f3cb007306..cded81afa7f 100644 --- a/.rubocop_todo/style/string_literals_in_interpolation.yml +++ b/.rubocop_todo/style/string_literals_in_interpolation.yml @@ -24,7 +24,6 @@ Style/StringLiteralsInInterpolation: - 'ee/app/models/license.rb' - 'ee/app/services/epics/tree_reorder_service.rb' - 'ee/lib/gitlab/elastic/helper.rb' - - 'ee/lib/pseudonymizer/pager.rb' - 'ee/spec/features/admin/admin_settings_spec.rb' - 'lib/api/helpers/snippets_helpers.rb' - 'lib/api/validations/validators/check_assignees_count.rb' @@ -54,7 +53,6 @@ Style/StringLiteralsInInterpolation: - 'qa/qa/specs/helpers/context_selector.rb' - 'qa/qa/tools/generate_perf_testdata.rb' - 'rubocop/cop/migration/prevent_index_creation.rb' - - 'spec/controllers/projects/serverless/functions_controller_spec.rb' - 'spec/features/commits_spec.rb' - 'spec/features/dashboard/merge_requests_spec.rb' - 'spec/features/users/login_spec.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c8d2417bb4..1c178782ebb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 15.0.3 (2022-06-16) + +### Fixed (2 changes) + +- [Disconnect alternates when unlinking from a repository pool](gitlab-org/gitlab@a6f4b701af0d5850a10d77feeb4842b1fe017047) ([merge request](gitlab-org/gitlab!90269)) +- [Add GitLab agent image tag to install command](gitlab-org/gitlab@09decb04e391f095139412ed623164cab8023a7c) ([merge request](gitlab-org/gitlab!90269)) + ## 15.0.2 (2022-06-06) ### Added (1 change) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index e8f59312593..2422b2f9464 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -14.7.3 +14.7.4 diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 8d46ea76be1..e7a5f5ecc06 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -1,4 +1,4 @@ -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { __ } from '~/locale'; import axios from './lib/utils/axios_utils'; import { joinPaths } from './lib/utils/url_utility'; @@ -464,7 +464,7 @@ const Api = { throw error; } - createFlash({ + createAlert({ message: __('Something went wrong while fetching projects'), }); @@ -654,7 +654,7 @@ const Api = { }) .then(({ data }) => callback(data)) .catch(() => - createFlash({ + createAlert({ message: __('Something went wrong while fetching projects'), }), ); diff --git a/app/assets/javascripts/behaviors/components/sandboxed_mermaid.vue b/app/assets/javascripts/behaviors/components/sandboxed_mermaid.vue new file mode 100644 index 00000000000..6b4110cff02 --- /dev/null +++ b/app/assets/javascripts/behaviors/components/sandboxed_mermaid.vue @@ -0,0 +1,77 @@ +<script> +import { + getSandboxFrameSrc, + BUFFER_IFRAME_HEIGHT, + SANDBOX_ATTRIBUTES, +} from '../markdown/render_sandboxed_mermaid'; + +export default { + name: 'SandboxedMermaid', + + props: { + source: { + type: String, + required: true, + }, + }, + + data() { + return { + iframeHeight: BUFFER_IFRAME_HEIGHT, + sandboxFrameSrc: getSandboxFrameSrc(), + }; + }, + + watch: { + source() { + this.updateDiagram(); + }, + }, + + mounted() { + window.addEventListener('message', this.onPostMessage, false); + }, + + destroyed() { + window.removeEventListener('message', this.onPostMessage); + }, + + methods: { + getSandboxFrameSrc, + + onPostMessage(event) { + const container = this.$refs.diagramContainer; + + if (event.source === container?.contentWindow) { + this.iframeHeight = Number(event.data.h) + BUFFER_IFRAME_HEIGHT; + } + }, + + updateDiagram() { + const container = this.$refs.diagramContainer; + + // Potential risk associated with '*' discussed in below thread + // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74414#note_735183398 + container.contentWindow?.postMessage(this.source, '*'); + container.addEventListener('load', () => { + container.contentWindow?.postMessage(this.source, '*'); + }); + }, + }, + + sandboxFrameSrc: getSandboxFrameSrc(), + sandboxAttributes: SANDBOX_ATTRIBUTES, +}; +</script> +<template> + <iframe + ref="diagramContainer" + :src="$options.sandboxFrameSrc" + :sandbox="$options.sandboxAttributes" + frameborder="0" + scrolling="no" + width="100%" + :height="iframeHeight" + > + </iframe> +</template> diff --git a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js index 543e676e85e..077e96b2fee 100644 --- a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js @@ -32,7 +32,8 @@ const MAX_CHAR_LIMIT = 2000; const MAX_MERMAID_BLOCK_LIMIT = 50; // Max # of `&` allowed in Chaining of links syntax const MAX_CHAINING_OF_LINKS_LIMIT = 30; -const BUFFER_IFRAME_HEIGHT = 10; +export const BUFFER_IFRAME_HEIGHT = 10; +export const SANDBOX_ATTRIBUTES = 'allow-scripts allow-popups'; // Keep a map of mermaid blocks we've already rendered. const elsProcessingMap = new WeakMap(); let renderedMermaidBlocks = 0; @@ -56,7 +57,7 @@ function fixElementSource(el) { return { source }; } -function getSandboxFrameSrc() { +export function getSandboxFrameSrc() { const path = joinPaths(gon.relative_url_root || '', SANDBOX_FRAME_PATH); if (!darkModeEnabled()) { return path; @@ -69,7 +70,7 @@ function renderMermaidEl(el, source) { const iframeEl = document.createElement('iframe'); setAttributes(iframeEl, { src: getSandboxFrameSrc(), - sandbox: 'allow-scripts allow-popups', + sandbox: SANDBOX_ATTRIBUTES, frameBorder: 0, scrolling: 'no', width: '100%', diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue b/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue index 4ed89140e5b..6c0ac8e54d2 100644 --- a/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue +++ b/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue @@ -36,16 +36,19 @@ export default { directives: { GlTooltip, }, - inject: ['tiptapEditor'], + inject: ['tiptapEditor', 'contentEditor'], data() { return { codeBlockType: undefined, - selectedLanguage: {}, filterTerm: '', filteredLanguages: [], showCustomLanguageInput: false, customLanguageType: '', + + selectedLanguage: {}, + isDiagram: false, + showPreview: false, }; }, watch: { @@ -61,21 +64,36 @@ export default { return CODE_BLOCK_NODE_TYPES.some((type) => editor.isActive(type)); }, - updateSelectedLanguage() { + async updateCodeBlockInfoToState() { this.codeBlockType = CODE_BLOCK_NODE_TYPES.find((type) => this.tiptapEditor.isActive(type)); - if (this.codeBlockType) { - const { language } = this.tiptapEditor.getAttributes(this.codeBlockType); - this.selectedLanguage = codeBlockLanguageLoader.findOrCreateLanguageBySyntax(language); - } + if (!this.codeBlockType) return; + + const { language, isDiagram, showPreview } = this.tiptapEditor.getAttributes( + this.codeBlockType, + ); + this.selectedLanguage = codeBlockLanguageLoader.findOrCreateLanguageBySyntax( + language, + isDiagram, + ); + this.isDiagram = isDiagram; + this.showPreview = showPreview; }, - copyCodeBlockText() { + getCodeBlockText() { const { view } = this.tiptapEditor; const { from } = this.tiptapEditor.state.selection; const node = getParentByTagName(view.domAtPos(from).node, 'pre'); + return node?.textContent || ''; + }, - navigator.clipboard.writeText(node?.textContent || ''); + copyCodeBlockText() { + navigator.clipboard.writeText(this.getCodeBlockText()); + }, + + togglePreview() { + this.showPreview = !this.showPreview; + this.tiptapEditor.commands.updateAttributes(Diagram.name, { showPreview: this.showPreview }); }, async applyLanguage(language) { @@ -125,7 +143,7 @@ export default { getReferenceClientRect, } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */" > - <editor-state-observer @transaction="updateSelectedLanguage"> + <editor-state-observer @transaction="updateCodeBlockInfoToState"> <gl-button-group> <gl-dropdown category="tertiary" @@ -228,6 +246,19 @@ export default { @click="copyCodeBlockText" /> <gl-button + v-if="isDiagram" + v-gl-tooltip + variant="default" + category="tertiary" + size="medium" + :class="{ active: showPreview }" + data-testid="preview-diagram" + :aria-label="__('Preview diagram')" + :title="__('Preview diagram')" + icon="eye" + @click="togglePreview" + /> + <gl-button v-gl-tooltip variant="default" category="tertiary" diff --git a/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue b/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue new file mode 100644 index 00000000000..ecde593147c --- /dev/null +++ b/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue @@ -0,0 +1,34 @@ +<script> +import { GlDropdown, GlDropdownItem, GlTooltipDirective as GlTooltip } from '@gitlab/ui'; + +export default { + components: { + GlDropdown, + GlDropdownItem, + }, + directives: { + GlTooltip, + }, + inject: ['tiptapEditor'], + methods: { + execute(contentType, attrs) { + this.tiptapEditor.chain().focus().setNode(contentType, attrs).run(); + + this.$emit('execute', { contentType }); + }, + }, +}; +</script> +<template> + <gl-dropdown size="small" category="tertiary" icon="plus"> + <gl-dropdown-item @click="execute('diagram', { language: 'mermaid' })"> + {{ __('Mermaid diagram') }} + </gl-dropdown-item> + <gl-dropdown-item @click="execute('diagram', { language: 'plantuml' })"> + {{ __('PlantUML diagram') }} + </gl-dropdown-item> + <gl-dropdown-item @click="execute('horizontalRule')"> + {{ __('Horizontal rule') }} + </gl-dropdown-item> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/top_toolbar.vue index 19e150a4da9..b652e634b0c 100644 --- a/app/assets/javascripts/content_editor/components/top_toolbar.vue +++ b/app/assets/javascripts/content_editor/components/top_toolbar.vue @@ -5,6 +5,7 @@ import ToolbarImageButton from './toolbar_image_button.vue'; import ToolbarLinkButton from './toolbar_link_button.vue'; import ToolbarTableButton from './toolbar_table_button.vue'; import ToolbarTextStyleDropdown from './toolbar_text_style_dropdown.vue'; +import ToolbarMoreDropdown from './toolbar_more_dropdown.vue'; export default { components: { @@ -13,6 +14,7 @@ export default { ToolbarLinkButton, ToolbarTableButton, ToolbarImageButton, + ToolbarMoreDropdown, }, methods: { trackToolbarControlExecution({ contentType, value }) { @@ -117,16 +119,8 @@ export default { :label="__('Add a collapsible section')" @execute="trackToolbarControlExecution" /> - <toolbar-button - data-testid="horizontal-rule" - content-type="horizontalRule" - icon-name="dash" - class="gl-mx-2" - editor-command="setHorizontalRule" - :label="__('Add a horizontal rule')" - @execute="trackToolbarControlExecution" - /> - <toolbar-table-button @execute="trackToolbarControlExecution" /> + <toolbar-table-button data-testid="table" @execute="trackToolbarControlExecution" /> + <toolbar-more-dropdown data-testid="more" @execute="trackToolbarControlExecution" /> </div> </template> <style> diff --git a/app/assets/javascripts/content_editor/components/wrappers/code_block.vue b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue index 647f0798364..81f9b1f0af5 100644 --- a/app/assets/javascripts/content_editor/components/wrappers/code_block.vue +++ b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue @@ -1,15 +1,26 @@ <script> +import { debounce } from 'lodash'; import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2'; import { __ } from '~/locale'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; +import SandboxedMermaid from '~/behaviors/components/sandboxed_mermaid.vue'; import codeBlockLanguageLoader from '../../services/code_block_language_loader'; +import EditorStateObserver from '../editor_state_observer.vue'; export default { name: 'CodeBlock', components: { NodeViewWrapper, NodeViewContent, + EditorStateObserver, + SandboxedMermaid, }, + inject: ['contentEditor'], props: { + editor: { + type: Object, + required: true, + }, node: { type: Object, required: true, @@ -18,13 +29,48 @@ export default { type: Function, required: true, }, + selected: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + diagramUrl: '', + diagramSource: '', + }; }, async mounted() { + this.updateDiagramPreview = debounce( + this.updateDiagramPreview, + DEFAULT_DEBOUNCE_AND_THROTTLE_MS, + ); + const lang = codeBlockLanguageLoader.findOrCreateLanguageBySyntax(this.node.attrs.language); await codeBlockLanguageLoader.loadLanguage(lang.syntax); this.updateAttributes({ language: this.node.attrs.language }); }, + methods: { + async updateDiagramPreview() { + if (!this.node.attrs.showPreview) { + this.diagramSource = ''; + return; + } + + if (!this.editor.isActive('diagram')) return; + + this.diagramSource = this.$refs.nodeViewContent.$el.textContent; + + if (this.node.attrs.language !== 'mermaid') { + this.diagramUrl = await this.contentEditor.renderDiagram( + this.diagramSource, + this.node.attrs.language, + ); + } + }, + }, i18n: { frontmatter: __('frontmatter'), }, @@ -32,17 +78,26 @@ export default { }; </script> <template> - <node-view-wrapper - :class="`content-editor-code-block gl-relative code highlight ${$options.userColorScheme}`" - as="pre" - > - <span - v-if="node.attrs.isFrontmatter" - data-testid="frontmatter-label" - class="gl-absolute gl-top-0 gl-right-3" - contenteditable="false" - >{{ $options.i18n.frontmatter }}:{{ node.attrs.language }}</span + <editor-state-observer @transaction="updateDiagramPreview"> + <node-view-wrapper + :class="`content-editor-code-block gl-relative code highlight ${$options.userColorScheme}`" + as="pre" > - <node-view-content as="code" /> - </node-view-wrapper> + <div + v-if="node.attrs.showPreview" + class="gl-mt-n3! gl-ml-n4! gl-mr-n4! gl-mb-3 gl-bg-white! gl-p-4 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100" + > + <sandboxed-mermaid v-if="node.attrs.language === 'mermaid'" :source="diagramSource" /> + <img v-else ref="diagramContainer" :src="diagramUrl" /> + </div> + <span + v-if="node.attrs.isFrontmatter" + data-testid="frontmatter-label" + class="gl-absolute gl-top-0 gl-right-3" + contenteditable="false" + >{{ $options.i18n.frontmatter }}:{{ node.attrs.language }}</span + > + <node-view-content ref="nodeViewContent" as="code" /> + </node-view-wrapper> + </editor-state-observer> </template> diff --git a/app/assets/javascripts/content_editor/extensions/diagram.js b/app/assets/javascripts/content_editor/extensions/diagram.js index f9dfeb92e9a..c59ca8a28b8 100644 --- a/app/assets/javascripts/content_editor/extensions/diagram.js +++ b/app/assets/javascripts/content_editor/extensions/diagram.js @@ -1,6 +1,10 @@ +import { textblockTypeInputRule } from '@tiptap/core'; import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants'; +import languageLoader from '../services/code_block_language_loader'; import CodeBlockHighlight from './code_block_highlight'; +const backtickInputRegex = /^```(mermaid|plantuml)[\s\n]$/; + export default CodeBlockHighlight.extend({ name: 'diagram', @@ -17,6 +21,9 @@ export default CodeBlockHighlight.extend({ isDiagram: { default: true, }, + showPreview: { + default: true, + }, }; }, @@ -24,6 +31,11 @@ export default CodeBlockHighlight.extend({ return [ { priority: PARSE_HTML_PRIORITY_HIGHEST, + tag: 'pre[lang="mermaid"]', + getAttrs: () => ({ language: 'mermaid' }), + }, + { + priority: PARSE_HTML_PRIORITY_HIGHEST, tag: '[data-diagram]', getContent(element, schema) { const source = atob(element.dataset.diagramSrc.replace('data:text/plain;base64,', '')); @@ -54,6 +66,14 @@ export default CodeBlockHighlight.extend({ }, addInputRules() { - return []; + const getAttributes = (match) => languageLoader?.loadLanguageFromInputRule(match) || {}; + + return [ + textblockTypeInputRule({ + find: backtickInputRegex, + type: this.type, + getAttributes, + }), + ]; }, }); diff --git a/app/assets/javascripts/content_editor/services/asset_resolver.js b/app/assets/javascripts/content_editor/services/asset_resolver.js index 942457b9664..c0bcddbe58d 100644 --- a/app/assets/javascripts/content_editor/services/asset_resolver.js +++ b/app/assets/javascripts/content_editor/services/asset_resolver.js @@ -1,13 +1,24 @@ import { memoize } from 'lodash'; +const parser = new DOMParser(); + export default ({ renderMarkdown }) => ({ resolveUrl: memoize(async (canonicalSrc) => { const html = await renderMarkdown(`[link](${canonicalSrc})`); if (!html) return canonicalSrc; - const parser = new DOMParser(); const { body } = parser.parseFromString(html, 'text/html'); - return body.querySelector('a').getAttribute('href'); }), + + renderDiagram: memoize(async (code, language) => { + const backticks = '`'.repeat(4); + const html = await renderMarkdown(`${backticks}${language}\n${code}\n${backticks}`); + + const { body } = parser.parseFromString(html, 'text/html'); + const img = body.querySelector('img'); + if (!img) return ''; + + return img.dataset.src || img.getAttribute('src'); + }), }); diff --git a/app/assets/javascripts/content_editor/services/code_block_language_loader.js b/app/assets/javascripts/content_editor/services/code_block_language_loader.js index fa0549d4183..b7cf1bb087c 100644 --- a/app/assets/javascripts/content_editor/services/code_block_language_loader.js +++ b/app/assets/javascripts/content_editor/services/code_block_language_loader.js @@ -8,7 +8,7 @@ const codeBlockLanguageLoader = { allLanguages: CODE_BLOCK_LANGUAGES, - findOrCreateLanguageBySyntax(value) { + findOrCreateLanguageBySyntax(value, isDiagram) { const lowercaseValue = value?.toLowerCase() || 'plaintext'; return ( this.allLanguages.find( @@ -16,7 +16,9 @@ const codeBlockLanguageLoader = { syntax === lowercaseValue || variants?.toLowerCase().split(', ').includes(lowercaseValue), ) || { syntax: lowercaseValue, - label: sprintf(__(`Custom (%{language})`), { language: lowercaseValue }), + label: sprintf(isDiagram ? __(`Diagram (%{language})`) : __(`Custom (%{language})`), { + language: lowercaseValue, + }), } ); }, diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js index 52dacb84153..06757e7a280 100644 --- a/app/assets/javascripts/content_editor/services/content_editor.js +++ b/app/assets/javascripts/content_editor/services/content_editor.js @@ -52,6 +52,10 @@ export class ContentEditor { return this._assetResolver.resolveUrl(canonicalSrc); } + renderDiagram(code, language) { + return this._assetResolver.renderDiagram(code, language); + } + async setSerializedContent(serializedContent) { const { _tiptapEditor: editor, _eventHub: eventHub } = this; const { doc, tr } = editor.state; diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue index fc69dca73a7..54b648e8d03 100644 --- a/app/assets/javascripts/diffs/components/commit_item.vue +++ b/app/assets/javascripts/diffs/components/commit_item.vue @@ -121,7 +121,7 @@ export default { <div class="d-flex float-left align-items-center align-self-start"> <input v-if="isSelectable" - class="mr-2" + class="gl-mr-3" type="checkbox" :checked="checked" @change="$emit('handleCheckboxChange', $event.target.checked)" diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue index f926169549d..b8b7cbb5bdc 100644 --- a/app/assets/javascripts/issues/show/components/description.vue +++ b/app/assets/javascripts/issues/show/components/description.vue @@ -453,13 +453,7 @@ export default { > </textarea> - <gl-modal - ref="modal" - modal-id="create-task-modal" - :title="s__('WorkItem|New Task')" - hide-footer - body-class="gl-p-0!" - > + <gl-modal ref="modal" size="lg" modal-id="create-task-modal" hide-footer body-class="gl-p-0!"> <create-work-item is-modal :initial-title="activeTask.title" diff --git a/app/assets/javascripts/lib/utils/color_utils.js b/app/assets/javascripts/lib/utils/color_utils.js index 66d52051905..3d8df4fde05 100644 --- a/app/assets/javascripts/lib/utils/color_utils.js +++ b/app/assets/javascripts/lib/utils/color_utils.js @@ -67,7 +67,7 @@ export function darkModeEnabled() { const ideDarkThemes = ['dark', 'solarized-dark', 'monokai']; // eslint-disable-next-line @gitlab/require-i18n-strings - const isWebIde = document.body.dataset.page.startsWith('ide:'); + const isWebIde = document.body.dataset.page?.startsWith('ide:'); if (isWebIde) { return ideDarkThemes.includes(window.gon?.user_color_scheme); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index f397a86a368..e7ac27c5e3e 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -416,7 +416,7 @@ export default { <gl-form-checkbox v-if="internalNotesEnabled && canSetInternalNote" v-model="noteIsInternal" - class="gl-mb-6" + class="gl-mb-2" data-testid="internal-note-checkbox" > {{ $options.i18n.internal }} diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue index 74c0cb44c51..a3bbd569f41 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue @@ -1,30 +1,68 @@ <script> +import { GlAlert } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; +import { s__ } from '~/locale'; import Composer from '~/packages_and_registries/package_registry/components/details/metadata/composer.vue'; import Conan from '~/packages_and_registries/package_registry/components/details/metadata/conan.vue'; import Maven from '~/packages_and_registries/package_registry/components/details/metadata/maven.vue'; import Nuget from '~/packages_and_registries/package_registry/components/details/metadata/nuget.vue'; import Pypi from '~/packages_and_registries/package_registry/components/details/metadata/pypi.vue'; import { + FETCH_PACKAGE_METADATA_ERROR_MESSAGE, PACKAGE_TYPE_COMPOSER, PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN, PACKAGE_TYPE_NUGET, PACKAGE_TYPE_PYPI, } from '~/packages_and_registries/package_registry/constants'; +import getPackageMetadataQuery from '../../graphql/queries/get_package_metadata.query.graphql'; +import AdditionalMetadataLoader from './additional_metadata_loader.vue'; export default { components: { Composer, Conan, + GlAlert, Maven, Nuget, Pypi, + AdditionalMetadataLoader, }, props: { - packageEntity: { - type: Object, + packageId: { + type: String, required: true, }, + packageType: { + type: String, + required: true, + }, + }, + apollo: { + packageMetadata: { + query: getPackageMetadataQuery, + context: { + isSingleRequest: true, + }, + variables() { + return { + id: this.packageId, + }; + }, + update(data) { + return data.package?.metadata || null; + }, + error(error) { + this.fetchPackageMetadataError = true; + Sentry.captureException(error); + }, + }, + }, + data() { + return { + packageMetadata: null, + fetchPackageMetadataError: false, + }; }, computed: { metadataComponent() { @@ -34,22 +72,43 @@ export default { [PACKAGE_TYPE_MAVEN]: Maven, [PACKAGE_TYPE_NUGET]: Nuget, [PACKAGE_TYPE_PYPI]: Pypi, - }[this.packageEntity.packageType]; + }[this.packageType]; }, showMetadata() { - return this.metadataComponent && this.packageEntity.metadata; + return this.metadataComponent && this.packageMetadata; + }, + isLoading() { + return this.$apollo.queries.packageMetadata.loading; }, }, + i18n: { + componentTitle: s__('PackageRegistry|Additional metadata'), + fetchPackageMetadataErrorMessage: FETCH_PACKAGE_METADATA_ERROR_MESSAGE, + }, }; </script> <template> - <div v-if="showMetadata"> - <h3 class="gl-font-lg" data-testid="title">{{ __('Additional Metadata') }}</h3> - <div class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base" data-testid="main"> + <div> + <h3 v-if="isLoading || showMetadata" class="gl-font-lg" data-testid="title"> + {{ $options.i18n.componentTitle }} + </h3> + <gl-alert + v-if="fetchPackageMetadataError" + variant="danger" + @dismiss="fetchPackageMetadataError = false" + > + {{ $options.i18n.fetchPackageMetadataErrorMessage }} + </gl-alert> + <additional-metadata-loader v-if="isLoading" /> + <div + v-if="showMetadata" + class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base" + data-testid="main" + > <component :is="metadataComponent" - :package-entity="packageEntity" + :package-metadata="packageMetadata" data-testid="component-is" /> </div> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata_loader.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata_loader.vue new file mode 100644 index 00000000000..628cf441831 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata_loader.vue @@ -0,0 +1,30 @@ +<script> +import { GlSkeletonLoader } from '@gitlab/ui'; + +export default { + components: { + GlSkeletonLoader, + }, + loader: { + width: 302, + height: 16, + repeat: 2, + }, +}; +</script> + +<template> + <div class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base"> + <div + v-for="index in $options.loader.repeat" + :key="index" + class="gl-display-flex gl-align-items-center gl-p-4 gl-border-gray-100 gl-border-b-1" + > + <div class="gl-md-max-w-30p"> + <gl-skeleton-loader :width="$options.loader.width" :height="$options.loader.height"> + <rect :width="$options.loader.width" :height="$options.loader.height" rx="4" /> + </gl-skeleton-loader> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/composer.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/composer.vue index b6a36a0b00f..e3edaa3e45e 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/composer.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/composer.vue @@ -18,7 +18,7 @@ export default { ClipboardButton, }, props: { - packageEntity: { + packageMetadata: { type: Object, required: true, }, @@ -31,10 +31,10 @@ export default { <details-row icon="information-o" padding="gl-p-4" dashed data-testid="composer-target-sha"> <gl-sprintf :message="$options.i18n.targetSha"> <template #sha> - <strong>{{ packageEntity.metadata.targetSha }}</strong> + <strong>{{ packageMetadata.targetSha }}</strong> <clipboard-button :title="$options.i18n.targetShaCopyButton" - :text="packageEntity.metadata.targetSha" + :text="packageMetadata.targetSha" category="tertiary" css-class="gl-p-0!" /> @@ -44,10 +44,10 @@ export default { <details-row icon="information-o" padding="gl-p-4" data-testid="composer-json"> <gl-sprintf :message="$options.i18n.composerJson"> <template #license> - <strong>{{ packageEntity.metadata.composerJson.license }}</strong> + <strong>{{ packageMetadata.composerJson.license }}</strong> </template> <template #version> - <strong>{{ packageEntity.metadata.composerJson.version }}</strong> + <strong>{{ packageMetadata.composerJson.version }}</strong> </template> </gl-sprintf> </details-row> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/conan.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/conan.vue index 10797d74acf..de7c1bc4cd3 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/conan.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/conan.vue @@ -13,7 +13,7 @@ export default { GlSprintf, }, props: { - packageEntity: { + packageMetadata: { type: Object, required: true, }, @@ -25,7 +25,7 @@ export default { <div> <details-row icon="information-o" padding="gl-p-4" data-testid="conan-recipe"> <gl-sprintf :message="$options.i18n.recipeText"> - <template #recipe>{{ packageEntity.metadata.recipe }}</template> + <template #recipe>{{ packageMetadata.recipe }}</template> </gl-sprintf> </details-row> </div> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/maven.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/maven.vue index fd9fb49a9f2..7c3eb476a99 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/maven.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/maven.vue @@ -14,7 +14,7 @@ export default { GlSprintf, }, props: { - packageEntity: { + packageMetadata: { type: Object, required: true, }, @@ -27,14 +27,14 @@ export default { <details-row icon="information-o" padding="gl-p-4" dashed data-testid="maven-app"> <gl-sprintf :message="$options.i18n.appName"> <template #name> - <strong>{{ packageEntity.metadata.appName }}</strong> + <strong>{{ packageMetadata.appName }}</strong> </template> </gl-sprintf> </details-row> <details-row icon="information-o" padding="gl-p-4" data-testid="maven-group"> <gl-sprintf :message="$options.i18n.appGroup"> <template #group> - <strong>{{ packageEntity.metadata.appGroup }}</strong> + <strong>{{ packageMetadata.appGroup }}</strong> </template> </gl-sprintf> </details-row> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue index 1360b03856f..1ddd419a639 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue @@ -14,7 +14,7 @@ export default { GlSprintf, }, props: { - packageEntity: { + packageMetadata: { type: Object, required: true, }, @@ -25,7 +25,7 @@ export default { <template> <div> <details-row - v-if="packageEntity.metadata.projectUrl" + v-if="packageMetadata.projectUrl" icon="project" padding="gl-p-4" dashed @@ -33,22 +33,22 @@ export default { > <gl-sprintf :message="$options.i18n.sourceText"> <template #link> - <gl-link :href="packageEntity.metadata.projectUrl" target="_blank">{{ - packageEntity.metadata.projectUrl + <gl-link :href="packageMetadata.projectUrl" target="_blank">{{ + packageMetadata.projectUrl }}</gl-link> </template> </gl-sprintf> </details-row> <details-row - v-if="packageEntity.metadata.licenseUrl" + v-if="packageMetadata.licenseUrl" icon="license" padding="gl-p-4" data-testid="nuget-license" > <gl-sprintf :message="$options.i18n.licenseText"> <template #link> - <gl-link :href="packageEntity.metadata.licenseUrl" target="_blank">{{ - packageEntity.metadata.licenseUrl + <gl-link :href="packageMetadata.licenseUrl" target="_blank">{{ + packageMetadata.licenseUrl }}</gl-link> </template> </gl-sprintf> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue index 6534eef532c..ef35349c228 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue @@ -13,7 +13,7 @@ export default { GlSprintf, }, props: { - packageEntity: { + packageMetadata: { type: Object, required: true, }, @@ -26,7 +26,7 @@ export default { <details-row icon="information-o" padding="gl-p-4" data-testid="pypi-required-python"> <gl-sprintf :message="$options.i18n.requiredPython"> <template #pythonVersion> - <strong>{{ packageEntity.metadata.requiredPython }}</strong> + <strong>{{ packageMetadata.requiredPython }}</strong> </template> </gl-sprintf> </details-row> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue index 8b3d781b2df..96b82a20364 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue @@ -1,7 +1,7 @@ <script> -import { GlLink, GlSprintf } from '@gitlab/ui'; +import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; import { first } from 'lodash'; -import createFlash from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { truncateSha } from '~/lib/utils/text_utility'; import { s__, n__ } from '~/locale'; @@ -27,8 +27,10 @@ export default { combinedUpdateText: s__( 'PackageRegistry|Package updated by commit %{link} on branch %{branch}, built by pipeline %{pipeline}, and published to the registry %{datetime}', ), + fetchPackagePipelinesErrorMessage: FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE, }, components: { + GlAlert, GlLink, GlSprintf, HistoryItem, @@ -54,15 +56,16 @@ export default { update(data) { return data.package?.pipelines?.nodes || []; }, - error() { - createFlash({ message: FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE }); + error(error) { + this.fetchPackagePipelinesError = true; + Sentry.captureException(error); }, }, }, data() { return { pipelines: [], - showDescription: false, + fetchPackagePipelinesError: false, }; }, computed: { @@ -109,6 +112,13 @@ export default { <template> <div class="issuable-discussion"> <h3 class="gl-font-lg" data-testid="title">{{ __('History') }}</h3> + <gl-alert + v-if="fetchPackagePipelinesError" + variant="danger" + @dismiss="fetchPackagePipelinesError = false" + > + {{ $options.i18n.fetchPackagePipelinesErrorMessage }} + </gl-alert> <package-history-loader v-if="isLoading" /> <ul v-else class="timeline main-notes-list notes gl-mb-4" data-testid="timeline"> <history-item icon="clock" data-testid="created-on"> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js index 65930d94f3e..3c090951b7d 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js +++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js @@ -75,6 +75,9 @@ export const FETCH_PACKAGE_DETAILS_ERROR_MESSAGE = s__( export const FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE = s__( 'PackageRegistry|Something went wrong while fetching the package history.', ); +export const FETCH_PACKAGE_METADATA_ERROR_MESSAGE = s__( + 'PackageRegistry|Something went wrong while fetching the package metadata.', +); export const DELETE_PACKAGE_SUCCESS_MESSAGE = s__('PackageRegistry|Package deleted successfully'); export const PACKAGE_REGISTRY_TITLE = __('Package Registry'); diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql index 651bd2b4d63..5574020c9e4 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql @@ -27,6 +27,17 @@ query getPackageDetails($id: PackagesPackageID!) { name } } + pipelines(first: 1) { + nodes { + ref + id + project { + id + name + webUrl + } + } + } packageFiles(first: 100) { nodes { id @@ -72,37 +83,15 @@ query getPackageDetails($id: PackagesPackageID!) { } } metadata { - ... on ComposerMetadata { - targetSha - composerJson { - license - version - } - } - ... on PypiMetadata { - id - requiredPython - } - ... on ConanMetadata { - id - packageChannel - packageUsername - recipe - recipePath - } ... on MavenMetadata { id appName appGroup appVersion - path } - ... on NugetMetadata { id iconUrl - licenseUrl - projectUrl } } } diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql new file mode 100644 index 00000000000..fc8b39b37ab --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql @@ -0,0 +1,39 @@ +query getPackageMetadata($id: PackagesPackageID!) { + package(id: $id) { + id + packageType + metadata { + ... on ComposerMetadata { + targetSha + composerJson { + license + version + } + } + ... on PypiMetadata { + id + requiredPython + } + ... on ConanMetadata { + id + packageChannel + packageUsername + recipe + recipePath + } + ... on MavenMetadata { + id + appName + appGroup + appVersion + path + } + ... on NugetMetadata { + id + iconUrl + licenseUrl + projectUrl + } + } + } +} diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue index 162b420a784..768c8d6478b 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue @@ -27,6 +27,9 @@ import DeletePackage from '~/packages_and_registries/package_registry/components import { PACKAGE_TYPE_NUGET, PACKAGE_TYPE_COMPOSER, + PACKAGE_TYPE_CONAN, + PACKAGE_TYPE_MAVEN, + PACKAGE_TYPE_PYPI, DELETE_PACKAGE_TRACKING_ACTION, REQUEST_DELETE_PACKAGE_TRACKING_ACTION, CANCEL_DELETE_PACKAGE_TRACKING_ACTION, @@ -122,6 +125,9 @@ export default { packageFiles() { return this.packageEntity.packageFiles?.nodes; }, + packageType() { + return this.packageEntity.packageType; + }, isLoading() { return this.$apollo.queries.packageEntity.loading; }, @@ -130,7 +136,7 @@ export default { }, tracking() { return { - category: packageTypeToTrackCategory(this.packageEntity.packageType), + category: packageTypeToTrackCategory(this.packageType), }; }, hasVersions() { @@ -140,10 +146,19 @@ export default { return this.packageEntity.dependencyLinks?.nodes || []; }, showDependencies() { - return this.packageEntity.packageType === PACKAGE_TYPE_NUGET; + return this.packageType === PACKAGE_TYPE_NUGET; }, showFiles() { - return this.packageEntity.packageType !== PACKAGE_TYPE_COMPOSER; + return this.packageType !== PACKAGE_TYPE_COMPOSER; + }, + showMetadata() { + return [ + PACKAGE_TYPE_COMPOSER, + PACKAGE_TYPE_CONAN, + PACKAGE_TYPE_MAVEN, + PACKAGE_TYPE_NUGET, + PACKAGE_TYPE_PYPI, + ].includes(this.packageType); }, }, methods: { @@ -262,7 +277,11 @@ export default { <installation-commands :package-entity="packageEntity" /> - <additional-metadata :package-entity="packageEntity" /> + <additional-metadata + v-if="showMetadata" + :package-id="packageEntity.id" + :package-type="packageType" + /> </div> <package-files diff --git a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue index 5ecacb84d65..ccb449f96e1 100644 --- a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue +++ b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue @@ -12,6 +12,7 @@ import { import { toSafeInteger } from 'lodash'; import csrf from '~/lib/utils/csrf'; import { __, n__, s__, sprintf } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import SignupCheckbox from './signup_checkbox.vue'; const DENYLIST_TYPE_RAW = 'raw'; @@ -31,7 +32,12 @@ export default { GlLink, SignupCheckbox, GlModal, + PasswordComplexityCheckboxGroup: () => + import( + 'ee_component/pages/admin/application_settings/general/components/password_complexity_checkbox_group.vue' + ), }, + mixins: [glFeatureFlagMixin()], inject: [ 'host', 'settingsPath', @@ -178,6 +184,9 @@ export default { this.submitForm(); }, + setPasswordComplexity({ name, value }) { + this.$set(this.form, name, value); + }, submitForm() { this.$refs.form.submit(); }, @@ -291,9 +300,7 @@ export default { <template #description> <gl-sprintf :message=" - s__( - 'ApplicationSettings|See GitLab\'s %{linkStart}Password Policy Guidelines%{linkEnd}.', - ) + s__('ApplicationSettings|See %{linkStart}password policy guidelines%{linkEnd}.') " > <template #link="{ content }"> @@ -305,6 +312,10 @@ export default { </template> </gl-form-group> + <password-complexity-checkbox-group + v-if="glFeatures.passwordComplexity" + @set-password-complexity="setPasswordComplexity" + /> <gl-form-group :description="$options.i18n.domainAllowListDescription" :label="$options.i18n.domainAllowListLabel" diff --git a/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js b/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js index a50d8de0e88..0d5c55cb87b 100644 --- a/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js +++ b/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js @@ -18,6 +18,10 @@ export default function initSignupRestrictions(elementSelector = '#js-signup-for 'domainDenylistEnabled', 'denylistTypeRawSelected', 'emailRestrictionsEnabled', + 'passwordNumberRequired', + 'passwordLowercaseRequired', + 'passwordUppercaseRequired', + 'passwordSymbolRequired', ], }); diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue index be7a89c2869..ef99d540c86 100644 --- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue +++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue @@ -274,7 +274,7 @@ export default { <template #collapsed> <div v-gl-tooltip.viewport.left :title="dateLabel" class="sidebar-collapsed-icon"> <gl-icon :size="16" name="calendar" /> - <span class="collapse-truncated-title">{{ formattedDate }}</span> + <span class="gl-pt-2 gl-px-3 gl-font-sm">{{ formattedDate }}</span> </div> <sidebar-inherit-date v-if="canInherit && !initialLoading" diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index 77e41648e9b..b8804de653f 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -99,7 +99,9 @@ export default { > <gl-icon name="users" /> <gl-loading-icon v-if="loading" size="sm" /> - <span v-else data-testid="collapsed-count"> {{ participantCount }} </span> + <span v-else data-testid="collapsed-count" class="gl-pt-2 gl-px-3 gl-font-sm"> + {{ participantCount }} + </span> </div> <div v-if="showParticipantLabel" diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue index 897cab45fe4..3d8a2cd847c 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue @@ -322,7 +322,7 @@ export default { class="sidebar-collapsed-icon" > <gl-icon :aria-label="attributeTypeTitle" :name="attributeTypeIcon" /> - <span class="collapse-truncated-title"> + <span class="collapse-truncated-title gl-pt-2 gl-px-3 gl-font-sm"> {{ attributeTitle }} </span> </div> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue index 7b67c34ded6..465f971717f 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue @@ -58,7 +58,7 @@ export default { } else if (this.showEstimateOnlyState || this.showSpentOnlyState) { return 'bold'; } else if (this.showNoTimeTrackingState) { - return 'no-value'; + return 'no-value collapse-truncated-title gl-pt-2 gl-px-3 gl-font-sm'; } return ''; diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue index 914d146fcc3..178c57a5666 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue @@ -126,7 +126,7 @@ export default { <div class="gl-display-flex gl-align-items-center"> <span :style="{ backgroundColor: label.color }" - class="gl-display-inline-block mr-2 p-2" + class="gl-display-inline-block gl-mr-3 gl-p-3" ></span> <div>{{ getLabelName(label) }}</div> </div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue index 57ee816c4c7..57e3ee4aaa5 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue @@ -92,7 +92,9 @@ export default { @click="handleCollapsedClick" > <gl-icon name="labels" /> - <span class="gl-font-base gl-line-height-24">{{ selectedLabels.length }}</span> + <span class="collapse-truncated-title gl-pt-2 gl-px-3 gl-font-sm">{{ + selectedLabels.length + }}</span> </div> <span v-if="!selectedLabels.length" diff --git a/app/assets/javascripts/work_items/components/item_state.vue b/app/assets/javascripts/work_items/components/item_state.vue index 0b6c1a75bb2..69670d3471c 100644 --- a/app/assets/javascripts/work_items/components/item_state.vue +++ b/app/assets/javascripts/work_items/components/item_state.vue @@ -49,14 +49,28 @@ export default { </script> <template> - <gl-form-group :label="$options.i18n.status" :label-for="$options.labelId"> + <gl-form-group + :label="$options.i18n.status" + :label-for="$options.labelId" + label-cols="3" + label-cols-lg="2" + label-class="gl-pb-0!" + class="gl-align-items-center" + > <gl-form-select :id="$options.labelId" :value="state" :options="$options.states" :disabled="loading" - class="gl-w-auto" + class="gl-w-auto hide-select-decoration" @change="setState" /> </gl-form-group> </template> + +<style> +.hide-select-decoration:not(:focus, :hover) { + background-image: none; + box-shadow: none; +} +</style> diff --git a/app/assets/javascripts/work_items/components/item_title.vue b/app/assets/javascripts/work_items/components/item_title.vue index 232510b108d..ce2fa158596 100644 --- a/app/assets/javascripts/work_items/components/item_title.vue +++ b/app/assets/javascripts/work_items/components/item_title.vue @@ -40,18 +40,18 @@ export default { <template> <h2 - class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5 gl-display-inline-block" + class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5 gl-w-full" :class="{ 'gl-cursor-not-allowed': disabled }" aria-labelledby="item-title" > - <span + <div id="item-title" ref="titleEl" role="textbox" :aria-label="__('Title')" :data-placeholder="placeholder" :contenteditable="!disabled" - class="gl-pseudo-placeholder" + class="gl-pseudo-placeholder gl-px-4 gl-py-3 gl-ml-n4 gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base" @blur="handleBlur" @keyup="handleInput" @keydown.enter.exact="handleSubmit" @@ -59,7 +59,8 @@ export default { @keydown.meta.u.prevent @keydown.ctrl.b.prevent @keydown.meta.b.prevent - >{{ title }}</span > + {{ title }} + </div> </h2> </template> diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index d9e92ead86c..da8fe222b6a 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -115,7 +115,7 @@ export default { </gl-skeleton-loader> </div> <template v-else> - <div class="gl-display-flex"> + <div class="gl-display-flex gl-align-items-start"> <work-item-title :work-item-id="workItem.id" :work-item-title="workItem.title" @@ -127,7 +127,7 @@ export default { <work-item-actions :work-item-id="workItem.id" :can-delete="canDelete" - class="gl-ml-auto gl-mt-5" + class="gl-ml-auto gl-mt-6" @deleteWorkItem="$emit('deleteWorkItem')" @error="error = $event" /> diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue index 425b8290d44..d1c8022ac57 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue @@ -98,7 +98,15 @@ export default { </script> <template> - <gl-modal ref="modal" hide-footer size="lg" modal-id="work-item-detail-modal" @hide="closeModal"> + <gl-modal + ref="modal" + hide-footer + size="lg" + modal-id="work-item-detail-modal" + header-class="gl-p-0 gl-pb-2!" + body-class="gl-pb-6!" + @hide="closeModal" + > <gl-alert v-if="error" variant="danger" @dismiss="error = false"> {{ error }} </gl-alert> @@ -106,6 +114,7 @@ export default { <work-item-detail :work-item-parent-id="issueGid" :work-item-id="workItemId" + class="gl-p-5 gl-mt-n3" @deleteWorkItem="deleteWorkItem" /> </gl-modal> @@ -114,7 +123,7 @@ export default { <style> /* hide the existing modal header */ -#work-item-detail-modal .modal-header { +#work-item-detail-modal .modal-header * { display: none; } </style> diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss index 904d041fdc9..47471310ef3 100644 --- a/app/assets/stylesheets/framework/diffs.scss +++ b/app/assets/stylesheets/framework/diffs.scss @@ -82,7 +82,7 @@ } &:hover { - background-color: $gray-normal; + background-color: $gray-10; } svg { diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss index 36f1b1f2903..0a01b14a37d 100644 --- a/app/assets/stylesheets/framework/feature_highlight.scss +++ b/app/assets/stylesheets/framework/feature_highlight.scss @@ -27,7 +27,7 @@ background-color: $indigo-50; border-top-left-radius: 2px; border-top-right-radius: 2px; - border-bottom: 1px solid darken($gray-normal, 8%); + border-bottom: 1px solid darken($gray-10, 8%); } .feature-highlight-popover { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index ced62926218..cbecae24031 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -464,7 +464,7 @@ float: left; margin-right: 5px; border-radius: 50%; - border: 1px solid $gray-normal; + border: 1px solid $gray-10; } .notification-dot { diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 2cea3b96ff7..0d1196851e0 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -452,7 +452,7 @@ @mixin avatar-counter($border-radius: 1em) { background-color: $gray-darkest; color: $white; - border: 1px solid $gray-normal; + border: 1px solid $gray-10; border-radius: $border-radius; font-family: $regular-font; font-size: 9px; diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss index f57d906e73c..98808c25c8e 100644 --- a/app/assets/stylesheets/framework/responsive_tables.scss +++ b/app/assets/stylesheets/framework/responsive_tables.scss @@ -95,7 +95,7 @@ display: block; align-self: stretch; min-height: 0; - background-color: $gray-normal; + background-color: $gray-10; border-top: 1px solid $border-color; .table-action-buttons { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index c9bc1c9189e..76c1f4c2093 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -92,7 +92,6 @@ $white-transparent: rgba($white, 0.8) !default; $gray-lightest: #fdfdfd !default; $gray-light: #fafafa !default; $gray-lighter: #f9f9f9 !default; -$gray-normal: #f5f5f5 !default; $gray-dark: darken($gray-light, $darken-dark-factor) !default; $gray-darker: #eee !default; $gray-darkest: #c4c4c4 !default; @@ -169,7 +168,7 @@ $purple-800: #453894 !default; $purple-900: #2f2a6b !default; $purple-950: #232150 !default; -$gray-10: #fafafa !default; +$gray-10: #f5f5f5 !default; $gray-50: #f0f0f0 !default; $gray-100: #dbdbdb !default; $gray-200: #bfbfbf !default; @@ -351,13 +350,13 @@ $border-white-light: darken($white, $darken-border-factor) !default; $border-white-normal: darken($white-normal, $darken-border-factor) !default; $border-gray-light: darken($gray-light, $darken-border-factor); -$border-gray-normal: darken($gray-normal, $darken-border-factor); -$border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor); +$border-gray-normal: darken($gray-10, $darken-border-factor); +$border-gray-normal-dashed: darken($gray-10, $darken-border-dashed-factor); /* * UI elements */ -$contextual-sidebar-bg-color: #f5f5f5; +$contextual-sidebar-bg-color: $gray-10; $contextual-sidebar-border-color: #e9e9e9; $border-color: $gray-100; $shadow-color: $t-gray-a-08; diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss index cfd215b81b8..3885a5c835d 100644 --- a/app/assets/stylesheets/framework/wells.scss +++ b/app/assets/stylesheets/framework/wells.scss @@ -76,7 +76,7 @@ } .dark-well { - background-color: $gray-normal; + background-color: $gray-10; } .card.card-body-centered { diff --git a/app/assets/stylesheets/highlight/conflict_colors.scss b/app/assets/stylesheets/highlight/conflict_colors.scss index 910dc6f4ad6..d5e278ae4b4 100644 --- a/app/assets/stylesheets/highlight/conflict_colors.scss +++ b/app/assets/stylesheets/highlight/conflict_colors.scss @@ -95,17 +95,17 @@ $conflict-colors: ( solarized_dark_header_not_chosen : rgba(#839496, 0.25), solarized_dark_line_not_chosen : rgba(#839496, 0.15), - none_header_head_neutral : $gray-normal, - none_line_head_neutral : $gray-normal, - none_button_head_neutral : $gray-normal, + none_header_head_neutral : $gray-10, + none_line_head_neutral : $gray-10, + none_button_head_neutral : $gray-10, none_header_head_chosen : $gray-darker, none_line_head_chosen : $gray-darker, none_button_head_chosen : $gray-darker, - none_header_origin_neutral : $gray-normal, - none_line_origin_neutral : $gray-normal, - none_button_origin_neutral : $gray-normal, + none_header_origin_neutral : $gray-10, + none_line_origin_neutral : $gray-10, + none_button_origin_neutral : $gray-10, none_header_origin_chosen : $gray-darker, none_line_origin_chosen : $gray-darker, diff --git a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss index 25a565ce2ba..b13d7997769 100644 --- a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss +++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss @@ -140,7 +140,7 @@ } hr { - border-color: var(--ide-border-color, darken($gray-normal, 8%)); + border-color: var(--ide-border-color, darken($gray-10, 8%)); } .md h1, diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index a4a82fdcef3..10d5ce54573 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -93,7 +93,7 @@ $ide-commit-header-height: 48px; display: flex; align-items: center; padding: $grid-size $gl-padding; - background-color: var(--ide-background-hover, $gray-normal); + background-color: var(--ide-background-hover, $gray-10); border-right: 1px solid var(--ide-border-color, $white-dark); border-bottom: 1px solid var(--ide-border-color, $white-dark); @@ -135,7 +135,7 @@ $ide-commit-header-height: 48px; box-shadow: none !important; font-weight: normal !important; - background-color: var(--ide-background-hover, $gray-normal); + background-color: var(--ide-background-hover, $gray-10); border-right: 1px solid var(--ide-border-color, $white-dark); border-bottom: 1px solid var(--ide-border-color, $white-dark); diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index c177d0b74a2..e8519157d98 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -5,7 +5,7 @@ border-left: 1px solid $border-color; border-bottom: 0; border-radius: $border-radius-small $border-radius-small 0 0; - background: $gray-normal; + background: $gray-10; } #editor, diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 04e0ef6631e..10a81365a1b 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -17,14 +17,14 @@ .issue-token:hover &, .issue-token-link:focus > & { - background-color: $gray-normal; + background-color: $gray-10; color: $blue-800; text-decoration: none; } } .issue-token-title { - background-color: $gray-normal; + background-color: $gray-10; transition: background $general-hover-transition-duration $general-hover-transition-curve; .issue-token:hover &, @@ -34,7 +34,7 @@ } .issue-token-remove-button { - background-color: $gray-normal; + background-color: $gray-10; transition: background $general-hover-transition-duration $general-hover-transition-curve; &:hover, diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 8755db83d35..b74166fd723 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -276,7 +276,7 @@ input[type='checkbox']:hover { width: $search-avatar-size; height: $search-avatar-size; border-radius: 50%; - border: 1px solid $gray-normal; + border: 1px solid $gray-10; } } diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index 4cefa60b12a..b958418e916 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -40,7 +40,7 @@ body { line-height: 1.5; color: #fafafa; text-align: left; - background-color: #1f1f1f; + background-color: #333; } ul { margin-top: 0; @@ -430,7 +430,7 @@ a.gl-badge.badge-warning:active { .gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only, .gl-form-input.form-control:disabled, .gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only { - background-color: #1f1f1f; + background-color: #333; box-shadow: inset 0 0 0 1px #404040; } .gl-form-input:disabled, @@ -1034,7 +1034,7 @@ input { z-index: 600; width: 256px; top: var(--header-height, 48px); - background-color: #f5f5f5; + background-color: #333; border-right: 1px solid #e9e9e9; transform: translate3d(0, 0, 0); } @@ -1402,7 +1402,7 @@ input { color: #999; display: flex; align-items: center; - background-color: #f5f5f5; + background-color: #333; position: fixed; bottom: 0; width: 255px; @@ -1698,7 +1698,7 @@ svg.s16 { border-radius: 4px; } body.gl-dark { - --gray-10: #1f1f1f; + --gray-10: #333; --gray-50: #303030; --gray-100: #404040; --gray-200: #525252; @@ -1939,7 +1939,7 @@ body.gl-dark .navbar-gitlab .search form .search-input { } body.gl-dark { - --gray-10: #1f1f1f; + --gray-10: #333; --gray-50: #303030; --gray-100: #404040; --gray-200: #525252; diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index 6753f0cb46b..cb3c97f18a3 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -415,7 +415,7 @@ a.gl-badge.badge-warning:active { .gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only, .gl-form-input.form-control:disabled, .gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only { - background-color: #fafafa; + background-color: #f5f5f5; box-shadow: inset 0 0 0 1px #dbdbdb; } .gl-form-input:disabled, diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss index 7ac1a187bb8..3090edfb123 100644 --- a/app/assets/stylesheets/startup/startup-signin.scss +++ b/app/assets/stylesheets/startup/startup-signin.scss @@ -297,7 +297,7 @@ fieldset:disabled a.btn { .gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only, .gl-form-input.form-control:disabled, .gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only { - background-color: #fafafa; + background-color: #f5f5f5; box-shadow: inset 0 0 0 1px #dbdbdb; } .gl-form-input:disabled, diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss index fe8a5aec1b3..ff36257e3cf 100644 --- a/app/assets/stylesheets/themes/_dark.scss +++ b/app/assets/stylesheets/themes/_dark.scss @@ -86,7 +86,7 @@ $purple-950: #f4f0ff; $gray-lightest: #222; $gray-light: $gray-50; $gray-lighter: #303030; -$gray-normal: #333; +$gray-10: #333; $gray-dark: $gray-100; $gray-darker: #4f4f4f; $gray-darkest: #c4c4c4; diff --git a/app/finders/crm/contacts_finder.rb b/app/finders/crm/contacts_finder.rb index f7c2625f788..29f3d6f0f16 100644 --- a/app/finders/crm/contacts_finder.rb +++ b/app/finders/crm/contacts_finder.rb @@ -8,6 +8,7 @@ # group: Group, required # search: String, optional # state: CustomerRelations::ContactStateEnum, optional +# ids: int[], optional module Crm class ContactsFinder include Gitlab::Allowable @@ -24,6 +25,7 @@ module Crm return CustomerRelations::Contact.none unless root_group contacts = root_group.contacts + contacts = by_ids(contacts) contacts = by_state(contacts) contacts = by_search(contacts) contacts.sort_by_name @@ -53,6 +55,12 @@ module Crm contacts.search_by_state(params[:state]) end + def by_ids(contacts) + return contacts unless ids? + + contacts.id_in(params[:ids]) + end + def search? params[:search].present? end @@ -60,5 +68,9 @@ module Crm def state? params[:state].present? end + + def ids? + params[:ids].present? + end end end diff --git a/app/finders/crm/organizations_finder.rb b/app/finders/crm/organizations_finder.rb index 1a3df05aa11..5a8ab148ef3 100644 --- a/app/finders/crm/organizations_finder.rb +++ b/app/finders/crm/organizations_finder.rb @@ -8,6 +8,7 @@ # group: Group, required # search: String, optional # state: CustomerRelations::OrganizationStateEnum, optional +# ids: int[], optional module Crm class OrganizationsFinder include Gitlab::Allowable @@ -24,6 +25,7 @@ module Crm return CustomerRelations::Organization.none unless root_group organizations = root_group.organizations + organizations = by_ids(organizations) organizations = by_search(organizations) organizations = by_state(organizations) organizations.sort_by_name @@ -53,6 +55,12 @@ module Crm organizations.search_by_state(params[:state]) end + def by_ids(organizations) + return organizations unless ids? + + organizations.id_in(params[:ids]) + end + def search? params[:search].present? end @@ -60,5 +68,9 @@ module Crm def state? params[:state].present? end + + def ids? + params[:ids].present? + end end end diff --git a/app/graphql/mutations/incident_management/timeline_event/create.rb b/app/graphql/mutations/incident_management/timeline_event/create.rb index cbc708a2530..1907954cada 100644 --- a/app/graphql/mutations/incident_management/timeline_event/create.rb +++ b/app/graphql/mutations/incident_management/timeline_event/create.rb @@ -23,7 +23,9 @@ module Mutations authorize!(incident) - response ::IncidentManagement::TimelineEvents::CreateService.new(incident, current_user, args).execute + response ::IncidentManagement::TimelineEvents::CreateService.new( + incident, current_user, args.merge(editable: true) + ).execute end private diff --git a/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb b/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb index 73a20b8a380..31ae29d896b 100644 --- a/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb +++ b/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb @@ -21,7 +21,8 @@ module Mutations current_user, promoted_from_note: note, note: note.note, - occurred_at: note.created_at + occurred_at: note.created_at, + editable: true ).execute end diff --git a/app/graphql/resolvers/crm/contacts_resolver.rb b/app/graphql/resolvers/crm/contacts_resolver.rb index 9e235669657..58d0e2ce13d 100644 --- a/app/graphql/resolvers/crm/contacts_resolver.rb +++ b/app/graphql/resolvers/crm/contacts_resolver.rb @@ -4,6 +4,7 @@ module Resolvers module Crm class ContactsResolver < BaseResolver include Gitlab::Graphql::Authorize::AuthorizeResource + include ResolvesIds authorize :read_crm_contact @@ -17,7 +18,13 @@ module Resolvers required: false, description: 'State of the contacts to search for.' + argument :ids, [::Types::GlobalIDType[CustomerRelations::Contact]], + required: false, + description: 'Filter contacts by IDs.' + def resolve(**args) + args[:ids] = resolve_ids(args.delete(:ids)) + ::Crm::ContactsFinder.new(current_user, { group: group }.merge(args)).execute end diff --git a/app/graphql/resolvers/crm/organizations_resolver.rb b/app/graphql/resolvers/crm/organizations_resolver.rb index ef0c930a94a..ca0a908ee22 100644 --- a/app/graphql/resolvers/crm/organizations_resolver.rb +++ b/app/graphql/resolvers/crm/organizations_resolver.rb @@ -4,6 +4,7 @@ module Resolvers module Crm class OrganizationsResolver < BaseResolver include Gitlab::Graphql::Authorize::AuthorizeResource + include ResolvesIds authorize :read_crm_organization @@ -17,7 +18,13 @@ module Resolvers required: false, description: 'State of the organization to search for.' + argument :ids, [Types::GlobalIDType[CustomerRelations::Organization]], + required: false, + description: 'Filter organizations by IDs.' + def resolve(**args) + args[:ids] = resolve_ids(args.delete(:ids)) + ::Crm::OrganizationsFinder.new(current_user, { group: group }.merge(args)).execute end diff --git a/app/models/projects/build_artifacts_size_refresh.rb b/app/models/projects/build_artifacts_size_refresh.rb index c92898068c3..dee4afdefa6 100644 --- a/app/models/projects/build_artifacts_size_refresh.rb +++ b/app/models/projects/build_artifacts_size_refresh.rb @@ -50,6 +50,7 @@ module Projects scope :stale, -> { with_state(:running).where('updated_at < ?', STALE_WINDOW.ago) } scope :remaining, -> { with_state(:created, :pending).or(stale) } + scope :processing_queue, -> { remaining.order(state: :desc) } def self.enqueue_refresh(projects) now = Time.zone.now @@ -65,8 +66,7 @@ module Projects next_refresh = nil transaction do - next_refresh = remaining - .order(:state, :updated_at) + next_refresh = processing_queue .lock('FOR UPDATE SKIP LOCKED') .take diff --git a/app/models/release.rb b/app/models/release.rb index 655a2ba4709..ee5d7bab190 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -31,6 +31,7 @@ class Release < ApplicationRecord validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, if: :description_changed? validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") } validates :links, nested_attributes_duplicates: { scope: :release, child_attributes: %i[name url filepath] } + validates :author_id, presence: true, on: :create, if: :validate_release_with_author? scope :sorted, -> { order(released_at: :desc) } scope :preloaded, -> { @@ -117,6 +118,10 @@ class Release < ApplicationRecord end end + def validate_release_with_author? + Feature.enabled?(:validate_release_with_author, self.project) + end + def set_released_at self.released_at ||= created_at end diff --git a/app/services/incident_management/timeline_events/create_service.rb b/app/services/incident_management/timeline_events/create_service.rb index 7d287e1bd82..3a7e8d2509d 100644 --- a/app/services/incident_management/timeline_events/create_service.rb +++ b/app/services/incident_management/timeline_events/create_service.rb @@ -3,6 +3,7 @@ module IncidentManagement module TimelineEvents DEFAULT_ACTION = 'comment' + DEFAULT_EDITABLE = false class CreateService < TimelineEvents::BaseService def initialize(incident, user, params) @@ -23,7 +24,8 @@ module IncidentManagement action: params.fetch(:action, DEFAULT_ACTION), note_html: params[:note_html].presence || params[:note], occurred_at: params[:occurred_at], - promoted_from_note: params[:promoted_from_note] + promoted_from_note: params[:promoted_from_note], + editable: params.fetch(:editable, DEFAULT_EDITABLE) } timeline_event = IncidentManagement::TimelineEvent.new(timeline_event_params) diff --git a/app/services/incident_management/timeline_events/update_service.rb b/app/services/incident_management/timeline_events/update_service.rb index fe8b4879561..347c6cd3e05 100644 --- a/app/services/incident_management/timeline_events/update_service.rb +++ b/app/services/incident_management/timeline_events/update_service.rb @@ -17,6 +17,7 @@ module IncidentManagement end def execute + return error_non_editable unless timeline_event.editable? return error_no_permissions unless allowed? if timeline_event.update(update_params) @@ -56,6 +57,10 @@ module IncidentManagement :none end + + def error_non_editable + error(_('You cannot edit this timeline event.')) + end end end end diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml index db13a7f7ba4..a749fbd1eec 100644 --- a/app/views/profiles/keys/_form.html.haml +++ b/app/views/profiles/keys/_form.html.haml @@ -1,7 +1,7 @@ - max_date = ::Gitlab::CurrentSettings.max_ssh_key_lifetime_from_now.to_date if ssh_key_expiration_policy_enabled? %div = form_for [:profile, @key], html: { class: 'js-requires-input' } do |f| - = form_errors(@key) + = form_errors(@key, pajamas_alert: true) .form-group = f.label :key, s_('Profiles|Key'), class: 'label-bold' diff --git a/config/feature_flags/development/validate_release_with_author.yml b/config/feature_flags/development/validate_release_with_author.yml new file mode 100644 index 00000000000..45102103183 --- /dev/null +++ b/config/feature_flags/development/validate_release_with_author.yml @@ -0,0 +1,8 @@ +--- +name: validate_release_with_author +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89694 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/365350 +milestone: '15.1' +type: development +group: group::release +default_enabled: false diff --git a/config/metrics/counts_28d/20210216174910_analytics_unique_visits_for_any_target_monthly.yml b/config/metrics/counts_28d/20210216174910_analytics_unique_visits_for_any_target_monthly.yml index f1bff756c5f..791cad7b6e2 100644 --- a/config/metrics/counts_28d/20210216174910_analytics_unique_visits_for_any_target_monthly.yml +++ b/config/metrics/counts_28d/20210216174910_analytics_unique_visits_for_any_target_monthly.yml @@ -10,6 +10,29 @@ value_type: number status: active time_frame: 28d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - users_viewing_analytics_group_devops_adoption + - i_analytics_dev_ops_adoption + - i_analytics_dev_ops_score + - p_analytics_merge_request + - i_analytics_instance_statistics + - g_analytics_contribution + - g_analytics_insights + - g_analytics_issues + - g_analytics_productivity + - g_analytics_valuestream + - p_analytics_pipelines + - p_analytics_code_reviews + - p_analytics_valuestream + - p_analytics_insights + - p_analytics_issues + - p_analytics_repo + - i_analytics_cohorts + - p_analytics_ci_cd_pipelines + - p_analytics_ci_cd_deployment_frequency + - p_analytics_ci_cd_lead_time distribution: - ce - ee diff --git a/config/metrics/counts_all/20210216174846_p_analytics_pipelines.yml b/config/metrics/counts_7d/20210216174846_p_analytics_pipelines.yml index c34f5644053..8c491bc24b1 100644 --- a/config/metrics/counts_all/20210216174846_p_analytics_pipelines.yml +++ b/config/metrics/counts_7d/20210216174846_p_analytics_pipelines.yml @@ -8,8 +8,12 @@ product_group: group::optimize product_category: value_type: number status: active -time_frame: all +time_frame: 7d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - p_analytics_pipelines distribution: - ce - ee diff --git a/config/metrics/counts_all/20210216174850_p_analytics_valuestream.yml b/config/metrics/counts_7d/20210216174850_p_analytics_valuestream.yml index 671cd9a10b3..0cb77ed4933 100644 --- a/config/metrics/counts_all/20210216174850_p_analytics_valuestream.yml +++ b/config/metrics/counts_7d/20210216174850_p_analytics_valuestream.yml @@ -8,8 +8,12 @@ product_group: group::optimize product_category: value_type: number status: active -time_frame: all +time_frame: 7d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - p_analytics_valuestream distribution: - ce - ee diff --git a/config/metrics/counts_all/20210216174856_p_analytics_repo.yml b/config/metrics/counts_7d/20210216174856_p_analytics_repo.yml index 5e6e308d8d2..fc1fb3470db 100644 --- a/config/metrics/counts_all/20210216174856_p_analytics_repo.yml +++ b/config/metrics/counts_7d/20210216174856_p_analytics_repo.yml @@ -8,8 +8,12 @@ product_group: group::optimize product_category: value_type: number status: active -time_frame: all +time_frame: 7d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - p_analytics_repo distribution: - ce - ee diff --git a/config/metrics/counts_all/20210216174858_i_analytics_cohorts.yml b/config/metrics/counts_7d/20210216174858_i_analytics_cohorts.yml index 9a06f4c44df..705cfefc5b5 100644 --- a/config/metrics/counts_all/20210216174858_i_analytics_cohorts.yml +++ b/config/metrics/counts_7d/20210216174858_i_analytics_cohorts.yml @@ -8,8 +8,12 @@ product_group: group::optimize product_category: value_type: number status: active -time_frame: all +time_frame: 7d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - i_analytics_cohorts distribution: - ce - ee diff --git a/config/metrics/counts_all/20210216174900_i_analytics_dev_ops_score.yml b/config/metrics/counts_7d/20210216174900_i_analytics_dev_ops_score.yml index 9130eb837e7..72db511d1e7 100644 --- a/config/metrics/counts_all/20210216174900_i_analytics_dev_ops_score.yml +++ b/config/metrics/counts_7d/20210216174900_i_analytics_dev_ops_score.yml @@ -8,8 +8,12 @@ product_group: group::optimize product_category: value_type: number status: active -time_frame: all +time_frame: 7d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - i_analytics_dev_ops_score distribution: - ce - ee diff --git a/config/metrics/counts_all/20210216174902_g_analytics_merge_request.yml b/config/metrics/counts_7d/20210216174902_g_analytics_merge_request.yml index af9338f028a..0e726cfceae 100644 --- a/config/metrics/counts_all/20210216174902_g_analytics_merge_request.yml +++ b/config/metrics/counts_7d/20210216174902_g_analytics_merge_request.yml @@ -8,8 +8,12 @@ product_group: group::optimize product_category: value_type: number status: removed -time_frame: all +time_frame: 7d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - g_analytics_merge_request distribution: - ce - ee diff --git a/config/metrics/counts_all/20210216174906_i_analytics_instance_statistics.yml b/config/metrics/counts_7d/20210216174906_i_analytics_instance_statistics.yml index 5ee75f27529..06123553499 100644 --- a/config/metrics/counts_all/20210216174906_i_analytics_instance_statistics.yml +++ b/config/metrics/counts_7d/20210216174906_i_analytics_instance_statistics.yml @@ -8,8 +8,12 @@ product_group: group::optimize product_category: value_type: number status: active -time_frame: all +time_frame: 7d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - i_analytics_instance_statistics distribution: - ce - ee diff --git a/config/metrics/counts_7d/20210216174908_analytics_unique_visits_for_any_target.yml b/config/metrics/counts_7d/20210216174908_analytics_unique_visits_for_any_target.yml new file mode 100644 index 00000000000..cdc7ee79752 --- /dev/null +++ b/config/metrics/counts_7d/20210216174908_analytics_unique_visits_for_any_target.yml @@ -0,0 +1,44 @@ +--- +data_category: optional +key_path: analytics_unique_visits.analytics_unique_visits_for_any_target +description: Unique visitors to any analytics feature by week +product_section: dev +product_stage: manage +product_group: group::optimize +product_category: +value_type: number +status: active +time_frame: 7d +data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - users_viewing_analytics_group_devops_adoption + - i_analytics_dev_ops_adoption + - i_analytics_dev_ops_score + - p_analytics_merge_request + - i_analytics_instance_statistics + - g_analytics_contribution + - g_analytics_insights + - g_analytics_issues + - g_analytics_productivity + - g_analytics_valuestream + - p_analytics_pipelines + - p_analytics_code_reviews + - p_analytics_valuestream + - p_analytics_insights + - p_analytics_issues + - p_analytics_repo + - i_analytics_cohorts + - p_analytics_ci_cd_pipelines + - p_analytics_ci_cd_deployment_frequency + - p_analytics_ci_cd_lead_time +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +performance_indicator_type: [] +milestone: "<13.9" diff --git a/config/metrics/counts_all/20210423005644_i_analytics_dev_ops_adoption.yml b/config/metrics/counts_7d/20210423005644_i_analytics_dev_ops_adoption.yml index 7a524dee720..b79b021901c 100644 --- a/config/metrics/counts_all/20210423005644_i_analytics_dev_ops_adoption.yml +++ b/config/metrics/counts_7d/20210423005644_i_analytics_dev_ops_adoption.yml @@ -9,8 +9,12 @@ product_category: value_type: number status: active milestone: "13.11" -time_frame: all +time_frame: 7d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - i_analytics_dev_ops_adoption distribution: - ce - ee diff --git a/config/metrics/counts_all/20210428142406_users_viewing_analytics_group_devops_adoption.yml b/config/metrics/counts_7d/20210428142406_users_viewing_analytics_group_devops_adoption.yml index b8ae877b0be..2f5415c7802 100644 --- a/config/metrics/counts_all/20210428142406_users_viewing_analytics_group_devops_adoption.yml +++ b/config/metrics/counts_7d/20210428142406_users_viewing_analytics_group_devops_adoption.yml @@ -10,8 +10,12 @@ product_category: value_type: number status: active milestone: "13.12" -time_frame: all +time_frame: 7d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - users_viewing_analytics_group_devops_adoption distribution: - ce - ee diff --git a/config/metrics/counts_all/20211126090001_p_analytics_ci_cd_pipelines.yml b/config/metrics/counts_7d/20211126090001_p_analytics_ci_cd_pipelines.yml index 7e6820b0d24..ec686425d7c 100644 --- a/config/metrics/counts_all/20211126090001_p_analytics_ci_cd_pipelines.yml +++ b/config/metrics/counts_7d/20211126090001_p_analytics_ci_cd_pipelines.yml @@ -10,8 +10,12 @@ value_type: number status: active milestone: '14.6' introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75187 -time_frame: all +time_frame: 7d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - p_analytics_ci_cd_pipelines distribution: - ce - ee diff --git a/config/metrics/counts_all/20211126090002_p_analytics_ci_cd_deployment_frequency.yml b/config/metrics/counts_7d/20211126090002_p_analytics_ci_cd_deployment_frequency.yml index 134f43b1b27..bee4b88889e 100644 --- a/config/metrics/counts_all/20211126090002_p_analytics_ci_cd_deployment_frequency.yml +++ b/config/metrics/counts_7d/20211126090002_p_analytics_ci_cd_deployment_frequency.yml @@ -10,8 +10,12 @@ value_type: number status: active milestone: '14.6' introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75187 -time_frame: all +time_frame: 7d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - p_analytics_ci_cd_deployment_frequency distribution: - ce - ee diff --git a/config/metrics/counts_all/20211126090003_p_analytics_ci_cd_lead_time.yml b/config/metrics/counts_7d/20211126090003_p_analytics_ci_cd_lead_time.yml index a167a380432..3c1f6082049 100644 --- a/config/metrics/counts_all/20211126090003_p_analytics_ci_cd_lead_time.yml +++ b/config/metrics/counts_7d/20211126090003_p_analytics_ci_cd_lead_time.yml @@ -10,8 +10,12 @@ value_type: number status: active milestone: '14.6' introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75187 -time_frame: all +time_frame: 7d data_source: redis_hll +instrumentation_class: RedisHLLMetric +options: + events: + - p_analytics_ci_cd_lead_time distribution: - ce - ee diff --git a/config/metrics/counts_all/20210216174908_analytics_unique_visits_for_any_target.yml b/config/metrics/counts_all/20210216174908_analytics_unique_visits_for_any_target.yml deleted file mode 100644 index 488bb48be0e..00000000000 --- a/config/metrics/counts_all/20210216174908_analytics_unique_visits_for_any_target.yml +++ /dev/null @@ -1,21 +0,0 @@ ---- -data_category: optional -key_path: analytics_unique_visits.analytics_unique_visits_for_any_target -description: Unique visitors to any analytics feature by week -product_section: dev -product_stage: manage -product_group: group::optimize -product_category: -value_type: number -status: active -time_frame: all -data_source: redis_hll -distribution: -- ce -- ee -tier: -- free -- premium -- ultimate -performance_indicator_type: [] -milestone: "<13.9" diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 15cf2f4d6c0..62369551055 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -11739,6 +11739,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | +| <a id="groupcontactsids"></a>`ids` | [`[CustomerRelationsContactID!]`](#customerrelationscontactid) | Filter contacts by IDs. | | <a id="groupcontactssearch"></a>`search` | [`String`](#string) | Search term to find contacts with. | | <a id="groupcontactsstate"></a>`state` | [`CustomerRelationsContactState`](#customerrelationscontactstate) | State of the contacts to search for. | @@ -12095,6 +12096,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | +| <a id="grouporganizationsids"></a>`ids` | [`[CustomerRelationsOrganizationID!]`](#customerrelationsorganizationid) | Filter organizations by IDs. | | <a id="grouporganizationssearch"></a>`search` | [`String`](#string) | Search term used to find organizations with. | | <a id="grouporganizationsstate"></a>`state` | [`CustomerRelationsOrganizationState`](#customerrelationsorganizationstate) | State of the organization to search for. | diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md index c50c8aef793..8231cf4316b 100644 --- a/doc/development/i18n/proofreader.md +++ b/doc/development/i18n/proofreader.md @@ -60,6 +60,7 @@ are very appreciative of the work done by translators and proofreaders! - German - Michael Hahnle - [GitLab](https://gitlab.com/mhah), [Crowdin](https://crowdin.com/profile/mhah) - Katrin Leinweber - [GitLab](https://gitlab.com/katrinleinweber), [Crowdin](https://crowdin.com/profile/katrinleinweber) + - Justman10000 - [GitLab](https://gitlab.com/Justman10000), [Crowdin](https://crowdin.com/profile/Justman10000) - Greek - Proofreaders needed. - Hebrew diff --git a/lib/gitlab/analytics/unique_visits.rb b/lib/gitlab/analytics/unique_visits.rb index 3546a7e3ddb..7fcfe60a3e7 100644 --- a/lib/gitlab/analytics/unique_visits.rb +++ b/lib/gitlab/analytics/unique_visits.rb @@ -10,9 +10,7 @@ module Gitlab # @param [ActiveSupport::TimeWithZone] end_date end of time frame # @return [Integer] number of unique visitors def unique_visits_for(targets:, start_date: 7.days.ago, end_date: start_date + 1.week) - events = if targets == :analytics - self.class.analytics_events - elsif targets == :compliance + events = if targets == :compliance self.class.compliance_events else Array(targets) @@ -22,10 +20,6 @@ module Gitlab end class << self - def analytics_events - Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category('analytics') - end - def compliance_events Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category('compliance') end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index f86394971e7..f623e207fb9 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -643,16 +643,6 @@ module Gitlab } end - def analytics_unique_visits_data - results = ::Gitlab::Analytics::UniqueVisits.analytics_events.each_with_object({}) do |target, hash| - hash[target] = redis_usage_data { unique_visit_service.unique_visits_for(targets: target) } - end - results['analytics_unique_visits_for_any_target'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :analytics) } - results['analytics_unique_visits_for_any_target_monthly'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :analytics, **monthly_time_range) } - - { analytics_unique_visits: results } - end - def compliance_unique_visits_data results = ::Gitlab::Analytics::UniqueVisits.compliance_events.each_with_object({}) do |target, hash| hash[target] = redis_usage_data { unique_visit_service.unique_visits_for(targets: target) } @@ -710,7 +700,6 @@ module Gitlab .merge(topology_usage_data) .merge(usage_activity_by_stage) .merge(usage_activity_by_stage(:usage_activity_by_stage_monthly, monthly_time_range_db_params)) - .merge(analytics_unique_visits_data) .merge(compliance_unique_visits_data) .merge(redis_hll_counters) .deep_merge(aggregated_metrics_data) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c15522c123f..d82e155eeaa 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2103,9 +2103,6 @@ msgstr "" msgid "Add a homepage to your wiki that contains information about your project and GitLab will display it here instead of this message." msgstr "" -msgid "Add a horizontal rule" -msgstr "" - msgid "Add a new issue" msgstr "" @@ -2358,9 +2355,6 @@ msgstr "" msgid "Adding new applications is disabled in your GitLab instance. Please contact your GitLab administrator to get the permission" msgstr "" -msgid "Additional Metadata" -msgstr "" - msgid "Additional minutes" msgstr "" @@ -4527,13 +4521,25 @@ msgstr "" msgid "ApplicationSettings|Require admin approval for new sign-ups" msgstr "" +msgid "ApplicationSettings|Require lowercase letters" +msgstr "" + +msgid "ApplicationSettings|Require numbers" +msgstr "" + +msgid "ApplicationSettings|Require symbols" +msgstr "" + +msgid "ApplicationSettings|Require uppercase letters" +msgstr "" + msgid "ApplicationSettings|Restricts sign-ups for email addresses that match the given regex. %{linkStart}What is the supported syntax?%{linkEnd}" msgstr "" msgid "ApplicationSettings|Save changes" msgstr "" -msgid "ApplicationSettings|See GitLab's %{linkStart}Password Policy Guidelines%{linkEnd}." +msgid "ApplicationSettings|See %{linkStart}password policy guidelines%{linkEnd}." msgstr "" msgid "ApplicationSettings|Send confirmation email on sign-up" @@ -4560,6 +4566,18 @@ msgstr "" msgid "ApplicationSettings|Users with e-mail addresses that match these domain(s) cannot sign up. Wildcards allowed. Use separate lines or commas for multiple entries." msgstr "" +msgid "ApplicationSettings|When enabled, new passwords must contain at least one lowercase letter (a-z)." +msgstr "" + +msgid "ApplicationSettings|When enabled, new passwords must contain at least one number (0-9)." +msgstr "" + +msgid "ApplicationSettings|When enabled, new passwords must contain at least one symbol (!@#$%%^&())." +msgstr "" + +msgid "ApplicationSettings|When enabled, new passwords must contain at least one uppercase letter (A-Z)." +msgstr "" + msgid "ApplicationSettings|domain.com" msgstr "" @@ -13173,6 +13191,9 @@ msgstr "" msgid "DevopsReport|Your usage" msgstr "" +msgid "Diagram (%{language})" +msgstr "" + msgid "Did not delete the source branch." msgstr "" @@ -18992,6 +19013,9 @@ msgstr "" msgid "Hook was successfully updated." msgstr "" +msgid "Horizontal rule" +msgstr "" + msgid "Hostname" msgstr "" @@ -24301,6 +24325,9 @@ msgstr "" msgid "Merging immediately isn't recommended as it may negatively impact the existing merge train. Read the %{docsLinkStart}documentation%{docsLinkEnd} for more information." msgstr "" +msgid "Mermaid diagram" +msgstr "" + msgid "Message" msgstr "" @@ -27039,6 +27066,9 @@ msgstr "" msgid "PackageRegistry|Add composer registry" msgstr "" +msgid "PackageRegistry|Additional metadata" +msgstr "" + msgid "PackageRegistry|Allow duplicates" msgstr "" @@ -27305,6 +27335,9 @@ msgstr "" msgid "PackageRegistry|Something went wrong while fetching the package history." msgstr "" +msgid "PackageRegistry|Something went wrong while fetching the package metadata." +msgstr "" + msgid "PackageRegistry|Sorry, your filter produced no results" msgstr "" @@ -28520,6 +28553,9 @@ msgstr "" msgid "PlantUML URL" msgstr "" +msgid "PlantUML diagram" +msgstr "" + msgid "Play" msgstr "" @@ -28943,6 +28979,9 @@ msgstr "" msgid "Preview changes" msgstr "" +msgid "Preview diagram" +msgstr "" + msgid "Preview payload" msgstr "" @@ -43265,9 +43304,6 @@ msgstr "" msgid "WorkItem|Expand child items" msgstr "" -msgid "WorkItem|New Task" -msgstr "" - msgid "WorkItem|No child items are currently assigned. Use child items to prioritize tasks that your team should complete in order to accomplish your goals!" msgstr "" @@ -43672,6 +43708,9 @@ msgstr "" msgid "You cannot combine replace_ids with add_ids or remove_ids" msgstr "" +msgid "You cannot edit this timeline event." +msgstr "" + msgid "You cannot impersonate a blocked user" msgstr "" diff --git a/spec/factories/incident_management/timeline_events.rb b/spec/factories/incident_management/timeline_events.rb index e2e216d24b8..831f78369b7 100644 --- a/spec/factories/incident_management/timeline_events.rb +++ b/spec/factories/incident_management/timeline_events.rb @@ -10,5 +10,14 @@ FactoryBot.define do note { 'timeline created' } note_html { '<strong>timeline created</strong>' } action { 'comment' } + editable + end + + trait :editable do + editable { true } + end + + trait :non_editable do + editable { false } end end diff --git a/spec/factories/releases.rb b/spec/factories/releases.rb index 52a9341b955..a07d4ef6c2e 100644 --- a/spec/factories/releases.rb +++ b/spec/factories/releases.rb @@ -14,7 +14,11 @@ FactoryBot.define do trait :legacy do sha { nil } - author { nil } + + # Legacy releases which are created during tags creation have empty users. + after(:create) do |release, _| + release.update_column(:author_id, nil) + end end trait :with_evidence do diff --git a/spec/finders/crm/contacts_finder_spec.rb b/spec/finders/crm/contacts_finder_spec.rb index 14f838812a6..fee4f049123 100644 --- a/spec/finders/crm/contacts_finder_spec.rb +++ b/spec/finders/crm/contacts_finder_spec.rb @@ -143,6 +143,14 @@ RSpec.describe Crm::ContactsFinder do expect(finder.execute).to match_array([search_test_b]) end end + + context 'when searching for contacts ids' do + it 'returns the expected contacts' do + finder = described_class.new(user, group: search_test_group, ids: [search_test_b.id]) + + expect(finder.execute).to match_array([search_test_b]) + end + end end end end diff --git a/spec/finders/crm/organizations_finder_spec.rb b/spec/finders/crm/organizations_finder_spec.rb index 94b5d9e5874..807c9f36484 100644 --- a/spec/finders/crm/organizations_finder_spec.rb +++ b/spec/finders/crm/organizations_finder_spec.rb @@ -129,6 +129,14 @@ RSpec.describe Crm::OrganizationsFinder do expect(finder.execute).to match_array([search_test_b]) end end + + context 'when searching for organizations ids' do + it 'returns the expected organizations' do + finder = described_class.new(user, group: search_test_group, ids: [search_test_a.id]) + + expect(finder.execute).to match_array([search_test_a]) + end + end end end end diff --git a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js index 5b4f954b672..6a859873a9d 100644 --- a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js +++ b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js @@ -16,6 +16,9 @@ describe('Signup Form', () => { wrapper = extendedWrapper( mountFn(SignupForm, { provide: { + glFeatures: { + passwordComplexity: true, + }, ...mockData, ...injectedProps, }, @@ -58,6 +61,10 @@ describe('Signup Form', () => { ${'minimumPasswordLength'} | ${mockData.minimumPasswordLength} | ${'[name="application_setting[minimum_password_length]"]'} | ${'attribute'} | ${'value'} | ${mockData.minimumPasswordLength} ${'minimumPasswordLengthMin'} | ${mockData.minimumPasswordLengthMin} | ${'[name="application_setting[minimum_password_length]"]'} | ${'attribute'} | ${'min'} | ${mockData.minimumPasswordLengthMin} ${'minimumPasswordLengthMax'} | ${mockData.minimumPasswordLengthMax} | ${'[name="application_setting[minimum_password_length]"]'} | ${'attribute'} | ${'max'} | ${mockData.minimumPasswordLengthMax} + ${'passwordNumberRequired'} | ${mockData.passwordNumberRequired} | ${'[name="application_setting[password_number_required]"]'} | ${'prop'} | ${'value'} | ${mockData.passwordNumberRequired} + ${'passwordLowercaseRequired'} | ${mockData.passwordLowercaseRequired} | ${'[name="application_setting[password_lowercase_required]"]'} | ${'prop'} | ${'value'} | ${mockData.passwordLowercaseRequired} + ${'passwordUppercaseRequired'} | ${mockData.passwordUppercaseRequired} | ${'[name="application_setting[password_uppercase_required]"]'} | ${'prop'} | ${'value'} | ${mockData.passwordUppercaseRequired} + ${'passwordSymbolRequired'} | ${mockData.passwordSymbolRequired} | ${'[name="application_setting[password_symbol_required]"]'} | ${'prop'} | ${'value'} | ${mockData.passwordSymbolRequired} ${'domainAllowlistRaw'} | ${mockData.domainAllowlistRaw} | ${'[name="application_setting[domain_allowlist_raw]"]'} | ${'value'} | ${'value'} | ${mockData.domainAllowlistRaw} ${'domainDenylistEnabled'} | ${mockData.domainDenylistEnabled} | ${'[name="application_setting[domain_denylist_enabled]"]'} | ${'prop'} | ${'value'} | ${mockData.domainDenylistEnabled} ${'denylistTypeRawSelected'} | ${mockData.denylistTypeRawSelected} | ${'[name="denylist_type"]'} | ${'attribute'} | ${'checked'} | ${'raw'} diff --git a/spec/frontend/admin/signup_restrictions/mock_data.js b/spec/frontend/admin/signup_restrictions/mock_data.js index 135fc8caae0..9e001e122a4 100644 --- a/spec/frontend/admin/signup_restrictions/mock_data.js +++ b/spec/frontend/admin/signup_restrictions/mock_data.js @@ -18,6 +18,10 @@ export const rawMockData = { emailRestrictions: 'user1@domain.com, user2@domain.com', afterSignUpText: 'Congratulations on your successful sign-up!', pendingUserCount: '0', + passwordNumberRequired: 'true', + passwordLowercaseRequired: 'true', + passwordUppercaseRequired: 'true', + passwordSymbolRequired: 'true', }; export const mockData = { @@ -40,4 +44,8 @@ export const mockData = { emailRestrictions: 'user1@domain.com, user2@domain.com', afterSignUpText: 'Congratulations on your successful sign-up!', pendingUserCount: '0', + passwordNumberRequired: true, + passwordLowercaseRequired: true, + passwordUppercaseRequired: true, + passwordSymbolRequired: true, }; diff --git a/spec/frontend/admin/signup_restrictions/utils_spec.js b/spec/frontend/admin/signup_restrictions/utils_spec.js index fd5c4c3317b..f07e14430f9 100644 --- a/spec/frontend/admin/signup_restrictions/utils_spec.js +++ b/spec/frontend/admin/signup_restrictions/utils_spec.js @@ -14,6 +14,10 @@ describe('utils', () => { 'domainDenylistEnabled', 'denylistTypeRawSelected', 'emailRestrictionsEnabled', + 'passwordNumberRequired', + 'passwordLowercaseRequired', + 'passwordUppercaseRequired', + 'passwordSymbolRequired', ], }), ).toEqual(mockData); diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index 5f162f498c4..9526277f06b 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; import Api, { DEFAULT_PER_PAGE } from '~/api'; import axios from '~/lib/utils/axios_utils'; import httpStatus from '~/lib/utils/http_status'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; jest.mock('~/flash'); @@ -622,8 +622,8 @@ describe('Api', () => { const query = 'dummy query'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`; const flashCallback = (callCount) => { - expect(createFlash).toHaveBeenCalledTimes(callCount); - createFlash.mockClear(); + expect(createAlert).toHaveBeenCalledTimes(callCount); + createAlert.mockClear(); }; mock.onGet(expectedUrl).reply(500, null); diff --git a/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js b/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js index f19bd02443f..646d068e795 100644 --- a/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js +++ b/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js @@ -12,6 +12,7 @@ import { stubComponent } from 'helpers/stub_component'; import CodeBlockBubbleMenu from '~/content_editor/components/bubble_menus/code_block.vue'; import eventHubFactory from '~/helpers/event_hub_factory'; import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight'; +import Diagram from '~/content_editor/extensions/diagram'; import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader'; import { createTestEditor, emitEditorEvent } from '../../test_utils'; @@ -20,11 +21,13 @@ const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jes describe('content_editor/components/bubble_menus/code_block', () => { let wrapper; let tiptapEditor; + let contentEditor; let bubbleMenu; let eventHub; const buildEditor = () => { - tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight] }); + tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight, Diagram] }); + contentEditor = { renderDiagram: jest.fn() }; eventHub = eventHubFactory(); }; @@ -32,6 +35,7 @@ describe('content_editor/components/bubble_menus/code_block', () => { wrapper = mountExtended(CodeBlockBubbleMenu, { provide: { tiptapEditor, + contentEditor, eventHub, }, stubs: { @@ -85,6 +89,15 @@ describe('content_editor/components/bubble_menus/code_block', () => { expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Javascript'); }); + it('selects diagram sytnax for mermaid', async () => { + tiptapEditor.commands.insertContent('<pre lang="mermaid">test</pre>'); + bubbleMenu = wrapper.findComponent(BubbleMenu); + + await emitEditorEvent({ event: 'transaction', tiptapEditor }); + + expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Diagram (mermaid)'); + }); + it("selects Custom (syntax) if the language doesn't exist in the list", async () => { tiptapEditor.commands.insertContent('<pre lang="nomnoml">test</pre>'); bubbleMenu = wrapper.findComponent(BubbleMenu); @@ -116,6 +129,39 @@ describe('content_editor/components/bubble_menus/code_block', () => { }); }); + describe('preview button', () => { + it('does not appear for a regular code block', async () => { + tiptapEditor.commands.insertContent('<pre lang="javascript">var a = 2;</pre>'); + + expect(wrapper.findByTestId('preview-diagram').exists()).toBe(false); + }); + + it.each` + diagramType | diagramCode + ${'mermaid'} | ${'<pre lang="mermaid">graph TD;\n A-->B;</pre>'} + ${'nomnoml'} | ${'<img data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,WzxmcmFtZT5EZWNvcmF0b3IgcGF0dGVybl0=">'} + `('toggles preview for a $diagramType diagram', async ({ diagramType, diagramCode }) => { + tiptapEditor.commands.insertContent(diagramCode); + + await nextTick(); + await wrapper.findByTestId('preview-diagram').vm.$emit('click'); + + expect(tiptapEditor.getAttributes(Diagram.name)).toEqual({ + isDiagram: true, + language: diagramType, + showPreview: false, + }); + + await wrapper.findByTestId('preview-diagram').vm.$emit('click'); + + expect(tiptapEditor.getAttributes(Diagram.name)).toEqual({ + isDiagram: true, + language: diagramType, + showPreview: true, + }); + }); + }); + describe('when opened and search is changed', () => { beforeEach(async () => { tiptapEditor.commands.insertContent('<pre lang="javascript">var a = 2;</pre>'); diff --git a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js new file mode 100644 index 00000000000..0334a18c9a1 --- /dev/null +++ b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js @@ -0,0 +1,54 @@ +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import ToolbarMoreDropdown from '~/content_editor/components/toolbar_more_dropdown.vue'; +import Diagram from '~/content_editor/extensions/diagram'; +import HorizontalRule from '~/content_editor/extensions/horizontal_rule'; +import { createTestEditor, mockChainedCommands } from '../test_utils'; + +describe('content_editor/components/toolbar_more_dropdown', () => { + let wrapper; + let tiptapEditor; + + const buildEditor = () => { + tiptapEditor = createTestEditor({ + extensions: [Diagram, HorizontalRule], + }); + }; + + const buildWrapper = (propsData = {}) => { + wrapper = mountExtended(ToolbarMoreDropdown, { + provide: { + tiptapEditor, + }, + propsData, + }); + }; + + beforeEach(() => { + buildEditor(); + buildWrapper(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe.each` + label | contentType | data + ${'Mermaid diagram'} | ${'diagram'} | ${{ language: 'mermaid' }} + ${'PlantUML diagram'} | ${'diagram'} | ${{ language: 'plantuml' }} + ${'Horizontal rule'} | ${'horizontalRule'} | ${undefined} + `('when option $label is clicked', ({ label, contentType, data }) => { + it(`inserts a ${contentType}`, async () => { + const commands = mockChainedCommands(tiptapEditor, ['setNode', 'focus', 'run']); + + const btn = wrapper.findByRole('menuitem', { name: label }); + await btn.trigger('click'); + + expect(commands.focus).toHaveBeenCalled(); + expect(commands.setNode).toHaveBeenCalledWith(contentType, data); + expect(commands.run).toHaveBeenCalled(); + + expect(wrapper.emitted('execute')).toEqual([[{ contentType }]]); + }); + }); +}); diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/top_toolbar_spec.js index ec58877470c..d98a9a52aff 100644 --- a/spec/frontend/content_editor/components/top_toolbar_spec.js +++ b/spec/frontend/content_editor/components/top_toolbar_spec.js @@ -23,20 +23,21 @@ describe('content_editor/components/top_toolbar', () => { }); describe.each` - testId | controlProps - ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold' }} - ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic' }} - ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike' }} - ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }} - ${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote', editorCommand: 'toggleBlockquote' }} - ${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }} - ${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }} - ${'details'} | ${{ contentType: 'details', iconName: 'details-block', label: 'Add a collapsible section', editorCommand: 'toggleDetails' }} - ${'horizontal-rule'} | ${{ contentType: 'horizontalRule', iconName: 'dash', label: 'Add a horizontal rule', editorCommand: 'setHorizontalRule' }} - ${'code-block'} | ${{ contentType: 'codeBlock', iconName: 'doc-code', label: 'Insert a code block', editorCommand: 'toggleCodeBlock' }} - ${'text-styles'} | ${{}} - ${'link'} | ${{}} - ${'image'} | ${{}} + testId | controlProps + ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold' }} + ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic' }} + ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike' }} + ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }} + ${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote', editorCommand: 'toggleBlockquote' }} + ${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }} + ${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }} + ${'details'} | ${{ contentType: 'details', iconName: 'details-block', label: 'Add a collapsible section', editorCommand: 'toggleDetails' }} + ${'code-block'} | ${{ contentType: 'codeBlock', iconName: 'doc-code', label: 'Insert a code block', editorCommand: 'toggleCodeBlock' }} + ${'text-styles'} | ${{}} + ${'link'} | ${{}} + ${'image'} | ${{}} + ${'table'} | ${{}} + ${'more'} | ${{}} `('given a $testId toolbar control', ({ testId, controlProps }) => { beforeEach(() => { buildWrapper(); diff --git a/spec/frontend/content_editor/components/wrappers/code_block_spec.js b/spec/frontend/content_editor/components/wrappers/code_block_spec.js index 2e59cd9714a..17a365e12bb 100644 --- a/spec/frontend/content_editor/components/wrappers/code_block_spec.js +++ b/spec/frontend/content_editor/components/wrappers/code_block_spec.js @@ -1,8 +1,14 @@ import { nextTick } from 'vue'; import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2'; -import { shallowMount } from '@vue/test-utils'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { stubComponent } from 'helpers/stub_component'; +import eventHubFactory from '~/helpers/event_hub_factory'; +import SandboxedMermaid from '~/behaviors/components/sandboxed_mermaid.vue'; +import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight'; +import Diagram from '~/content_editor/extensions/diagram'; import CodeBlockWrapper from '~/content_editor/components/wrappers/code_block.vue'; import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader'; +import { emitEditorEvent, createTestEditor } from '../../test_utils'; jest.mock('~/content_editor/services/code_block_language_loader'); @@ -10,21 +16,42 @@ describe('content/components/wrappers/code_block', () => { const language = 'yaml'; let wrapper; let updateAttributesFn; + let tiptapEditor; + let contentEditor; + let eventHub; + + const buildEditor = () => { + tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight, Diagram] }); + contentEditor = { renderDiagram: jest.fn().mockResolvedValue('url/to/some/diagram') }; + eventHub = eventHubFactory(); + }; const createWrapper = async (nodeAttrs = { language }) => { updateAttributesFn = jest.fn(); - wrapper = shallowMount(CodeBlockWrapper, { + wrapper = mountExtended(CodeBlockWrapper, { propsData: { + editor: tiptapEditor, node: { attrs: nodeAttrs, }, updateAttributes: updateAttributesFn, }, + stubs: { + NodeViewContent: stubComponent(NodeViewContent), + NodeViewWrapper: stubComponent(NodeViewWrapper), + }, + provide: { + contentEditor, + tiptapEditor, + eventHub, + }, }); }; beforeEach(() => { + buildEditor(); + codeBlockLanguageLoader.findOrCreateLanguageBySyntax.mockReturnValue({ syntax: language }); }); @@ -68,4 +95,56 @@ describe('content/components/wrappers/code_block', () => { expect(updateAttributesFn).toHaveBeenCalledWith({ language }); }); + + describe('diagrams', () => { + beforeEach(() => { + jest.spyOn(tiptapEditor, 'isActive').mockReturnValue(true); + }); + + it('does not render a preview if showPreview: false', async () => { + createWrapper({ language: 'plantuml', isDiagram: true, showPreview: false }); + + expect(wrapper.find({ ref: 'diagramContainer' }).exists()).toBe(false); + }); + + it('does not update preview when diagram is not active', async () => { + createWrapper({ language: 'plantuml', isDiagram: true, showPreview: true }); + + await emitEditorEvent({ event: 'transaction', tiptapEditor }); + await nextTick(); + + expect(wrapper.find('img').attributes('src')).toBe('url/to/some/diagram'); + + jest.spyOn(tiptapEditor, 'isActive').mockReturnValue(false); + + const alternateUrl = 'url/to/another/diagram'; + + contentEditor.renderDiagram.mockResolvedValue(alternateUrl); + + await emitEditorEvent({ event: 'transaction', tiptapEditor }); + await nextTick(); + + expect(wrapper.find('img').attributes('src')).toBe('url/to/some/diagram'); + }); + + it('renders an image with preview for a plantuml/kroki diagram', async () => { + createWrapper({ language: 'plantuml', isDiagram: true, showPreview: true }); + + await emitEditorEvent({ event: 'transaction', tiptapEditor }); + await nextTick(); + + expect(wrapper.find('img').attributes('src')).toBe('url/to/some/diagram'); + expect(wrapper.find(SandboxedMermaid).exists()).toBe(false); + }); + + it('renders an iframe with preview for a mermaid diagram', async () => { + createWrapper({ language: 'mermaid', isDiagram: true, showPreview: true }); + + await emitEditorEvent({ event: 'transaction', tiptapEditor }); + await nextTick(); + + expect(wrapper.find(SandboxedMermaid).props('source')).toBe(''); + expect(wrapper.find('img').exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/content_editor/services/asset_resolver_spec.js b/spec/frontend/content_editor/services/asset_resolver_spec.js index f4e7d9bf881..0a99f823be3 100644 --- a/spec/frontend/content_editor/services/asset_resolver_spec.js +++ b/spec/frontend/content_editor/services/asset_resolver_spec.js @@ -20,4 +20,14 @@ describe('content_editor/services/asset_resolver', () => { ); }); }); + + describe('renderDiagram', () => { + it('resolves a diagram code to a url containing the diagram image', async () => { + renderMarkdown.mockResolvedValue( + '<p><img data-diagram="nomnoml" src="url/to/some/diagram"></p>', + ); + + expect(await assetResolver.renderDiagram('test')).toBe('url/to/some/diagram'); + }); + }); }); diff --git a/spec/frontend/content_editor/services/code_block_language_loader_spec.js b/spec/frontend/content_editor/services/code_block_language_loader_spec.js index 9b2600f85d9..795f5219a3f 100644 --- a/spec/frontend/content_editor/services/code_block_language_loader_spec.js +++ b/spec/frontend/content_editor/services/code_block_language_loader_spec.js @@ -35,6 +35,13 @@ describe('content_editor/services/code_block_language_loader', () => { }); }); + it('returns Diagram (syntax) if the language does not exist, and isDiagram = true', () => { + expect(languageLoader.findOrCreateLanguageBySyntax('foobar', true)).toMatchObject({ + syntax: 'foobar', + label: 'Diagram (foobar)', + }); + }); + it('returns plaintext if no syntax is passed', () => { expect(languageLoader.findOrCreateLanguageBySyntax('')).toMatchObject({ syntax: 'plaintext', diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js index 7a71a1cea0f..4f3d780b149 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js @@ -1,4 +1,9 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlAlert } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { conanMetadata, mavenMetadata, @@ -6,9 +11,11 @@ import { packageData, composerMetadata, pypiMetadata, + packageMetadataQuery, } from 'jest/packages_and_registries/package_registry/mock_data'; import component from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue'; import { + FETCH_PACKAGE_METADATA_ERROR_MESSAGE, PACKAGE_TYPE_NUGET, PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN, @@ -16,6 +23,9 @@ import { PACKAGE_TYPE_COMPOSER, PACKAGE_TYPE_PYPI, } from '~/packages_and_registries/package_registry/constants'; +import AdditionalMetadataLoader from '~/packages_and_registries/package_registry/components/details/additional_metadata_loader.vue'; +import waitForPromises from 'helpers/wait_for_promises'; +import getPackageMetadata from '~/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql'; const mavenPackage = { packageType: PACKAGE_TYPE_MAVEN, metadata: mavenMetadata() }; const conanPackage = { packageType: PACKAGE_TYPE_CONAN, metadata: conanMetadata() }; @@ -24,16 +34,26 @@ const composerPackage = { packageType: PACKAGE_TYPE_COMPOSER, metadata: composer const pypiPackage = { packageType: PACKAGE_TYPE_PYPI, metadata: pypiMetadata() }; const npmPackage = { packageType: PACKAGE_TYPE_NPM, metadata: {} }; -describe('Package Additional Metadata', () => { +Vue.use(VueApollo); + +describe('Package Additional metadata', () => { let wrapper; + let apolloProvider; + const defaultProps = { - packageEntity: { - ...packageData(mavenPackage), - }, + packageId: packageData().id, + packageType: PACKAGE_TYPE_MAVEN, }; - const mountComponent = (props) => { + const mountComponent = ({ + props = {}, + resolver = jest.fn().mockResolvedValue(packageMetadataQuery(mavenPackage)), + } = {}) => { + const requestHandlers = [[getPackageMetadata, resolver]]; + apolloProvider = createMockApollo(requestHandlers); + wrapper = shallowMountExtended(component, { + apolloProvider, propsData: { ...defaultProps, ...props }, stubs: { component: { template: '<div data-testid="component-is"></div>' }, @@ -41,6 +61,10 @@ describe('Package Additional Metadata', () => { }); }; + beforeEach(() => { + jest.spyOn(Sentry, 'captureException').mockImplementation(); + }); + afterEach(() => { wrapper.destroy(); wrapper = null; @@ -49,6 +73,22 @@ describe('Package Additional Metadata', () => { const findTitle = () => wrapper.findByTestId('title'); const findMainArea = () => wrapper.findByTestId('main'); const findComponentIs = () => wrapper.findByTestId('component-is'); + const findAdditionalMetadataLoader = () => wrapper.findComponent(AdditionalMetadataLoader); + const findPackageMetadataAlert = () => wrapper.findComponent(GlAlert); + + it('renders the loading container when loading', () => { + mountComponent(); + + expect(findAdditionalMetadataLoader().exists()).toBe(true); + }); + + it('does not render the loading container once resolved', async () => { + mountComponent(); + await waitForPromises(); + + expect(findAdditionalMetadataLoader().exists()).toBe(false); + expect(Sentry.captureException).not.toHaveBeenCalled(); + }); it('has the correct title', () => { mountComponent(); @@ -56,7 +96,25 @@ describe('Package Additional Metadata', () => { const title = findTitle(); expect(title.exists()).toBe(true); - expect(title.text()).toBe('Additional Metadata'); + expect(title.text()).toMatchInterpolatedText(component.i18n.componentTitle); + }); + + it('does not render gl-alert', () => { + mountComponent(); + + expect(findPackageMetadataAlert().exists()).toBe(false); + }); + + it('renders gl-alert if load fails', async () => { + mountComponent({ resolver: jest.fn().mockRejectedValue() }); + + await waitForPromises(); + + expect(findPackageMetadataAlert().exists()).toBe(true); + expect(findPackageMetadataAlert().text()).toMatchInterpolatedText( + FETCH_PACKAGE_METADATA_ERROR_MESSAGE, + ); + expect(Sentry.captureException).toHaveBeenCalled(); }); it.each` @@ -68,16 +126,22 @@ describe('Package Additional Metadata', () => { ${pypiPackage} | ${true} | ${PACKAGE_TYPE_PYPI} ${npmPackage} | ${false} | ${PACKAGE_TYPE_NPM} `( - `It is $visible that the component is visible when the package is $packageType`, - ({ packageEntity, visible }) => { - mountComponent({ packageEntity }); + `component visibility is $visible when the package is $packageType`, + async ({ packageEntity, visible, packageType }) => { + const resolved = packageMetadataQuery(packageType); + const resolver = jest.fn().mockResolvedValue(resolved); + + mountComponent({ props: { packageType }, resolver }); + + await waitForPromises(); + await nextTick(); expect(findTitle().exists()).toBe(visible); expect(findMainArea().exists()).toBe(visible); expect(findComponentIs().exists()).toBe(visible); if (visible) { - expect(findComponentIs().props('packageEntity')).toEqual(packageEntity); + expect(findComponentIs().props('packageMetadata')).toEqual(packageEntity.metadata); } }, ); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js index e744680cb9a..bb6846d354f 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js @@ -1,22 +1,16 @@ import { GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { - packageData, - composerMetadata, -} from 'jest/packages_and_registries/package_registry/mock_data'; +import { composerMetadata } from 'jest/packages_and_registries/package_registry/mock_data'; import component from '~/packages_and_registries/package_registry/components/details/metadata/composer.vue'; -import { PACKAGE_TYPE_COMPOSER } from '~/packages_and_registries/package_registry/constants'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; -const composerPackage = { packageType: PACKAGE_TYPE_COMPOSER, metadata: composerMetadata() }; - describe('Composer Metadata', () => { let wrapper; const mountComponent = () => { wrapper = shallowMountExtended(component, { - propsData: { packageEntity: packageData(composerPackage) }, + propsData: { packageMetadata: composerMetadata() }, stubs: { DetailsRow, GlSprintf, diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js index 46593047f1f..e7e47401aa1 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js @@ -1,22 +1,16 @@ import { GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { - conanMetadata, - packageData, -} from 'jest/packages_and_registries/package_registry/mock_data'; +import { conanMetadata } from 'jest/packages_and_registries/package_registry/mock_data'; import component from '~/packages_and_registries/package_registry/components/details/metadata/conan.vue'; -import { PACKAGE_TYPE_CONAN } from '~/packages_and_registries/package_registry/constants'; import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; -const conanPackage = { packageType: PACKAGE_TYPE_CONAN, metadata: conanMetadata() }; - describe('Conan Metadata', () => { let wrapper; const mountComponent = () => { wrapper = shallowMountExtended(component, { propsData: { - packageEntity: packageData(conanPackage), + packageMetadata: conanMetadata(), }, stubs: { DetailsRow, diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js index bc54cf1cb98..8680d983042 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js @@ -1,24 +1,16 @@ import { GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { - mavenMetadata, - packageData, -} from 'jest/packages_and_registries/package_registry/mock_data'; +import { mavenMetadata } from 'jest/packages_and_registries/package_registry/mock_data'; import component from '~/packages_and_registries/package_registry/components/details/metadata/maven.vue'; -import { PACKAGE_TYPE_MAVEN } from '~/packages_and_registries/package_registry/constants'; import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; -const mavenPackage = { packageType: PACKAGE_TYPE_MAVEN, metadata: mavenMetadata() }; - describe('Maven Metadata', () => { let wrapper; const mountComponent = () => { wrapper = shallowMountExtended(component, { propsData: { - packageEntity: { - ...packageData(mavenPackage), - }, + packageMetadata: mavenMetadata(), }, stubs: { DetailsRow, diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js index f759fe7a81c..af3692023f0 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js @@ -1,25 +1,17 @@ import { GlLink, GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { - nugetMetadata, - packageData, -} from 'jest/packages_and_registries/package_registry/mock_data'; +import { nugetMetadata } from 'jest/packages_and_registries/package_registry/mock_data'; import component from '~/packages_and_registries/package_registry/components/details/metadata/nuget.vue'; -import { PACKAGE_TYPE_NUGET } from '~/packages_and_registries/package_registry/constants'; import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; describe('Nuget Metadata', () => { - let nugetPackage = { packageType: PACKAGE_TYPE_NUGET, metadata: nugetMetadata() }; + let nugetPackageMetadata = { ...nugetMetadata() }; let wrapper; - const mountComponent = () => { + const mountComponent = (props) => { wrapper = shallowMountExtended(component, { - propsData: { - packageEntity: { - ...packageData(nugetPackage), - }, - }, + propsData: { ...props }, stubs: { DetailsRow, GlSprintf, @@ -37,7 +29,7 @@ describe('Nuget Metadata', () => { const findElementLink = (container) => container.findComponent(GlLink); beforeEach(() => { - mountComponent({ packageEntity: nugetPackage }); + mountComponent({ packageMetadata: nugetPackageMetadata }); }); it.each` @@ -49,14 +41,14 @@ describe('Nuget Metadata', () => { expect(element.exists()).toBe(true); expect(element.text()).toBe(text); expect(element.props('icon')).toBe(icon); - expect(findElementLink(element).attributes('href')).toBe(nugetPackage.metadata[link]); + expect(findElementLink(element).attributes('href')).toBe(nugetPackageMetadata[link]); }); describe('without source', () => { beforeAll(() => { - nugetPackage = { - packageType: PACKAGE_TYPE_NUGET, - metadata: { iconUrl: 'iconUrl', licenseUrl: 'licenseUrl' }, + nugetPackageMetadata = { + iconUrl: 'iconUrl', + licenseUrl: 'licenseUrl', }; }); @@ -67,9 +59,9 @@ describe('Nuget Metadata', () => { describe('without license', () => { beforeAll(() => { - nugetPackage = { - packageType: PACKAGE_TYPE_NUGET, - metadata: { iconUrl: 'iconUrl', projectUrl: 'projectUrl' }, + nugetPackageMetadata = { + iconUrl: 'iconUrl', + projectUrl: 'projectUrl', }; }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js index c4481c3f20b..d7c6ea8379d 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js @@ -1,22 +1,17 @@ import { GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { packageData, pypiMetadata } from 'jest/packages_and_registries/package_registry/mock_data'; +import { pypiMetadata } from 'jest/packages_and_registries/package_registry/mock_data'; import component from '~/packages_and_registries/package_registry/components/details/metadata/pypi.vue'; -import { PACKAGE_TYPE_PYPI } from '~/packages_and_registries/package_registry/constants'; import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; -const pypiPackage = { packageType: PACKAGE_TYPE_PYPI, metadata: pypiMetadata() }; - describe('Package Additional Metadata', () => { let wrapper; const mountComponent = () => { wrapper = shallowMountExtended(component, { propsData: { - packageEntity: { - ...packageData(pypiPackage), - }, + packageMetadata: pypiMetadata(), }, stubs: { DetailsRow, diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js index e68916ecb39..f4e6d43812d 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { GlLink, GlSprintf } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; import { stubComponent } from 'helpers/stub_component'; import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; @@ -11,7 +11,6 @@ import { packagePipelinesQuery, } from 'jest/packages_and_registries/package_registry/mock_data'; import { HISTORY_PIPELINES_LIMIT } from '~/packages_and_registries/shared/constants'; -import { FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE } from '~/packages_and_registries/package_registry/constants'; import component from '~/packages_and_registries/package_registry/components/details/package_history.vue'; import PackageHistoryLoader from '~/packages_and_registries/package_registry/components/details/package_history_loader.vue'; import HistoryItem from '~/vue_shared/components/registry/history_item.vue'; @@ -19,7 +18,8 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import waitForPromises from 'helpers/wait_for_promises'; import getPackagePipelines from '~/packages_and_registries/package_registry/graphql/queries/get_package_pipelines.query.graphql'; -jest.mock('~/flash'); +Vue.use(VueApollo); + describe('Package History', () => { let wrapper; let apolloProvider; @@ -34,12 +34,10 @@ describe('Package History', () => { const createPipelines = (amount) => [...Array(amount)].map((x, index) => packagePipelines({ id: index + 1 })[0]); - const mountComponent = ( - props, + const mountComponent = ({ + props = {}, resolver = jest.fn().mockResolvedValue(packagePipelinesQuery()), - ) => { - Vue.use(VueApollo); - + } = {}) => { const requestHandlers = [[getPackagePipelines, resolver]]; apolloProvider = createMockApollo(requestHandlers); @@ -55,14 +53,20 @@ describe('Package History', () => { }); }; + beforeEach(() => { + jest.spyOn(Sentry, 'captureException').mockImplementation(); + }); + afterEach(() => { wrapper.destroy(); + wrapper = null; }); const findPackageHistoryLoader = () => wrapper.findComponent(PackageHistoryLoader); const findHistoryElement = (testId) => wrapper.findByTestId(testId); const findElementLink = (container) => container.findComponent(GlLink); const findElementTimeAgo = (container) => container.findComponent(TimeAgoTooltip); + const findPackageHistoryAlert = () => wrapper.findComponent(GlAlert); const findTitle = () => wrapper.findByTestId('title'); const findTimeline = () => wrapper.findByTestId('timeline'); @@ -77,6 +81,7 @@ describe('Package History', () => { await waitForPromises(); expect(findPackageHistoryLoader().exists()).toBe(false); + expect(Sentry.captureException).not.toHaveBeenCalled(); }); it('has the correct title', async () => { @@ -101,16 +106,22 @@ describe('Package History', () => { ); }); - it('calls createFlash function if load fails', async () => { - mountComponent({}, jest.fn().mockRejectedValue()); + it('does not render gl-alert', () => { + mountComponent(); + + expect(findPackageHistoryAlert().exists()).toBe(false); + }); + + it('renders gl-alert if load fails', async () => { + mountComponent({ resolver: jest.fn().mockRejectedValue() }); await waitForPromises(); - expect(createFlash).toHaveBeenCalledWith( - expect.objectContaining({ - message: FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE, - }), + expect(findPackageHistoryAlert().exists()).toBe(true); + expect(findPackageHistoryAlert().text()).toEqual( + 'Something went wrong while fetching the package history.', ); + expect(Sentry.captureException).toHaveBeenCalled(); }); describe.each` @@ -132,13 +143,16 @@ describe('Package History', () => { const pipelinesResolver = jest .fn() .mockResolvedValue(packagePipelinesQuery(createPipelines(amount))); - mountComponent( - { + + mountComponent({ + props: { packageEntity, }, - pipelinesResolver, - ); + resolver: pipelinesResolver, + }); + await waitForPromises(); + element = findHistoryElement(name); }); diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js index 3dfcec37ea7..d40feee582f 100644 --- a/spec/frontend/packages_and_registries/package_registry/mock_data.js +++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js @@ -148,6 +148,8 @@ export const conanMetadata = () => ({ recipePath: 'package-8/1.0.0/gitlab-org+gitlab-test/stable', }); +const conanMetadataQuery = () => ({ ...conanMetadata(), __typename: 'ConanMetadata' }); + export const composerMetadata = () => ({ targetSha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0', composerJson: { @@ -156,23 +158,45 @@ export const composerMetadata = () => ({ }, }); +const composerMetadataQuery = () => ({ + ...composerMetadata(), + __typename: 'ComposerMetadata', +}); + export const pypiMetadata = () => ({ + id: 'pypi-1', requiredPython: '1.0.0', }); +const pypiMetadataQuery = () => ({ ...pypiMetadata(), __typename: 'PypiMetadata' }); + export const mavenMetadata = () => ({ + id: 'maven-1', appName: 'appName', appGroup: 'appGroup', appVersion: 'appVersion', path: 'path', }); +const mavenMetadataQuery = () => ({ ...mavenMetadata(), __typename: 'MavenMetadata' }); + export const nugetMetadata = () => ({ + id: 'nuget-1', iconUrl: 'iconUrl', licenseUrl: 'licenseUrl', projectUrl: 'projectUrl', }); +const nugetMetadataQuery = () => ({ ...nugetMetadata(), __typename: 'NugetMetadata' }); + +const packageTypeMetadataQueryMapping = { + CONAN: conanMetadataQuery, + COMPOSER: composerMetadataQuery, + PYPI: pypiMetadataQuery, + MAVEN: mavenMetadataQuery, + NUGET: nugetMetadataQuery, +}; + export const pagination = (extend) => ({ endCursor: 'eyJpZCI6IjIwNSIsIm5hbWUiOiJteS9jb21wYW55L2FwcC9teS1hcHAifQ', hasNextPage: true, @@ -202,6 +226,10 @@ export const packageDetailsQuery = (extendPackage) => ({ nodes: packageTags(), __typename: 'PackageTagConnection', }, + pipelines: { + nodes: packagePipelines(), + __typename: 'PipelineConnection', + }, packageFiles: { nodes: packageFiles(), __typename: 'PackageFileConnection', @@ -240,6 +268,21 @@ export const emptyPackageDetailsQuery = () => ({ }, }); +export const packageMetadataQuery = (packageType) => { + return { + data: { + package: { + id: 'gid://gitlab/Packages::Package/111', + packageType, + metadata: { + ...(packageTypeMetadataQueryMapping[packageType]?.() ?? {}), + }, + __typename: 'PackageDetailsType', + }, + }, + }; +}; + export const packageDestroyMutation = () => ({ data: { destroyPackage: { diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js index a7e31d42c9e..3cadb001c58 100644 --- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js @@ -23,6 +23,10 @@ import { DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, DELETE_PACKAGE_FILE_ERROR_MESSAGE, PACKAGE_TYPE_NUGET, + PACKAGE_TYPE_MAVEN, + PACKAGE_TYPE_CONAN, + PACKAGE_TYPE_PYPI, + PACKAGE_TYPE_NPM, } from '~/packages_and_registries/package_registry/constants'; import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql'; @@ -160,15 +164,38 @@ describe('PackagesApp', () => { }); }); - it('renders additional metadata and has the right props', async () => { - createComponent(); + describe('additional metadata', () => { + it.each` + packageType | visible + ${PACKAGE_TYPE_MAVEN} | ${true} + ${PACKAGE_TYPE_CONAN} | ${true} + ${PACKAGE_TYPE_NUGET} | ${true} + ${PACKAGE_TYPE_COMPOSER} | ${true} + ${PACKAGE_TYPE_PYPI} | ${true} + ${PACKAGE_TYPE_NPM} | ${false} + `( + `It is $visible that the component is visible when the package is $packageType`, + async ({ packageType, visible }) => { + createComponent({ + resolver: jest.fn().mockResolvedValue( + packageDetailsQuery({ + packageType, + }), + ), + }); - await waitForPromises(); + await waitForPromises(); - expect(findAdditionalMetadata().exists()).toBe(true); - expect(findAdditionalMetadata().props()).toMatchObject({ - packageEntity: expect.objectContaining(packageWithoutTypename), - }); + expect(findAdditionalMetadata().exists()).toBe(visible); + + if (visible) { + expect(findAdditionalMetadata().props()).toMatchObject({ + packageId: packageWithoutTypename.id, + packageType, + }); + } + }, + ); }); it('renders installation commands and has the right props', async () => { diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js index 0d85df25b4f..2c3f6ef8634 100644 --- a/spec/frontend/work_items/components/item_title_spec.js +++ b/spec/frontend/work_items/components/item_title_spec.js @@ -15,7 +15,7 @@ const createComponent = ({ title = 'Sample title', disabled = false } = {}) => describe('ItemTitle', () => { let wrapper; const mockUpdatedTitle = 'Updated title'; - const findInputEl = () => wrapper.find('span#item-title'); + const findInputEl = () => wrapper.find('[aria-label="Title"]'); beforeEach(() => { wrapper = createComponent(); diff --git a/spec/graphql/mutations/incident_management/timeline_event/create_spec.rb b/spec/graphql/mutations/incident_management/timeline_event/create_spec.rb index 63faecad5d5..ea74e427dd6 100644 --- a/spec/graphql/mutations/incident_management/timeline_event/create_spec.rb +++ b/spec/graphql/mutations/incident_management/timeline_event/create_spec.rb @@ -22,7 +22,8 @@ RSpec.describe Mutations::IncidentManagement::TimelineEvent::Create do occurred_at: args[:occurred_at].to_s, incident: incident, author: current_user, - promoted_from_note: nil + promoted_from_note: nil, + editable: true ) end diff --git a/spec/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb b/spec/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb index 598ee496cf1..4541f8af7d3 100644 --- a/spec/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb +++ b/spec/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb @@ -27,7 +27,8 @@ RSpec.describe Mutations::IncidentManagement::TimelineEvent::PromoteFromNote do occurred_at: comment.created_at.to_s, incident: incident, author: current_user, - promoted_from_note: comment + promoted_from_note: comment, + editable: true ) end diff --git a/spec/graphql/resolvers/crm/contacts_resolver_spec.rb b/spec/graphql/resolvers/crm/contacts_resolver_spec.rb index eba26c8c71f..98da4aeac28 100644 --- a/spec/graphql/resolvers/crm/contacts_resolver_spec.rb +++ b/spec/graphql/resolvers/crm/contacts_resolver_spec.rb @@ -41,7 +41,7 @@ RSpec.describe Resolvers::Crm::ContactsResolver do end context 'with authorized user' do - it 'does not rise an error and returns all contacts' do + it 'does not rise an error and returns all contacts in the correct order' do group.add_reporter(user) expect { resolve_contacts(group) }.not_to raise_error @@ -61,20 +61,26 @@ RSpec.describe Resolvers::Crm::ContactsResolver do end context 'when no filter is provided' do - it 'returns all the contacts' do - expect(resolve_contacts(group)).to match_array([contact_a, contact_b]) + it 'returns all the contacts in the correct order' do + expect(resolve_contacts(group)).to eq([contact_a, contact_b]) end end context 'when search term is provided' do it 'returns the correct contacts' do - expect(resolve_contacts(group, { search: "x@test.com" })).to match_array([contact_b]) + expect(resolve_contacts(group, { search: "x@test.com" })).to contain_exactly(contact_b) end end context 'when state is provided' do it 'returns the correct contacts' do - expect(resolve_contacts(group, { state: :inactive })).to match_array([contact_a]) + expect(resolve_contacts(group, { state: :inactive })).to contain_exactly(contact_a) + end + end + + context 'when ids are provided' do + it 'returns the correct contacts' do + expect(resolve_contacts(group, { ids: [contact_a.to_global_id] })).to contain_exactly(contact_a) end end end diff --git a/spec/graphql/resolvers/crm/organizations_resolver_spec.rb b/spec/graphql/resolvers/crm/organizations_resolver_spec.rb index c80caf91f90..323f134ffc3 100644 --- a/spec/graphql/resolvers/crm/organizations_resolver_spec.rb +++ b/spec/graphql/resolvers/crm/organizations_resolver_spec.rb @@ -35,7 +35,7 @@ RSpec.describe Resolvers::Crm::OrganizationsResolver do end context 'with authorized user' do - it 'does not rise an error and returns all organizations' do + it 'does not rise an error and returns all organizations in the correct order' do group.add_reporter(user) expect { resolve_organizations(group) }.not_to raise_error @@ -55,20 +55,28 @@ RSpec.describe Resolvers::Crm::OrganizationsResolver do end context 'when no filter is provided' do - it 'returns all the organizations' do - expect(resolve_organizations(group)).to match_array([organization_a, organization_b]) + it 'returns all the organizations in the correct order' do + expect(resolve_organizations(group)).to eq([organization_a, organization_b]) end end context 'when search term is provided' do it 'returns the correct organizations' do - expect(resolve_organizations(group, { search: "def" })).to match_array([organization_b]) + expect(resolve_organizations(group, { search: "def" })).to contain_exactly(organization_b) end end context 'when state is provided' do it 'returns the correct organizations' do - expect(resolve_organizations(group, { state: :inactive })).to match_array([organization_a]) + expect(resolve_organizations(group, { state: :inactive })).to contain_exactly(organization_a) + end + end + + context 'when ids are provided' do + it 'returns the correct organizations' do + expect(resolve_organizations(group, { + ids: [organization_b.to_global_id] + })).to contain_exactly(organization_b) end end end diff --git a/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb index 85c25938fcc..2633598b48d 100644 --- a/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb +++ b/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb @@ -31,7 +31,8 @@ RSpec.describe BulkImports::Projects::Pipelines::ReleasesPipeline do 'created_at' => '2019-12-26T10:17:14.621Z', 'updated_at' => '2019-12-26T10:17:14.621Z', 'released_at' => '2019-12-26T10:17:14.615Z', - 'sha' => '901de3a8bd5573f4a049b1457d28bc1592ba6bf9' + 'sha' => '901de3a8bd5573f4a049b1457d28bc1592ba6bf9', + 'author_id' => user.id }.merge(attributes) end @@ -62,6 +63,7 @@ RSpec.describe BulkImports::Projects::Pipelines::ReleasesPipeline do expect(imported_release.updated_at.to_s).to eq('2019-12-26 10:17:14 UTC') expect(imported_release.released_at.to_s).to eq('2019-12-26 10:17:14 UTC') expect(imported_release.sha).to eq(release['sha']) + expect(imported_release.author_id).to eq(release['author_id']) end end diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb index f81ad9b193d..8cdfa28d40a 100644 --- a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb +++ b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb @@ -77,8 +77,8 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do context 'for redis metrics' do it_behaves_like 'name suggestion' do - # corresponding metric is collected with redis_usage_data { unique_visit_service.unique_visits_for(targets: :analytics) } - let(:key_path) { 'analytics_unique_visits.analytics_unique_visits_for_any_target' } + # corresponding metric is collected with redis_usage_data { unique_visit_service.unique_visits_for(targets: :compliance) } + let(:key_path) { 'compliance_unique_visits.compliance_unique_visits_for_any_target' } let(:name_suggestion) { /<please fill metric name, suggested format is: {subject}_{verb}{ing|ed}_{object} eg: users_creating_epics or merge_requests_viewed_in_single_file_mode>/ } end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 9d7f464756d..a0a1a216cb6 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -1204,46 +1204,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do end end - describe '.analytics_unique_visits_data' do - subject { described_class.analytics_unique_visits_data } - - it 'returns the number of unique visits to pages with analytics features' do - ::Gitlab::Analytics::UniqueVisits.analytics_events.each do |target| - expect_any_instance_of(::Gitlab::Analytics::UniqueVisits).to receive(:unique_visits_for).with(targets: target).and_return(123) - end - - expect_any_instance_of(::Gitlab::Analytics::UniqueVisits).to receive(:unique_visits_for).with(targets: :analytics).and_return(543) - expect_any_instance_of(::Gitlab::Analytics::UniqueVisits).to receive(:unique_visits_for).with(targets: :analytics, start_date: 4.weeks.ago.to_date, end_date: Date.current).and_return(987) - - expect(subject).to eq({ - analytics_unique_visits: { - 'g_analytics_contribution' => 123, - 'g_analytics_insights' => 123, - 'g_analytics_issues' => 123, - 'g_analytics_productivity' => 123, - 'g_analytics_valuestream' => 123, - 'p_analytics_pipelines' => 123, - 'p_analytics_code_reviews' => 123, - 'p_analytics_valuestream' => 123, - 'p_analytics_insights' => 123, - 'p_analytics_issues' => 123, - 'p_analytics_repo' => 123, - 'i_analytics_cohorts' => 123, - 'i_analytics_dev_ops_score' => 123, - 'i_analytics_instance_statistics' => 123, - 'p_analytics_ci_cd_deployment_frequency' => 123, - 'p_analytics_ci_cd_lead_time' => 123, - 'p_analytics_ci_cd_pipelines' => 123, - 'p_analytics_merge_request' => 123, - 'i_analytics_dev_ops_adoption' => 123, - 'users_viewing_analytics_group_devops_adoption' => 123, - 'analytics_unique_visits_for_any_target' => 543, - 'analytics_unique_visits_for_any_target_monthly' => 987 - } - }) - end - end - describe '.compliance_unique_visits_data' do subject { described_class.compliance_unique_visits_data } diff --git a/spec/models/customer_relations/contact_spec.rb b/spec/models/customer_relations/contact_spec.rb index 86f868b269e..f91546f5240 100644 --- a/spec/models/customer_relations/contact_spec.rb +++ b/spec/models/customer_relations/contact_spec.rb @@ -142,4 +142,99 @@ RSpec.describe CustomerRelations::Contact, type: :model do expect(issue_contact2.reload.contact_id).to eq(dupe_contact1.id) end end + + describe '.search' do + let_it_be(:contact_a) do + create( + :contact, + group: group, + first_name: "ABC", + last_name: "DEF", + email: "ghi@test.com", + description: "LMNO", + state: "inactive" + ) + end + + let_it_be(:contact_b) do + create( + :contact, + group: group, + first_name: "PQR", + last_name: "STU", + email: "vwx@test.com", + description: "YZ", + state: "active" + ) + end + + subject(:found_contacts) { group.contacts.search(search_term) } + + context 'when search term is empty' do + let(:search_term) { "" } + + it 'returns all group contacts' do + expect(found_contacts).to contain_exactly(contact_a, contact_b) + end + end + + context 'when search term is not empty' do + context 'when searching for first name ignoring casing' do + let(:search_term) { "aBc" } + + it { is_expected.to contain_exactly(contact_a) } + end + + context 'when searching for last name ignoring casing' do + let(:search_term) { "StU" } + + it { is_expected.to contain_exactly(contact_b) } + end + + context 'when searching for email' do + let(:search_term) { "ghi" } + + it { is_expected.to contain_exactly(contact_a) } + end + + context 'when searching description ignoring casing' do + let(:search_term) { "Yz" } + + it { is_expected.to contain_exactly(contact_b) } + end + + context 'when fuzzy searching for email and last name' do + let(:search_term) { "s" } + + it { is_expected.to contain_exactly(contact_a, contact_b) } + end + end + end + + describe '.search_by_state' do + let_it_be(:contact_a) { create(:contact, group: group, state: "inactive") } + let_it_be(:contact_b) { create(:contact, group: group, state: "active") } + + context 'when searching for contacts state' do + it 'returns only inactive contacts' do + expect(group.contacts.search_by_state(:inactive)).to contain_exactly(contact_a) + end + + it 'returns only active contacts' do + expect(group.contacts.search_by_state(:active)).to contain_exactly(contact_b) + end + end + end + + describe '.sort_by_name' do + let_it_be(:contact_a) { create(:contact, group: group, first_name: "c", last_name: "d") } + let_it_be(:contact_b) { create(:contact, group: group, first_name: "a", last_name: "b") } + let_it_be(:contact_c) { create(:contact, group: group, first_name: "e", last_name: "d") } + + context 'when sorting the contacts' do + it 'sorts them by last name then first name in ascendent order' do + expect(group.contacts.sort_by_name).to eq([contact_b, contact_a, contact_c]) + end + end + end end diff --git a/spec/models/customer_relations/organization_spec.rb b/spec/models/customer_relations/organization_spec.rb index 06ba9c5b7ad..1833fcf5385 100644 --- a/spec/models/customer_relations/organization_spec.rb +++ b/spec/models/customer_relations/organization_spec.rb @@ -78,4 +78,83 @@ RSpec.describe CustomerRelations::Organization, type: :model do expect(contact2.reload.organization_id).to eq(dupe_organization1.id) end end + + describe '.search' do + let_it_be(:organization_a) do + create( + :organization, + group: group, + name: "DEF", + description: "ghi_st", + state: "inactive" + ) + end + + let_it_be(:organization_b) do + create( + :organization, + group: group, + name: "ABC_st", + description: "JKL", + state: "active" + ) + end + + subject(:found_organizations) { group.organizations.search(search_term) } + + context 'when search term is empty' do + let(:search_term) { "" } + + it 'returns all group organizations' do + expect(found_organizations).to contain_exactly(organization_a, organization_b) + end + end + + context 'when search term is not empty' do + context 'when searching for name' do + let(:search_term) { "aBc" } + + it { is_expected.to contain_exactly(organization_b) } + end + + context 'when searching for description' do + let(:search_term) { "ghI" } + + it { is_expected.to contain_exactly(organization_a) } + end + + context 'when searching for name and description' do + let(:search_term) { "_st" } + + it { is_expected.to contain_exactly(organization_a, organization_b) } + end + end + end + + describe '.search_by_state' do + let_it_be(:organization_a) { create(:organization, group: group, state: "inactive") } + let_it_be(:organization_b) { create(:organization, group: group, state: "active") } + + context 'when searching for organizations state' do + it 'returns only inactive organizations' do + expect(group.organizations.search_by_state(:inactive)).to contain_exactly(organization_a) + end + + it 'returns only active organizations' do + expect(group.organizations.search_by_state(:active)).to contain_exactly(organization_b) + end + end + end + + describe '.sort_by_name' do + let_it_be(:organization_a) { create(:organization, group: group, name: "c") } + let_it_be(:organization_b) { create(:organization, group: group, name: "a") } + let_it_be(:organization_c) { create(:organization, group: group, name: "b") } + + context 'when sorting the organizations' do + it 'sorts them by name in ascendent order' do + expect(group.organizations.sort_by_name).to eq([organization_b, organization_c, organization_a]) + end + end + end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 06044cf53cc..72a57b6076a 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -82,14 +82,16 @@ RSpec.describe Milestone do context 'when it is tied to a release for another project' do it 'creates a validation error' do other_project = create(:project) - milestone.releases << build(:release, project: other_project) + milestone.releases << build(:release, + project: other_project, author_id: other_project.members.first.user_id) expect(milestone).not_to be_valid end end context 'when it is tied to a release for the same project' do it 'is valid' do - milestone.releases << build(:release, project: project) + milestone.releases << build(:release, + project: project, author_id: project.members.first.user_id) expect(milestone).to be_valid end end diff --git a/spec/models/projects/build_artifacts_size_refresh_spec.rb b/spec/models/projects/build_artifacts_size_refresh_spec.rb index f8cd8fb5c76..052e654af76 100644 --- a/spec/models/projects/build_artifacts_size_refresh_spec.rb +++ b/spec/models/projects/build_artifacts_size_refresh_spec.rb @@ -30,6 +30,12 @@ RSpec.describe Projects::BuildArtifactsSizeRefresh, type: :model do expect(described_class.remaining).to match_array([refresh_1, refresh_3, refresh_4]) end end + + describe 'processing_queue' do + it 'prioritizes pending -> stale -> created' do + expect(described_class.processing_queue).to eq([refresh_3, refresh_1, refresh_4]) + end + end end describe 'state machine', :clean_gitlab_redis_shared_state do @@ -165,15 +171,13 @@ RSpec.describe Projects::BuildArtifactsSizeRefresh, type: :model do end describe '.process_next_refresh!' do - let!(:refresh_running) { create(:project_build_artifacts_size_refresh, :running) } let!(:refresh_created) { create(:project_build_artifacts_size_refresh, :created) } - let!(:refresh_stale) { create(:project_build_artifacts_size_refresh, :stale) } let!(:refresh_pending) { create(:project_build_artifacts_size_refresh, :pending) } subject(:processed_refresh) { described_class.process_next_refresh! } it 'picks the first record from the remaining work' do - expect(processed_refresh).to eq(refresh_created) + expect(processed_refresh).to eq(refresh_pending) expect(processed_refresh.reload).to be_running end end diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb index 4ae1927dcca..83d7596ff51 100644 --- a/spec/models/release_spec.rb +++ b/spec/models/release_spec.rb @@ -66,6 +66,32 @@ RSpec.describe Release do expect { release.milestones << milestone }.to change { MilestoneRelease.count }.by(1) end end + + context 'when creating new release' do + subject { build(:release, project: project, name: 'Release 1.0') } + + it { is_expected.to validate_presence_of(:author_id) } + + context 'when feature flag is disabled' do + before do + stub_feature_flags(validate_release_with_author: false) + end + + it { is_expected.not_to validate_presence_of(:author_id) } + end + end + + # Mimic releases created before 11.7 + # See: https://gitlab.com/gitlab-org/gitlab/-/blob/8e5a110b01f842d8b6a702197928757a40ce9009/app/models/release.rb#L14 + context 'when updating existing release without author' do + let(:release) { create(:release, :legacy) } + + it 'updates successfully' do + release.description += 'Update' + + expect { release.save! }.not_to raise_error + end + end end describe '#assets_count' do diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb index 7ff020f05e8..df3e4b985ab 100644 --- a/spec/presenters/project_presenter_spec.rb +++ b/spec/presenters/project_presenter_spec.rb @@ -73,8 +73,6 @@ RSpec.describe ProjectPresenter do context 'when repository is not empty' do let_it_be(:project) { create(:project, :public, :repository) } - let(:release) { create(:release, project: project, author: user) } - it 'returns files and readme if user has repository access' do allow(presenter).to receive(:can?).with(nil, :download_code, project).and_return(true) @@ -98,6 +96,9 @@ RSpec.describe ProjectPresenter do end it 'returns releases anchor' do + user = create(:user) + release = create(:release, project: project, author: user) + expect(release).to be_truthy expect(presenter.releases_anchor_data).to have_attributes( is_link: true, diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb index 3ea8b38e20f..923e12a3c06 100644 --- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb +++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb @@ -53,7 +53,7 @@ RSpec.describe 'Creating an incident timeline event' do }, 'note' => note, 'action' => 'comment', - 'editable' => false, + 'editable' => true, 'occurredAt' => event_occurred_at.iso8601 ) end diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb index faff3bfe23a..85208869ad9 100644 --- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb @@ -56,7 +56,7 @@ RSpec.describe 'Removing an incident timeline event' do }, 'note' => timeline_event.note, 'noteHtml' => timeline_event.note_html, - 'editable' => false, + 'editable' => true, 'action' => timeline_event.action, 'occurredAt' => timeline_event.occurred_at.iso8601, 'createdAt' => timeline_event.created_at.iso8601, diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb index b92f6af1d3d..9272e218172 100644 --- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb +++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb @@ -55,7 +55,7 @@ RSpec.describe 'Promote an incident timeline event from a comment' do }, 'note' => comment.note, 'action' => 'comment', - 'editable' => false, + 'editable' => true, 'occurredAt' => comment.created_at.iso8601 ) end diff --git a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb index 708fa96986c..31fef75f679 100644 --- a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb +++ b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb @@ -94,7 +94,7 @@ RSpec.describe 'getting incident timeline events' do 'id' => promoted_from_note.to_global_id.to_s, 'body' => promoted_from_note.note }, - 'editable' => false, + 'editable' => true, 'action' => timeline_event.action, 'occurredAt' => timeline_event.occurred_at.iso8601, 'createdAt' => timeline_event.created_at.iso8601, diff --git a/spec/services/incident_management/timeline_events/create_service_spec.rb b/spec/services/incident_management/timeline_events/create_service_spec.rb index 38ce15e74f1..a88a9400832 100644 --- a/spec/services/incident_management/timeline_events/create_service_spec.rb +++ b/spec/services/incident_management/timeline_events/create_service_spec.rb @@ -18,6 +18,7 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do } end + let(:editable) { false } let(:current_user) { user_with_permissions } let(:service) { described_class.new(incident, current_user, args) } @@ -45,6 +46,7 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do expect(result.project).to eq(project) expect(result.note).to eq(args[:note]) expect(result.promoted_from_note).to eq(comment) + expect(result.editable).to eq(editable) end end @@ -90,6 +92,30 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do end end + context 'with editable param' do + let(:args) do + { + note: 'note', + occurred_at: Time.current, + action: 'new comment', + promoted_from_note: comment, + editable: editable + } + end + + context 'when editable is true' do + let(:editable) { true } + + it_behaves_like 'success response' + end + + context 'when editable is false' do + let(:editable) { false } + + it_behaves_like 'success response' + end + end + it 'successfully creates a database record', :aggregate_failures do expect { execute }.to change { ::IncidentManagement::TimelineEvent.count }.by(1) end diff --git a/spec/services/incident_management/timeline_events/update_service_spec.rb b/spec/services/incident_management/timeline_events/update_service_spec.rb index 8bc0e5ce0ed..b1699e993d5 100644 --- a/spec/services/incident_management/timeline_events/update_service_spec.rb +++ b/spec/services/incident_management/timeline_events/update_service_spec.rb @@ -135,6 +135,14 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do execute end end + + context 'when timeline event is non-editable' do + let!(:timeline_event) do + create(:incident_management_timeline_event, :non_editable, project: project, incident: incident) + end + + it_behaves_like 'error response', 'You cannot edit this timeline event.' + end end context 'when user does not have permissions' do diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb index 2fd1b2e9048..591f7973454 100644 --- a/spec/support/shared_examples/features/content_editor_shared_examples.rb +++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb @@ -76,4 +76,49 @@ RSpec.shared_examples 'edits content using the content editor' do expect(find('[data-testid="code-block-bubble-menu"]')).to have_text('Custom (nomnoml)') end end + + describe 'mermaid diagram' do + before do + find(content_editor_testid).send_keys [:enter, :enter] + + find(content_editor_testid).send_keys '```mermaid ' + find(content_editor_testid).send_keys ['graph TD;', :enter, ' JohnDoe12 --> HelloWorld34'] + end + + it 'renders and updates the diagram correctly in a sandboxed iframe' do + iframe = find(content_editor_testid).find('iframe') + expect(iframe['src']).to include('/-/sandbox/mermaid') + + within_frame(iframe) do + expect(find('svg').text).to include('JohnDoe12') + expect(find('svg').text).to include('HelloWorld34') + end + + expect(iframe['height'].to_i).to be > 100 + + find(content_editor_testid).send_keys [:enter, ' JaneDoe34 --> HelloWorld56'] + + within_frame(iframe) do + page.has_content?('JaneDoe34') + + expect(find('svg').text).to include('JaneDoe34') + expect(find('svg').text).to include('HelloWorld56') + end + end + + it 'toggles the diagram when preview button is clicked' do + find('[data-testid="preview-diagram"]').click + + expect(find(content_editor_testid)).not_to have_selector('iframe') + + find('[data-testid="preview-diagram"]').click + + iframe = find(content_editor_testid).find('iframe') + + within_frame(iframe) do + expect(find('svg').text).to include('JohnDoe12') + expect(find('svg').text).to include('HelloWorld34') + end + end + end end diff --git a/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb index b989dbc6524..cd591248ff6 100644 --- a/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb @@ -21,6 +21,7 @@ RSpec.shared_examples 'creating an incident timeline event' do expect(timeline_event.occurred_at.to_s).to eq(expected_timeline_event.occurred_at) expect(timeline_event.incident).to eq(expected_timeline_event.incident) expect(timeline_event.author).to eq(expected_timeline_event.author) + expect(timeline_event.editable).to eq(expected_timeline_event.editable) end end |