diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-20 09:08:43 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-20 09:08:43 +0000 |
commit | f5f1f221ba08228dbbdd7080509028a7cac2fce2 (patch) | |
tree | 7a95ad0d16829f719c429276a8ed4ddaa097392a | |
parent | 1ad2f1981f05721d92d04c490cfc0f234737fec1 (diff) | |
download | gitlab-ce-f5f1f221ba08228dbbdd7080509028a7cac2fce2.tar.gz |
Add latest changes from gitlab-org/gitlab@master
29 files changed, 1726 insertions, 254 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index 657385ba66d..d34b133edee 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -442,6 +442,15 @@ Graphql/JSONType: - 'spec/**/*.rb' - 'ee/spec/**/*.rb' +Graphql/OldTypes: + Enabled: true + Include: + - 'app/graphql/**/*' + - 'ee/app/graphql/**/*' + Exclude: + - 'spec/**/*.rb' + - 'ee/spec/**/*.rb' + RSpec/EnvAssignment: Enable: true Include: diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index 61593c3b464..084ae1a0e41 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -23,6 +23,410 @@ Graphql/Descriptions: - 'ee/app/graphql/types/vulnerability_severity_enum.rb' - 'ee/app/graphql/types/vulnerability_state_enum.rb' +# WIP See https://gitlab.com/gitlab-org/gitlab/-/issues/336292 +Graphql/OldTypes: + Exclude: + - 'spec/**/*.rb' + - 'ee/spec/**/*.rb' + - 'app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb' + - 'app/graphql/mutations/alert_management/base.rb' + - 'app/graphql/mutations/alert_management/http_integration/create.rb' + - 'app/graphql/mutations/alert_management/http_integration/update.rb' + - 'app/graphql/mutations/alert_management/prometheus_integration/create.rb' + - 'app/graphql/mutations/alert_management/prometheus_integration/update.rb' + - 'app/graphql/mutations/award_emojis/base.rb' + - 'app/graphql/mutations/award_emojis/toggle.rb' + - 'app/graphql/mutations/boards/common_mutation_arguments.rb' + - 'app/graphql/mutations/boards/issues/issue_move_list.rb' + - 'app/graphql/mutations/boards/lists/base_create.rb' + - 'app/graphql/mutations/boards/lists/base_update.rb' + - 'app/graphql/mutations/branches/create.rb' + - 'app/graphql/mutations/ci/ci_cd_settings_update.rb' + - 'app/graphql/mutations/ci/job_token_scope/add_project.rb' + - 'app/graphql/mutations/ci/job_token_scope/remove_project.rb' + - 'app/graphql/mutations/ci/runner/update.rb' + - 'app/graphql/mutations/ci/runners_registration_token/reset.rb' + - 'app/graphql/mutations/commits/create.rb' + - 'app/graphql/mutations/concerns/mutations/can_mutate_spammable.rb' + - 'app/graphql/mutations/concerns/mutations/resolves_resource_parent.rb' + - 'app/graphql/mutations/concerns/mutations/resolves_subscription.rb' + - 'app/graphql/mutations/container_expiration_policies/update.rb' + - 'app/graphql/mutations/custom_emoji/create.rb' + - 'app/graphql/mutations/design_management/base.rb' + - 'app/graphql/mutations/discussions/toggle_resolve.rb' + - 'app/graphql/mutations/environments/canary_ingress/update.rb' + - 'app/graphql/mutations/issues/base.rb' + - 'app/graphql/mutations/issues/common_mutation_arguments.rb' + - 'app/graphql/mutations/issues/create.rb' + - 'app/graphql/mutations/issues/move.rb' + - 'app/graphql/mutations/issues/set_confidential.rb' + - 'app/graphql/mutations/issues/set_locked.rb' + - 'app/graphql/mutations/issues/set_subscription.rb' + - 'app/graphql/mutations/issues/update.rb' + - 'app/graphql/mutations/jira_import/import_users.rb' + - 'app/graphql/mutations/jira_import/start.rb' + - 'app/graphql/mutations/labels/create.rb' + - 'app/graphql/mutations/merge_requests/base.rb' + - 'app/graphql/mutations/merge_requests/create.rb' + - 'app/graphql/mutations/merge_requests/set_draft.rb' + - 'app/graphql/mutations/merge_requests/set_locked.rb' + - 'app/graphql/mutations/merge_requests/set_subscription.rb' + - 'app/graphql/mutations/merge_requests/set_wip.rb' + - 'app/graphql/mutations/merge_requests/update.rb' + - 'app/graphql/mutations/metrics/dashboard/annotations/create.rb' + - 'app/graphql/mutations/namespace/package_settings/update.rb' + - 'app/graphql/mutations/notes/create/base.rb' + - 'app/graphql/mutations/notes/update/image_diff_note.rb' + - 'app/graphql/mutations/notes/update/note.rb' + - 'app/graphql/mutations/release_asset_links/create.rb' + - 'app/graphql/mutations/release_asset_links/update.rb' + - 'app/graphql/mutations/releases/base.rb' + - 'app/graphql/mutations/releases/create.rb' + - 'app/graphql/mutations/releases/delete.rb' + - 'app/graphql/mutations/releases/update.rb' + - 'app/graphql/mutations/security/ci_configuration/base_security_analyzer.rb' + - 'app/graphql/mutations/security/ci_configuration/configure_sast.rb' + - 'app/graphql/mutations/security/ci_configuration/configure_secret_detection.rb' + - 'app/graphql/mutations/snippets/create.rb' + - 'app/graphql/mutations/snippets/update.rb' + - 'app/graphql/mutations/user_callouts/create.rb' + - 'app/graphql/resolvers/alert_management/alert_resolver.rb' + - 'app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb' + - 'app/graphql/resolvers/blobs_resolver.rb' + - 'app/graphql/resolvers/ci/config_resolver.rb' + - 'app/graphql/resolvers/ci/runners_resolver.rb' + - 'app/graphql/resolvers/ci/template_resolver.rb' + - 'app/graphql/resolvers/concerns/group_issuable_resolver.rb' + - 'app/graphql/resolvers/concerns/issue_resolver_arguments.rb' + - 'app/graphql/resolvers/concerns/resolves_pipelines.rb' + - 'app/graphql/resolvers/container_repositories_resolver.rb' + - 'app/graphql/resolvers/design_management/design_resolver.rb' + - 'app/graphql/resolvers/design_management/version/design_at_version_resolver.rb' + - 'app/graphql/resolvers/design_management/version_in_collection_resolver.rb' + - 'app/graphql/resolvers/design_management/versions_resolver.rb' + - 'app/graphql/resolvers/environments_resolver.rb' + - 'app/graphql/resolvers/full_path_resolver.rb' + - 'app/graphql/resolvers/group_labels_resolver.rb' + - 'app/graphql/resolvers/group_milestones_resolver.rb' + - 'app/graphql/resolvers/labels_resolver.rb' + - 'app/graphql/resolvers/members_resolver.rb' + - 'app/graphql/resolvers/merge_request_resolver.rb' + - 'app/graphql/resolvers/merge_requests_resolver.rb' + - 'app/graphql/resolvers/metrics/dashboard_resolver.rb' + - 'app/graphql/resolvers/milestones_resolver.rb' + - 'app/graphql/resolvers/namespace_projects_resolver.rb' + - 'app/graphql/resolvers/packages_base_resolver.rb' + - 'app/graphql/resolvers/project_milestones_resolver.rb' + - 'app/graphql/resolvers/project_pipeline_resolver.rb' + - 'app/graphql/resolvers/projects/jira_projects_resolver.rb' + - 'app/graphql/resolvers/projects/services_resolver.rb' + - 'app/graphql/resolvers/projects_resolver.rb' + - 'app/graphql/resolvers/release_resolver.rb' + - 'app/graphql/resolvers/repository_branch_names_resolver.rb' + - 'app/graphql/resolvers/snippets_resolver.rb' + - 'app/graphql/resolvers/terraform/states_resolver.rb' + - 'app/graphql/resolvers/tree_resolver.rb' + - 'app/graphql/resolvers/user_resolver.rb' + - 'app/graphql/resolvers/user_starred_projects_resolver.rb' + - 'app/graphql/resolvers/users_resolver.rb' + - 'app/graphql/types/access_level_type.rb' + - 'app/graphql/types/admin/analytics/usage_trends/measurement_type.rb' + - 'app/graphql/types/admin/sidekiq_queues/delete_jobs_response_type.rb' + - 'app/graphql/types/alert_management/alert_status_counts_type.rb' + - 'app/graphql/types/alert_management/alert_type.rb' + - 'app/graphql/types/alert_management/integration_type.rb' + - 'app/graphql/types/award_emojis/award_emoji_type.rb' + - 'app/graphql/types/blob_viewer_type.rb' + - 'app/graphql/types/board_list_type.rb' + - 'app/graphql/types/board_type.rb' + - 'app/graphql/types/boards/board_issuable_input_base_type.rb' + - 'app/graphql/types/boards/board_issue_input_base_type.rb' + - 'app/graphql/types/boards/board_issue_input_type.rb' + - 'app/graphql/types/branch_type.rb' + - 'app/graphql/types/ci/application_setting_type.rb' + - 'app/graphql/types/ci/build_need_type.rb' + - 'app/graphql/types/ci/ci_cd_setting_type.rb' + - 'app/graphql/types/ci/config/config_type.rb' + - 'app/graphql/types/ci/config/group_type.rb' + - 'app/graphql/types/ci/config/job_type.rb' + - 'app/graphql/types/ci/config/need_type.rb' + - 'app/graphql/types/ci/config/stage_type.rb' + - 'app/graphql/types/ci/detailed_status_type.rb' + - 'app/graphql/types/ci/group_type.rb' + - 'app/graphql/types/ci/job_artifact_type.rb' + - 'app/graphql/types/ci/job_type.rb' + - 'app/graphql/types/ci/pipeline_type.rb' + - 'app/graphql/types/ci/recent_failures_type.rb' + - 'app/graphql/types/ci/runner_architecture_type.rb' + - 'app/graphql/types/ci/runner_platform_type.rb' + - 'app/graphql/types/ci/runner_setup_type.rb' + - 'app/graphql/types/ci/runner_type.rb' + - 'app/graphql/types/ci/stage_type.rb' + - 'app/graphql/types/ci/status_action_type.rb' + - 'app/graphql/types/ci/template_type.rb' + - 'app/graphql/types/ci/test_case_type.rb' + - 'app/graphql/types/ci/test_report_total_type.rb' + - 'app/graphql/types/ci/test_suite_summary_type.rb' + - 'app/graphql/types/ci/test_suite_type.rb' + - 'app/graphql/types/ci_configuration/sast/analyzers_entity_input_type.rb' + - 'app/graphql/types/ci_configuration/sast/analyzers_entity_type.rb' + - 'app/graphql/types/ci_configuration/sast/entity_input_type.rb' + - 'app/graphql/types/ci_configuration/sast/entity_type.rb' + - 'app/graphql/types/ci_configuration/sast/options_entity_type.rb' + - 'app/graphql/types/container_expiration_policy_type.rb' + - 'app/graphql/types/container_repository_tag_type.rb' + - 'app/graphql/types/container_repository_type.rb' + - 'app/graphql/types/countable_connection_type.rb' + - 'app/graphql/types/custom_emoji_type.rb' + - 'app/graphql/types/design_management/design_fields.rb' + - 'app/graphql/types/design_management/version_type.rb' + - 'app/graphql/types/diff_paths_input_type.rb' + - 'app/graphql/types/diff_refs_type.rb' + - 'app/graphql/types/diff_stats_summary_type.rb' + - 'app/graphql/types/diff_stats_type.rb' + - 'app/graphql/types/environment_type.rb' + - 'app/graphql/types/error_tracking/sentry_detailed_error_type.rb' + - 'app/graphql/types/error_tracking/sentry_error_collection_type.rb' + - 'app/graphql/types/error_tracking/sentry_error_frequency_type.rb' + - 'app/graphql/types/error_tracking/sentry_error_stack_trace_context_type.rb' + - 'app/graphql/types/error_tracking/sentry_error_stack_trace_entry_type.rb' + - 'app/graphql/types/error_tracking/sentry_error_stack_trace_type.rb' + - 'app/graphql/types/error_tracking/sentry_error_tags_type.rb' + - 'app/graphql/types/error_tracking/sentry_error_type.rb' + - 'app/graphql/types/event_type.rb' + - 'app/graphql/types/evidence_type.rb' + - 'app/graphql/types/grafana_integration_type.rb' + - 'app/graphql/types/invitation_interface.rb' + - 'app/graphql/types/issue_type.rb' + - 'app/graphql/types/issues/negated_issue_filter_input_type.rb' + - 'app/graphql/types/jira_import_type.rb' + - 'app/graphql/types/jira_user_type.rb' + - 'app/graphql/types/jira_users_mapping_input_type.rb' + - 'app/graphql/types/label_type.rb' + - 'app/graphql/types/member_interface.rb' + - 'app/graphql/types/merge_request_type.rb' + - 'app/graphql/types/metadata/kas_type.rb' + - 'app/graphql/types/metadata_type.rb' + - 'app/graphql/types/metrics/dashboard_type.rb' + - 'app/graphql/types/metrics/dashboards/annotation_type.rb' + - 'app/graphql/types/milestone_stats_type.rb' + - 'app/graphql/types/milestone_type.rb' + - 'app/graphql/types/namespace/package_settings_type.rb' + - 'app/graphql/types/namespace_type.rb' + - 'app/graphql/types/notes/diff_image_position_input_type.rb' + - 'app/graphql/types/notes/diff_position_base_input_type.rb' + - 'app/graphql/types/notes/diff_position_input_type.rb' + - 'app/graphql/types/notes/diff_position_type.rb' + - 'app/graphql/types/notes/note_type.rb' + - 'app/graphql/types/notes/update_diff_image_position_input_type.rb' + - 'app/graphql/types/packages/composer/json_type.rb' + - 'app/graphql/types/packages/composer/metadatum_type.rb' + - 'app/graphql/types/packages/conan/file_metadatum_type.rb' + - 'app/graphql/types/packages/conan/metadatum_type.rb' + - 'app/graphql/types/packages/maven/metadatum_type.rb' + - 'app/graphql/types/packages/nuget/metadatum_type.rb' + - 'app/graphql/types/packages/package_file_type.rb' + - 'app/graphql/types/packages/package_tag_type.rb' + - 'app/graphql/types/packages/package_type.rb' + - 'app/graphql/types/packages/pypi/metadatum_type.rb' + - 'app/graphql/types/project_type.rb' + - 'app/graphql/types/projects/service_type.rb' + - 'app/graphql/types/projects/services/jira_project_type.rb' + - 'app/graphql/types/prometheus_alert_type.rb' + - 'app/graphql/types/query_complexity_type.rb' + - 'app/graphql/types/release_asset_link_shared_input_arguments.rb' + - 'app/graphql/types/release_asset_link_type.rb' + - 'app/graphql/types/release_assets_type.rb' + - 'app/graphql/types/release_links_type.rb' + - 'app/graphql/types/release_source_type.rb' + - 'app/graphql/types/release_type.rb' + - 'app/graphql/types/repository/blob_type.rb' + - 'app/graphql/types/repository_type.rb' + - 'app/graphql/types/resolvable_interface.rb' + - 'app/graphql/types/snippet_type.rb' + - 'app/graphql/types/snippets/blob_action_input_type.rb' + - 'app/graphql/types/snippets/blob_type.rb' + - 'app/graphql/types/task_completion_status.rb' + - 'app/graphql/types/terraform/state_type.rb' + - 'app/graphql/types/terraform/state_version_type.rb' + - 'app/graphql/types/timelog_type.rb' + - 'app/graphql/types/todo_type.rb' + - 'app/graphql/types/tree/blob_type.rb' + - 'app/graphql/types/tree/entry_type.rb' + - 'app/graphql/types/tree/tree_entry_type.rb' + - 'app/graphql/types/user_status_type.rb' + - 'ee/app/graphql/ee/mutations/ci/ci_cd_settings_update.rb' + - 'ee/app/graphql/ee/resolvers/issues_resolver.rb' + - 'ee/app/graphql/ee/resolvers/namespace_projects_resolver.rb' + - 'ee/app/graphql/ee/types/board_list_type.rb' + - 'ee/app/graphql/ee/types/boards/board_issue_input_base_type.rb' + - 'ee/app/graphql/ee/types/issue_connection_type.rb' + - 'ee/app/graphql/ee/types/issue_type.rb' + - 'ee/app/graphql/ee/types/issues/negated_issue_filter_input_type.rb' + - 'ee/app/graphql/ee/types/merge_request_type.rb' + - 'ee/app/graphql/ee/types/namespace_type.rb' + - 'ee/app/graphql/ee/types/project_type.rb' + - 'ee/app/graphql/mutations/app_sec/fuzzing/api/ci_configuration/create.rb' + - 'ee/app/graphql/mutations/boards/epic_boards/create.rb' + - 'ee/app/graphql/mutations/boards/epics/create.rb' + - 'ee/app/graphql/mutations/boards/lists/update_limit_metrics.rb' + - 'ee/app/graphql/mutations/boards/scoped_issue_board_arguments.rb' + - 'ee/app/graphql/mutations/boards/update_epic_user_preferences.rb' + - 'ee/app/graphql/mutations/clusters/agent_tokens/create.rb' + - 'ee/app/graphql/mutations/clusters/agents/create.rb' + - 'ee/app/graphql/mutations/compliance_management/frameworks/create.rb' + - 'ee/app/graphql/mutations/concerns/mutations/shared_epic_arguments.rb' + - 'ee/app/graphql/mutations/dast/profiles/create.rb' + - 'ee/app/graphql/mutations/dast/profiles/run.rb' + - 'ee/app/graphql/mutations/dast/profiles/update.rb' + - 'ee/app/graphql/mutations/dast_on_demand_scans/create.rb' + - 'ee/app/graphql/mutations/dast_scanner_profiles/create.rb' + - 'ee/app/graphql/mutations/dast_scanner_profiles/delete.rb' + - 'ee/app/graphql/mutations/dast_scanner_profiles/update.rb' + - 'ee/app/graphql/mutations/dast_site_profiles/create.rb' + - 'ee/app/graphql/mutations/dast_site_profiles/delete.rb' + - 'ee/app/graphql/mutations/dast_site_profiles/update.rb' + - 'ee/app/graphql/mutations/dast_site_tokens/create.rb' + - 'ee/app/graphql/mutations/dast_site_validations/create.rb' + - 'ee/app/graphql/mutations/dast_site_validations/revoke.rb' + - 'ee/app/graphql/mutations/epics/add_issue.rb' + - 'ee/app/graphql/mutations/epics/base.rb' + - 'ee/app/graphql/mutations/epics/set_subscription.rb' + - 'ee/app/graphql/mutations/gitlab_subscriptions/activate.rb' + - 'ee/app/graphql/mutations/incident_management/escalation_policy/create.rb' + - 'ee/app/graphql/mutations/incident_management/escalation_policy/update.rb' + - 'ee/app/graphql/mutations/incident_management/oncall_rotation/create.rb' + - 'ee/app/graphql/mutations/incident_management/oncall_rotation/destroy.rb' + - 'ee/app/graphql/mutations/incident_management/oncall_rotation/update.rb' + - 'ee/app/graphql/mutations/incident_management/oncall_schedule/create.rb' + - 'ee/app/graphql/mutations/incident_management/oncall_schedule/destroy.rb' + - 'ee/app/graphql/mutations/incident_management/oncall_schedule/update.rb' + - 'ee/app/graphql/mutations/issues/common_ee_mutation_arguments.rb' + - 'ee/app/graphql/mutations/issues/promote_to_epic.rb' + - 'ee/app/graphql/mutations/issues/set_weight.rb' + - 'ee/app/graphql/mutations/iterations/cadences/create.rb' + - 'ee/app/graphql/mutations/iterations/cadences/update.rb' + - 'ee/app/graphql/mutations/iterations/create.rb' + - 'ee/app/graphql/mutations/iterations/update.rb' + - 'ee/app/graphql/mutations/quality_management/test_cases/create.rb' + - 'ee/app/graphql/mutations/requirements_management/base_requirement.rb' + - 'ee/app/graphql/mutations/requirements_management/export_requirements.rb' + - 'ee/app/graphql/mutations/requirements_management/update_requirement.rb' + - 'ee/app/graphql/mutations/security/ci_configuration/configure_dependency_scanning.rb' + - 'ee/app/graphql/mutations/security_policy/assign_security_policy_project.rb' + - 'ee/app/graphql/mutations/security_policy/commit_scan_execution_policy.rb' + - 'ee/app/graphql/mutations/security_policy/create_security_policy_project.rb' + - 'ee/app/graphql/mutations/vulnerabilities/dismiss.rb' + - 'ee/app/graphql/resolvers/alert_management/payload_alert_field_resolver.rb' + - 'ee/app/graphql/resolvers/clusters/agents_resolver.rb' + - 'ee/app/graphql/resolvers/concerns/common_requirement_arguments.rb' + - 'ee/app/graphql/resolvers/epic_ancestors_resolver.rb' + - 'ee/app/graphql/resolvers/epics_resolver.rb' + - 'ee/app/graphql/resolvers/geo/geo_node_resolver.rb' + - 'ee/app/graphql/resolvers/instance_security_dashboard/projects_resolver.rb' + - 'ee/app/graphql/resolvers/iterations/cadences_resolver.rb' + - 'ee/app/graphql/resolvers/iterations_resolver.rb' + - 'ee/app/graphql/resolvers/requirements_management/requirements_resolver.rb' + - 'ee/app/graphql/resolvers/vulnerabilities_grade_resolver.rb' + - 'ee/app/graphql/resolvers/vulnerabilities_resolver.rb' + - 'ee/app/graphql/resolvers/vulnerability_severities_count_resolver.rb' + - 'ee/app/graphql/types/admin/cloud_licenses/current_license_type.rb' + - 'ee/app/graphql/types/admin/cloud_licenses/license_type.rb' + - 'ee/app/graphql/types/alert_management/payload_alert_field_input_type.rb' + - 'ee/app/graphql/types/alert_management/payload_alert_field_type.rb' + - 'ee/app/graphql/types/alert_management/payload_alert_mapping_field_type.rb' + - 'ee/app/graphql/types/analytics/devops_adoption/enabled_namespace_type.rb' + - 'ee/app/graphql/types/analytics/devops_adoption/snapshot_type.rb' + - 'ee/app/graphql/types/app_sec/fuzzing/api/scan_profile_type.rb' + - 'ee/app/graphql/types/boards/board_epic_input_type.rb' + - 'ee/app/graphql/types/boards/epic_board_type.rb' + - 'ee/app/graphql/types/boards/epic_list_type.rb' + - 'ee/app/graphql/types/boards/epic_user_preferences_type.rb' + - 'ee/app/graphql/types/burnup_chart_daily_totals_type.rb' + - 'ee/app/graphql/types/ci/code_coverage_activity_type.rb' + - 'ee/app/graphql/types/ci/code_coverage_summary_type.rb' + - 'ee/app/graphql/types/ci/code_quality_degradation_type.rb' + - 'ee/app/graphql/types/clusters/agent_token_type.rb' + - 'ee/app/graphql/types/clusters/agent_type.rb' + - 'ee/app/graphql/types/compliance_management/compliance_framework_input_type.rb' + - 'ee/app/graphql/types/compliance_management/compliance_framework_type.rb' + - 'ee/app/graphql/types/dast/profile_branch_type.rb' + - 'ee/app/graphql/types/dast/profile_type.rb' + - 'ee/app/graphql/types/dast/site_profile_auth_input_type.rb' + - 'ee/app/graphql/types/dast/site_profile_auth_type.rb' + - 'ee/app/graphql/types/dast_scanner_profile_type.rb' + - 'ee/app/graphql/types/dast_site_profile_type.rb' + - 'ee/app/graphql/types/dast_site_validation_type.rb' + - 'ee/app/graphql/types/dora_metric_type.rb' + - 'ee/app/graphql/types/epic_descendant_count_type.rb' + - 'ee/app/graphql/types/epic_descendant_weight_sum_type.rb' + - 'ee/app/graphql/types/epic_health_status_type.rb' + - 'ee/app/graphql/types/epic_issue_type.rb' + - 'ee/app/graphql/types/epic_type.rb' + - 'ee/app/graphql/types/epics/negated_epic_filter_input_type.rb' + - 'ee/app/graphql/types/external_issue_type.rb' + - 'ee/app/graphql/types/geo/geo_node_type.rb' + - 'ee/app/graphql/types/geo/group_wiki_repository_registry_type.rb' + - 'ee/app/graphql/types/geo/lfs_object_registry_type.rb' + - 'ee/app/graphql/types/geo/merge_request_diff_registry_type.rb' + - 'ee/app/graphql/types/geo/package_file_registry_type.rb' + - 'ee/app/graphql/types/geo/pipeline_artifact_registry_type.rb' + - 'ee/app/graphql/types/geo/registry_type.rb' + - 'ee/app/graphql/types/geo/snippet_repository_registry_type.rb' + - 'ee/app/graphql/types/geo/terraform_state_version_registry_type.rb' + - 'ee/app/graphql/types/group_release_stats_type.rb' + - 'ee/app/graphql/types/incident_management/escalation_policy_type.rb' + - 'ee/app/graphql/types/incident_management/escalation_rule_input_type.rb' + - 'ee/app/graphql/types/incident_management/escalation_rule_type.rb' + - 'ee/app/graphql/types/incident_management/oncall_participant_type.rb' + - 'ee/app/graphql/types/incident_management/oncall_rotation_active_period_input_type.rb' + - 'ee/app/graphql/types/incident_management/oncall_rotation_active_period_type.rb' + - 'ee/app/graphql/types/incident_management/oncall_rotation_date_input_type.rb' + - 'ee/app/graphql/types/incident_management/oncall_rotation_length_input_type.rb' + - 'ee/app/graphql/types/incident_management/oncall_rotation_type.rb' + - 'ee/app/graphql/types/incident_management/oncall_schedule_type.rb' + - 'ee/app/graphql/types/incident_management/oncall_user_input_type.rb' + - 'ee/app/graphql/types/iteration_type.rb' + - 'ee/app/graphql/types/iterations/cadence_type.rb' + - 'ee/app/graphql/types/kas/agent_configuration_type.rb' + - 'ee/app/graphql/types/metric_image_type.rb' + - 'ee/app/graphql/types/network_policy_type.rb' + - 'ee/app/graphql/types/path_lock_type.rb' + - 'ee/app/graphql/types/push_rules_type.rb' + - 'ee/app/graphql/types/requirements_management/requirement_states_count_type.rb' + - 'ee/app/graphql/types/requirements_management/requirement_type.rb' + - 'ee/app/graphql/types/requirements_management/test_report_type.rb' + - 'ee/app/graphql/types/scan_execution_policy_type.rb' + - 'ee/app/graphql/types/scan_type.rb' + - 'ee/app/graphql/types/scanned_resource_type.rb' + - 'ee/app/graphql/types/security_report_summary_section_type.rb' + - 'ee/app/graphql/types/timebox_metrics_type.rb' + - 'ee/app/graphql/types/vulnerabilities_count_by_day_type.rb' + - 'ee/app/graphql/types/vulnerability/issue_link_type.rb' + - 'ee/app/graphql/types/vulnerability_details/base_type.rb' + - 'ee/app/graphql/types/vulnerability_details/boolean_type.rb' + - 'ee/app/graphql/types/vulnerability_details/code_type.rb' + - 'ee/app/graphql/types/vulnerability_details/commit_type.rb' + - 'ee/app/graphql/types/vulnerability_details/diff_type.rb' + - 'ee/app/graphql/types/vulnerability_details/file_location_type.rb' + - 'ee/app/graphql/types/vulnerability_details/int_type.rb' + - 'ee/app/graphql/types/vulnerability_details/markdown_type.rb' + - 'ee/app/graphql/types/vulnerability_details/module_location_type.rb' + - 'ee/app/graphql/types/vulnerability_details/text_type.rb' + - 'ee/app/graphql/types/vulnerability_details/url_type.rb' + - 'ee/app/graphql/types/vulnerability_identifier_type.rb' + - 'ee/app/graphql/types/vulnerability_location/container_scanning_type.rb' + - 'ee/app/graphql/types/vulnerability_location/coverage_fuzzing_type.rb' + - 'ee/app/graphql/types/vulnerability_location/dast_type.rb' + - 'ee/app/graphql/types/vulnerability_location/dependency_scanning_type.rb' + - 'ee/app/graphql/types/vulnerability_location/sast_type.rb' + - 'ee/app/graphql/types/vulnerability_location/secret_detection_type.rb' + - 'ee/app/graphql/types/vulnerability_scanner_type.rb' + - 'ee/app/graphql/types/vulnerability_type.rb' + - 'ee/app/graphql/types/vulnerable_dependency_type.rb' + - 'ee/app/graphql/types/vulnerable_package_type.rb' + - 'ee/app/graphql/types/vulnerable_projects_by_grade_type.rb' + # WIP: See https://gitlab.com/gitlab-org/gitlab/-/issues/220040 Rails/SaveBang: Exclude: diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/cycle_analytics/components/base.vue index 8492f0b73e1..e637bd0d819 100644 --- a/app/assets/javascripts/cycle_analytics/components/base.vue +++ b/app/assets/javascripts/cycle_analytics/components/base.vue @@ -1,16 +1,10 @@ <script> -import { GlIcon, GlEmptyState, GlLoadingIcon, GlSprintf } from '@gitlab/ui'; +import { GlIcon, GlLoadingIcon, GlSprintf } from '@gitlab/ui'; import Cookies from 'js-cookie'; import { mapActions, mapState, mapGetters } from 'vuex'; import PathNavigation from '~/cycle_analytics/components/path_navigation.vue'; +import StageTable from '~/cycle_analytics/components/stage_table.vue'; import { __ } from '~/locale'; -import banner from './banner.vue'; -import stageCodeComponent from './stage_code_component.vue'; -import stageComponent from './stage_component.vue'; -import stageNavItem from './stage_nav_item.vue'; -import stageReviewComponent from './stage_review_component.vue'; -import stageStagingComponent from './stage_staging_component.vue'; -import stageTestComponent from './stage_test_component.vue'; const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; @@ -18,19 +12,10 @@ export default { name: 'CycleAnalytics', components: { GlIcon, - GlEmptyState, GlLoadingIcon, GlSprintf, - banner, - 'stage-issue-component': stageComponent, - 'stage-plan-component': stageComponent, - 'stage-code-component': stageCodeComponent, - 'stage-test-component': stageTestComponent, - 'stage-review-component': stageReviewComponent, - 'stage-staging-component': stageStagingComponent, - 'stage-production-component': stageComponent, - 'stage-nav-item': stageNavItem, PathNavigation, + StageTable, }, props: { noDataSvgPath: { @@ -75,12 +60,20 @@ export default { return !this.isLoadingStage && this.selectedStage; }, emptyStageTitle() { + if (this.displayNoAccess) { + return __('You need permission.'); + } return this.selectedStageError ? this.selectedStageError : __("We don't have enough data to show this stage."); }, emptyStageText() { - return !this.selectedStageError ? this.selectedStage.emptyStageText : ''; + if (this.displayNoAccess) { + return __('Want to see the data? Please ask an administrator for access.'); + } + return !this.selectedStageError && this.selectedStage?.emptyStageText + ? this.selectedStage?.emptyStageText + : ''; }, }, methods: { @@ -160,72 +153,16 @@ export default { </div> </div> </div> - <div class="stage-panel-container" data-testid="vsa-stage-table"> - <div class="card stage-panel gl-px-5"> - <div class="card-header border-bottom-0"> - <nav class="col-headers"> - <ul class="gl-display-flex gl-justify-content-space-between gl-list-style-none"> - <li> - <span v-if="selectedStage" class="stage-name font-weight-bold">{{ - selectedStage.legend ? __(selectedStage.legend) : __('Related Issues') - }}</span> - <span - class="has-tooltip" - data-placement="top" - :title=" - __('The collection of events added to the data gathered for that stage.') - " - aria-hidden="true" - > - <gl-icon name="question-o" class="gl-text-gray-500" /> - </span> - </li> - <li> - <span class="stage-name font-weight-bold">{{ __('Time') }}</span> - <span - class="has-tooltip" - data-placement="top" - :title="__('The time taken by each data entry gathered by that stage.')" - aria-hidden="true" - > - <gl-icon name="question-o" class="gl-text-gray-500" /> - </span> - </li> - </ul> - </nav> - </div> - <div class="stage-panel-body"> - <section class="stage-events gl-overflow-auto gl-w-full"> - <gl-loading-icon v-if="isLoadingStage" size="lg" /> - <template v-else> - <gl-empty-state - v-if="displayNoAccess" - class="js-empty-state" - :title="__('You need permission.')" - :svg-path="noAccessSvgPath" - :description="__('Want to see the data? Please ask an administrator for access.')" - /> - <template v-else> - <gl-empty-state - v-if="displayNotEnoughData" - class="js-empty-state" - :description="emptyStageText" - :svg-path="noDataSvgPath" - :title="emptyStageTitle" - /> - <component - :is="selectedStage.component" - v-if="displayStageEvents" - :stage="selectedStage" - :items="selectedStageEvents" - data-testid="stage-table-events" - /> - </template> - </template> - </section> - </div> - </div> - </div> + <stage-table + :is-loading="isLoading || isLoadingStage" + :stage-events="selectedStageEvents" + :selected-stage="selectedStage" + :stage-count="null" + :empty-state-title="emptyStageTitle" + :empty-state-message="emptyStageText" + :no-data-svg-path="noDataSvgPath" + :pagination="null" + /> </div> </div> </template> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_table.vue b/app/assets/javascripts/cycle_analytics/components/stage_table.vue new file mode 100644 index 00000000000..2e225d90f9c --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_table.vue @@ -0,0 +1,305 @@ +<script> +import { + GlEmptyState, + GlIcon, + GlLink, + GlLoadingIcon, + GlPagination, + GlTable, + GlBadge, +} from '@gitlab/ui'; +import FormattedStageCount from '~/cycle_analytics/components/formatted_stage_count.vue'; +import { __ } from '~/locale'; +import Tracking from '~/tracking'; +import { + NOT_ENOUGH_DATA_ERROR, + PAGINATION_SORT_FIELD_END_EVENT, + PAGINATION_SORT_FIELD_DURATION, + PAGINATION_SORT_DIRECTION_ASC, + PAGINATION_SORT_DIRECTION_DESC, + STAGE_TITLE_STAGING, + STAGE_TITLE_TEST, +} from '../constants'; +import TotalTime from './total_time_component.vue'; + +const DEFAULT_WORKFLOW_TITLE_PROPERTIES = { + thClass: 'gl-w-half', + key: PAGINATION_SORT_FIELD_END_EVENT, + sortable: true, +}; +const WORKFLOW_COLUMN_TITLES = { + issues: { ...DEFAULT_WORKFLOW_TITLE_PROPERTIES, label: __('Issues') }, + jobs: { ...DEFAULT_WORKFLOW_TITLE_PROPERTIES, label: __('Jobs') }, + deployments: { ...DEFAULT_WORKFLOW_TITLE_PROPERTIES, label: __('Deployments') }, + mergeRequests: { ...DEFAULT_WORKFLOW_TITLE_PROPERTIES, label: __('Merge requests') }, +}; + +export default { + name: 'StageTable', + components: { + GlEmptyState, + GlIcon, + GlLink, + GlLoadingIcon, + GlPagination, + GlTable, + GlBadge, + TotalTime, + FormattedStageCount, + }, + mixins: [Tracking.mixin()], + props: { + selectedStage: { + type: Object, + required: false, + default: () => ({ custom: false }), + }, + isLoading: { + type: Boolean, + required: true, + }, + stageEvents: { + type: Array, + required: true, + }, + stageCount: { + type: Number, + required: false, + default: null, + }, + noDataSvgPath: { + type: String, + required: true, + }, + emptyStateTitle: { + type: String, + required: false, + default: null, + }, + emptyStateMessage: { + type: String, + required: false, + default: '', + }, + pagination: { + type: Object, + required: false, + default: null, + }, + }, + data() { + if (this.pagination) { + const { + pagination: { sort, direction }, + } = this; + return { + sort, + direction, + sortDesc: direction === PAGINATION_SORT_DIRECTION_DESC, + }; + } + return { sort: null, direction: null, sortDesc: null }; + }, + computed: { + isEmptyStage() { + return !this.stageEvents.length; + }, + emptyStateTitleText() { + return this.emptyStateTitle || NOT_ENOUGH_DATA_ERROR; + }, + isDefaultTestStage() { + const { selectedStage } = this; + return ( + !selectedStage.custom && selectedStage.title?.toLowerCase().trim() === STAGE_TITLE_TEST + ); + }, + isDefaultStagingStage() { + const { selectedStage } = this; + return ( + !selectedStage.custom && selectedStage.title?.toLowerCase().trim() === STAGE_TITLE_STAGING + ); + }, + isMergeRequestStage() { + const [firstEvent] = this.stageEvents; + return this.isMrLink(firstEvent.url); + }, + workflowTitle() { + if (this.isDefaultTestStage) { + return WORKFLOW_COLUMN_TITLES.jobs; + } else if (this.isDefaultStagingStage) { + return WORKFLOW_COLUMN_TITLES.deployments; + } else if (this.isMergeRequestStage) { + return WORKFLOW_COLUMN_TITLES.mergeRequests; + } + return WORKFLOW_COLUMN_TITLES.issues; + }, + fields() { + return [ + this.workflowTitle, + { + key: PAGINATION_SORT_FIELD_DURATION, + label: __('Time'), + thClass: 'gl-w-half', + sortable: true, + }, + ]; + }, + prevPage() { + return Math.max(this.pagination.page - 1, 0); + }, + nextPage() { + return this.pagination.hasNextPage ? this.pagination.page + 1 : null; + }, + }, + methods: { + isMrLink(url = '') { + return url.includes('/merge_request'); + }, + itemId({ url, iid }) { + return this.isMrLink(url) ? `!${iid}` : `#${iid}`; + }, + itemTitle(item) { + return item.title || item.name; + }, + onSelectPage(page) { + const { sort, direction } = this.pagination; + this.track('click_button', { label: 'pagination' }); + this.$emit('handleUpdatePagination', { sort, direction, page }); + }, + onSort({ sortBy, sortDesc }) { + const direction = sortDesc ? PAGINATION_SORT_DIRECTION_DESC : PAGINATION_SORT_DIRECTION_ASC; + this.sort = sortBy; + this.sortDesc = sortDesc; + this.$emit('handleUpdatePagination', { sort: sortBy, direction }); + this.track('click_button', { label: `sort_${sortBy}_${direction}` }); + }, + }, +}; +</script> +<template> + <div data-testid="vsa-stage-table"> + <gl-loading-icon v-if="isLoading" class="gl-mt-4" size="md" /> + <gl-empty-state + v-else-if="isEmptyStage" + :title="emptyStateTitleText" + :description="emptyStateMessage" + :svg-path="noDataSvgPath" + /> + <gl-table + v-else + head-variant="white" + stacked="lg" + thead-class="border-bottom" + show-empty + :sort-by.sync="sort" + :sort-direction.sync="direction" + :sort-desc.sync="sortDesc" + :fields="fields" + :items="stageEvents" + :empty-text="emptyStateMessage" + @sort-changed="onSort" + > + <template v-if="stageCount" #head(end_event)="data"> + <span>{{ data.label }}</span + ><gl-badge class="gl-ml-2" size="sm" + ><formatted-stage-count :stage-count="stageCount" + /></gl-badge> + </template> + <template #cell(end_event)="{ item }"> + <div data-testid="vsa-stage-event"> + <div v-if="item.id" data-testid="vsa-stage-content"> + <p class="gl-m-0"> + <template v-if="isDefaultTestStage"> + <span + class="icon-build-status gl-vertical-align-middle gl-text-green-500" + data-testid="vsa-stage-event-build-status" + > + <gl-icon name="status_success" :size="14" /> + </span> + <gl-link + class="gl-text-black-normal item-build-name" + data-testid="vsa-stage-event-build-name" + :href="item.url" + > + {{ item.name }} + </gl-link> + · + </template> + <gl-link class="gl-text-black-normal pipeline-id" :href="item.url" + >#{{ item.id }}</gl-link + > + <gl-icon :size="16" name="fork" /> + <gl-link + v-if="item.branch" + :href="item.branch.url" + class="gl-text-black-normal ref-name" + >{{ item.branch.name }}</gl-link + > + <span class="icon-branch gl-text-gray-400"> + <gl-icon name="commit" :size="14" /> + </span> + <gl-link + class="commit-sha" + :href="item.commitUrl" + data-testid="vsa-stage-event-build-sha" + >{{ item.shortSha }}</gl-link + > + </p> + <p class="gl-m-0"> + <span v-if="isDefaultTestStage" data-testid="vsa-stage-event-build-status-date"> + <gl-link class="gl-text-black-normal issue-date" :href="item.url">{{ + item.date + }}</gl-link> + </span> + <span v-else data-testid="vsa-stage-event-build-author-and-date"> + <gl-link class="gl-text-black-normal build-date" :href="item.url">{{ + item.date + }}</gl-link> + {{ s__('ByAuthor|by') }} + <gl-link + class="gl-text-black-normal issue-author-link" + :href="item.author.webUrl" + >{{ item.author.name }}</gl-link + > + </span> + </p> + </div> + <div v-else data-testid="vsa-stage-content"> + <h5 class="gl-font-weight-bold gl-my-1" data-testid="vsa-stage-event-title"> + <gl-link class="gl-text-black-normal" :href="item.url">{{ itemTitle(item) }}</gl-link> + </h5> + <p class="gl-m-0"> + <gl-link class="gl-text-black-normal" :href="item.url">{{ itemId(item) }}</gl-link> + <span class="gl-font-lg">·</span> + <span data-testid="vsa-stage-event-date"> + {{ s__('OpenedNDaysAgo|Opened') }} + <gl-link class="gl-text-black-normal" :href="item.url">{{ + item.createdAt + }}</gl-link> + </span> + <span data-testid="vsa-stage-event-author"> + {{ s__('ByAuthor|by') }} + <gl-link class="gl-text-black-normal" :href="item.author.webUrl">{{ + item.author.name + }}</gl-link> + </span> + </p> + </div> + </div> + </template> + <template #cell(duration)="{ item }"> + <total-time :time="item.totalTime" data-testid="vsa-stage-event-time" /> + </template> + </gl-table> + <gl-pagination + v-if="pagination && !isLoading && !isEmptyStage" + :value="pagination.page" + :prev-page="prevPage" + :next-page="nextPage" + align="center" + class="gl-mt-3" + data-testid="vsa-stage-pagination" + @input="onSelectPage" + /> + </div> +</template> diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue index f52438ca2cb..a5a90a56974 100644 --- a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue @@ -1,4 +1,6 @@ <script> +import { n__, s__ } from '~/locale'; + export default { props: { time: { @@ -11,24 +13,48 @@ export default { hasData() { return Object.keys(this.time).length; }, + calculatedTime() { + const { + time: { days = null, mins = null, hours = null, seconds = null }, + } = this; + + if (days) { + return { + duration: days, + units: n__('day', 'days', days), + }; + } + + if (hours) { + return { + duration: hours, + units: n__('Time|hr', 'Time|hrs', hours), + }; + } + + if (mins && !days) { + return { + duration: mins, + units: n__('Time|min', 'Time|mins', mins), + }; + } + + if ((seconds && this.hasData === 1) || seconds === 0) { + return { + duration: seconds, + units: s__('Time|s'), + }; + } + + return { duration: null, units: null }; + }, }, }; </script> <template> <span class="total-time"> <template v-if="hasData"> - <template v-if="time.days"> - {{ time.days }} <span> {{ n__('day', 'days', time.days) }} </span> - </template> - <template v-if="time.hours"> - {{ time.hours }} <span> {{ n__('Time|hr', 'Time|hrs', time.hours) }} </span> - </template> - <template v-if="time.mins && !time.days"> - {{ time.mins }} <span> {{ n__('Time|min', 'Time|mins', time.mins) }} </span> - </template> - <template v-if="(time.seconds && hasData === 1) || time.seconds === 0"> - {{ time.seconds }} <span> {{ s__('Time|s') }} </span> - </template> + {{ calculatedTime.duration }} <span>{{ calculatedTime.units }}</span> </template> <template v-else> -- </template> </span> diff --git a/app/assets/javascripts/cycle_analytics/constants.js b/app/assets/javascripts/cycle_analytics/constants.js index 97f502326e5..755977f87df 100644 --- a/app/assets/javascripts/cycle_analytics/constants.js +++ b/app/assets/javascripts/cycle_analytics/constants.js @@ -1,3 +1,5 @@ +import { s__ } from '~/locale'; + export const DEFAULT_DAYS_IN_PAST = 30; export const DEFAULT_DAYS_TO_DISPLAY = 30; export const OVERVIEW_STAGE_ID = 'overview'; @@ -7,3 +9,16 @@ export const DEFAULT_VALUE_STREAM = { slug: 'default', name: 'default', }; + +export const NOT_ENOUGH_DATA_ERROR = s__( + "ValueStreamAnalyticsStage|We don't have enough data to show this stage.", +); + +export const PAGINATION_TYPE = 'keyset'; +export const PAGINATION_SORT_FIELD_END_EVENT = 'end_event'; +export const PAGINATION_SORT_FIELD_DURATION = 'duration'; +export const PAGINATION_SORT_DIRECTION_DESC = 'desc'; +export const PAGINATION_SORT_DIRECTION_ASC = 'asc'; + +export const STAGE_TITLE_STAGING = 'staging'; +export const STAGE_TITLE_TEST = 'test'; diff --git a/app/assets/javascripts/cycle_analytics/store/mutations.js b/app/assets/javascripts/cycle_analytics/store/mutations.js index a8b7a607b66..118d5174fd0 100644 --- a/app/assets/javascripts/cycle_analytics/store/mutations.js +++ b/app/assets/javascripts/cycle_analytics/store/mutations.js @@ -47,13 +47,7 @@ export default { state.stages = []; }, [types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS](state, { stages = [] }) { - state.stages = stages.map((s) => ({ - ...convertObjectPropsToCamelCase(s, { deep: true }), - // NOTE: we set the component type here to match the current behaviour - // this can be removed when we migrate to the update stage table - // https://gitlab.com/gitlab-org/gitlab/-/issues/326704 - component: `stage-${s.id}-component`, - })); + state.stages = stages.map((s) => convertObjectPropsToCamelCase(s, { deep: true })); }, [types.RECEIVE_VALUE_STREAM_STAGES_ERROR](state) { state.stages = []; diff --git a/app/assets/javascripts/design_management/components/image.vue b/app/assets/javascripts/design_management/components/image.vue index e64ee4a5a34..8ab94cd2c4b 100644 --- a/app/assets/javascripts/design_management/components/image.vue +++ b/app/assets/javascripts/design_management/components/image.vue @@ -1,6 +1,8 @@ <script> import { GlIcon } from '@gitlab/ui'; import { throttle } from 'lodash'; +import { DESIGN_MARK_APP_START, DESIGN_MAIN_IMAGE_OUTPUT } from '~/performance/constants'; +import { performanceMarkAndMeasure } from '~/performance/utils'; export default { components: { @@ -39,7 +41,9 @@ export default { window.removeEventListener('resize', this.resizeThrottled, false); }, mounted() { - this.onImgLoad(); + if (!this.image) { + this.onImgLoad(); + } this.resizeThrottled = throttle(() => { // NOTE: if imageStyle is set, then baseImageSize @@ -53,6 +57,14 @@ export default { methods: { onImgLoad() { requestIdleCallback(this.setBaseImageSize, { timeout: 1000 }); + performanceMarkAndMeasure({ + measures: [ + { + name: DESIGN_MAIN_IMAGE_OUTPUT, + start: DESIGN_MARK_APP_START, + }, + ], + }); }, onImgError() { this.imageError = true; diff --git a/app/assets/javascripts/design_management/index.js b/app/assets/javascripts/design_management/index.js index aa9f377ef16..11666587265 100644 --- a/app/assets/javascripts/design_management/index.js +++ b/app/assets/javascripts/design_management/index.js @@ -1,4 +1,6 @@ import Vue from 'vue'; +import { DESIGN_MARK_APP_START, DESIGN_MEASURE_BEFORE_APP } from '~/performance/constants'; +import { performanceMarkAndMeasure } from '~/performance/utils'; import App from './components/app.vue'; import apolloProvider from './graphql'; import activeDiscussionQuery from './graphql/queries/active_discussion.query.graphql'; @@ -28,6 +30,16 @@ export default () => { projectPath, issueIid, }, + mounted() { + performanceMarkAndMeasure({ + mark: DESIGN_MARK_APP_START, + measures: [ + { + name: DESIGN_MEASURE_BEFORE_APP, + }, + ], + }); + }, render(createElement) { return createElement(App); }, diff --git a/app/assets/javascripts/performance/constants.js b/app/assets/javascripts/performance/constants.js index b9a9ef215af..28a4257c0c3 100644 --- a/app/assets/javascripts/performance/constants.js +++ b/app/assets/javascripts/performance/constants.js @@ -89,3 +89,14 @@ export const REPO_BLOB_LOAD_VIEWER_FINISH = 'blobviewer-load-viewer-finish'; // Measures export const REPO_BLOB_LOAD_VIEWER = 'Repository File Viewer: loading the viewer'; export const REPO_BLOB_SWITCH_VIEWER = 'Repository File Viewer: switching the viewer'; + +// +// DESIGN MANAGEMENT NAMESPACE +// + +// Marks +export const DESIGN_MARK_APP_START = 'design-app-start'; + +// Measures +export const DESIGN_MEASURE_BEFORE_APP = 'Design Management: Before the Vue app'; +export const DESIGN_MAIN_IMAGE_OUTPUT = 'Design Management: Single image preview'; diff --git a/app/assets/stylesheets/application_dark.scss b/app/assets/stylesheets/application_dark.scss index 30db4e2296d..7d6ccc40278 100644 --- a/app/assets/stylesheets/application_dark.scss +++ b/app/assets/stylesheets/application_dark.scss @@ -58,7 +58,7 @@ body.gl-dark { } } - .md code { + .md :not(pre.code) > code { background-color: $gray-200; } } diff --git a/app/views/admin/application_settings/_realtime.html.haml b/app/views/admin/application_settings/_realtime.html.haml index 545c27d2a7e..6a7ec05d206 100644 --- a/app/views/admin/application_settings/_realtime.html.haml +++ b/app/views/admin/application_settings/_realtime.html.haml @@ -6,7 +6,6 @@ = f.label :polling_interval_multiplier, _('Polling interval multiplier'), class: 'label-bold' = f.text_field :polling_interval_multiplier, class: 'form-control gl-form-input' .form-text.text-muted - = _("Change this value to influence how frequently the GitLab UI polls for updates. If you set the value to 2 all polling intervals are multiplied by 2, which means that polling happens half as frequently. The multiplier can also have a decimal value. The default value (1) is a reasonable choice for the majority of GitLab installations. Set to 0 to completely disable polling.") - = link_to sprite_icon('question-o'), help_page_path('administration/polling') + = _('Multiplier to apply to polling intervals. Decimal values are supported. Defaults to 1.') = f.submit _('Save changes'), class: "gl-button btn btn-confirm" diff --git a/app/views/admin/application_settings/preferences.html.haml b/app/views/admin/application_settings/preferences.html.haml index 0dfc3d7a60d..08e5aaefb39 100644 --- a/app/views/admin/application_settings/preferences.html.haml +++ b/app/views/admin/application_settings/preferences.html.haml @@ -50,11 +50,12 @@ %section.settings.as-realtime.no-animate#js-realtime-settings{ class: ('expanded' if expanded_by_default?) } .settings-header %h4 - = _('Real-time features') + = _('Polling interval multiplier') %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' } = expanded_by_default? ? _('Collapse') : _('Expand') %p - = _('Change this value to influence how frequently the GitLab UI polls for updates.') + = _('Adjust how frequently the GitLab UI polls for updates.') + = link_to _('Learn more.'), help_page_path('administration/polling.md'), target: '_blank', rel: 'noopener noreferrer' .settings-content = render 'realtime' diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 86172499118..8e048da26cf 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -88,12 +88,11 @@ = render_if_exists "projects/home_mirror" - if @project.badges.present? - = cache_if(cache_enabled, [@project, :badges], expires_in: 1.day) do - .project-badges.mb-2 - - @project.badges.each do |badge| - %a.gl-mr-3{ href: badge.rendered_link_url(@project), - target: '_blank', - rel: 'noopener noreferrer' }> - %img.project-badge{ src: badge.rendered_image_url(@project), - 'aria-hidden': true, - alt: 'Project badge' }> + .project-badges.mb-2 + - @project.badges.each do |badge| + %a.gl-mr-3{ href: badge.rendered_link_url(@project), + target: '_blank', + rel: 'noopener noreferrer' }> + %img.project-badge{ src: badge.rendered_image_url(@project), + 'aria-hidden': true, + alt: 'Project badge' }> diff --git a/config.ru b/config.ru index ed76239ef2e..e9964ddc96e 100644 --- a/config.ru +++ b/config.ru @@ -13,7 +13,7 @@ warmup do |app| client.get('/') end -map ENV['RAILS_RELATIVE_URL_ROOT'] || "/" do +map ENV['RAILS_RELATIVE_URL_ROOT'].presence || "/" do use Gitlab::Middleware::ReleaseEnv run Gitlab::Application end diff --git a/doc/administration/polling.md b/doc/administration/polling.md index ec5d6cd45d8..5c4ee837057 100644 --- a/doc/administration/polling.md +++ b/doc/administration/polling.md @@ -4,29 +4,31 @@ group: Distribution info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Polling configuration **(FREE SELF)** +# Polling interval multiplier **(FREE SELF)** -The GitLab UI polls for updates for different resources (issue notes, issue -titles, pipeline statuses, and so on) on a schedule appropriate to the resource. +The GitLab UI polls for updates for different resources (issue notes, issue titles, pipeline +statuses, and so on) on a schedule appropriate to the resource. -To configure the polling interval multiplier: +Adjust the multiplier on these schedules to adjust how frequently the GitLab UI polls for updates. If +you set the multiplier to: + +- A value greater than `1`, UI polling slows down. If you see issues with database load from lots of + clients polling for updates, increasing the multiplier can be a good alternative to disabling + polling completely. For example, if you set the value to `2`, all polling intervals + are multiplied by 2, which means that polling happens half as frequently. +- A value between `0` and `1`, the UI polls more frequently so updates occur more frequently. + **Not recommended**. +- `0`, all polling is disabled. On the next poll, clients stop polling for updates. + +The default value (`1`) is recommended for the majority of GitLab installations. + +## Configure + +To adjust the polling interval multiplier: 1. On the top bar, select **Menu >** **{admin}** **Admin**. 1. On the left sidebar, select **Settings > Preferences**. -1. Expand **Real-time features**. -1. Set a value for the polling interval multiplier. This multiplier is applied - to all resources at once, and decimal values are supported: - - - `1.0` is the default, and recommended for most installations. - - `0` disables UI polling completely. On the next poll, clients stop - polling for updates. - - A value greater than `1` slows polling down. If you see issues with - database load from lots of clients polling for updates, increasing the - multiplier from 1 can be a good compromise, rather than disabling polling - completely. For example, if you set the value to `2`, all polling intervals - are multiplied by 2, which means that polling happens half as frequently. - - A value between `0` and `1` makes the UI poll more frequently (so updates - show in other sessions faster), but is **not recommended**. `1` should be - fast enough. - +1. Expand **Polling interval multiplier**. +1. Set a value for the polling interval multiplier. This multiplier is applied to all resources at + once. 1. Select **Save changes**. diff --git a/doc/user/admin_area/settings/index.md b/doc/user/admin_area/settings/index.md index d21b6c36224..cc3861403e0 100644 --- a/doc/user/admin_area/settings/index.md +++ b/doc/user/admin_area/settings/index.md @@ -115,7 +115,7 @@ To access the default page for Admin Area settings: | [What's new](../../../administration/whats-new.md) | Configure What's new drawer and content. | | [Help page](help_page.md) | Help page text and support page URL. | | [Pages](../../../administration/pages/index.md#custom-domain-verification) | Size and domain settings for static websites | -| [Real-time features](../../../administration/polling.md) | Change this value to influence how frequently the GitLab UI polls for updates. | +| [Polling interval multiplier](../../../administration/polling.md) | Configure how frequently the GitLab UI polls for updates. | | [Gitaly timeouts](gitaly_timeouts.md) | Configure Gitaly timeouts. | | Localization | [Default first day of the week](../../profile/preferences.md) and [Time tracking](../../project/time_tracking.md#limit-displayed-units-to-hours). | diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 8cab2f65726..0877a31e0f9 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -199,13 +199,29 @@ module Gitlab return unless valid_scoped_token?(token, all_available_scopes) - return if project && token.user.project_bot? && !project.bots.include?(token.user) + if project && token.user.project_bot? + return unless token_bot_in_project?(token.user, project) || token_bot_in_group?(token.user, project) + end if can_user_login_with_non_expired_password?(token.user) || token.user.project_bot? Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes)) end end + def token_bot_in_project?(user, project) + project.bots.include?(user) + end + + # rubocop: disable CodeReuse/ActiveRecord + + # A workaround for adding group-level automation is to add the bot user of a project access token as a group member. + # In order to make project access tokens work this way during git authentication, we need to add an additional check for group membership. + # This is a temporary workaround until service accounts are implemented. + def token_bot_in_group?(user, project) + project.group && project.group.members_with_parents.where(user_id: user.id).exists? + end + # rubocop: enable CodeReuse/ActiveRecord + def valid_oauth_token?(token) token && token.accessible? && valid_scoped_token?(token, [:api]) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8e1502bce6f..c865d1ceed2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2212,6 +2212,9 @@ msgstr "" msgid "Adds email participant(s)" msgstr "" +msgid "Adjust how frequently the GitLab UI polls for updates." +msgstr "" + msgid "Adjust your filters/search criteria above. If you believe this may be an error, please refer to the %{linkStart}Geo Troubleshooting%{linkEnd} documentation for more information." msgstr "" @@ -6172,12 +6175,6 @@ msgstr "" msgid "Change template" msgstr "" -msgid "Change this value to influence how frequently the GitLab UI polls for updates." -msgstr "" - -msgid "Change this value to influence how frequently the GitLab UI polls for updates. If you set the value to 2 all polling intervals are multiplied by 2, which means that polling happens half as frequently. The multiplier can also have a decimal value. The default value (1) is a reasonable choice for the majority of GitLab installations. Set to 0 to completely disable polling." -msgstr "" - msgid "Change title" msgstr "" @@ -21477,6 +21474,9 @@ msgstr "" msgid "Multiple uploaders found: %{uploader_types}" msgstr "" +msgid "Multiplier to apply to polling intervals. Decimal values are supported. Defaults to 1." +msgstr "" + msgid "Must match with the %{codeStart}external_url%{codeEnd} in %{codeStart}/etc/gitlab/gitlab.rb%{codeEnd}." msgstr "" @@ -26882,9 +26882,6 @@ msgstr "" msgid "Ready to get started with GitLab? Follow these steps to set up your workspace, plan and commit changes, and deploy your project." msgstr "" -msgid "Real-time features" -msgstr "" - msgid "Reauthenticating with SAML provider." msgstr "" @@ -27061,9 +27058,6 @@ msgstr "" msgid "Rejected (closed)" msgstr "" -msgid "Related Issues" -msgstr "" - msgid "Related feature flags" msgstr "" @@ -32488,9 +32482,6 @@ msgstr "" msgid "The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git." msgstr "" -msgid "The collection of events added to the data gathered for that stage." -msgstr "" - msgid "The comment you are editing has been changed by another user. Would you like to keep your changes and overwrite the new description or discard your changes?" msgstr "" @@ -32853,9 +32844,6 @@ msgstr "" msgid "The tag name can't be changed for an existing release." msgstr "" -msgid "The time taken by each data entry gathered by that stage." -msgstr "" - msgid "The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination." msgstr "" diff --git a/rubocop/cop/graphql/old_types.rb b/rubocop/cop/graphql/old_types.rb new file mode 100644 index 00000000000..2df594c7016 --- /dev/null +++ b/rubocop/cop/graphql/old_types.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# This cop checks for use of older GraphQL types in GraphQL fields +# and arguments. +# GraphQL::ID_TYPE, GraphQL::INT_TYPE, GraphQL::STRING_TYPE, GraphQL::BOOLEAN_TYPE +# +# @example +# +# # bad +# class AwfulClass +# field :some_field, GraphQL::STRING_TYPE +# end +# +# # good +# class GreatClass +# field :some_field, GraphQL::Types::String +# end + +module RuboCop + module Cop + module Graphql + class OldTypes < RuboCop::Cop::Cop + MSG_ID = 'Avoid using GraphQL::ID_TYPE. Use GraphQL::Types::ID instead' + MSG_INT = 'Avoid using GraphQL::INT_TYPE. Use GraphQL::Types::Int instead' + MSG_STRING = 'Avoid using GraphQL::STRING_TYPE. Use GraphQL::Types::String instead' + MSG_BOOLEAN = 'Avoid using GraphQL::BOOLEAN_TYPE. Use GraphQL::Types::Boolean instead' + + def_node_matcher :has_old_type?, <<~PATTERN + (send nil? {:field :argument} + (sym _) + (const (const nil? :GraphQL) ${:ID_TYPE :INT_TYPE :STRING_TYPE :BOOLEAN_TYPE}) + (...)?) + PATTERN + + def on_send(node) + old_constant = has_old_type?(node) + return unless old_constant + + add_offense(node, location: :expression, message: "#{self.class}::MSG_#{old_constant[0..-6]}".constantize) + end + end + end + end +end diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index d0f8767884e..418247c88aa 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -6,6 +6,7 @@ RSpec.describe 'Value Stream Analytics', :js do let_it_be(:user) { create(:user) } let_it_be(:guest) { create(:user) } let_it_be(:project) { create(:project, :repository) } + let_it_be(:stage_table_selector) { '[data-testid="vsa-stage-table"]' } let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } let(:milestone) { create(:milestone, project: project) } @@ -119,13 +120,13 @@ RSpec.describe 'Value Stream Analytics', :js do end it 'needs permissions to see restricted stages' do - expect(find('.stage-events')).to have_content(issue.title) + expect(find(stage_table_selector)).to have_content(issue.title) click_stage('Code') - expect(find('.stage-events')).to have_content('You need permission.') + expect(find(stage_table_selector)).to have_content('You need permission.') click_stage('Review') - expect(find('.stage-events')).to have_content('You need permission.') + expect(find(stage_table_selector)).to have_content('You need permission.') end end @@ -154,21 +155,21 @@ RSpec.describe 'Value Stream Analytics', :js do end def expect_issue_to_be_present - expect(find('.stage-events')).to have_content(issue.title) - expect(find('.stage-events')).to have_content(issue.author.name) - expect(find('.stage-events')).to have_content("##{issue.iid}") + expect(find(stage_table_selector)).to have_content(issue.title) + expect(find(stage_table_selector)).to have_content(issue.author.name) + expect(find(stage_table_selector)).to have_content("##{issue.iid}") end def expect_build_to_be_present - expect(find('.stage-events')).to have_content(@build.ref) - expect(find('.stage-events')).to have_content(@build.short_sha) - expect(find('.stage-events')).to have_content("##{@build.id}") + expect(find(stage_table_selector)).to have_content(@build.ref) + expect(find(stage_table_selector)).to have_content(@build.short_sha) + expect(find(stage_table_selector)).to have_content("##{@build.id}") end def expect_merge_request_to_be_present - expect(find('.stage-events')).to have_content(mr.title) - expect(find('.stage-events')).to have_content(mr.author.name) - expect(find('.stage-events')).to have_content("!#{mr.iid}") + expect(find(stage_table_selector)).to have_content(mr.title) + expect(find(stage_table_selector)).to have_content(mr.author.name) + expect(find(stage_table_selector)).to have_content("!#{mr.iid}") end def click_stage(stage_name) diff --git a/spec/frontend/cycle_analytics/__snapshots__/base_spec.js.snap b/spec/frontend/cycle_analytics/__snapshots__/base_spec.js.snap index 1af612ed029..771625a3e51 100644 --- a/spec/frontend/cycle_analytics/__snapshots__/base_spec.js.snap +++ b/spec/frontend/cycle_analytics/__snapshots__/base_spec.js.snap @@ -1,9 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Value stream analytics component isEmptyStage = true renders the empty stage with \`Not enough data\` message 1`] = `"<gl-empty-state-stub title=\\"We don't have enough data to show this stage.\\" svgpath=\\"path/to/no/data\\" description=\\"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.\\" class=\\"js-empty-state\\"></gl-empty-state-stub>"`; - -exports[`Value stream analytics component isEmptyStage = true with a selectedStageError renders the empty stage with \`There is too much data to calculate\` message 1`] = `"<gl-empty-state-stub title=\\"There is too much data to calculate\\" svgpath=\\"path/to/no/data\\" description=\\"\\" class=\\"js-empty-state\\"></gl-empty-state-stub>"`; - exports[`Value stream analytics component isLoading = true renders the path navigation component with prop \`loading\` set to true 1`] = `"<path-navigation-stub loading=\\"true\\" stages=\\"\\" selectedstage=\\"[object Object]\\" class=\\"js-path-navigation gl-w-full gl-pb-2\\"></path-navigation-stub>"`; - -exports[`Value stream analytics component without enough permissions renders the empty stage with \`You need permission\` message 1`] = `"<gl-empty-state-stub title=\\"You need permission.\\" svgpath=\\"path/to/no/access\\" description=\\"Want to see the data? Please ask an administrator for access.\\" class=\\"js-empty-state\\"></gl-empty-state-stub>"`; diff --git a/spec/frontend/cycle_analytics/__snapshots__/total_time_component_spec.js.snap b/spec/frontend/cycle_analytics/__snapshots__/total_time_component_spec.js.snap new file mode 100644 index 00000000000..e688df8f281 --- /dev/null +++ b/spec/frontend/cycle_analytics/__snapshots__/total_time_component_spec.js.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TotalTimeComponent with a blank object should render -- 1`] = `"<span class=\\"total-time\\"> -- </span>"`; + +exports[`TotalTimeComponent with a valid time object with {"days": 3, "mins": 47, "seconds": 3} 1`] = ` +"<span class=\\"total-time\\"> + 3 <span>days</span></span>" +`; + +exports[`TotalTimeComponent with a valid time object with {"hours": 7, "mins": 20, "seconds": 10} 1`] = ` +"<span class=\\"total-time\\"> + 7 <span>hrs</span></span>" +`; + +exports[`TotalTimeComponent with a valid time object with {"hours": 23, "mins": 10} 1`] = ` +"<span class=\\"total-time\\"> + 23 <span>hrs</span></span>" +`; + +exports[`TotalTimeComponent with a valid time object with {"mins": 47, "seconds": 3} 1`] = ` +"<span class=\\"total-time\\"> + 47 <span>mins</span></span>" +`; + +exports[`TotalTimeComponent with a valid time object with {"seconds": 35} 1`] = ` +"<span class=\\"total-time\\"> + 35 <span>s</span></span>" +`; diff --git a/spec/frontend/cycle_analytics/base_spec.js b/spec/frontend/cycle_analytics/base_spec.js index 2f85cc04051..6449010b78e 100644 --- a/spec/frontend/cycle_analytics/base_spec.js +++ b/spec/frontend/cycle_analytics/base_spec.js @@ -5,6 +5,8 @@ import Vuex from 'vuex'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import BaseComponent from '~/cycle_analytics/components/base.vue'; import PathNavigation from '~/cycle_analytics/components/path_navigation.vue'; +import StageTable from '~/cycle_analytics/components/stage_table.vue'; +import { NOT_ENOUGH_DATA_ERROR } from '~/cycle_analytics/constants'; import initState from '~/cycle_analytics/store/state'; import { selectedStage, convertedEvents as selectedStageEvents } from './mock_data'; @@ -38,6 +40,9 @@ function createComponent({ initialState } = {}) { noDataSvgPath, noAccessSvgPath, }, + stubs: { + StageTable, + }, }), ); } @@ -45,9 +50,9 @@ function createComponent({ initialState } = {}) { const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findPathNavigation = () => wrapper.findComponent(PathNavigation); const findOverviewMetrics = () => wrapper.findByTestId('vsa-stage-overview-metrics'); -const findStageTable = () => wrapper.findByTestId('vsa-stage-table'); -const findEmptyStage = () => wrapper.findComponent(GlEmptyState); -const findStageEvents = () => wrapper.findByTestId('stage-table-events'); +const findStageTable = () => wrapper.findComponent(StageTable); +const findStageEvents = () => findStageTable().props('stageEvents'); +const findEmptyStageTitle = () => wrapper.findComponent(GlEmptyState).props('title'); describe('Value stream analytics component', () => { beforeEach(() => { @@ -81,8 +86,7 @@ describe('Value stream analytics component', () => { }); it('renders the stage table events', () => { - expect(findEmptyStage().exists()).toBe(false); - expect(findStageEvents().exists()).toBe(true); + expect(findStageEvents()).toEqual(selectedStageEvents); }); it('does not render the loading icon', () => { @@ -135,7 +139,7 @@ describe('Value stream analytics component', () => { }); it('renders the empty stage with `Not enough data` message', () => { - expect(findEmptyStage().html()).toMatchSnapshot(); + expect(findEmptyStageTitle()).toBe(NOT_ENOUGH_DATA_ERROR); }); describe('with a selectedStageError', () => { @@ -150,7 +154,7 @@ describe('Value stream analytics component', () => { }); it('renders the empty stage with `There is too much data to calculate` message', () => { - expect(findEmptyStage().html()).toMatchSnapshot(); + expect(findEmptyStageTitle()).toBe('There is too much data to calculate'); }); }); }); @@ -166,8 +170,8 @@ describe('Value stream analytics component', () => { }); }); - it('renders the empty stage with `You need permission` message', () => { - expect(findEmptyStage().html()).toMatchSnapshot(); + it('renders the empty stage with `You need permission.` message', () => { + expect(findEmptyStageTitle()).toBe('You need permission.'); }); }); @@ -187,7 +191,7 @@ describe('Value stream analytics component', () => { }); it('does not render the stage table events', () => { - expect(findStageEvents().exists()).toBe(false); + expect(findStageEvents()).toHaveLength(0); }); it('does not render the loading icon', () => { diff --git a/spec/frontend/cycle_analytics/mock_data.js b/spec/frontend/cycle_analytics/mock_data.js index 4e6471d5f7b..367a8247813 100644 --- a/spec/frontend/cycle_analytics/mock_data.js +++ b/spec/frontend/cycle_analytics/mock_data.js @@ -18,7 +18,7 @@ export const summary = [ { value: null, title: 'Deployment Frequency', unit: 'per day' }, ]; -const issueStage = { +export const issueStage = { id: 'issue', title: 'Issue', name: 'issue', @@ -27,7 +27,7 @@ const issueStage = { value: null, }; -const planStage = { +export const planStage = { id: 'plan', title: 'Plan', name: 'plan', @@ -36,7 +36,7 @@ const planStage = { value: 75600, }; -const codeStage = { +export const codeStage = { id: 'code', title: 'Code', name: 'code', @@ -45,7 +45,7 @@ const codeStage = { value: 172800, }; -const testStage = { +export const testStage = { id: 'test', title: 'Test', name: 'test', @@ -54,7 +54,7 @@ const testStage = { value: 17550, }; -const reviewStage = { +export const reviewStage = { id: 'review', title: 'Review', name: 'review', @@ -63,7 +63,7 @@ const reviewStage = { value: null, }; -const stagingStage = { +export const stagingStage = { id: 'staging', title: 'Staging', name: 'staging', @@ -79,7 +79,7 @@ export const selectedStage = { isUserAllowed: true, emptyStageText: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.', - component: 'stage-issue-component', + slug: 'issue', }; @@ -290,7 +290,189 @@ export const rawValueStreamStages = [ }, ]; -export const valueStreamStages = rawValueStreamStages.map((s) => ({ - ...convertObjectPropsToCamelCase(s, { deep: true }), - component: `stage-${s.id}-component`, -})); +export const valueStreamStages = rawValueStreamStages.map((s) => + convertObjectPropsToCamelCase(s, { deep: true }), +); + +// Temporary workaronud until we have relevant backend fixtures endpoints +export const testEvents = [ + { + name: 'test', + id: 53, + branch: { + name: 'master', + url: 'http://localhost/group3/project9/-/tree/master', + }, + shortSha: 'b83d6e39', + author: { + id: 18, + name: 'John Doe21', + username: 'user12', + state: 'active', + avatarUrl: + 'https://www.gravatar.com/avatar/70a85d1042e02066f7451ae831689be0?s=80&d=identicon', + webUrl: 'http://localhost/user12', + showStatus: false, + path: '/user12', + }, + date: 'about 1 hour ago', + totalTime: { mins: 2 }, + url: 'http://localhost/group3/project9/-/jobs/53', + commitUrl: 'http://localhost/group3/project9/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0', + }, + { + name: 'test', + id: 54, + branch: { + name: 'master', + url: 'http://localhost/group3/project9/-/tree/master', + }, + shortSha: 'b83d6e39', + author: { + id: 18, + name: 'John Doe21', + username: 'user12', + state: 'active', + avatarUrl: + 'https://www.gravatar.com/avatar/70a85d1042e02066f7451ae831689be0?s=80&d=identicon', + webUrl: 'http://localhost/user12', + showStatus: false, + path: '/user12', + }, + date: 'about 1 hour ago', + totalTime: { mins: 2 }, + url: 'http://localhost/group3/project9/-/jobs/54', + commitUrl: 'http://localhost/group3/project9/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0', + }, +]; + +export const stagingEvents = [ + { + name: 'test', + id: 83, + branch: { + name: 'master', + url: 'http://localhost/group3/project9/-/tree/master', + }, + shortSha: 'b83d6e39', + author: { + id: 18, + name: 'John Doe21', + username: 'user12', + state: 'active', + avatarUrl: + 'https://www.gravatar.com/avatar/70a85d1042e02066f7451ae831689be0?s=80&d=identicon', + webUrl: 'http://localhost/user12', + showStatus: false, + path: '/user12', + }, + date: 'about 1 hour ago', + totalTime: { mins: 2 }, + url: 'http://localhost/group3/project9/-/jobs/83', + commitUrl: 'http://localhost/group3/project9/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0', + }, + { + name: 'test', + id: 84, + branch: { + name: 'master', + url: 'http://localhost/group3/project9/-/tree/master', + }, + shortSha: 'b83d6e39', + author: { + id: 18, + name: 'John Doe21', + username: 'user12', + state: 'active', + avatarUrl: + 'https://www.gravatar.com/avatar/70a85d1042e02066f7451ae831689be0?s=80&d=identicon', + webUrl: 'http://localhost/user12', + showStatus: false, + path: '/user12', + }, + date: 'about 1 hour ago', + totalTime: { mins: 2 }, + url: 'http://localhost/group3/project9/-/jobs/84', + commitUrl: 'http://localhost/group3/project9/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0', + }, +]; + +export const reviewEvents = [ + { + title: 'My title 98', + author: { + id: 17, + name: 'John Doe20', + username: 'user11', + state: 'active', + avatarUrl: + 'https://www.gravatar.com/avatar/fb32cf62136a195ec4f40ec6d1cfffdc?s=80&d=identicon', + webUrl: 'http://localhost/user11', + showStatus: false, + path: '/user11', + }, + iid: '3', + totalTime: { days: 15 }, + createdAt: '20 days ago', + url: 'http://localhost/group3/project9/-/merge_requests/3', + state: 'opened', + }, + { + title: 'My title 99', + author: { + id: 17, + name: 'John Doe20', + username: 'user11', + state: 'active', + avatarUrl: + 'https://www.gravatar.com/avatar/fb32cf62136a195ec4f40ec6d1cfffdc?s=80&d=identicon', + webUrl: 'http://localhost/user11', + showStatus: false, + path: '/user11', + }, + iid: '4', + totalTime: { days: 9 }, + createdAt: '19 days ago', + url: 'http://localhost/group3/project9/-/merge_requests/4', + state: 'opened', + }, +]; + +export const issueEvents = [ + { + title: 'My title 24', + author: { + id: 17, + name: 'John Doe20', + username: 'user11', + state: 'active', + avatarUrl: + 'https://www.gravatar.com/avatar/fb32cf62136a195ec4f40ec6d1cfffdc?s=80&d=identicon', + webUrl: 'http://localhost/user11', + showStatus: false, + path: '/user11', + }, + iid: '3', + totalTime: { days: 2 }, + createdAt: '4 days ago', + url: 'http://localhost/group3/project9/-/issues/3', + }, + { + title: 'My title 23', + author: { + id: 17, + name: 'John Doe20', + username: 'user11', + state: 'active', + avatarUrl: + 'https://www.gravatar.com/avatar/fb32cf62136a195ec4f40ec6d1cfffdc?s=80&d=identicon', + webUrl: 'http://localhost/user11', + showStatus: false, + path: '/user11', + }, + iid: '2', + totalTime: { days: 2 }, + createdAt: '5 days ago', + url: 'http://localhost/group3/project9/-/issues/2', + }, +]; diff --git a/spec/frontend/cycle_analytics/stage_table_spec.js b/spec/frontend/cycle_analytics/stage_table_spec.js new file mode 100644 index 00000000000..11402b5f547 --- /dev/null +++ b/spec/frontend/cycle_analytics/stage_table_spec.js @@ -0,0 +1,377 @@ +import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui'; +import { shallowMount, mount } from '@vue/test-utils'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import StageTable from '~/cycle_analytics/components/stage_table.vue'; +import { PAGINATION_SORT_FIELD_DURATION } from '~/cycle_analytics/constants'; +import { + stagingEvents, + stagingStage, + issueEvents, + issueStage, + testEvents, + testStage, + reviewStage, + reviewEvents, +} from './mock_data'; + +let wrapper = null; +let trackingSpy = null; + +const noDataSvgPath = 'path/to/no/data'; +const emptyStateTitle = 'Too much data'; +const notEnoughDataError = "We don't have enough data to show this stage."; +const [firstIssueEvent] = issueEvents; +const [firstStagingEvent] = stagingEvents; +const [firstTestEvent] = testEvents; +const [firstReviewEvent] = reviewEvents; +const pagination = { page: 1, hasNextPage: true }; + +const findStageEvents = () => wrapper.findAllByTestId('vsa-stage-event'); +const findPagination = () => wrapper.findByTestId('vsa-stage-pagination'); +const findTable = () => wrapper.findComponent(GlTable); +const findStageEventTitle = (ev) => extendedWrapper(ev).findByTestId('vsa-stage-event-title'); + +function createComponent(props = {}, shallow = false) { + const func = shallow ? shallowMount : mount; + return extendedWrapper( + func(StageTable, { + propsData: { + isLoading: false, + stageEvents: issueEvents, + noDataSvgPath, + selectedStage: issueStage, + pagination, + ...props, + }, + stubs: { + GlLoadingIcon, + GlEmptyState, + }, + }), + ); +} + +describe('StageTable', () => { + afterEach(() => { + wrapper.destroy(); + }); + + describe('is loaded with data', () => { + beforeEach(() => { + wrapper = createComponent(); + }); + + it('will render the correct events', () => { + const evs = findStageEvents(); + expect(evs).toHaveLength(issueEvents.length); + + const titles = evs.wrappers.map((ev) => findStageEventTitle(ev).text()); + issueEvents.forEach((ev, index) => { + expect(titles[index]).toBe(ev.title); + }); + }); + + it('will not display the default data message', () => { + expect(wrapper.html()).not.toContain(notEnoughDataError); + }); + }); + + describe('with minimal stage data', () => { + beforeEach(() => { + wrapper = createComponent({ currentStage: { title: 'New stage title' } }); + }); + + it('will render the correct events', () => { + const evs = findStageEvents(); + expect(evs).toHaveLength(issueEvents.length); + + const titles = evs.wrappers.map((ev) => findStageEventTitle(ev).text()); + issueEvents.forEach((ev, index) => { + expect(titles[index]).toBe(ev.title); + }); + }); + }); + + describe('default event', () => { + beforeEach(() => { + wrapper = createComponent({ + stageEvents: [{ ...firstIssueEvent }], + selectedStage: { ...issueStage, custom: false }, + }); + }); + + it('will render the event title', () => { + expect(wrapper.findByTestId('vsa-stage-event-title').text()).toBe(firstIssueEvent.title); + }); + + it('will set the workflow title to "Issues"', () => { + expect(wrapper.find('thead').text()).toContain('Issues'); + }); + + it('does not render the fork icon', () => { + expect(wrapper.findByTestId('fork-icon').exists()).toBe(false); + }); + + it('does not render the branch icon', () => { + expect(wrapper.findByTestId('commit-icon').exists()).toBe(false); + }); + + it('will render the total time', () => { + expect(wrapper.findByTestId('vsa-stage-event-time').text()).toBe('2 days'); + }); + + it('will render the author', () => { + expect(wrapper.findByTestId('vsa-stage-event-author').text()).toContain( + firstIssueEvent.author.name, + ); + }); + + it('will render the created at date', () => { + expect(wrapper.findByTestId('vsa-stage-event-date').text()).toContain( + firstIssueEvent.createdAt, + ); + }); + }); + + describe('merge request event', () => { + beforeEach(() => { + wrapper = createComponent({ + stageEvents: [{ ...firstReviewEvent }], + selectedStage: { ...reviewStage, custom: false }, + }); + }); + + it('will set the workflow title to "Merge requests"', () => { + expect(wrapper.find('thead').text()).toContain('Merge requests'); + expect(wrapper.find('thead').text()).not.toContain('Issues'); + }); + }); + + describe('staging event', () => { + beforeEach(() => { + wrapper = createComponent({ + stageEvents: [{ ...firstStagingEvent }], + selectedStage: { ...stagingStage, custom: false }, + }); + }); + + it('will set the workflow title to "Deployments"', () => { + expect(wrapper.find('thead').text()).toContain('Deployments'); + expect(wrapper.find('thead').text()).not.toContain('Issues'); + }); + + it('will not render the event title', () => { + expect(wrapper.findByTestId('vsa-stage-event-title').exists()).toBe(false); + }); + + it('will render the fork icon', () => { + expect(wrapper.findByTestId('fork-icon').exists()).toBe(true); + }); + + it('will render the branch icon', () => { + expect(wrapper.findByTestId('commit-icon').exists()).toBe(true); + }); + + it('will render the total time', () => { + expect(wrapper.findByTestId('vsa-stage-event-time').text()).toBe('2 mins'); + }); + + it('will render the build shortSha', () => { + expect(wrapper.findByTestId('vsa-stage-event-build-sha').text()).toBe( + firstStagingEvent.shortSha, + ); + }); + + it('will render the author and date', () => { + const content = wrapper.findByTestId('vsa-stage-event-build-author-and-date').text(); + expect(content).toContain(firstStagingEvent.author.name); + expect(content).toContain(firstStagingEvent.date); + }); + }); + + describe('test event', () => { + beforeEach(() => { + wrapper = createComponent({ + stageEvents: [{ ...firstTestEvent }], + selectedStage: { ...testStage, custom: false }, + }); + }); + + it('will set the workflow title to "Jobs"', () => { + expect(wrapper.find('thead').text()).toContain('Jobs'); + expect(wrapper.find('thead').text()).not.toContain('Issues'); + }); + + it('will not render the event title', () => { + expect(wrapper.findByTestId('vsa-stage-event-title').exists()).toBe(false); + }); + + it('will render the fork icon', () => { + expect(wrapper.findByTestId('fork-icon').exists()).toBe(true); + }); + + it('will render the branch icon', () => { + expect(wrapper.findByTestId('commit-icon').exists()).toBe(true); + }); + + it('will render the total time', () => { + expect(wrapper.findByTestId('vsa-stage-event-time').text()).toBe('2 mins'); + }); + + it('will render the build shortSha', () => { + expect(wrapper.findByTestId('vsa-stage-event-build-sha').text()).toBe( + firstTestEvent.shortSha, + ); + }); + + it('will render the build pipeline success icon', () => { + expect(wrapper.findByTestId('status_success-icon').exists()).toBe(true); + }); + + it('will render the build date', () => { + const content = wrapper.findByTestId('vsa-stage-event-build-status-date').text(); + expect(content).toContain(firstTestEvent.date); + }); + + it('will render the build event name', () => { + expect(wrapper.findByTestId('vsa-stage-event-build-name').text()).toContain( + firstTestEvent.name, + ); + }); + }); + + describe('isLoading = true', () => { + beforeEach(() => { + wrapper = createComponent({ isLoading: true }, true); + }); + + it('will display the loading icon', () => { + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + }); + + it('will not display pagination', () => { + expect(findPagination().exists()).toBe(false); + }); + }); + + describe('with no stageEvents', () => { + beforeEach(() => { + wrapper = createComponent({ stageEvents: [] }); + }); + + it('will render the empty state', () => { + expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true); + }); + + it('will display the default no data message', () => { + expect(wrapper.html()).toContain(notEnoughDataError); + }); + + it('will not display the pagination component', () => { + expect(findPagination().exists()).toBe(false); + }); + }); + + describe('emptyStateTitle set', () => { + beforeEach(() => { + wrapper = createComponent({ stageEvents: [], emptyStateTitle }); + }); + + it('will display the custom message', () => { + expect(wrapper.html()).not.toContain(notEnoughDataError); + expect(wrapper.html()).toContain(emptyStateTitle); + }); + }); + + describe('Pagination', () => { + beforeEach(() => { + wrapper = createComponent(); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); + wrapper.destroy(); + }); + + it('will display the pagination component', () => { + expect(findPagination().exists()).toBe(true); + }); + + it('clicking prev or next will emit an event', async () => { + expect(wrapper.emitted('handleUpdatePagination')).toBeUndefined(); + + findPagination().vm.$emit('input', 2); + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted('handleUpdatePagination')[0]).toEqual([{ page: 2 }]); + }); + + it('clicking prev or next will send tracking information', () => { + findPagination().vm.$emit('input', 2); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', { label: 'pagination' }); + }); + + describe('with `hasNextPage=false', () => { + beforeEach(() => { + wrapper = createComponent({ pagination: { page: 1, hasNextPage: false } }); + }); + + it('will not display the pagination component', () => { + expect(findPagination().exists()).toBe(false); + }); + }); + }); + + describe('Sorting', () => { + const triggerTableSort = (sortDesc = true) => + findTable().vm.$emit('sort-changed', { + sortBy: PAGINATION_SORT_FIELD_DURATION, + sortDesc, + }); + + beforeEach(() => { + wrapper = createComponent(); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); + wrapper.destroy(); + }); + + it('clicking a table column will send tracking information', () => { + triggerTableSort(); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', { + label: 'sort_duration_desc', + }); + }); + + it('clicking a table column will update the sort field', () => { + expect(wrapper.emitted('handleUpdatePagination')).toBeUndefined(); + triggerTableSort(); + + expect(wrapper.emitted('handleUpdatePagination')[0]).toEqual([ + { + direction: 'desc', + sort: 'duration', + }, + ]); + }); + + it('with sortDesc=false will toggle the direction field', async () => { + expect(wrapper.emitted('handleUpdatePagination')).toBeUndefined(); + triggerTableSort(false); + + expect(wrapper.emitted('handleUpdatePagination')[0]).toEqual([ + { + direction: 'asc', + sort: 'duration', + }, + ]); + }); + }); +}); diff --git a/spec/frontend/cycle_analytics/total_time_component_spec.js b/spec/frontend/cycle_analytics/total_time_component_spec.js index e831bc311ed..9003c0330c0 100644 --- a/spec/frontend/cycle_analytics/total_time_component_spec.js +++ b/spec/frontend/cycle_analytics/total_time_component_spec.js @@ -1,11 +1,11 @@ -import { shallowMount } from '@vue/test-utils'; -import TotalTime from '~/cycle_analytics/components/total_time_component.vue'; +import { mount } from '@vue/test-utils'; +import TotalTimeComponent from '~/cycle_analytics/components/total_time_component.vue'; -describe('Total time component', () => { - let wrapper; +describe('TotalTimeComponent', () => { + let wrapper = null; const createComponent = (propsData) => { - wrapper = shallowMount(TotalTime, { + return mount(TotalTimeComponent, { propsData, }); }; @@ -14,45 +14,32 @@ describe('Total time component', () => { wrapper.destroy(); }); - describe('With data', () => { - it('should render information for days and hours', () => { - createComponent({ - time: { - days: 3, - hours: 4, - }, + describe('with a valid time object', () => { + it.each` + time + ${{ seconds: 35 }} + ${{ mins: 47, seconds: 3 }} + ${{ days: 3, mins: 47, seconds: 3 }} + ${{ hours: 23, mins: 10 }} + ${{ hours: 7, mins: 20, seconds: 10 }} + `('with $time', ({ time }) => { + wrapper = createComponent({ + time, }); - expect(wrapper.text()).toMatchInterpolatedText('3 days 4 hrs'); - }); - - it('should render information for hours and minutes', () => { - createComponent({ - time: { - hours: 4, - mins: 35, - }, - }); - - expect(wrapper.text()).toMatchInterpolatedText('4 hrs 35 mins'); + expect(wrapper.html()).toMatchSnapshot(); }); + }); - it('should render information for seconds', () => { - createComponent({ - time: { - seconds: 45, - }, + describe('with a blank object', () => { + beforeEach(() => { + wrapper = createComponent({ + time: {}, }); - - expect(wrapper.text()).toMatchInterpolatedText('45 s'); }); - }); - - describe('Without data', () => { - it('should render no information', () => { - createComponent(); - expect(wrapper.text()).toBe('--'); + it('should render --', () => { + expect(wrapper.html()).toMatchSnapshot(); }); }); }); diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index d529d4a96e1..2e3dce3f418 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -360,32 +360,23 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do end end - context 'when using a project access token' do - let_it_be(:project_bot_user) { create(:user, :project_bot) } - let_it_be(:project_access_token) { create(:personal_access_token, user: project_bot_user) } - - context 'with valid project access token' do - before do - project.add_maintainer(project_bot_user) - end - + context 'when using a resource access token' do + shared_examples 'with a valid access token' do it 'successfully authenticates the project bot' do - expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: project, ip: 'ip')) + expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip')) .to eq(Gitlab::Auth::Result.new(project_bot_user, nil, :personal_access_token, described_class.full_authentication_abilities)) end it 'successfully authenticates the project bot with a nil project' do - expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: nil, ip: 'ip')) + expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: nil, ip: 'ip')) .to eq(Gitlab::Auth::Result.new(project_bot_user, nil, :personal_access_token, described_class.full_authentication_abilities)) end end - context 'with invalid project access token' do - context 'when project bot is not a project member' do - it 'fails for a non-project member' do - expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: project, ip: 'ip')) - .to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil)) - end + shared_examples 'with an invalid access token' do + it 'fails for a non-member' do + expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip')) + .to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil)) end context 'when project bot user is blocked' do @@ -394,11 +385,61 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do end it 'fails for a blocked project bot' do - expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: project, ip: 'ip')) + expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip')) .to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil)) end end end + + context 'when using a personal namespace project access token' do + let_it_be(:project_bot_user) { create(:user, :project_bot) } + let_it_be(:access_token) { create(:personal_access_token, user: project_bot_user) } + + context 'when the token belongs to the project' do + before do + project.add_maintainer(project_bot_user) + end + + it_behaves_like 'with a valid access token' + end + + it_behaves_like 'with an invalid access token' + end + + context 'when in a group namespace' do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + + context 'when using a project access token' do + let_it_be(:project_bot_user) { create(:user, :project_bot) } + let_it_be(:access_token) { create(:personal_access_token, user: project_bot_user) } + + context 'when token user belongs to the project' do + before do + project.add_maintainer(project_bot_user) + end + + it_behaves_like 'with a valid access token' + end + + it_behaves_like 'with an invalid access token' + end + + context 'when using a group access token' do + let_it_be(:project_bot_user) { create(:user, name: 'Group token bot', email: "group_#{group.id}_bot@example.com", username: "group_#{group.id}_bot", user_type: :project_bot) } + let_it_be(:access_token) { create(:personal_access_token, user: project_bot_user) } + + context 'when the token belongs to the group' do + before do + group.add_maintainer(project_bot_user) + end + + it_behaves_like 'with a valid access token' + end + + it_behaves_like 'with an invalid access token' + end + end end end diff --git a/spec/rubocop/cop/graphql/old_types_spec.rb b/spec/rubocop/cop/graphql/old_types_spec.rb new file mode 100644 index 00000000000..396bf4ce997 --- /dev/null +++ b/spec/rubocop/cop/graphql/old_types_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rspec-parameterized' +require_relative '../../../../rubocop/cop/graphql/old_types' + +RSpec.describe RuboCop::Cop::Graphql::OldTypes do + using RSpec::Parameterized::TableSyntax + + subject(:cop) { described_class.new } + + where(:old_type, :message) do + 'GraphQL::ID_TYPE' | 'Avoid using GraphQL::ID_TYPE. Use GraphQL::Types::ID instead' + 'GraphQL::INT_TYPE' | 'Avoid using GraphQL::INT_TYPE. Use GraphQL::Types::Int instead' + 'GraphQL::STRING_TYPE' | 'Avoid using GraphQL::STRING_TYPE. Use GraphQL::Types::String instead' + 'GraphQL::BOOLEAN_TYPE' | 'Avoid using GraphQL::BOOLEAN_TYPE. Use GraphQL::Types::Boolean instead' + end + + with_them do + context 'fields' do + it 'adds an offense when an old type is used' do + expect_offense(<<~RUBY) + class MyType + field :some_field, #{old_type} + ^^^^^^^^^^^^^^^^^^^#{'^' * old_type.length} #{message} + end + RUBY + end + + it "adds an offense when an old type is used with other keywords" do + expect_offense(<<~RUBY) + class MyType + field :some_field, #{old_type}, null: true, description: 'My description' + ^^^^^^^^^^^^^^^^^^^#{'^' * old_type.length}^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{message} + end + RUBY + end + end + + context 'arguments' do + it 'adds an offense when an old type is used' do + expect_offense(<<~RUBY) + class MyType + field :some_arg, #{old_type} + ^^^^^^^^^^^^^^^^^#{'^' * old_type.length} #{message} + end + RUBY + end + + it 'adds an offense when an old type is used with other keywords' do + expect_offense(<<~RUBY) + class MyType + argument :some_arg, #{old_type}, null: true, description: 'My description' + ^^^^^^^^^^^^^^^^^^^^#{'^' * old_type.length}^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{message} + end + RUBY + end + end + end + + it 'does not add an offense for other types in fields' do + expect_no_offenses(<<~RUBY.strip) + class MyType + field :some_field, GraphQL::Types::JSON + end + RUBY + end + + it 'does not add an offense for other types in arguments' do + expect_no_offenses(<<~RUBY.strip) + class MyType + argument :some_arg, GraphQL::Types::JSON + end + RUBY + end + + it 'does not add an offense for uses outside of field or argument' do + expect_no_offenses(<<~RUBY.strip) + class MyType + foo :some_field, GraphQL::ID_TYPE + end + RUBY + end +end |