summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-07-20 09:08:43 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-07-20 09:08:43 +0000
commitf5f1f221ba08228dbbdd7080509028a7cac2fce2 (patch)
tree7a95ad0d16829f719c429276a8ed4ddaa097392a
parent1ad2f1981f05721d92d04c490cfc0f234737fec1 (diff)
downloadgitlab-ce-f5f1f221ba08228dbbdd7080509028a7cac2fce2.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop.yml9
-rw-r--r--.rubocop_manual_todo.yml404
-rw-r--r--app/assets/javascripts/cycle_analytics/components/base.vue107
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_table.vue305
-rw-r--r--app/assets/javascripts/cycle_analytics/components/total_time_component.vue50
-rw-r--r--app/assets/javascripts/cycle_analytics/constants.js15
-rw-r--r--app/assets/javascripts/cycle_analytics/store/mutations.js8
-rw-r--r--app/assets/javascripts/design_management/components/image.vue14
-rw-r--r--app/assets/javascripts/design_management/index.js12
-rw-r--r--app/assets/javascripts/performance/constants.js11
-rw-r--r--app/assets/stylesheets/application_dark.scss2
-rw-r--r--app/views/admin/application_settings/_realtime.html.haml3
-rw-r--r--app/views/admin/application_settings/preferences.html.haml5
-rw-r--r--app/views/projects/_home_panel.html.haml17
-rw-r--r--config.ru2
-rw-r--r--doc/administration/polling.md42
-rw-r--r--doc/user/admin_area/settings/index.md2
-rw-r--r--lib/gitlab/auth.rb18
-rw-r--r--locale/gitlab.pot24
-rw-r--r--rubocop/cop/graphql/old_types.rb44
-rw-r--r--spec/features/cycle_analytics_spec.rb25
-rw-r--r--spec/frontend/cycle_analytics/__snapshots__/base_spec.js.snap6
-rw-r--r--spec/frontend/cycle_analytics/__snapshots__/total_time_component_spec.js.snap28
-rw-r--r--spec/frontend/cycle_analytics/base_spec.js24
-rw-r--r--spec/frontend/cycle_analytics/mock_data.js204
-rw-r--r--spec/frontend/cycle_analytics/stage_table_spec.js377
-rw-r--r--spec/frontend/cycle_analytics/total_time_component_spec.js61
-rw-r--r--spec/lib/gitlab/auth_spec.rb77
-rw-r--r--spec/rubocop/cop/graphql/old_types_spec.rb84
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>
+ &middot;
+ </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">&middot;</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