Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-10-19 15:57:54 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-10-19 15:57:54 +0300
commit419c53ec62de6e97a517abd5fdd4cbde3a942a34 (patch)
tree1f43a548b46bca8a5fb8fe0c31cef1883d49c5b6 /app
parent1da20d9135b3ad9e75e65b028bffc921aaf8deb7 (diff)
Add latest changes from gitlab-org/gitlab@16-5-stable-eev16.5.0-rc42
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/callouts/rich_text_editor_illustration.svg79
-rw-r--r--app/assets/images/jobs-empty-state.svg33
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/report_actions.vue15
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/user_details.vue3
-rw-r--r--app/assets/javascripts/admin/abuse_report/constants.js6
-rw-r--r--app/assets/javascripts/admin/application_settings/inactive_project_deletion/components/form.vue6
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue2
-rw-r--r--app/assets/javascripts/alert.js54
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_empty_state.vue6
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/base.vue4
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/stage_table.vue1
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue124
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/actions.js4
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/mutations.js3
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/state.js1
-rw-r--r--app/assets/javascripts/analytics/devops_reports/components/devops_score.vue1
-rw-r--r--app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue6
-rw-r--r--app/assets/javascripts/analytics/shared/components/date_ranges_dropdown.vue131
-rw-r--r--app/assets/javascripts/analytics/shared/components/daterange.vue23
-rw-r--r--app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue24
-rw-r--r--app/assets/javascripts/analytics/shared/constants.js69
-rw-r--r--app/assets/javascripts/api.js14
-rw-r--r--app/assets/javascripts/badges/components/badge.vue2
-rw-r--r--app/assets/javascripts/badges/components/badge_list.vue4
-rw-r--r--app/assets/javascripts/batch_comments/components/preview_dropdown.vue8
-rw-r--r--app/assets/javascripts/batch_comments/components/review_bar.vue2
-rw-r--r--app/assets/javascripts/batch_comments/components/submit_dropdown.vue6
-rw-r--r--app/assets/javascripts/behaviors/autosize.js11
-rw-r--r--app/assets/javascripts/behaviors/components/global_alerts.vue50
-rw-r--r--app/assets/javascripts/behaviors/global_alerts.js17
-rw-r--r--app/assets/javascripts/behaviors/index.js3
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js14
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js45
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_observability.js25
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js20
-rw-r--r--app/assets/javascripts/blob/components/blob_content.vue2
-rw-r--r--app/assets/javascripts/blob/components/blob_edit_header.vue2
-rw-r--r--app/assets/javascripts/blob/components/blob_header_default_actions.vue5
-rw-r--r--app/assets/javascripts/blob/components/blob_header_filepath.vue2
-rw-r--r--app/assets/javascripts/blob/csv/constants.js1
-rw-r--r--app/assets/javascripts/blob/csv/csv_viewer.vue35
-rw-r--r--app/assets/javascripts/boards/components/board_app.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue43
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue12
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue30
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue11
-rw-r--r--app/assets/javascripts/boards/components/board_top_bar.vue3
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue272
-rw-r--r--app/assets/javascripts/boards/components/config_toggle.vue2
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue12
-rw-r--r--app/assets/javascripts/boards/components/toggle_focus.vue2
-rw-r--r--app/assets/javascripts/boards/graphql/cache_updates.js4
-rw-r--r--app/assets/javascripts/boards/graphql/client/selected_board_items.query.graphql3
-rw-r--r--app/assets/javascripts/boards/graphql/client/set_selected_board_items.mutation.graphql3
-rw-r--r--app/assets/javascripts/boards/graphql/client/unset_selected_board_items.mutation.graphql3
-rw-r--r--app/assets/javascripts/boards/issue_board_filters.js14
-rw-r--r--app/assets/javascripts/boards/stores/actions.js6
-rw-r--r--app/assets/javascripts/branches/components/delete_merged_branches.vue19
-rw-r--r--app/assets/javascripts/branches/components/sort_dropdown.vue4
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue2
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue17
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/constants.js3
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql1
-rw-r--r--app/assets/javascripts/ci/artifacts/components/bulk_delete_modal.vue1
-rw-r--r--app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue67
-rw-r--r--app/assets/javascripts/ci/catalog/components/ci_catalog_home.vue8
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue120
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue103
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue41
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue130
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_header_skeleton_loader.vue13
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue55
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_header.vue59
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue57
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/ci_resources_list.vue74
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue144
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/empty_state.vue22
-rw-r--r--app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue109
-rw-r--r--app/assets/javascripts/ci/catalog/constants.js35
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql25
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql20
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql29
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql6
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql7
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/settings.js32
-rw-r--r--app/assets/javascripts/ci/catalog/router/constants.js2
-rw-r--r--app/assets/javascripts/ci/catalog/router/index.js13
-rw-r--r--app/assets/javascripts/ci/catalog/router/routes.js9
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue43
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue368
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue19
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue10
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue5
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/utils.js23
-rw-r--r--app/assets/javascripts/ci/common/pipelines_table.vue110
-rw-r--r--app/assets/javascripts/ci/common/private/job_action_component.vue2
-rw-r--r--app/assets/javascripts/ci/constants.js15
-rw-r--r--app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue4
-rw-r--r--app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql1
-rw-r--r--app/assets/javascripts/ci/job_details/components/job_header.vue43
-rw-r--r--app/assets/javascripts/ci/job_details/components/job_log_controllers.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line.vue8
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_header.vue4
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_number.vue5
-rw-r--r--app/assets/javascripts/ci/job_details/components/manual_variables_form.vue8
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue14
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue6
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue34
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue4
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue141
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue6
-rw-r--r--app/assets/javascripts/ci/job_details/components/stuck_block.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql2
-rw-r--r--app/assets/javascripts/ci/job_details/index.js32
-rw-r--r--app/assets/javascripts/ci/job_details/job_app.vue33
-rw-r--r--app/assets/javascripts/ci/job_details/store/actions.js6
-rw-r--r--app/assets/javascripts/ci/job_details/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/ci/job_details/store/mutations.js6
-rw-r--r--app/assets/javascripts/ci/job_details/store/utils.js83
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue6
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue87
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue29
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue (renamed from app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue)23
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/jobs_table.vue36
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue1
-rw-r--r--app/assets/javascripts/ci/jobs_page/constants.js15
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql2
-rw-r--r--app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/constants.js2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/dag/dag.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue17
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue22
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue14
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue248
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js11
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipelines_index.js30
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue11
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue17
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue24
-rw-r--r--app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue15
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue85
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue26
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/constants.js1
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql18
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue1
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue20
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue33
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue59
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue (renamed from app/assets/javascripts/ci/pipelines_page/components/pipelines_status_badge.vue)16
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue31
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue19
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue3
-rw-r--r--app/assets/javascripts/ci/pipelines_page/pipelines.vue81
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/utils.js45
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_details.vue15
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_form_fields.vue10
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_header.vue3
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_type_icon.vue62
-rw-r--r--app/assets/javascripts/ci/runner/constants.js50
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql4
-rw-r--r--app/assets/javascripts/ci/runner/sentry_utils.js17
-rw-r--r--app/assets/javascripts/ci/utils.js16
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_empty_state.vue6
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_empty_state.vue6
-rw-r--r--app/assets/javascripts/clusters_list/store/actions.js7
-rw-r--r--app/assets/javascripts/comment_templates/components/form.vue2
-rw-r--r--app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue18
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js2
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue16
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor_provider.vue8
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue2
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue2
-rw-r--r--app/assets/javascripts/content_editor/extensions/selection.js12
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js9
-rw-r--r--app/assets/javascripts/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql (renamed from app/assets/javascripts/crm/organizations/components/graphql/create_organization.mutation.graphql)2
-rw-r--r--app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue4
-rw-r--r--app/assets/javascripts/custom_emoji/components/app.vue2
-rw-r--r--app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue2
-rw-r--r--app/assets/javascripts/design_management/components/design_description/description_form.vue3
-rw-r--r--app/assets/javascripts/diffs/components/app.vue77
-rw-r--r--app/assets/javascripts/diffs/components/compare_dropdown_layout.vue2
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue24
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue17
-rw-r--r--app/assets/javascripts/diffs/components/diff_row.vue2
-rw-r--r--app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql46
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue7
-rw-r--r--app/assets/javascripts/diffs/index.js5
-rw-r--r--app/assets/javascripts/diffs/store/actions.js6
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js5
-rw-r--r--app/assets/javascripts/diffs/store/utils.js15
-rw-r--r--app/assets/javascripts/diffs/utils/diff_file.js1
-rw-r--r--app/assets/javascripts/diffs/utils/merge_request.js7
-rw-r--r--app/assets/javascripts/diffs/utils/sort_findings_by_file.js17
-rw-r--r--app/assets/javascripts/editor/schema/ci.json147
-rw-r--r--app/assets/javascripts/editor/source_editor.js5
-rw-r--r--app/assets/javascripts/emoji/awards_app/index.js8
-rw-r--r--app/assets/javascripts/emoji/components/picker.vue13
-rw-r--r--app/assets/javascripts/environments/components/canary_ingress.vue63
-rw-r--r--app/assets/javascripts/environments/components/empty_state.vue6
-rw-r--r--app/assets/javascripts/environments/components/environment_form.vue11
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_overview.vue8
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_pods.vue2
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_summary.vue2
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_tabs.vue4
-rw-r--r--app/assets/javascripts/environments/components/new_environment_item.vue2
-rw-r--r--app/assets/javascripts/environments/environment_details/deployments_table.vue7
-rw-r--r--app/assets/javascripts/environments/environment_details/index.vue8
-rw-r--r--app/assets/javascripts/environments/graphql/queries/k8s_services.query.graphql4
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/flux.js4
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/kubernetes.js81
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue6
-rw-r--r--app/assets/javascripts/feature_flags/components/empty_state.vue1
-rw-r--r--app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js50
-rw-r--r--app/assets/javascripts/google_cloud/databases/cloudsql/instance_table.vue1
-rw-r--r--app/assets/javascripts/google_tag_manager/index.js311
-rw-r--r--app/assets/javascripts/graphql_shared/client/is_showing_labels.query.graphql3
-rw-r--r--app/assets/javascripts/graphql_shared/client/set_is_showing_labels.mutation.graphql3
-rw-r--r--app/assets/javascripts/graphql_shared/issuable_client.js58
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json3
-rw-r--r--app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql15
-rw-r--r--app/assets/javascripts/groups/components/empty_states/groups_dashboard_empty_state.vue1
-rw-r--r--app/assets/javascripts/groups/components/empty_states/groups_explore_empty_state.vue6
-rw-r--r--app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue1
-rw-r--r--app/assets/javascripts/groups/components/group_name_and_path.vue6
-rw-r--r--app/assets/javascripts/groups/components/transfer_group_form.vue2
-rw-r--r--app/assets/javascripts/helpers/startup_css_helper.js36
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue1
-rw-r--r--app/assets/javascripts/ide/init_gitlab_web_ide.js17
-rw-r--r--app/assets/javascripts/ide/lib/gitlab_web_ide/handle_tracking_event.js2
-rw-r--r--app/assets/javascripts/import/constants.js4
-rw-r--r--app/assets/javascripts/import_entities/components/group_dropdown.vue72
-rw-r--r--app/assets/javascripts/import_entities/components/import_target_dropdown.vue54
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue15
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue57
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue1
-rw-r--r--app/assets/javascripts/integrations/constants.js8
-rw-r--r--app/assets/javascripts/integrations/gitlab_slack_application/components/gitlab_slack_application.vue2
-rw-r--r--app/assets/javascripts/integrations/gitlab_slack_application/components/projects_dropdown.vue65
-rw-r--r--app/assets/javascripts/invite_members/components/invite_groups_modal.vue2
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue16
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue1
-rw-r--r--app/assets/javascripts/issuable/components/hidden_badge.vue36
-rw-r--r--app/assets/javascripts/issuable/components/locked_badge.vue36
-rw-r--r--app/assets/javascripts/issuable/components/related_issuable_item.vue2
-rw-r--r--app/assets/javascripts/issuable/issuable_label_selector.js2
-rw-r--r--app/assets/javascripts/issues/constants.js2
-rw-r--r--app/assets/javascripts/issues/index.js11
-rw-r--r--app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue4
-rw-r--r--app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue21
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue15
-rw-r--r--app/assets/javascripts/issues/list/queries/issue.fragment.graphql1
-rw-r--r--app/assets/javascripts/issues/service_desk/components/empty_state_with_any_issues.vue1
-rw-r--r--app/assets/javascripts/issues/service_desk/components/empty_state_without_any_issues.vue4
-rw-r--r--app/assets/javascripts/issues/service_desk/filtered_search_service_desk.js (renamed from app/assets/javascripts/issues/filtered_search_service_desk.js)0
-rw-r--r--app/assets/javascripts/issues/service_desk/index.js11
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue6
-rw-r--r--app/assets/javascripts/issues/show/components/delete_issue_modal.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue18
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue4
-rw-r--r--app/assets/javascripts/issues/show/components/form.vue15
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue36
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue4
-rw-r--r--app/assets/javascripts/issues/show/components/new_header_actions_popover.vue80
-rw-r--r--app/assets/javascripts/issues/show/components/sticky_header.vue45
-rw-r--r--app/assets/javascripts/issues/show/constants.js2
-rw-r--r--app/assets/javascripts/issues/show/index.js4
-rw-r--r--app/assets/javascripts/jira_connect/branches/pages/index.vue1
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/constants.js8
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_progress.vue1
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_setup.vue1
-rw-r--r--app/assets/javascripts/labels/index.js4
-rw-r--r--app/assets/javascripts/labels/label_manager.js2
-rw-r--r--app/assets/javascripts/lazy_loader.js2
-rw-r--r--app/assets/javascripts/lib/utils/global_alerts.js37
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js22
-rw-r--r--app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue2
-rw-r--r--app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue1
-rw-r--r--app/assets/javascripts/members/components/app.vue5
-rw-r--r--app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue3
-rw-r--r--app/assets/javascripts/members/components/members_tabs.vue3
-rw-r--r--app/assets/javascripts/merge_request_tabs.js16
-rw-r--r--app/assets/javascripts/merge_requests/components/compare_app.vue4
-rw-r--r--app/assets/javascripts/merge_requests/components/compare_dropdown.vue1
-rw-r--r--app/assets/javascripts/merge_requests/components/header_metadata.vue69
-rw-r--r--app/assets/javascripts/merge_requests/components/merge_request_header.vue113
-rw-r--r--app/assets/javascripts/merge_requests/components/merge_request_status_badge.vue74
-rw-r--r--app/assets/javascripts/merge_requests/components/sticky_header.vue33
-rw-r--r--app/assets/javascripts/merge_requests/index.js19
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue6
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue167
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js7
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue1
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue1
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/index.js3
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue16
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue27
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue35
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/translations.js17
-rw-r--r--app/assets/javascripts/mr_notes/init_notes.js1
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue8
-rw-r--r--app/assets/javascripts/notes/components/comment_type_dropdown.vue5
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue9
-rw-r--r--app/assets/javascripts/notes/components/discussion_actions.vue3
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue7
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue4
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter_note.vue2
-rw-r--r--app/assets/javascripts/notes/components/email_participants_warning.vue7
-rw-r--r--app/assets/javascripts/notes/components/multiline_comment_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue23
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue7
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue2
-rw-r--r--app/assets/javascripts/notes/components/notes_activity_header.vue8
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue9
-rw-r--r--app/assets/javascripts/notes/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/actions.js5
-rw-r--r--app/assets/javascripts/notes/stores/getters.js7
-rw-r--r--app/assets/javascripts/observability/client.js64
-rw-r--r--app/assets/javascripts/observability/components/observability_app.vue87
-rw-r--r--app/assets/javascripts/observability/components/observability_container.vue12
-rw-r--r--app/assets/javascripts/observability/components/skeleton/dashboards.vue30
-rw-r--r--app/assets/javascripts/observability/components/skeleton/embed.vue16
-rw-r--r--app/assets/javascripts/observability/components/skeleton/explore.vue28
-rw-r--r--app/assets/javascripts/observability/components/skeleton/index.vue27
-rw-r--r--app/assets/javascripts/observability/components/skeleton/manage.vue26
-rw-r--r--app/assets/javascripts/observability/constants.js28
-rw-r--r--app/assets/javascripts/observability/index.js60
-rw-r--r--app/assets/javascripts/observability/mock_traces.json107
-rw-r--r--app/assets/javascripts/organizations/index/components/app.vue61
-rw-r--r--app/assets/javascripts/organizations/index/components/organizations_list.vue26
-rw-r--r--app/assets/javascripts/organizations/index/components/organizations_list_item.vue54
-rw-r--r--app/assets/javascripts/organizations/index/components/organizations_view.vue52
-rw-r--r--app/assets/javascripts/organizations/index/graphql/organizations.query.graphql14
-rw-r--r--app/assets/javascripts/organizations/index/index.js33
-rw-r--r--app/assets/javascripts/organizations/mock_data.js40
-rw-r--r--app/assets/javascripts/organizations/new/components/app.vue82
-rw-r--r--app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql9
-rw-r--r--app/assets/javascripts/organizations/new/graphql/typedefs.graphql5
-rw-r--r--app/assets/javascripts/organizations/new/index.js35
-rw-r--r--app/assets/javascripts/organizations/shared/components/new_edit_form.vue125
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/resolvers.js36
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_empty_state.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/exceptions_input.vue2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue5
-rw-r--r--app/assets/javascripts/pages/groups/custom_emoji/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/observability/dashboards/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/observability/datasources/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/observability/explore/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/observability/manage/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/work_items/show/index.js4
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue50
-rw-r--r--app/assets/javascripts/pages/organizations/organizations/index/index.js3
-rw-r--r--app/assets/javascripts/pages/organizations/organizations/new/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/find_file/ref_switcher/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/find_file/ref_switcher/ref_switcher_utils.js15
-rw-r--r--app/assets/javascripts/pages/projects/find_file/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/graphs/charts/index.js373
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/jobs/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js1
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js16
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/page.js4
-rw-r--r--app/assets/javascripts/pages/projects/ml/models/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue32
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/tree/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/work_items/index.js2
-rw-r--r--app/assets/javascripts/pages/registrations/new/index.js4
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue2
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue5
-rw-r--r--app/assets/javascripts/pages/users/terms/index/index.js3
-rw-r--r--app/assets/javascripts/performance_bar/components/add_request.vue10
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue30
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue194
-rw-r--r--app/assets/javascripts/performance_bar/components/request_selector.vue11
-rw-r--r--app/assets/javascripts/performance_bar/components/request_warning.vue21
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue17
-rw-r--r--app/assets/javascripts/projects/components/shared/delete_button.vue2
-rw-r--r--app/assets/javascripts/projects/project_find_file.js11
-rw-r--r--app/assets/javascripts/projects/settings/api/access_dropdown_api.js16
-rw-r--r--app/assets/javascripts/projects/settings/components/access_dropdown.vue4
-rw-r--r--app/assets/javascripts/projects/settings/components/transfer_project_form.vue2
-rw-r--r--app/assets/javascripts/ref/components/ambiguous_ref_modal.vue80
-rw-r--r--app/assets/javascripts/ref/components/ref_selector.vue16
-rw-r--r--app/assets/javascripts/ref/constants.js3
-rw-r--r--app/assets/javascripts/ref/init_ambiguous_ref_modal.js20
-rw-r--r--app/assets/javascripts/related_issues/components/add_issuable_form.vue2
-rw-r--r--app/assets/javascripts/related_issues/components/related_issuable_input.vue2
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_block.vue25
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_list.vue4
-rw-r--r--app/assets/javascripts/releases/components/releases_empty_state.vue3
-rw-r--r--app/assets/javascripts/releases/components/tag_field_new.vue31
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/actions.js143
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/getters.js4
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js1
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/mutations.js3
-rw-r--r--app/assets/javascripts/repository/components/blob_controls.vue6
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue3
-rw-r--r--app/assets/javascripts/repository/components/commit_info.vue116
-rw-r--r--app/assets/javascripts/repository/components/fork_info.vue2
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue150
-rw-r--r--app/assets/javascripts/repository/components/preview/index.vue2
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue2
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue2
-rw-r--r--app/assets/javascripts/repository/index.js15
-rw-r--r--app/assets/javascripts/repository/queries/blob_controls.query.graphql4
-rw-r--r--app/assets/javascripts/repository/router.js3
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue24
-rw-r--r--app/assets/javascripts/search/sidebar/components/archived_filter/data.js9
-rw-r--r--app/assets/javascripts/search/sidebar/components/archived_filter/index.vue17
-rw-r--r--app/assets/javascripts/search/sidebar/components/issues_filters.vue5
-rw-r--r--app/assets/javascripts/search/sidebar/components/label_filter/index.vue2
-rw-r--r--app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue7
-rw-r--r--app/assets/javascripts/search/sidebar/components/milestones_filters.vue18
-rw-r--r--app/assets/javascripts/search/sidebar/constants/index.js1
-rw-r--r--app/assets/javascripts/search/store/constants.js4
-rw-r--r--app/assets/javascripts/search/topbar/components/app.vue26
-rw-r--r--app/assets/javascripts/search/topbar/constants.js3
-rw-r--r--app/assets/javascripts/search/topbar/index.js11
-rw-r--r--app/assets/javascripts/security_configuration/components/feature_card.vue7
-rw-r--r--app/assets/javascripts/sentry/init_sentry.js23
-rw-r--r--app/assets/javascripts/sentry/sentry_browser_wrapper.js16
-rw-r--r--app/assets/javascripts/settings_panels.js2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewers.vue13
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue50
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue23
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo_button.vue2
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js17
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue5
-rw-r--r--app/assets/javascripts/snippets/components/embed_dropdown.vue2
-rw-r--r--app/assets/javascripts/snippets/components/show.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue3
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_edit.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_description_edit.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_description_view.vue2
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue7
-rw-r--r--app/assets/javascripts/snippets/components/snippet_title.vue2
-rw-r--r--app/assets/javascripts/snippets/components/snippet_visibility_edit.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/brand_logo.vue4
-rw-r--r--app/assets/javascripts/super_sidebar/components/counter.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/create_menu.vue4
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue3
-rw-r--r--app/assets/javascripts/super_sidebar/components/help_center.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/menu_section.vue18
-rw-r--r--app/assets/javascripts/super_sidebar/components/nav_item.vue26
-rw-r--r--app/assets/javascripts/super_sidebar/components/pinned_section.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_menu.vue14
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar.vue17
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue62
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue11
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu.vue9
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu_profile_item.vue83
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_name_group.vue91
-rw-r--r--app/assets/javascripts/super_sidebar/constants.js2
-rw-r--r--app/assets/javascripts/super_sidebar/event_hub.js3
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js4
-rw-r--r--app/assets/javascripts/super_sidebar/utils.js22
-rw-r--r--app/assets/javascripts/terms/components/app.vue2
-rw-r--r--app/assets/javascripts/terraform/components/empty_state.vue5
-rw-r--r--app/assets/javascripts/token_access/components/inbound_token_access.vue2
-rw-r--r--app/assets/javascripts/tracking/constants.js1
-rw-r--r--app/assets/javascripts/tracking/internal_events.js35
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue77
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue44
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js91
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue129
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/state_container.vue20
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue72
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue53
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/badges/beta_badge.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/badges/experiment_badge.stories.js24
-rw-r--r--app/assets/javascripts/vue_shared/components/badges/experiment_badge.vue43
-rw-r--r--app/assets/javascripts/vue_shared/components/badges/hover_badge.vue52
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/incubation/pagination.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue99
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/utils.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/mr_more_dropdown.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue51
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/utils.js37
-rw-r--r--app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_labels.vue62
-rw-r--r--app/assets/javascripts/vue_shared/components/vuex_module_provider.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue2
-rw-r--r--app/assets/javascripts/vue_shared/constants.js3
-rw-r--r--app/assets/javascripts/vue_shared/global_search/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue5
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue32
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue43
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue32
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue5
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue16
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue1
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_discussion.vue9
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue27
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue5
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue5
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue20
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue1
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_token_input.vue26
-rw-r--r--app/assets/javascripts/work_items/components/work_item_actions.vue89
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue11
-rw-r--r--app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue35
-rw-r--r--app/assets/javascripts/work_items/components/work_item_created_updated.vue15
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue11
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue34
-rw-r--r--app/assets/javascripts/work_items/components/work_item_labels.vue11
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue29
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue17
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue15
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue7
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue1
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone.vue5
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_parent.vue249
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue249
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue15
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue142
-rw-r--r--app/assets/javascripts/work_items/components/work_item_todos.vue10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_type_icon.vue5
-rw-r--r--app/assets/javascripts/work_items/constants.js28
-rw-r--r--app/assets/javascripts/work_items/graphql/add_linked_items.mutation.graphql10
-rw-r--r--app/assets/javascripts/work_items/graphql/cache_utils.js15
-rw-r--r--app/assets/javascripts/work_items/graphql/group_work_item_by_iid.query.graphql12
-rw-r--r--app/assets/javascripts/work_items/graphql/project_work_items.query.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/remove_linked_items.mutation.graphql6
-rw-r--r--app/assets/javascripts/work_items/graphql/update_work_item_notifications.mutation.graphql15
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql8
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql2
-rw-r--r--app/assets/javascripts/work_items/index.js19
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue5
-rw-r--r--app/assets/javascripts/work_items/utils.js5
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss2
-rw-r--r--app/assets/stylesheets/components/detail_page.scss4
-rw-r--r--app/assets/stylesheets/components/related_items_list.scss1
-rw-r--r--app/assets/stylesheets/fonts.scss8
-rw-r--r--app/assets/stylesheets/framework/blocks.scss4
-rw-r--r--app/assets/stylesheets/framework/common.scss47
-rw-r--r--app/assets/stylesheets/framework/emojis.scss3
-rw-r--r--app/assets/stylesheets/framework/mixins.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss14
-rw-r--r--app/assets/stylesheets/framework/variables.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/build.scss17
-rw-r--r--app/assets/stylesheets/page_bundles/escalation_policies.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/issuable.scss13
-rw-r--r--app/assets/stylesheets/page_bundles/labels.scss (renamed from app/assets/stylesheets/pages/labels.scss)44
-rw-r--r--app/assets/stylesheets/page_bundles/merge_request.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/ml_experiment_tracking.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/organizations.scss10
-rw-r--r--app/assets/stylesheets/page_bundles/projects.scss (renamed from app/assets/stylesheets/pages/projects.scss)38
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss29
-rw-r--r--app/assets/stylesheets/performance_bar.scss35
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss14
-rw-r--r--app/assets/stylesheets/utilities.scss4
-rw-r--r--app/components/pajamas/alert_component.html.haml5
-rw-r--r--app/components/pajamas/alert_component.rb2
-rw-r--r--app/components/pajamas/banner_component.html.haml36
-rw-r--r--app/components/pajamas/empty_state_component.html.haml16
-rw-r--r--app/components/projects/ml/models_index_component.rb25
-rw-r--r--app/components/projects/ml/show_ml_model_component.html.haml1
-rw-r--r--app/components/projects/ml/show_ml_model_component.rb27
-rw-r--r--app/controllers/acme_challenges_controller.rb4
-rw-r--r--app/controllers/admin/identities_controller.rb2
-rw-r--r--app/controllers/admin/topics_controller.rb4
-rw-r--r--app/controllers/application_controller.rb26
-rw-r--r--app/controllers/base_action_controller.rb31
-rw-r--r--app/controllers/chaos_controller.rb4
-rw-r--r--app/controllers/concerns/access_tokens_actions.rb1
-rw-r--r--app/controllers/concerns/creates_commit.rb14
-rw-r--r--app/controllers/concerns/enforces_two_factor_authentication.rb2
-rw-r--r--app/controllers/concerns/google_analytics_csp.rb24
-rw-r--r--app/controllers/concerns/google_syndication_csp.rb21
-rw-r--r--app/controllers/concerns/import/github_oauth.rb20
-rw-r--r--app/controllers/concerns/membership_actions.rb8
-rw-r--r--app/controllers/concerns/onboarding/redirectable.rb31
-rw-r--r--app/controllers/concerns/onboarding/status.rb9
-rw-r--r--app/controllers/concerns/planning_hierarchy.rb2
-rw-r--r--app/controllers/concerns/product_analytics_tracking.rb6
-rw-r--r--app/controllers/concerns/renders_projects_list.rb8
-rw-r--r--app/controllers/concerns/request_payload_logger.rb32
-rw-r--r--app/controllers/concerns/snippets/blobs_actions.rb15
-rw-r--r--app/controllers/confirmations_controller.rb2
-rw-r--r--app/controllers/graphql_controller.rb8
-rw-r--r--app/controllers/groups/autocomplete_sources_controller.rb5
-rw-r--r--app/controllers/groups/custom_emoji_controller.rb12
-rw-r--r--app/controllers/groups/milestones_controller.rb4
-rw-r--r--app/controllers/groups/observability_controller.rb36
-rw-r--r--app/controllers/health_controller.rb4
-rw-r--r--app/controllers/import/bulk_imports_controller.rb2
-rw-r--r--app/controllers/import/fogbugz_controller.rb2
-rw-r--r--app/controllers/import/gitea_controller.rb3
-rw-r--r--app/controllers/import/github_controller.rb6
-rw-r--r--app/controllers/jwt_controller.rb13
-rw-r--r--app/controllers/metrics_controller.rb4
-rw-r--r--app/controllers/oauth/tokens_controller.rb3
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb34
-rw-r--r--app/controllers/organizations/application_controller.rb4
-rw-r--r--app/controllers/organizations/settings_controller.rb11
-rw-r--r--app/controllers/profiles/notifications_controller.rb8
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb1
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb5
-rw-r--r--app/controllers/projects/application_controller.rb2
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb5
-rw-r--r--app/controllers/projects/blame_controller.rb1
-rw-r--r--app/controllers/projects/blob_controller.rb7
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb1
-rw-r--r--app/controllers/projects/find_file_controller.rb6
-rw-r--r--app/controllers/projects/incidents_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb14
-rw-r--r--app/controllers/projects/mattermosts_controller.rb6
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb5
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb1
-rw-r--r--app/controllers/projects/merge_requests_controller.rb11
-rw-r--r--app/controllers/projects/milestones_controller.rb4
-rw-r--r--app/controllers/projects/ml/models_controller.rb15
-rw-r--r--app/controllers/projects/prometheus/metrics_controller.rb137
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/controllers/registrations/welcome_controller.rb99
-rw-r--r--app/controllers/registrations_controller.rb23
-rw-r--r--app/controllers/repositories/lfs_api_controller.rb6
-rw-r--r--app/controllers/search_controller.rb12
-rw-r--r--app/controllers/sessions_controller.rb2
-rw-r--r--app/controllers/users/namespace_visits_controller.rb1
-rw-r--r--app/controllers/users/terms_controller.rb5
-rw-r--r--app/events/merge_requests/draft_state_change_event.rb19
-rw-r--r--app/events/merge_requests/unblocked_state_event.rb19
-rw-r--r--app/experiments/build_ios_app_guide_email_experiment.rb6
-rw-r--r--app/finders/concerns/packages/finder_helper.rb4
-rw-r--r--app/finders/merge_requests/oldest_per_commit_finder.rb14
-rw-r--r--app/finders/packages/maven/package_finder.rb6
-rw-r--r--app/finders/packages/npm/packages_for_user_finder.rb7
-rw-r--r--app/finders/projects/ml/model_finder.rb2
-rw-r--r--app/finders/projects_finder.rb2
-rw-r--r--app/finders/snippets_finder.rb14
-rw-r--r--app/finders/vs_code/settings/settings_finder.rb23
-rw-r--r--app/graphql/mutations/achievements/update_user_achievement_priorities.rb37
-rw-r--r--app/graphql/mutations/ci/job/retry.rb6
-rw-r--r--app/graphql/mutations/merge_requests/accept.rb4
-rw-r--r--app/graphql/mutations/packages/protection/rule/create.rb60
-rw-r--r--app/graphql/mutations/users/set_namespace_commit_email.rb2
-rw-r--r--app/graphql/mutations/work_items/linked_items/add.rb2
-rw-r--r--app/graphql/mutations/work_items/linked_items/base.rb5
-rw-r--r--app/graphql/mutations/work_items/update.rb12
-rw-r--r--app/graphql/resolvers/achievements/user_achievements_for_user_resolver.rb13
-rw-r--r--app/graphql/resolvers/admin/analytics/usage_trends/measurements_resolver.rb2
-rw-r--r--app/graphql/resolvers/analytics/cycle_analytics/base_issue_resolver.rb2
-rw-r--r--app/graphql/resolvers/base_resolver.rb28
-rw-r--r--app/graphql/resolvers/blobs_resolver.rb2
-rw-r--r--app/graphql/resolvers/ci/config_resolver.rb13
-rw-r--r--app/graphql/resolvers/clusters/agent_tokens_resolver.rb2
-rw-r--r--app/graphql/resolvers/concerns/caching_array_resolver.rb2
-rw-r--r--app/graphql/resolvers/concerns/work_items/look_ahead_preloads.rb6
-rw-r--r--app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb5
-rw-r--r--app/graphql/resolvers/group_issues_resolver.rb8
-rw-r--r--app/graphql/resolvers/issues_resolver.rb8
-rw-r--r--app/graphql/resolvers/kas/agent_configurations_resolver.rb2
-rw-r--r--app/graphql/resolvers/last_commit_resolver.rb2
-rw-r--r--app/graphql/resolvers/merge_request_pipelines_resolver.rb4
-rw-r--r--app/graphql/resolvers/noteable/notes_resolver.rb21
-rw-r--r--app/graphql/resolvers/package_pipelines_resolver.rb11
-rw-r--r--app/graphql/resolvers/paginated_tree_resolver.rb5
-rw-r--r--app/graphql/resolvers/project_packages_protection_rules_resolver.rb15
-rw-r--r--app/graphql/resolvers/projects_resolver.rb2
-rw-r--r--app/graphql/resolvers/user_notes_count_resolver.rb2
-rw-r--r--app/graphql/resolvers/work_items/ancestors_resolver.rb58
-rw-r--r--app/graphql/resolvers/work_items/linked_items_resolver.rb2
-rw-r--r--app/graphql/resolvers/work_items/work_item_discussions_resolver.rb6
-rw-r--r--app/graphql/types/achievements/user_achievement_type.rb5
-rw-r--r--app/graphql/types/base_argument.rb1
-rw-r--r--app/graphql/types/base_enum.rb6
-rw-r--r--app/graphql/types/base_field.rb19
-rw-r--r--app/graphql/types/ci/ci_cd_setting_type.rb6
-rw-r--r--app/graphql/types/ci/detailed_status_type.rb24
-rw-r--r--app/graphql/types/ci/job_trace_type.rb2
-rw-r--r--app/graphql/types/ci/pipeline_type.rb23
-rw-r--r--app/graphql/types/clusters/agent_type.rb2
-rw-r--r--app/graphql/types/custom_emoji_type.rb2
-rw-r--r--app/graphql/types/error_tracking/sentry_error_collection_type.rb3
-rw-r--r--app/graphql/types/issues/negated_issue_filter_input_type.rb2
-rw-r--r--app/graphql/types/merge_request_type.rb6
-rw-r--r--app/graphql/types/merge_requests/mergeability_check_identifier_enum.rb18
-rw-r--r--app/graphql/types/merge_requests/mergeability_check_status_enum.rb22
-rw-r--r--app/graphql/types/merge_requests/mergeability_check_type.rb28
-rw-r--r--app/graphql/types/mutation_type.rb2
-rw-r--r--app/graphql/types/namespace_type.rb2
-rw-r--r--app/graphql/types/notes/note_type.rb2
-rw-r--r--app/graphql/types/packages/helm/dependency_type.rb2
-rw-r--r--app/graphql/types/packages/helm/metadata_type.rb8
-rw-r--r--app/graphql/types/packages/package_base_type.rb1
-rw-r--r--app/graphql/types/packages/package_type.rb1
-rw-r--r--app/graphql/types/packages/protection/rule_access_level_enum.rb17
-rw-r--r--app/graphql/types/packages/protection/rule_package_type_enum.rb17
-rw-r--r--app/graphql/types/packages/protection/rule_type.rb33
-rw-r--r--app/graphql/types/project_type.rb8
-rw-r--r--app/graphql/types/query_type.rb4
-rw-r--r--app/graphql/types/repository_type.rb1
-rw-r--r--app/graphql/types/security/codequality_reports_comparer_type.rb2
-rw-r--r--app/graphql/types/snippet_type.rb5
-rw-r--r--app/graphql/types/todo_action_enum.rb1
-rw-r--r--app/graphql/types/user_interface.rb2
-rw-r--r--app/graphql/types/user_state_enum.rb9
-rw-r--r--app/graphql/types/user_type.rb3
-rw-r--r--app/graphql/types/work_item_type.rb10
-rw-r--r--app/graphql/types/work_items/widgets/hierarchy_type.rb6
-rw-r--r--app/graphql/types/work_items/widgets/notes_type.rb3
-rw-r--r--app/helpers/access_tokens_helper.rb9
-rw-r--r--app/helpers/appearances_helper.rb2
-rw-r--r--app/helpers/application_helper.rb8
-rw-r--r--app/helpers/application_settings_helper.rb8
-rw-r--r--app/helpers/auth_helper.rb20
-rw-r--r--app/helpers/blame_helper.rb15
-rw-r--r--app/helpers/blob_helper.rb13
-rw-r--r--app/helpers/ci/builds_helper.rb9
-rw-r--r--app/helpers/ci/jobs_helper.rb4
-rw-r--r--app/helpers/ci/pipelines_helper.rb5
-rw-r--r--app/helpers/ci/status_helper.rb34
-rw-r--r--app/helpers/ci/triggers_helper.rb4
-rw-r--r--app/helpers/commits_helper.rb5
-rw-r--r--app/helpers/diff_helper.rb34
-rw-r--r--app/helpers/dropdowns_helper.rb2
-rw-r--r--app/helpers/groups/observability_helper.rb39
-rw-r--r--app/helpers/groups_helper.rb14
-rw-r--r--app/helpers/ide_helper.rb30
-rw-r--r--app/helpers/integrations_helper.rb8
-rw-r--r--app/helpers/issuables_helper.rb18
-rw-r--r--app/helpers/merge_requests_helper.rb21
-rw-r--r--app/helpers/organizations/organization_helper.rb14
-rw-r--r--app/helpers/projects/ml/experiments_helper.rb15
-rw-r--r--app/helpers/projects_helper.rb67
-rw-r--r--app/helpers/registrations_helper.rb3
-rw-r--r--app/helpers/resource_events/abuse_report_events_helper.rb4
-rw-r--r--app/helpers/routing/projects_helper.rb12
-rw-r--r--app/helpers/safe_format_helper.rb4
-rw-r--r--app/helpers/sidekiq_helper.rb17
-rw-r--r--app/helpers/sorting_helper.rb39
-rw-r--r--app/helpers/todos_helper.rb7
-rw-r--r--app/helpers/users_helper.rb6
-rw-r--r--app/helpers/wiki_helper.rb11
-rw-r--r--app/helpers/work_items_helper.rb7
-rw-r--r--app/mailers/emails/in_product_marketing.rb42
-rw-r--r--app/mailers/emails/profile.rb18
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/mailers/previews/notify_preview.rb8
-rw-r--r--app/models/ability.rb2
-rw-r--r--app/models/abuse/reports/user_mention.rb12
-rw-r--r--app/models/abuse_report.rb11
-rw-r--r--app/models/achievements/user_achievement.rb17
-rw-r--r--app/models/analytics/cycle_analytics/issue_stage_event.rb41
-rw-r--r--app/models/analytics/cycle_analytics/merge_request_stage_event.rb4
-rw-r--r--app/models/analytics/cycle_analytics/value_stream.rb1
-rw-r--r--app/models/application_record.rb2
-rw-r--r--app/models/application_setting.rb39
-rw-r--r--app/models/application_setting_implementation.rb9
-rw-r--r--app/models/approval.rb3
-rw-r--r--app/models/award_emoji.rb2
-rw-r--r--app/models/badges/group_badge.rb2
-rw-r--r--app/models/bulk_import.rb4
-rw-r--r--app/models/bulk_imports/tracker.rb8
-rw-r--r--app/models/chat_name.rb2
-rw-r--r--app/models/ci/build.rb7
-rw-r--r--app/models/ci/build_metadata.rb2
-rw-r--r--app/models/ci/build_need.rb2
-rw-r--r--app/models/ci/catalog/components_project.rb94
-rw-r--r--app/models/ci/catalog/listing.rb2
-rw-r--r--app/models/ci/catalog/resource.rb2
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/ci/pipeline.rb5
-rw-r--r--app/models/ci/ref.rb8
-rw-r--r--app/models/ci/unlock_pipeline_request.rb53
-rw-r--r--app/models/clusters/agent_token.rb4
-rw-r--r--app/models/clusters/cluster.rb1
-rw-r--r--app/models/clusters/concerns/prometheus_client.rb2
-rw-r--r--app/models/clusters/platforms/kubernetes.rb2
-rw-r--r--app/models/commit_user_mention.rb2
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage_event_model.rb78
-rw-r--r--app/models/concerns/awardable.rb18
-rw-r--r--app/models/concerns/bulk_users_by_email_load.rb2
-rw-r--r--app/models/concerns/chronic_duration_attribute.rb2
-rw-r--r--app/models/concerns/ci/deployable.rb39
-rw-r--r--app/models/concerns/enums/issuable_link.rb12
-rw-r--r--app/models/concerns/import_state/sidekiq_job_tracker.rb2
-rw-r--r--app/models/concerns/integrations/enable_ssl_verification.rb21
-rw-r--r--app/models/concerns/issuable_link.rb28
-rw-r--r--app/models/concerns/noteable.rb4
-rw-r--r--app/models/concerns/protected_ref_access.rb2
-rw-r--r--app/models/concerns/repository_storage_movable.rb1
-rw-r--r--app/models/concerns/reset_on_column_errors.rb50
-rw-r--r--app/models/concerns/reset_on_union_error.rb37
-rw-r--r--app/models/concerns/routable.rb74
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb112
-rw-r--r--app/models/concerns/vulnerability_finding_helpers.rb1
-rw-r--r--app/models/container_expiration_policy.rb2
-rw-r--r--app/models/container_registry/protection.rb9
-rw-r--r--app/models/container_registry/protection/rule.rb20
-rw-r--r--app/models/design_user_mention.rb2
-rw-r--r--app/models/discussion_note.rb2
-rw-r--r--app/models/environment.rb4
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/group.rb50
-rw-r--r--app/models/identity.rb1
-rw-r--r--app/models/integration.rb41
-rw-r--r--app/models/integrations/asana.rb18
-rw-r--r--app/models/integrations/bamboo.rb37
-rw-r--r--app/models/integrations/base_chat_notification.rb20
-rw-r--r--app/models/integrations/base_slack_notification.rb3
-rw-r--r--app/models/integrations/chat_message/alert_message.rb4
-rw-r--r--app/models/integrations/chat_message/deployment_message.rb24
-rw-r--r--app/models/integrations/chat_message/issue_message.rb6
-rw-r--r--app/models/integrations/chat_message/pipeline_message.rb18
-rw-r--r--app/models/integrations/chat_message/push_message.rb8
-rw-r--r--app/models/integrations/discord.rb38
-rw-r--r--app/models/integrations/hangouts_chat.rb23
-rw-r--r--app/models/integrations/integration_list.rb29
-rw-r--r--app/models/integrations/jira.rb92
-rw-r--r--app/models/integrations/pipelines_email.rb4
-rw-r--r--app/models/integrations/pivotaltracker.rb4
-rw-r--r--app/models/integrations/prometheus.rb2
-rw-r--r--app/models/integrations/pushover.rb4
-rw-r--r--app/models/integrations/telegram.rb10
-rw-r--r--app/models/issue.rb48
-rw-r--r--app/models/issue_user_mention.rb2
-rw-r--r--app/models/lfs_download_object.rb9
-rw-r--r--app/models/loose_foreign_keys/deleted_record.rb34
-rw-r--r--app/models/member.rb9
-rw-r--r--app/models/members/group_member.rb10
-rw-r--r--app/models/members/last_group_owner_assigner.rb4
-rw-r--r--app/models/members/member_task.rb44
-rw-r--r--app/models/merge_request.rb104
-rw-r--r--app/models/merge_request_diff.rb12
-rw-r--r--app/models/merge_request_user_mention.rb2
-rw-r--r--app/models/milestone.rb1
-rw-r--r--app/models/ml/model.rb9
-rw-r--r--app/models/namespace.rb21
-rw-r--r--app/models/namespace/detail.rb7
-rw-r--r--app/models/namespace_setting.rb2
-rw-r--r--app/models/namespaces/traversal/linear.rb4
-rw-r--r--app/models/namespaces/traversal/linear_scopes.rb19
-rw-r--r--app/models/note.rb16
-rw-r--r--app/models/note_diff_file.rb2
-rw-r--r--app/models/packages/protection/rule.rb37
-rw-r--r--app/models/pages/lookup_path.rb6
-rw-r--r--app/models/pages_deployment.rb8
-rw-r--r--app/models/plan_limits.rb1
-rw-r--r--app/models/preloaders/group_root_ancestor_preloader.rb2
-rw-r--r--app/models/preloaders/project_root_ancestor_preloader.rb1
-rw-r--r--app/models/preloaders/user_max_access_level_in_groups_preloader.rb18
-rw-r--r--app/models/project.rb52
-rw-r--r--app/models/project_authorization.rb1
-rw-r--r--app/models/project_import_data.rb5
-rw-r--r--app/models/project_pages_metadatum.rb1
-rw-r--r--app/models/project_setting.rb2
-rw-r--r--app/models/project_team.rb6
-rw-r--r--app/models/repository.rb17
-rw-r--r--app/models/resource_events/abuse_report_event.rb7
-rw-r--r--app/models/service_desk/custom_email_credential.rb2
-rw-r--r--app/models/service_list.rb27
-rw-r--r--app/models/snippet.rb8
-rw-r--r--app/models/snippet_user_mention.rb2
-rw-r--r--app/models/ssh_host_key.rb2
-rw-r--r--app/models/storage/hashed.rb4
-rw-r--r--app/models/storage/legacy_project.rb22
-rw-r--r--app/models/suggestion.rb2
-rw-r--r--app/models/system/broadcast_message.rb2
-rw-r--r--app/models/system_note_metadata.rb2
-rw-r--r--app/models/timelog.rb2
-rw-r--r--app/models/todo.rb7
-rw-r--r--app/models/tree.rb4
-rw-r--r--app/models/upload.rb1
-rw-r--r--app/models/user.rb18
-rw-r--r--app/models/user_custom_attribute.rb15
-rw-r--r--app/models/users/callout.rb5
-rw-r--r--app/models/users/credit_card_validation.rb16
-rw-r--r--app/models/users/in_product_marketing_email.rb38
-rw-r--r--app/models/users/phone_number_validation.rb4
-rw-r--r--app/models/vs_code/settings/vs_code_setting.rb15
-rw-r--r--app/models/vulnerability.rb3
-rw-r--r--app/models/wiki_page.rb2
-rw-r--r--app/models/work_item.rb6
-rw-r--r--app/models/work_items/parent_link.rb17
-rw-r--r--app/models/work_items/related_link_restriction.rb16
-rw-r--r--app/models/work_items/related_work_item_link.rb39
-rw-r--r--app/models/work_items/type.rb1
-rw-r--r--app/models/work_items/widgets/hierarchy.rb20
-rw-r--r--app/policies/achievements/user_achievement_policy.rb11
-rw-r--r--app/policies/group_policy.rb16
-rw-r--r--app/policies/identity_provider_policy.rb4
-rw-r--r--app/policies/issue_policy.rb6
-rw-r--r--app/policies/namespaces/group_project_namespace_shared_policy.rb1
-rw-r--r--app/policies/namespaces/user_namespace_policy.rb1
-rw-r--r--app/policies/packages/protection/rule_policy.rb9
-rw-r--r--app/policies/personal_snippet_policy.rb9
-rw-r--r--app/policies/project_policy.rb8
-rw-r--r--app/policies/project_snippet_policy.rb9
-rw-r--r--app/presenters/blob_presenter.rb6
-rw-r--r--app/presenters/ci/pipeline_presenter.rb28
-rw-r--r--app/presenters/merge_request_presenter.rb4
-rw-r--r--app/presenters/ml/candidate_details_presenter.rb2
-rw-r--r--app/presenters/ml/model_presenter.rb4
-rw-r--r--app/presenters/tree_entry_presenter.rb2
-rw-r--r--app/presenters/vs_code/settings/vs_code_manifest_presenter.rb31
-rw-r--r--app/presenters/vs_code/settings/vs_code_setting_presenter.rb29
-rw-r--r--app/serializers/admin/abuse_report_details_entity.rb5
-rw-r--r--app/serializers/build_details_entity.rb7
-rw-r--r--app/serializers/ci/pipeline_entity.rb11
-rw-r--r--app/serializers/merge_request_noteable_entity.rb4
-rw-r--r--app/serializers/merge_requests/pipeline_entity.rb3
-rw-r--r--app/serializers/project_import_entity.rb2
-rw-r--r--app/services/achievements/update_user_achievement_priorities_service.rb44
-rw-r--r--app/services/admin/abuse_reports/moderate_user_service.rb5
-rw-r--r--app/services/audit_events/build_service.rb9
-rw-r--r--app/services/auth/container_registry_authentication_service.rb32
-rw-r--r--app/services/auto_merge/base_service.rb21
-rw-r--r--app/services/branches/delete_service.rb2
-rw-r--r--app/services/bulk_create_integration_service.rb4
-rw-r--r--app/services/bulk_imports/process_service.rb129
-rw-r--r--app/services/bulk_imports/relation_batch_export_service.rb11
-rw-r--r--app/services/bulk_imports/relation_export_service.rb11
-rw-r--r--app/services/chat_names/find_user_service.rb7
-rw-r--r--app/services/ci/catalog/resources/validate_service.rb48
-rw-r--r--app/services/ci/catalog/validate_resource_service.rb46
-rw-r--r--app/services/ci/components/fetch_service.rb15
-rw-r--r--app/services/ci/job_artifacts/destroy_all_expired_service.rb4
-rw-r--r--app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb24
-rw-r--r--app/services/ci/pipeline_processing/atomic_processing_service.rb2
-rw-r--r--app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb32
-rw-r--r--app/services/ci/retry_job_service.rb4
-rw-r--r--app/services/ci/retry_pipeline_service.rb8
-rw-r--r--app/services/ci/unlock_pipeline_service.rb107
-rw-r--r--app/services/clusters/agent_tokens/revoke_service.rb2
-rw-r--r--app/services/clusters/cleanup/project_namespace_service.rb2
-rw-r--r--app/services/clusters/cleanup/service_account_service.rb2
-rw-r--r--app/services/commits/create_service.rb6
-rw-r--r--app/services/concerns/update_repository_storage_methods.rb8
-rw-r--r--app/services/concerns/users/participable_service.rb2
-rw-r--r--app/services/deployments/create_for_job_service.rb2
-rw-r--r--app/services/deployments/create_service.rb1
-rw-r--r--app/services/git/branch_hooks_service.rb2
-rw-r--r--app/services/import/bitbucket_server_service.rb7
-rw-r--r--app/services/import/github_service.rb1
-rw-r--r--app/services/import/validate_remote_git_endpoint_service.rb81
-rw-r--r--app/services/issuable/clone/base_service.rb2
-rw-r--r--app/services/issuable_links/create_service.rb4
-rw-r--r--app/services/issue_links/create_service.rb2
-rw-r--r--app/services/issues/set_crm_contacts_service.rb4
-rw-r--r--app/services/jira_connect/sync_service.rb8
-rw-r--r--app/services/members/create_service.rb35
-rw-r--r--app/services/members/creator_service.rb21
-rw-r--r--app/services/merge_requests/approval_service.rb13
-rw-r--r--app/services/merge_requests/merge_service.rb2
-rw-r--r--app/services/merge_requests/mergeability/check_base_service.rb18
-rw-r--r--app/services/merge_requests/mergeability/check_broken_status_service.rb10
-rw-r--r--app/services/merge_requests/mergeability/check_ci_status_service.rb10
-rw-r--r--app/services/merge_requests/mergeability/check_conflict_status_service.rb27
-rw-r--r--app/services/merge_requests/mergeability/check_discussions_status_service.rb10
-rw-r--r--app/services/merge_requests/mergeability/check_draft_status_service.rb12
-rw-r--r--app/services/merge_requests/mergeability/check_open_status_service.rb10
-rw-r--r--app/services/merge_requests/mergeability/check_rebase_status_service.rb27
-rw-r--r--app/services/merge_requests/mergeability/detailed_merge_status_service.rb8
-rw-r--r--app/services/merge_requests/mergeability/run_checks_service.rb34
-rw-r--r--app/services/merge_requests/update_service.rb9
-rw-r--r--app/services/ml/find_or_create_model_version_service.rb1
-rw-r--r--app/services/notes/quick_actions_service.rb2
-rw-r--r--app/services/packages/create_dependency_service.rb2
-rw-r--r--app/services/packages/maven/find_or_create_package_service.rb2
-rw-r--r--app/services/packages/npm/create_package_service.rb2
-rw-r--r--app/services/packages/nuget/extract_metadata_file_service.rb42
-rw-r--r--app/services/packages/nuget/metadata_extraction_service.rb5
-rw-r--r--app/services/packages/nuget/odata_package_entry_service.rb39
-rw-r--r--app/services/packages/nuget/process_package_file_service.rb60
-rw-r--r--app/services/packages/nuget/symbols/create_symbol_files_service.rb69
-rw-r--r--app/services/packages/nuget/symbols/extract_symbol_signature_service.rb63
-rw-r--r--app/services/packages/nuget/update_package_from_metadata_service.rb39
-rw-r--r--app/services/packages/protection/create_rule_service.rb39
-rw-r--r--app/services/pages/migrate_from_legacy_storage_service.rb90
-rw-r--r--app/services/pages/migrate_legacy_storage_to_deployment_service.rb49
-rw-r--r--app/services/pages/zip_directory_service.rb97
-rw-r--r--app/services/projects/after_rename_service.rb17
-rw-r--r--app/services/projects/container_repository/cleanup_tags_base_service.rb2
-rw-r--r--app/services/projects/create_service.rb2
-rw-r--r--app/services/projects/group_links/create_service.rb2
-rw-r--r--app/services/projects/hashed_storage/base_repository_service.rb115
-rw-r--r--app/services/projects/hashed_storage/migrate_repository_service.rb55
-rw-r--r--app/services/projects/hashed_storage/migration_service.rb9
-rw-r--r--app/services/projects/hashed_storage/rollback_attachments_service.rb29
-rw-r--r--app/services/projects/hashed_storage/rollback_repository_service.rb53
-rw-r--r--app/services/projects/hashed_storage/rollback_service.rb37
-rw-r--r--app/services/projects/import_service.rb4
-rw-r--r--app/services/projects/in_product_marketing_campaign_emails_service.rb53
-rw-r--r--app/services/projects/participants_service.rb4
-rw-r--r--app/services/projects/record_target_platforms_service.rb15
-rw-r--r--app/services/projects/transfer_service.rb32
-rw-r--r--app/services/projects/update_pages_service.rb5
-rw-r--r--app/services/projects/update_repository_storage_service.rb22
-rw-r--r--app/services/releases/create_service.rb2
-rw-r--r--app/services/releases/destroy_service.rb2
-rw-r--r--app/services/repositories/base_service.rb4
-rw-r--r--app/services/repositories/replicate_service.rb24
-rw-r--r--app/services/spam/spam_verdict_service.rb2
-rw-r--r--app/services/system_notes/issuables_service.rb13
-rw-r--r--app/services/tasks_to_be_done/base_service.rb55
-rw-r--r--app/services/tasks_to_be_done/create_ci_task_service.rb44
-rw-r--r--app/services/tasks_to_be_done/create_code_task_service.rb52
-rw-r--r--app/services/tasks_to_be_done/create_issues_task_service.rb43
-rw-r--r--app/services/todo_service.rb10
-rw-r--r--app/services/todos/destroy/base_service.rb31
-rw-r--r--app/services/todos/destroy/confidential_issue_service.rb39
-rw-r--r--app/services/todos/destroy/group_private_service.rb36
-rw-r--r--app/services/todos/destroy/project_private_service.rb23
-rw-r--r--app/services/todos/destroy/unauthorized_features_service.rb8
-rw-r--r--app/services/update_container_registry_info_service.rb5
-rw-r--r--app/services/users/allow_possible_spam_service.rb18
-rw-r--r--app/services/users/auto_ban_service.rb33
-rw-r--r--app/services/users/in_product_marketing_email_records.rb3
-rw-r--r--app/services/users/set_namespace_commit_email_service.rb2
-rw-r--r--app/services/users/signup_service.rb34
-rw-r--r--app/services/users/trust_service.rb (renamed from app/services/users/disallow_possible_spam_service.rb)5
-rw-r--r--app/services/users/untrust_service.rb14
-rw-r--r--app/services/verify_pages_domain_service.rb8
-rw-r--r--app/services/vs_code/settings/create_or_update_service.rb41
-rw-r--r--app/services/web_hook_service.rb6
-rw-r--r--app/services/work_items/related_work_item_links/create_service.rb21
-rw-r--r--app/services/work_items/widgets/labels_service/update_service.rb1
-rw-r--r--app/services/work_items/widgets/start_and_due_date_service/update_service.rb1
-rw-r--r--app/validators/addressable_url_validator.rb2
-rw-r--r--app/validators/duration_validator.rb2
-rw-r--r--app/validators/json_schemas/catalog_resource_component_inputs.json3
-rw-r--r--app/validators/json_schemas/vulnerability_cvss_vectors.json22
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml4
-rw-r--r--app/views/admin/application_settings/_email.html.haml13
-rw-r--r--app/views/admin/application_settings/_ip_limits.html.haml15
-rw-r--r--app/views/admin/application_settings/_jira_connect.html.haml4
-rw-r--r--app/views/admin/application_settings/_localization.html.haml2
-rw-r--r--app/views/admin/application_settings/_outbound.html.haml4
-rw-r--r--app/views/admin/application_settings/_performance_bar.html.haml4
-rw-r--r--app/views/admin/application_settings/_sentry.html.haml25
-rw-r--r--app/views/admin/application_settings/_snowplow.html.haml8
-rw-r--r--app/views/admin/application_settings/_usage.html.haml2
-rw-r--r--app/views/admin/application_settings/general.html.haml4
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml2
-rw-r--r--app/views/admin/application_settings/network.html.haml4
-rw-r--r--app/views/admin/application_settings/preferences.html.haml2
-rw-r--r--app/views/admin/applications/_form.html.haml8
-rw-r--r--app/views/admin/applications/index.html.haml2
-rw-r--r--app/views/admin/health_check/show.html.haml11
-rw-r--r--app/views/admin/identities/_form.html.haml2
-rw-r--r--app/views/admin/jobs/index.html.haml2
-rw-r--r--app/views/admin/labels/_label.html.haml2
-rw-r--r--app/views/admin/labels/index.html.haml1
-rw-r--r--app/views/admin/projects/index.html.haml1
-rw-r--r--app/views/admin/projects/show.html.haml1
-rw-r--r--app/views/admin/sessions/_new_base.html.haml4
-rw-r--r--app/views/admin/users/_profile.html.haml2
-rw-r--r--app/views/admin/users/show.html.haml1
-rw-r--r--app/views/award_emoji/_awards_block.html.haml3
-rw-r--r--app/views/ci/status/_icon.html.haml14
-rw-r--r--app/views/dashboard/_projects_head.html.haml2
-rw-r--r--app/views/dashboard/projects/_blank_state_admin_welcome.html.haml2
-rw-r--r--app/views/dashboard/projects/_blank_state_welcome.html.haml2
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml2
-rw-r--r--app/views/dashboard/projects/index.html.haml1
-rw-r--r--app/views/dashboard/todos/index.html.haml42
-rw-r--r--app/views/devise/confirmations/almost_there.haml4
-rw-r--r--app/views/devise/registrations/new.html.haml4
-rw-r--r--app/views/devise/sessions/_new_base.html.haml6
-rw-r--r--app/views/devise/sessions/new.html.haml4
-rw-r--r--app/views/devise/shared/_signup_omniauth_providers.haml2
-rw-r--r--app/views/groups/_home_panel.html.haml2
-rw-r--r--app/views/groups/custom_emoji/index.html.haml8
-rw-r--r--app/views/groups/edit.html.haml2
-rw-r--r--app/views/groups/labels/index.html.haml1
-rw-r--r--app/views/groups/milestones/_form.html.haml4
-rw-r--r--app/views/groups/milestones/index.html.haml6
-rw-r--r--app/views/groups/observability/observability.html.haml3
-rw-r--r--app/views/groups/projects.html.haml1
-rw-r--r--app/views/groups/settings/_git_access_protocols.html.haml2
-rw-r--r--app/views/groups/settings/_transfer.html.haml2
-rw-r--r--app/views/groups/settings/access_tokens/index.html.haml1
-rw-r--r--app/views/groups/work_items/show.html.haml8
-rw-r--r--app/views/import/github/status.html.haml4
-rw-r--r--app/views/layouts/_flash.html.haml1
-rw-r--r--app/views/layouts/_google_tag_manager_body.html.haml4
-rw-r--r--app/views/layouts/_google_tag_manager_head.html.haml51
-rw-r--r--app/views/layouts/_head.html.haml6
-rw-r--r--app/views/layouts/_img_loader.html.haml2
-rw-r--r--app/views/layouts/_loading_hints.html.haml1
-rw-r--r--app/views/layouts/_snowplow.html.haml2
-rw-r--r--app/views/layouts/component_preview.html.haml2
-rw-r--r--app/views/layouts/header/_super_sidebar_logged_out.haml2
-rw-r--r--app/views/layouts/header/_title.html.haml2
-rw-r--r--app/views/layouts/mailer/_user_deactivated_additional_text.html.haml2
-rw-r--r--app/views/layouts/mailer/_user_deactivated_additional_text.text.erb2
-rw-r--r--app/views/layouts/nav/_ask_duo_button.html.haml2
-rw-r--r--app/views/layouts/nav/_top_bar.html.haml4
-rw-r--r--app/views/layouts/nav/breadcrumbs/_breadcrumbs.html.haml4
-rw-r--r--app/views/layouts/project_settings.html.haml1
-rw-r--r--app/views/layouts/terms.html.haml15
-rw-r--r--app/views/notify/build_ios_app_guide_email.html.haml13
-rw-r--r--app/views/notify/build_ios_app_guide_email.text.erb13
-rw-r--r--app/views/notify/resource_access_tokens_about_to_expire_email.html.haml2
-rw-r--r--app/views/notify/resource_access_tokens_about_to_expire_email.text.erb2
-rw-r--r--app/views/organizations/organizations/index.html.haml3
-rw-r--r--app/views/organizations/organizations/new.html.haml3
-rw-r--r--app/views/organizations/settings/general.html.haml1
-rw-r--r--app/views/profiles/chat_names/index.html.haml2
-rw-r--r--app/views/profiles/keys/_key_details.html.haml2
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml34
-rw-r--r--app/views/projects/_commit_button.html.haml2
-rw-r--r--app/views/projects/_export.html.haml6
-rw-r--r--app/views/projects/_files.html.haml2
-rw-r--r--app/views/projects/_find_file_link.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml10
-rw-r--r--app/views/projects/_last_push.html.haml2
-rw-r--r--app/views/projects/_transfer.html.haml2
-rw-r--r--app/views/projects/artifacts/file.html.haml2
-rw-r--r--app/views/projects/blame/show.html.haml1
-rw-r--r--app/views/projects/blob/_blob.html.haml1
-rw-r--r--app/views/projects/blob/_breadcrumb.html.haml6
-rw-r--r--app/views/projects/blob/_header.html.haml1
-rw-r--r--app/views/projects/blob/_viewer_switcher.html.haml12
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/blob/show.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_loading.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/buttons/_clone.html.haml4
-rw-r--r--app/views/projects/buttons/_compare.html.haml8
-rw-r--r--app/views/projects/buttons/_download.html.haml4
-rw-r--r--app/views/projects/commits/show.html.haml1
-rw-r--r--app/views/projects/edit.html.haml10
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/find_file/show.html.haml9
-rw-r--r--app/views/projects/issues/_details_content.html.haml2
-rw-r--r--app/views/projects/issues/_discussion.html.haml3
-rw-r--r--app/views/projects/issues/_emoji_block.html.haml3
-rw-r--r--app/views/projects/issues/new.html.haml1
-rw-r--r--app/views/projects/jobs/index.html.haml2
-rw-r--r--app/views/projects/labels/index.html.haml1
-rw-r--r--app/views/projects/merge_requests/_awards_block.html.haml2
-rw-r--r--app/views/projects/merge_requests/_description.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/merge_requests/_mr_box.html.haml2
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml10
-rw-r--r--app/views/projects/merge_requests/_page.html.haml9
-rw-r--r--app/views/projects/milestones/_form.html.haml4
-rw-r--r--app/views/projects/milestones/index.html.haml6
-rw-r--r--app/views/projects/ml/models/index.html.haml2
-rw-r--r--app/views/projects/ml/models/show.html.haml5
-rw-r--r--app/views/projects/new.html.haml1
-rw-r--r--app/views/projects/notes/_actions.html.haml2
-rw-r--r--app/views/projects/notes/_more_actions_dropdown.html.haml4
-rw-r--r--app/views/projects/pipelines/new.html.haml1
-rw-r--r--app/views/projects/settings/_archive.html.haml4
-rw-r--r--app/views/projects/settings/_general.html.haml4
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml2
-rw-r--r--app/views/projects/settings/integrations/_form.html.haml13
-rw-r--r--app/views/projects/settings/merge_requests/show.html.haml2
-rw-r--r--app/views/projects/snippets/show.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml5
-rw-r--r--app/views/projects/work_items/show.html.haml (renamed from app/views/projects/work_items/index.html.haml)2
-rw-r--r--app/views/protected_branches/shared/_dropdown.html.haml4
-rw-r--r--app/views/registrations/welcome/show.html.haml43
-rw-r--r--app/views/search/results/_error.html.haml2
-rw-r--r--app/views/search/results/_timeout.html.haml2
-rw-r--r--app/views/search/show.html.haml2
-rw-r--r--app/views/shared/_broadcast_message.html.haml4
-rw-r--r--app/views/shared/_clone_panel.html.haml6
-rw-r--r--app/views/shared/_label.html.haml2
-rw-r--r--app/views/shared/_logo.svg2
-rw-r--r--app/views/shared/_logo_with_black_text.svg2
-rw-r--r--app/views/shared/_logo_with_white_text.svg2
-rw-r--r--app/views/shared/_zen.html.haml6
-rw-r--r--app/views/shared/boards/_show.html.haml1
-rw-r--r--app/views/shared/doorkeeper/applications/_show.html.haml4
-rw-r--r--app/views/shared/empty_states/_labels.html.haml2
-rw-r--r--app/views/shared/empty_states/_merge_requests.html.haml4
-rw-r--r--app/views/shared/empty_states/_priority_labels.html.haml2
-rw-r--r--app/views/shared/empty_states/_snippets.html.haml4
-rw-r--r--app/views/shared/empty_states/_wikis.html.haml2
-rw-r--r--app/views/shared/empty_states/_wikis_layout.html.haml2
-rw-r--r--app/views/shared/file_hooks/_index.html.haml2
-rw-r--r--app/views/shared/integrations/edit.html.haml6
-rw-r--r--app/views/shared/integrations/mattermost_slash_commands/_detailed_help.html.haml2
-rw-r--r--app/views/shared/integrations/slack_slash_commands/_help.html.haml12
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml3
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml20
-rw-r--r--app/views/shared/issuable/_status_box.html.haml8
-rw-r--r--app/views/shared/issuable/form/_metadata.html.haml11
-rw-r--r--app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml2
-rw-r--r--app/views/shared/issuable/form/_template_selector.html.haml2
-rw-r--r--app/views/shared/labels/_form.html.haml9
-rw-r--r--app/views/shared/labels/_nav.html.haml4
-rw-r--r--app/views/shared/milestones/_description.html.haml6
-rw-r--r--app/views/shared/milestones/_form_dates.html.haml4
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml2
-rw-r--r--app/views/shared/milestones/_milestone.html.haml2
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml4
-rw-r--r--app/views/shared/notes/_comment_button.html.haml2
-rw-r--r--app/views/shared/notes/_edit_form.html.haml4
-rw-r--r--app/views/shared/notes/_form.html.haml4
-rw-r--r--app/views/shared/notes/_note.html.haml4
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml2
-rw-r--r--app/views/shared/projects/_list.html.haml3
-rw-r--r--app/views/shared/projects/_project.html.haml4
-rw-r--r--app/views/shared/projects/_search_form.html.haml2
-rw-r--r--app/views/shared/snippets/_snippet.html.haml6
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml4
-rw-r--r--app/views/shared/web_hooks/_form.html.haml2
-rw-r--r--app/views/shared/web_hooks/_hook.html.haml2
-rw-r--r--app/views/shared/wikis/_form.html.haml2
-rw-r--r--app/views/shared/wikis/_main_links.html.haml4
-rw-r--r--app/views/shared/wikis/_pages_wiki_page.html.haml2
-rw-r--r--app/views/shared/wikis/_sidebar.html.haml4
-rw-r--r--app/views/shared/wikis/_sidebar_wiki_page.html.haml2
-rw-r--r--app/views/shared/wikis/_wiki_content.html.haml2
-rw-r--r--app/views/shared/wikis/_wiki_directory.html.haml4
-rw-r--r--app/views/shared/wikis/show.html.haml4
-rw-r--r--app/views/snippets/notes/_actions.html.haml2
-rw-r--r--app/views/users/_cover_controls.html.haml2
-rw-r--r--app/views/users/_follow_user.html.haml2
-rw-r--r--app/views/users/_overview.html.haml2
-rw-r--r--app/views/users/_profile_basic_info.html.haml2
-rw-r--r--app/views/users/_view_user_in_admin_area.html.haml2
-rw-r--r--app/views/users/show.html.haml7
-rw-r--r--app/views/users/terms/index.html.haml4
-rw-r--r--app/workers/all_queues.yml154
-rw-r--r--app/workers/bulk_import_worker.rb116
-rw-r--r--app/workers/bulk_imports/entity_worker.rb114
-rw-r--r--app/workers/bulk_imports/finish_batched_pipeline_worker.rb8
-rw-r--r--app/workers/bulk_imports/pipeline_batch_worker.rb6
-rw-r--r--app/workers/bulk_imports/pipeline_worker.rb9
-rw-r--r--app/workers/ci/initial_pipeline_process_worker.rb2
-rw-r--r--app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb1
-rw-r--r--app/workers/ci/ref_delete_unlock_artifacts_worker.rb18
-rw-r--r--app/workers/ci/refs/unlock_previous_pipelines_worker.rb26
-rw-r--r--app/workers/ci/schedule_unlock_pipelines_in_queue_cron_worker.rb21
-rw-r--r--app/workers/ci/unlock_pipelines_in_queue_worker.rb55
-rw-r--r--app/workers/click_house/events_sync_worker.rb1
-rw-r--r--app/workers/concerns/auto_devops_queue.rb2
-rw-r--r--app/workers/concerns/chaos_queue.rb2
-rw-r--r--app/workers/concerns/gitlab/github_import/object_importer.rb36
-rw-r--r--app/workers/concerns/gitlab/github_import/queue.rb8
-rw-r--r--app/workers/concerns/gitlab/github_import/stage_methods.rb22
-rw-r--r--app/workers/concerns/limited_capacity/job_tracker.rb1
-rw-r--r--app/workers/concerns/limited_capacity/worker.rb36
-rw-r--r--app/workers/concerns/worker_attributes.rb4
-rw-r--r--app/workers/database/batched_background_migration/ci_database_worker.rb1
-rw-r--r--app/workers/delete_container_repository_worker.rb15
-rw-r--r--app/workers/environments/stop_job_failed_worker.rb25
-rw-r--r--app/workers/gitlab/bitbucket_import/advance_stage_worker.rb12
-rw-r--r--app/workers/gitlab/bitbucket_import/import_issue_notes_worker.rb13
-rw-r--r--app/workers/gitlab/bitbucket_import/import_issue_worker.rb13
-rw-r--r--app/workers/gitlab/bitbucket_import/import_lfs_object_worker.rb13
-rw-r--r--app/workers/gitlab/bitbucket_import/import_pull_request_notes_worker.rb13
-rw-r--r--app/workers/gitlab/bitbucket_import/stage/import_issues_notes_worker.rb30
-rw-r--r--app/workers/gitlab/bitbucket_import/stage/import_issues_worker.rb30
-rw-r--r--app/workers/gitlab/bitbucket_import/stage/import_lfs_objects_worker.rb30
-rw-r--r--app/workers/gitlab/bitbucket_import/stage/import_pull_requests_notes_worker.rb30
-rw-r--r--app/workers/gitlab/bitbucket_import/stage/import_pull_requests_worker.rb2
-rw-r--r--app/workers/gitlab/bitbucket_server_import/advance_stage_worker.rb6
-rw-r--r--app/workers/gitlab/github_gists_import/import_gist_worker.rb6
-rw-r--r--app/workers/gitlab/github_gists_import/start_import_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/advance_stage_worker.rb6
-rw-r--r--app/workers/gitlab/github_import/refresh_import_jid_worker.rb1
-rw-r--r--app/workers/gitlab/github_import/stage/finish_import_worker.rb1
-rw-r--r--app/workers/gitlab/github_import/stage/import_attachments_worker.rb1
-rw-r--r--app/workers/gitlab/github_import/stage/import_base_data_worker.rb17
-rw-r--r--app/workers/gitlab/github_import/stage/import_collaborators_worker.rb15
-rw-r--r--app/workers/gitlab/github_import/stage/import_issue_events_worker.rb1
-rw-r--r--app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb1
-rw-r--r--app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb1
-rw-r--r--app/workers/gitlab/github_import/stage/import_notes_worker.rb1
-rw-r--r--app/workers/gitlab/github_import/stage/import_protected_branches_worker.rb10
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb1
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker.rb1
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb1
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb15
-rw-r--r--app/workers/gitlab/github_import/stage/import_repository_worker.rb16
-rw-r--r--app/workers/gitlab/import/advance_stage.rb88
-rw-r--r--app/workers/gitlab/import/stuck_project_import_jobs_worker.rb1
-rw-r--r--app/workers/gitlab/jira_import/advance_stage_worker.rb6
-rw-r--r--app/workers/gitlab_shell_worker.rb25
-rw-r--r--app/workers/hashed_storage/migrator_worker.rb5
-rw-r--r--app/workers/hashed_storage/project_migrate_worker.rb13
-rw-r--r--app/workers/hashed_storage/project_rollback_worker.rb13
-rw-r--r--app/workers/hashed_storage/rollbacker_worker.rb5
-rw-r--r--app/workers/integrations/irker_worker.rb2
-rw-r--r--app/workers/issuable/related_links_create_worker.rb65
-rw-r--r--app/workers/jira_connect/sync_project_worker.rb5
-rw-r--r--app/workers/merge_worker.rb5
-rw-r--r--app/workers/pages/deactivated_deployments_delete_cron_worker.rb19
-rw-r--r--app/workers/projects/after_import_worker.rb2
-rw-r--r--app/workers/projects/record_target_platforms_worker.rb2
-rw-r--r--app/workers/run_pipeline_schedule_worker.rb2
-rw-r--r--app/workers/tasks_to_be_done/create_worker.rb17
1371 files changed, 13768 insertions, 9343 deletions
diff --git a/app/assets/images/callouts/rich_text_editor_illustration.svg b/app/assets/images/callouts/rich_text_editor_illustration.svg
deleted file mode 100644
index b07d8871fe6..00000000000
--- a/app/assets/images/callouts/rich_text_editor_illustration.svg
+++ /dev/null
@@ -1,79 +0,0 @@
-<svg width="280" height="130" viewBox="0 0 280 130" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_191_42179)">
-<circle cx="189.5" cy="-42.5" r="131.5" fill="url(#paint0_radial_191_42179)"/>
-<circle cx="-41.5" cy="-97.5" r="198.5" fill="url(#paint1_radial_191_42179)"/>
-<circle cx="309.5" cy="-7.5" r="121.5" fill="url(#paint2_radial_191_42179)"/>
-<g filter="url(#filter0_b_191_42179)">
-<path d="M0 4C0 1.79086 1.79086 0 4 0H276C278.209 0 280 1.79086 280 4V130H0V4Z" fill="white" fill-opacity="0.01"/>
-</g>
-</g>
-<g transform="translate(64, 16)">
-<path d="M135.455 109.089H47.0349C30.7979 109.089 17.6364 95.8523 17.6364 79.5229V0H106.056C122.293 0 135.455 13.2364 135.455 29.5658V109.091V109.089Z" fill="white"/>
-<path d="M37.0022 29H116C116 46 116 63 116 80C116 84.4183 112.549 88 108.293 88L37 88V29H37.0022Z" fill="white"/>
-<path d="M116 16H37V29H116V16Z" fill="#AEA5D6"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M36 15H101V16H37V42H36V15Z" fill="#171321"/>
-<path d="M53 22.5C53 23.8807 51.8807 25 50.5 25C49.1193 25 48 23.8807 48 22.5C48 21.1193 49.1193 20 50.5 20C51.8807 20 53 21.1193 53 22.5Z" fill="#A888F4"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M50.5 24C51.3284 24 52 23.3284 52 22.5C52 21.6716 51.3284 21 50.5 21C49.6716 21 49 21.6716 49 22.5C49 23.3284 49.6716 24 50.5 24ZM50.5 25C51.8807 25 53 23.8807 53 22.5C53 21.1193 51.8807 20 50.5 20C49.1193 20 48 21.1193 48 22.5C48 23.8807 49.1193 25 50.5 25Z" fill="#171321"/>
-<path d="M60 22.5C60 23.8807 58.8807 25 57.5 25C56.1193 25 55 23.8807 55 22.5C55 21.1193 56.1193 20 57.5 20C58.8807 20 60 21.1193 60 22.5Z" fill="#FF9D73"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M57.5 24C58.3284 24 59 23.3284 59 22.5C59 21.6716 58.3284 21 57.5 21C56.6716 21 56 21.6716 56 22.5C56 23.3284 56.6716 24 57.5 24ZM57.5 25C58.8807 25 60 23.8807 60 22.5C60 21.1193 58.8807 20 57.5 20C56.1193 20 55 21.1193 55 22.5C55 23.8807 56.1193 25 57.5 25Z" fill="#171321"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M22.5 10.5C30.3325 10.5 38.152 14.4668 42.923 22.3723L43 22.5L42.923 22.6277C38.152 30.5332 30.3325 34.5 22.5 34.5C14.6675 34.5 6.84799 30.5332 2.07704 22.6277L2 22.5L2.07704 22.3723C6.84799 14.4668 14.6675 10.5 22.5 10.5Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M41.8274 22.5C37.2212 15.1579 29.8576 11.5 22.5 11.5C15.1424 11.5 7.77878 15.1579 3.1726 22.5C7.77878 29.8421 15.1424 33.5 22.5 33.5C29.8576 33.5 37.2212 29.8421 41.8274 22.5ZM2 22.5L2.07704 22.6277C6.84799 30.5332 14.6675 34.5 22.5 34.5C30.3325 34.5 38.152 30.5332 42.923 22.6277L43 22.5L42.923 22.3723C38.152 14.4668 30.3325 10.5 22.5 10.5C14.6675 10.5 6.84799 14.4668 2.07704 22.3723L2 22.5Z" fill="#171321"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M0 22.5C0 21.3954 0.895434 20.5 2 20.5C3.10457 20.5 4 21.3954 4 22.5C4 23.6046 3.10457 24.5 2 24.5C0.895434 24.5 0 23.6046 0 22.5Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M2 21.5C1.44772 21.5 1 21.9477 1 22.5C1 23.0523 1.44772 23.5 2 23.5C2.55229 23.5 3 23.0523 3 22.5C3 21.9477 2.55229 21.5 2 21.5ZM2 20.5C0.895434 20.5 0 21.3954 0 22.5C0 23.6046 0.895434 24.5 2 24.5C3.10457 24.5 4 23.6046 4 22.5C4 21.3954 3.10457 20.5 2 20.5Z" fill="#171321"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M41 22.5C41 21.3954 41.8954 20.5 43 20.5C44.1046 20.5 45 21.3954 45 22.5C45 23.6046 44.1046 24.5 43 24.5C41.8954 24.5 41 23.6046 41 22.5Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M43 21.5C42.4477 21.5 42 21.9477 42 22.5C42 23.0523 42.4477 23.5 43 23.5C43.5523 23.5 44 23.0523 44 22.5C44 21.9477 43.5523 21.5 43 21.5ZM43 20.5C41.8954 20.5 41 21.3954 41 22.5C41 23.6046 41.8954 24.5 43 24.5C44.1046 24.5 45 23.6046 45 22.5C45 21.3954 44.1046 20.5 43 20.5Z" fill="#171321"/>
-<path d="M22.5 30C26.6421 30 30 26.6421 30 22.5C30 18.3579 26.6421 15 22.5 15C18.3579 15 15 18.3579 15 22.5C15 26.6421 18.3579 30 22.5 30Z" fill="#10B1B1"/>
-<path d="M27.0838 22.3192C27.0838 23.5746 25.3096 22.4248 23.8629 20.9715C22.4317 19.5337 21.3192 17.7969 22.5614 17.7969C25.0589 17.7969 27.0838 19.8217 27.0838 22.3192Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M37 34V65H36V34H37Z" fill="#171321"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M37 93V70.0117H36V94H57V93H37Z" fill="#171321"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M64 93H93V94H64V93Z" fill="#171321"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M116 65V38H117V65H116Z" fill="#171321"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M144 104H122V103H144V104Z" fill="#AEA5D6"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M138 97H129V96H138V97Z" fill="#AEA5D6"/>
-<path d="M104 34H47V46H104V34Z" fill="#E7E4F2"/>
-<path d="M74 51H48V83H74V51Z" fill="#FF9D73"/>
-<path d="M60.5 70C61.8807 70 63 68.8807 63 67.5C63 66.1193 61.8807 65 60.5 65C59.1193 65 58 66.1193 58 67.5C58 68.8807 59.1193 70 60.5 70Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M60.5 69C61.3284 69 62 68.3284 62 67.5C62 66.6716 61.3284 66 60.5 66C59.6716 66 59 66.6716 59 67.5C59 68.3284 59.6716 69 60.5 69ZM63 67.5C63 68.8807 61.8807 70 60.5 70C59.1193 70 58 68.8807 58 67.5C58 66.1193 59.1193 65 60.5 65C61.8807 65 63 66.1193 63 67.5Z" fill="#171321"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M74 50V84H73V50H74Z" fill="#171321"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M47 84V70H48V84H47Z" fill="#171321"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M47 65V50H48V65H47Z" fill="#171321"/>
-<path d="M104 51H78V71H104V51Z" fill="#E7E4F2"/>
-<path d="M104 76H78V83H104V76Z" fill="#E7E4F2"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M23 34V58.5C23 63.1944 26.8056 67 31.5 67H59V68H31.5C26.2533 68 22 63.7467 22 58.5V34H23Z" fill="#171321"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M46 51H75V52H46V51Z" fill="#171321"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M75 83H46V82H75V83Z" fill="#171321"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M14 67H32V68H14V67Z" fill="#171321"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M2 67H10V68H2V67Z" fill="#AEA5D6"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M58.5 69.5V95.5C58.5 100.194 62.3056 104 67 104H94L93.5 105H67C61.7533 105 57.5 100.747 57.5 95.5V69.5H58.5Z" fill="#171321"/>
-<rect x="130.598" y="54.4473" width="19" height="46" transform="rotate(45 130.598 54.4473)" fill="white"/>
-<path d="M111.506 100.41L98.0714 86.9746L93.4752 105.006L111.506 100.41Z" fill="#FF9D73"/>
-<path d="M140.498 44.5479L144.033 48.0834C146.666 50.7156 147.982 52.0318 148.701 53.443C150.154 56.2951 150.154 59.6706 148.701 62.5228C147.982 63.934 146.666 65.2501 144.033 67.8824L144.033 67.8824L130.598 54.4473L140.498 44.5479Z" fill="#5829CB"/>
-<path d="M130.598 54.4473L131.305 55.1544L98.7785 87.6813L98.0714 86.9742L130.598 54.4473Z" fill="#171321"/>
-<path d="M143.326 67.1758L144.033 67.8829L111.506 100.41L110.799 99.7027L143.326 67.1758Z" fill="#171321"/>
-<path d="M136.962 60.8115L137.669 61.5186L105.142 94.0455L104.435 93.3384L136.962 60.8115Z" fill="#171321"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M95.4861 97.1172L93.4752 105.006L101.364 102.995L95.4861 97.1172Z" fill="#171321"/>
-</g>
-<defs>
-<filter id="filter0_b_191_42179" x="-50" y="-50" width="380" height="230" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
-<feFlood flood-opacity="0" result="BackgroundImageFix"/>
-<feGaussianBlur in="BackgroundImageFix" stdDeviation="25"/>
-<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_191_42179"/>
-<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_191_42179" result="shape"/>
-</filter>
-<radialGradient id="paint0_radial_191_42179" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(189.5 -42.5) rotate(89.5818) scale(125.986)">
-<stop stop-color="#7759C2"/>
-<stop offset="1" stop-color="#7759C2" stop-opacity="0"/>
-</radialGradient>
-<radialGradient id="paint1_radial_191_42179" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(-41.5 -97.5) rotate(89.5818) scale(190.176)">
-<stop stop-color="#D64028"/>
-<stop offset="1" stop-color="#D64028" stop-opacity="0"/>
-</radialGradient>
-<radialGradient id="paint2_radial_191_42179" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(309.5 -7.5) rotate(89.5818) scale(116.405)">
-<stop stop-color="#EF76F1"/>
-<stop offset="1" stop-color="#EF76F1" stop-opacity="0"/>
-</radialGradient>
-<clipPath id="clip0_191_42179">
-<path d="M0 4C0 1.79086 1.79086 0 4 0H276C278.209 0 280 1.79086 280 4V130H0V4Z" fill="white"/>
-</clipPath>
-</defs>
-</svg>
diff --git a/app/assets/images/jobs-empty-state.svg b/app/assets/images/jobs-empty-state.svg
deleted file mode 100644
index e6e0681a002..00000000000
--- a/app/assets/images/jobs-empty-state.svg
+++ /dev/null
@@ -1,33 +0,0 @@
-<svg width="234" height="162" viewBox="0 0 234 162" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M174.68 56.344H200.5C215.412 56.344 227.5 44.1787 227.5 29.172C227.5 14.1653 215.412 2 200.5 2C185.588 2 173.5 14.1653 173.5 29.172C173.5 36.2548 176.193 42.7046 180.604 47.5412" stroke="#C2B7E6" stroke-width="4" stroke-linecap="round"/>
-<path d="M145.5 76.4714C145.5 65.3553 154.454 56.344 165.5 56.344" stroke="#C2B7E6" stroke-width="4" stroke-linecap="round"/>
-<path d="M102.5 121.758H29.5C14.5883 121.758 2.5 109.593 2.5 94.586C2.5 79.5794 14.5883 67.4141 29.5 67.4141C44.4117 67.4141 56.5 79.5794 56.5 94.586C56.5 101.669 53.8072 108.119 49.3957 112.955" stroke="#C2B7E6" stroke-width="4" stroke-linecap="round"/>
-<path d="M67.0466 121.758H52.5C42.5589 121.758 34.5 129.868 34.5 139.873C34.5 149.877 42.5589 157.987 52.5 157.987C62.4411 157.987 70.5 149.877 70.5 139.873C70.5 137.478 70.0384 135.192 69.1998 133.1" stroke="#C2B7E6" stroke-width="4" stroke-linecap="round"/>
-<g clip-path="url(#clip0)">
-<path d="M55.0188 135.3C55.1617 134.764 54.8451 134.211 54.3117 134.068C53.7782 133.925 53.2298 134.243 53.0869 134.78L49.9811 146.445C49.8381 146.981 50.1547 147.534 50.6882 147.677C51.2217 147.821 51.77 147.503 51.9129 146.965L55.0188 135.3Z" fill="#FC6D26"/>
-<path d="M49.2071 137.142C49.5976 137.534 49.5976 138.172 49.2071 138.565L46.9142 140.873L49.2071 143.18C49.5976 143.573 49.5976 144.211 49.2071 144.603C48.8166 144.997 48.1834 144.997 47.7929 144.603L44.7929 141.584C44.4024 141.192 44.4024 140.554 44.7929 140.161L47.7929 137.142C48.1834 136.748 48.8166 136.748 49.2071 137.142Z" fill="#FC6D26"/>
-<path d="M55.7929 137.142C55.4024 137.534 55.4024 138.172 55.7929 138.565L58.0858 140.873L55.7929 143.18C55.4024 143.573 55.4024 144.211 55.7929 144.603C56.1834 144.997 56.8166 144.997 57.2071 144.603L60.2071 141.584C60.5976 141.192 60.5976 140.554 60.2071 140.161L57.2071 137.142C56.8166 136.748 56.1834 136.748 55.7929 137.142Z" fill="#FC6D26"/>
-</g>
-<path d="M212.102 160C222.815 160 231.5 151.214 231.5 140.376C231.5 129.537 222.815 120.752 212.102 120.752H151.5" stroke="#C2B7E6" stroke-width="4" stroke-linecap="round"/>
-<path d="M126.5 138.866C107.171 138.866 91.5 123.096 91.5 103.643C91.5 84.191 107.171 68.4204 126.5 68.4204C145.829 68.4204 161.5 84.191 161.5 103.643C161.5 123.096 145.829 138.866 126.5 138.866ZM126.5 131.451C141.76 131.451 154.132 119.001 154.132 103.643C154.132 88.2861 141.76 75.8358 126.5 75.8358C111.24 75.8358 98.8684 88.2861 98.8684 103.643C98.8684 119.001 111.24 131.451 126.5 131.451Z" fill="#FC6D26"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M126.126 87.1326C135.355 87.1326 142.906 94.5624 142.906 103.643C142.906 112.724 135.355 120.154 126.126 120.154C120.672 120.154 115.638 117.265 112.281 113.137L126.126 103.643V87.1326Z" fill="#6E49CB"/>
-<g clip-path="url(#clip1)">
-<path d="M29.5 90.2659L24.3571 91.9534V93.1629C24.3571 94.9623 25.087 96.6872 26.3846 97.9546L29.5 100.997V90.2659Z" fill="#FC6D26"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5 86.8909L29.5 83.5159L41.5 86.8909V93.1115C41.5 96.6919 40.0551 100.126 37.4832 102.657L29.5 110.516L21.5168 102.657C18.9449 100.126 17.5 96.6919 17.5 93.1115V86.8909ZM20.9286 93.1115V89.4366L29.5 87.0259L38.0714 89.4366V93.1115C38.0714 95.7968 36.9878 98.3721 35.0588 100.271L29.5 105.743L23.9412 100.271C22.0122 98.3721 20.9286 95.7968 20.9286 93.1115Z" fill="#FC6D26"/>
-</g>
-<g clip-path="url(#clip2)">
-<path d="M210.857 19.7297L209.51 24.8237C208.922 27.0445 207.518 28.9576 205.581 30.1752L194.728 36.999L191.862 34.1146L198.642 23.1922C199.852 21.2431 201.753 19.8298 203.96 19.2386L209.022 17.8826C209.822 17.6681 210.644 18.1474 210.857 18.953C210.925 19.2075 210.925 19.4752 210.857 19.7297ZM207.292 21.4702L204.732 22.1561C203.261 22.5503 201.993 23.4925 201.187 24.7918L196.517 32.3146L203.992 27.6148C205.283 26.803 206.219 25.5276 206.611 24.0471L207.292 21.4702ZM196.5 38.2294L204 33.7007V35.2103C204 38.5451 201.314 41.2485 198 41.2485H196.5V38.2294ZM190.5 32.1912H187.5V30.6816C187.5 27.3468 190.186 24.6434 193.5 24.6434H195L190.5 32.1912Z" fill="#FC6D26"/>
-</g>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M209.914 132.822C209.384 132.822 208.875 133.032 208.5 133.407L204.796 137.111C204.613 137.293 204.5 137.544 204.5 137.822V144.822C204.5 145.926 205.395 146.822 206.5 146.822H216.5C217.605 146.822 218.5 145.926 218.5 144.822V137.822C218.5 137.546 218.388 137.296 218.207 137.115L214.5 133.407C214.125 133.032 213.616 132.822 213.086 132.822H209.914ZM215.086 136.822L213.086 134.822H212.5V136.822H215.086ZM210.5 134.822H209.914L207.914 136.822H210.5V134.822ZM206.5 138.822H216.5V144.822H206.5V138.822Z" fill="#FC6D26"/>
-<defs>
-<clipPath id="clip0">
-<rect width="16" height="13.6779" fill="white" transform="translate(44.5 134.033)"/>
-</clipPath>
-<clipPath id="clip1">
-<rect width="24" height="27.172" fill="white" transform="translate(17.5 83.5159)"/>
-</clipPath>
-<clipPath id="clip2">
-<rect width="24" height="24.1529" fill="white" transform="translate(187.5 17.0956)"/>
-</clipPath>
-</defs>
-</svg>
diff --git a/app/assets/javascripts/admin/abuse_report/components/report_actions.vue b/app/assets/javascripts/admin/abuse_report/components/report_actions.vue
index 560d733c10c..e005e183c9f 100644
--- a/app/assets/javascripts/admin/abuse_report/components/report_actions.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/report_actions.vue
@@ -14,8 +14,10 @@ import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import {
ACTIONS_I18N,
NO_ACTION,
+ TRUST_ACTION,
USER_ACTION_OPTIONS,
REASON_OPTIONS,
+ TRUST_REASON,
STATUS_OPEN,
SUCCESS_ALERT,
FAILED_ALERT,
@@ -77,6 +79,16 @@ export default {
userActionOptions() {
return this.isNotCurrentUser ? USER_ACTION_OPTIONS : [NO_ACTION];
},
+ reasonOptions() {
+ if (!this.isNotCurrentUser) {
+ return [];
+ }
+
+ if (this.form.user_action === TRUST_ACTION.value) {
+ return [TRUST_REASON];
+ }
+ return REASON_OPTIONS;
+ },
},
methods: {
toggleActionsDrawer() {
@@ -120,7 +132,6 @@ export default {
},
},
i18n: ACTIONS_I18N,
- reasonOptions: REASON_OPTIONS,
DRAWER_Z_INDEX,
};
</script>
@@ -173,7 +184,7 @@ export default {
id="reason"
v-model="form.reason"
data-testid="reason-select"
- :options="$options.reasonOptions"
+ :options="reasonOptions"
:state="validationState.reason"
@change="validateReason"
/>
diff --git a/app/assets/javascripts/admin/abuse_report/components/user_details.vue b/app/assets/javascripts/admin/abuse_report/components/user_details.vue
index fe0add1ba8d..0c32341652b 100644
--- a/app/assets/javascripts/admin/abuse_report/components/user_details.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/user_details.vue
@@ -60,9 +60,6 @@ export default {
data-testid="credit-card-verification"
:label="$options.i18n.creditCard"
>
- <gl-sprintf :message="$options.i18n.registeredWith">
- <template #name>{{ user.creditCard.name }}</template>
- </gl-sprintf>
<gl-sprintf v-if="showSimilarRecords" :message="$options.i18n.similarRecords">
<template #cardMatchesLink="{ content }">
<gl-link :href="user.creditCard.cardMatchesLink">
diff --git a/app/assets/javascripts/admin/abuse_report/constants.js b/app/assets/javascripts/admin/abuse_report/constants.js
index 6cae6b24f20..f028408bed7 100644
--- a/app/assets/javascripts/admin/abuse_report/constants.js
+++ b/app/assets/javascripts/admin/abuse_report/constants.js
@@ -25,11 +25,14 @@ export const ACTIONS_I18N = {
};
export const NO_ACTION = { value: '', text: s__('AbuseReport|No action') };
+export const TRUST_REASON = { value: 'trusted', text: s__(`AbuseReport|Confirmed trusted user`) };
+export const TRUST_ACTION = { value: 'trust_user', text: s__('AbuseReport|Trust user') };
export const USER_ACTION_OPTIONS = [
NO_ACTION,
{ value: 'block_user', text: s__('AbuseReport|Block user') },
{ value: 'ban_user', text: s__('AbuseReport|Ban user') },
+ TRUST_ACTION,
{ value: 'delete_user', text: s__('AbuseReport|Delete user') },
];
@@ -75,7 +78,6 @@ export const USER_DETAILS_I18N = {
reportedFor: s__(
'AbuseReport|%{reportLinkStart}Reported%{reportLinkEnd} for %{category} %{timeAgo}.',
),
- registeredWith: s__('AbuseReport|Registered with name %{name}.'),
similarRecords: s__(
'AbuseReport|Card matches %{cardMatchesLinkStart}%{count} accounts%{cardMatchesLinkEnd}',
),
@@ -87,6 +89,7 @@ export const REPORTED_CONTENT_I18N = {
comment: s__('AbuseReport|Reported comment'),
issue: s__('AbuseReport|Reported issue'),
merge_request: s__('AbuseReport|Reported merge request'),
+ epic: s__('AbuseReport|Reported epic'),
unknown: s__('AbuseReport|Reported content'),
},
viewScreenshot: s__('AbuseReport|View screenshot'),
@@ -96,6 +99,7 @@ export const REPORTED_CONTENT_I18N = {
comment: s__('AbuseReport|Go to comment'),
issue: s__('AbuseReport|Go to issue'),
merge_request: s__('AbuseReport|Go to merge request'),
+ epic: s__('AbuseReport|Go to epic'),
unknown: s__('AbuseReport|Go to content'),
},
reportedBy: s__('AbuseReport|Reported by'),
diff --git a/app/assets/javascripts/admin/application_settings/inactive_project_deletion/components/form.vue b/app/assets/javascripts/admin/application_settings/inactive_project_deletion/components/form.vue
index ef4a5319eec..0b640a34864 100644
--- a/app/assets/javascripts/admin/application_settings/inactive_project_deletion/components/form.vue
+++ b/app/assets/javascripts/admin/application_settings/inactive_project_deletion/components/form.vue
@@ -143,7 +143,7 @@ export default {
v-model="minSizeMb"
:state="isMinSizeMbValid"
name="application_setting[inactive_projects_min_size_mb]"
- size="md"
+ width="md"
type="number"
:min="0"
data-testid="min-size-input"
@@ -177,7 +177,7 @@ export default {
v-model="deleteAfterMonths"
:state="isDeleteAfterMonthsValid"
name="application_setting[inactive_projects_delete_after_months]"
- size="sm"
+ width="sm"
type="number"
:min="0"
data-testid="delete-after-months-input"
@@ -215,7 +215,7 @@ export default {
v-model="sendWarningEmailAfterMonths"
:state="isSendWarningEmailAfterMonthsValid"
name="application_setting[inactive_projects_send_warning_email_after_months]"
- size="sm"
+ width="sm"
type="number"
:min="0"
data-testid="send-warning-email-after-months-input"
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue b/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue
index 07814ef2511..253eefc323c 100644
--- a/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue
+++ b/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue
@@ -42,6 +42,6 @@ export default {
<template>
<div class="gl-display-flex gl-gap-3 gl-align-items-center">
<gl-datepicker v-model="date" />
- <gl-form-input v-model="time" size="sm" type="time" data-testid="time-picker" />
+ <gl-form-input v-model="time" width="sm" type="time" data-testid="time-picker" />
</div>
</template>
diff --git a/app/assets/javascripts/alert.js b/app/assets/javascripts/alert.js
index 006c4f50d09..4d724b17723 100644
--- a/app/assets/javascripts/alert.js
+++ b/app/assets/javascripts/alert.js
@@ -1,6 +1,7 @@
import * as Sentry from '@sentry/browser';
import Vue from 'vue';
-import { GlAlert } from '@gitlab/ui';
+import isEmpty from 'lodash/isEmpty';
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
export const VARIANT_SUCCESS = 'success';
@@ -32,6 +33,14 @@ export const VARIANT_TIP = 'tip';
* // Respond to the alert being dismissed
* createAlert({ message: 'Message', onDismiss: () => {} });
*
+ * @example
+ * // Add inline link in the message
+ * createAlert({ message: 'Read more at %{exampleLinkStart}example page%{exampleLinkEnd}.', messageLinks: { exampleLink: 'https://example.com' } });
+ *
+ * @example
+ * // Add inline links in the message with custom GlLink props
+ * createAlert({ message: 'Read more at %{exampleLinkStart}example page%{exampleLinkEnd}.', messageLinks: { exampleLink: { href: 'https://example.com', target: '_blank', isUnsafeLink: true }} });
+ *
* @param {object} options - Options to control the flash message
* @param {string} options.message - Alert message text
* @param {string} [options.title] - Alert title
@@ -48,6 +57,7 @@ export const VARIANT_TIP = 'tip';
* @param {string} [options.secondaryButton.link] - Href of secondary button
* @param {string} [options.secondaryButton.text] - Text of secondary button
* @param {Function} [options.secondaryButton.clickHandler] - Handler to call when secondary button is clicked on. The click event is sent as an argument.
+ * @param {object} [options.messageLinks] - Object containing mapping of sprintf tokens to URLs, used to format links within the message. If needed, you can pass a full props object for GlLink instead of a URL string
* @param {boolean} [options.captureError] - Whether to send error to Sentry
* @param {object} [options.error] - Error to be captured in Sentry
*/
@@ -63,6 +73,7 @@ export const createAlert = ({
onDismiss = null,
captureError = false,
error = null,
+ messageLinks = null,
}) => {
if (captureError && error) Sentry.captureException(error);
@@ -76,6 +87,45 @@ export const createAlert = ({
alertContainer.replaceChildren(el);
}
+ const createMessageNodes = (h) => {
+ if (isEmpty(messageLinks)) {
+ return message;
+ }
+
+ const normalizeLinkProps = (hrefOrProps) => {
+ const { href, ...otherLinkProps } =
+ typeof hrefOrProps === 'string' ? { href: hrefOrProps } : hrefOrProps;
+
+ return { href, linkProps: otherLinkProps };
+ };
+
+ return [
+ h(GlSprintf, {
+ props: {
+ message,
+ },
+ scopedSlots: Object.assign(
+ {},
+ ...Object.entries(messageLinks).map(([slotName, hrefOrProps]) => {
+ const { href, linkProps } = normalizeLinkProps(hrefOrProps);
+
+ return {
+ [slotName]: (props) =>
+ h(
+ GlLink,
+ {
+ props: linkProps,
+ attrs: { href },
+ },
+ props.content,
+ ),
+ };
+ }),
+ ),
+ }),
+ ];
+ };
+
return new Vue({
el,
components: {
@@ -130,7 +180,7 @@ export const createAlert = ({
},
on,
},
- message,
+ createMessageNodes(h),
);
},
});
diff --git a/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue b/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue
index 77c14d9f812..da9f300c941 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue
@@ -34,7 +34,11 @@ export default {
</script>
<template>
<div>
- <gl-empty-state :title="$options.i18n.emptyState.title" :svg-path="emptyAlertSvgPath">
+ <gl-empty-state
+ :title="$options.i18n.emptyState.title"
+ :svg-path="emptyAlertSvgPath"
+ :svg-height="null"
+ >
<template #description>
<div class="gl-display-block">
<span>{{ $options.i18n.emptyState.info }}</span>
diff --git a/app/assets/javascripts/analytics/cycle_analytics/components/base.vue b/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
index 84ee8f41b11..39fbc217278 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
@@ -56,6 +56,7 @@ export default {
'hasNoAccessError',
'groupPath',
'namespace',
+ 'predefinedDateRange',
]),
...mapGetters(['pathNavigationData', 'filterParams']),
isLoaded() {
@@ -132,6 +133,7 @@ export default {
'fetchStageData',
'setSelectedStage',
'setDateRange',
+ 'setPredefinedDateRange',
'updateStageTablePagination',
]),
onSetDateRange({ startDate, endDate }) {
@@ -170,7 +172,9 @@ export default {
:start-date="createdAfter"
:end-date="createdBefore"
:group-path="groupPath"
+ :predefined-date-range="predefinedDateRange"
@setDateRange="onSetDateRange"
+ @setPredefinedDateRange="setPredefinedDateRange"
/>
<div class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row">
<path-navigation
diff --git a/app/assets/javascripts/analytics/cycle_analytics/components/stage_table.vue b/app/assets/javascripts/analytics/cycle_analytics/components/stage_table.vue
index 38f9936c7c1..898633868cd 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/components/stage_table.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/stage_table.vue
@@ -195,6 +195,7 @@ export default {
:title="emptyStateTitleText"
:description="emptyStateMessage"
:svg-path="noDataSvgPath"
+ :svg-height="null"
/>
<gl-table
v-else
diff --git a/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue b/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue
index 0de62013a63..775c3827fc7 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue
@@ -2,7 +2,17 @@
import { GlTooltipDirective } from '@gitlab/ui';
import DateRange from '~/analytics/shared/components/daterange.vue';
import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
-import { DATE_RANGE_LIMIT, PROJECTS_PER_PAGE } from '~/analytics/shared/constants';
+import {
+ DATE_RANGE_LIMIT,
+ DATE_RANGE_CUSTOM_VALUE,
+ PROJECTS_PER_PAGE,
+ MAX_DATE_RANGE_TEXT,
+ DATE_RANGE_LAST_30_DAYS_VALUE,
+ LAST_30_DAYS,
+} from '~/analytics/shared/constants';
+import { getCurrentUtcDate, datesMatch } from '~/lib/utils/datetime_utility';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import DateRangesDropdown from '~/analytics/shared/components/date_ranges_dropdown.vue';
import FilterBar from './filter_bar.vue';
export default {
@@ -11,10 +21,12 @@ export default {
DateRange,
ProjectsDropdownFilter,
FilterBar,
+ DateRangesDropdown,
},
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
selectedProjects: {
type: Array,
@@ -31,6 +43,11 @@ export default {
required: false,
default: true,
},
+ hasPredefinedDateRangesFilter: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
namespacePath: {
type: String,
required: true,
@@ -49,6 +66,11 @@ export default {
required: false,
default: null,
},
+ predefinedDateRange: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
projectsQueryParams() {
@@ -58,42 +80,104 @@ export default {
};
},
currentDate() {
- const now = new Date();
- return new Date(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
+ return getCurrentUtcDate();
+ },
+ isDefaultDateRange() {
+ return datesMatch(this.startDate, LAST_30_DAYS) && datesMatch(this.endDate, this.currentDate);
+ },
+ supportsPredefinedDateRanges() {
+ return this.glFeatures?.vsaPredefinedDateRanges;
+ },
+ dateRangeOption() {
+ const { predefinedDateRange } = this;
+
+ if (predefinedDateRange) return predefinedDateRange;
+
+ if (!predefinedDateRange && !this.isDefaultDateRange) return DATE_RANGE_CUSTOM_VALUE;
+
+ return DATE_RANGE_LAST_30_DAYS_VALUE;
+ },
+ isCustomDateRangeSelected() {
+ return this.dateRangeOption === DATE_RANGE_CUSTOM_VALUE;
+ },
+ shouldShowPredefinedDateRanges() {
+ return this.supportsPredefinedDateRanges && this.hasPredefinedDateRangesFilter;
+ },
+ shouldShowDateRangePicker() {
+ if (this.shouldShowPredefinedDateRanges) {
+ return this.hasDateRangeFilter && this.isCustomDateRangeSelected;
+ }
+
+ return this.hasDateRangeFilter;
+ },
+ maxDateRangeTooltip() {
+ return this.$options.i18n.maxDateRangeTooltip(this.$options.maxDateRange);
+ },
+ shouldShowDateRangeFilters() {
+ return this.hasDateRangeFilter || this.hasPredefinedDateRangesFilter;
+ },
+ shouldShowFilterDropdowns() {
+ return this.hasProjectFilter || this.shouldShowDateRangeFilters;
+ },
+ },
+ methods: {
+ onSelectPredefinedDateRange({ value, startDate, endDate }) {
+ this.$emit('setPredefinedDateRange', value);
+ this.$emit('setDateRange', { startDate, endDate });
+ },
+ onSelectCustomDateRange() {
+ this.$emit('setPredefinedDateRange', DATE_RANGE_CUSTOM_VALUE);
},
},
multiProjectSelect: true,
maxDateRange: DATE_RANGE_LIMIT,
+ i18n: {
+ maxDateRangeTooltip: MAX_DATE_RANGE_TEXT,
+ },
};
</script>
<template>
<div
- class="gl-mt-3 gl-py-2 gl-px-3 gl-bg-gray-10 gl-border-b-1 gl-border-b-solid gl-border-t-1 gl-border-t-solid gl-border-gray-100"
+ class="gl-mt-3 gl-py-5 gl-px-3 gl-bg-gray-10 gl-border-b-1 gl-border-b-solid gl-border-t-1 gl-border-t-solid gl-border-gray-100"
>
<filter-bar
data-testid="vsa-filter-bar"
- class="filtered-search-box gl-display-flex gl-mb-2 gl-mr-3 gl-border-none"
+ class="filtered-search-box gl-display-flex gl-border-none"
:namespace-path="namespacePath"
/>
+ <hr v-if="shouldShowFilterDropdowns" class="gl-my-5" />
<div
- v-if="hasDateRangeFilter || hasProjectFilter"
- class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-justify-content-space-between"
+ v-if="shouldShowFilterDropdowns"
+ class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-gap-5"
>
- <div>
- <projects-dropdown-filter
- v-if="hasProjectFilter"
- toggle-classes="gl-max-w-26"
- class="js-projects-dropdown-filter project-select gl-mb-2 gl-lg-mb-0"
- :group-namespace="groupPath"
- :query-params="projectsQueryParams"
- :multi-select="$options.multiProjectSelect"
- :default-projects="selectedProjects"
- @selected="$emit('selectProject', $event)"
+ <projects-dropdown-filter
+ v-if="hasProjectFilter"
+ toggle-classes="gl-max-w-26"
+ class="js-projects-dropdown-filter project-select"
+ :group-namespace="groupPath"
+ :query-params="projectsQueryParams"
+ :multi-select="$options.multiProjectSelect"
+ :default-projects="selectedProjects"
+ @selected="$emit('selectProject', $event)"
+ />
+ <div
+ v-if="shouldShowDateRangeFilters"
+ class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-gap-3"
+ data-testid="vsa-date-range-filter-container"
+ >
+ <date-ranges-dropdown
+ v-if="shouldShowPredefinedDateRanges"
+ data-testid="vsa-predefined-date-ranges-dropdown"
+ :selected="dateRangeOption"
+ :tooltip="maxDateRangeTooltip"
+ include-end-date-in-days-selected
+ :include-custom-date-range-option="hasDateRangeFilter"
+ @selected="onSelectPredefinedDateRange"
+ @customDateRangeSelected="onSelectCustomDateRange"
/>
- </div>
- <div class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row">
<date-range
- v-if="hasDateRangeFilter"
+ v-if="shouldShowDateRangePicker"
+ data-testid="vsa-date-range-picker"
:start-date="startDate"
:end-date="endDate"
:max-date="currentDate"
diff --git a/app/assets/javascripts/analytics/cycle_analytics/store/actions.js b/app/assets/javascripts/analytics/cycle_analytics/store/actions.js
index 32fe0abe83e..90ac531aa87 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/store/actions.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/actions.js
@@ -163,6 +163,10 @@ export const setDateRange = ({ dispatch, commit }, { createdAfter, createdBefore
return dispatch('refetchStageData');
};
+export const setPredefinedDateRange = ({ commit }, predefinedDateRange) => {
+ commit(types.SET_PREDEFINED_DATE_RANGE, predefinedDateRange);
+};
+
export const setInitialStage = ({ dispatch, commit, state: { stages } }, stage) => {
if (!stages.length && !stage) {
commit(types.SET_NO_ACCESS_ERROR);
diff --git a/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js b/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js
index 9376d81f317..e0a7a4292e2 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js
@@ -4,6 +4,7 @@ export const SET_LOADING = 'SET_LOADING';
export const SET_SELECTED_VALUE_STREAM = 'SET_SELECTED_VALUE_STREAM';
export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE';
export const SET_DATE_RANGE = 'SET_DATE_RANGE';
+export const SET_PREDEFINED_DATE_RANGE = 'SET_PREDEFINED_DATE_RANGE';
export const SET_PAGINATION = 'SET_PAGINATION';
export const SET_NO_ACCESS_ERROR = 'SET_NO_ACCESS_ERROR';
diff --git a/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js b/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
index 4af96fc96e3..4fa88279fe0 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
@@ -34,6 +34,9 @@ export default {
state.createdBefore = createdBefore;
state.createdAfter = createdAfter;
},
+ [types.SET_PREDEFINED_DATE_RANGE](state, predefinedDateRange) {
+ state.predefinedDateRange = predefinedDateRange;
+ },
[types.SET_PAGINATION](state, { page, hasNextPage, sort, direction }) {
Vue.set(state, 'pagination', {
page,
diff --git a/app/assets/javascripts/analytics/cycle_analytics/store/state.js b/app/assets/javascripts/analytics/cycle_analytics/store/state.js
index 0c51656c59f..3d9b56b043d 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/store/state.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/state.js
@@ -32,4 +32,5 @@ export default () => ({
sort: PAGINATION_SORT_FIELD_END_EVENT,
direction: PAGINATION_SORT_DIRECTION_DESC,
},
+ predefinedDateRange: null,
});
diff --git a/app/assets/javascripts/analytics/devops_reports/components/devops_score.vue b/app/assets/javascripts/analytics/devops_reports/components/devops_score.vue
index 593de1dcee7..91f0019913c 100644
--- a/app/assets/javascripts/analytics/devops_reports/components/devops_score.vue
+++ b/app/assets/javascripts/analytics/devops_reports/components/devops_score.vue
@@ -72,6 +72,7 @@ export default {
v-if="isEmpty"
:title="__('Data is still calculating...')"
:svg-path="noDataImagePath"
+ :svg-height="null"
>
<template #description>
<p class="gl-mb-0">{{ __('It may be several days before you see feature usage data.') }}</p>
diff --git a/app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue b/app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue
index b9501107e37..c2e1e3f1bad 100644
--- a/app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue
+++ b/app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue
@@ -24,7 +24,11 @@ export default {
};
</script>
<template>
- <gl-empty-state :title="s__('ServicePing|Service ping is off')" :svg-path="svgPath">
+ <gl-empty-state
+ :title="s__('ServicePing|Service ping is off')"
+ :svg-path="svgPath"
+ :svg-height="null"
+ >
<template #description>
<gl-sprintf
v-if="!isAdmin"
diff --git a/app/assets/javascripts/analytics/shared/components/date_ranges_dropdown.vue b/app/assets/javascripts/analytics/shared/components/date_ranges_dropdown.vue
new file mode 100644
index 00000000000..7ea7aba6f44
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/components/date_ranges_dropdown.vue
@@ -0,0 +1,131 @@
+<script>
+import { GlCollapsibleListbox, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+
+import { isString } from 'lodash';
+import { isValidDate, getDayDifference } from '~/lib/utils/datetime_utility';
+import {
+ DATE_RANGE_CUSTOM_VALUE,
+ DEFAULT_DATE_RANGE_OPTIONS,
+ NUMBER_OF_DAYS_SELECTED,
+} from '~/analytics/shared/constants';
+import { __ } from '~/locale';
+
+export default {
+ name: 'DateRangesDropdown',
+ components: {
+ GlCollapsibleListbox,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ dateRangeOptions: {
+ type: Array,
+ required: false,
+ default: () => DEFAULT_DATE_RANGE_OPTIONS,
+ validator: (options) =>
+ options.length &&
+ options.every(
+ ({ text, value, startDate, endDate }) =>
+ isString(text) &&
+ isString(value) &&
+ isValidDate(startDate) &&
+ isValidDate(endDate) &&
+ endDate >= startDate,
+ ),
+ },
+ selected: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ tooltip: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ includeCustomDateRangeOption: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ includeEndDateInDaysSelected: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ selectedValue: this.selected || this.dateRangeOptions[0].value,
+ };
+ },
+ computed: {
+ items() {
+ const dateRangeOptions = this.dateRangeOptions.map(({ text, value }) => ({ text, value }));
+
+ if (!this.includeCustomDateRangeOption) return dateRangeOptions;
+
+ return [...dateRangeOptions, this.$options.customDateRangeItem];
+ },
+ isCustomDateRangeSelected() {
+ return this.selectedValue === DATE_RANGE_CUSTOM_VALUE;
+ },
+ groupedDateRangeOptionsByValue() {
+ return this.dateRangeOptions.reduce((acc, { value, startDate, endDate }) => {
+ acc[value] = { startDate, endDate };
+
+ return acc;
+ }, {});
+ },
+ selectedDateRange() {
+ if (this.isCustomDateRangeSelected) return null;
+
+ return this.groupedDateRangeOptionsByValue[this.selectedValue];
+ },
+ showDaysSelectedCount() {
+ return !this.isCustomDateRangeSelected && this.daysSelectedCount;
+ },
+ daysSelectedCount() {
+ const { selectedDateRange } = this;
+
+ if (!selectedDateRange) return '';
+
+ const { startDate, endDate } = selectedDateRange;
+
+ const daysCount = getDayDifference(startDate, endDate);
+
+ return this.$options.i18n.daysSelected(
+ this.includeEndDateInDaysSelected ? daysCount + 1 : daysCount,
+ );
+ },
+ },
+ methods: {
+ onSelect(value) {
+ if (this.isCustomDateRangeSelected) {
+ this.$emit('customDateRangeSelected');
+ } else {
+ this.$emit('selected', { value, ...this.selectedDateRange });
+ }
+ },
+ },
+ customDateRangeItem: {
+ text: __('Custom'),
+ value: DATE_RANGE_CUSTOM_VALUE,
+ },
+ i18n: {
+ daysSelected: NUMBER_OF_DAYS_SELECTED,
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-center gl-gap-3">
+ <gl-collapsible-listbox v-model="selectedValue" :items="items" @select="onSelect" />
+ <div v-if="showDaysSelectedCount" class="gl-text-gray-500">
+ <span data-testid="predefined-date-range-days-count">{{ daysSelectedCount }}</span>
+ <gl-icon v-if="tooltip" v-gl-tooltip class="gl-ml-2" name="information-o" :title="tooltip" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/analytics/shared/components/daterange.vue b/app/assets/javascripts/analytics/shared/components/daterange.vue
index f47e0ccbbf2..2126359cfe4 100644
--- a/app/assets/javascripts/analytics/shared/components/daterange.vue
+++ b/app/assets/javascripts/analytics/shared/components/daterange.vue
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlDaterangePicker } from '@gitlab/ui';
-import { n__, __, sprintf } from '~/locale';
+import { MAX_DATE_RANGE_TEXT, NUMBER_OF_DAYS_SELECTED } from '~/analytics/shared/constants';
export default {
components: {
@@ -46,18 +46,6 @@ export default {
default: false,
},
},
- data() {
- return {
- maxDateRangeTooltip: sprintf(
- __(
- 'Showing data for workflow items completed in this date range. Date range limited to %{maxDateRange} days.',
- ),
- {
- maxDateRange: this.maxDateRange,
- },
- ),
- };
- },
computed: {
dateRange: {
get() {
@@ -67,12 +55,19 @@ export default {
this.$emit('change', { startDate, endDate });
},
},
+ maxDateRangeTooltip() {
+ return this.$options.i18n.maxDateRangeTooltip(this.maxDateRange);
+ },
},
methods: {
numberOfDays(daysSelected) {
- return n__('1 day selected', '%d days selected', daysSelected);
+ return this.$options.i18n.daysSelected(daysSelected);
},
},
+ i18n: {
+ maxDateRangeTooltip: MAX_DATE_RANGE_TEXT,
+ daysSelected: NUMBER_OF_DAYS_SELECTED,
+ },
};
</script>
<template>
diff --git a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
index ddfc6baafa9..662451c5eb4 100644
--- a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
+++ b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlIcon, GlAvatar, GlCollapsibleListbox, GlTruncate } from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { debounce, unionBy } from 'lodash';
import { filterBySearchTerm } from '~/analytics/shared/utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
@@ -8,6 +8,8 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { n__, s__, __ } from '~/locale';
import getProjects from '../graphql/projects.query.graphql';
+const MIN_SEARCH_CHARS = 3;
+
const sortByProjectName = (projects = []) => projects.sort((a, b) => a.name.localeCompare(b.name));
const mapItemToListboxFormat = (item) => ({ ...item, value: item.id, text: item.name });
@@ -98,10 +100,6 @@ export default {
availableProjects() {
return filterBySearchTerm(this.projects, this.searchTerm);
},
- noResultsAvailable() {
- const { loading, availableProjects } = this;
- return !loading && !availableProjects.length;
- },
selectedItems() {
return sortByProjectName(this.selectedProjects);
},
@@ -152,6 +150,9 @@ export default {
singleSelectedProject(selectedObj, isMarking) {
return isMarking ? [selectedObj] : [];
},
+ getSelectedProjects(projects, selectedProjectIds) {
+ return projects.filter(({ id }) => selectedProjectIds.includes(id));
+ },
setSelectedProjects(payload) {
this.selectedProjects = this.multiSelect
? payload
@@ -163,8 +164,10 @@ export default {
this.handleUpdatedSelectedProjects();
},
onMultiSelectClick(projectIds) {
- const projects = this.availableProjects.filter(({ id }) => projectIds.includes(id));
- this.setSelectedProjects(projects);
+ const newlySelectedProjects = this.getSelectedProjects(this.availableProjects, projectIds);
+ const selectedProjects = this.getSelectedProjects(this.selectedProjects, projectIds);
+
+ this.setSelectedProjects(unionBy(newlySelectedProjects, selectedProjects, 'id'));
this.isDirty = true;
},
onSelected(payload) {
@@ -219,7 +222,12 @@ export default {
return getIdFromGraphQLId(project.id);
},
setSearchTerm(val) {
- this.searchTerm = val;
+ if (val && val.length >= MIN_SEARCH_CHARS) {
+ this.searchTerm = val;
+ return;
+ }
+
+ this.searchTerm = '';
},
},
AVATAR_SHAPE_OPTION_RECT,
diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js
index 7ec7eac24ec..f0d9bf201e5 100644
--- a/app/assets/javascripts/analytics/shared/constants.js
+++ b/app/assets/javascripts/analytics/shared/constants.js
@@ -1,9 +1,17 @@
import dateFormat, { masks } from '~/lib/dateformat';
-import { nDaysBefore, getStartOfDay } from '~/lib/utils/datetime_utility';
-import { s__ } from '~/locale';
+import {
+ nDaysBefore,
+ getStartOfDay,
+ dayAfter,
+ getDateInPast,
+ getCurrentUtcDate,
+ nWeeksBefore,
+} from '~/lib/utils/datetime_utility';
+import { s__, __, sprintf, n__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
export const DATE_RANGE_LIMIT = 180;
+export const DEFAULT_DATE_RANGE = 29; // 30 including current date
export const PROJECTS_PER_PAGE = 50;
const { isoDate, mediumDate } = masks;
@@ -14,10 +22,63 @@ export const dateFormats = {
month: 'mmmm',
};
+const TODAY = getCurrentUtcDate();
+const TOMORROW = dayAfter(TODAY, { utc: true });
+export const LAST_30_DAYS = getDateInPast(TOMORROW, 30, { utc: true });
+
const startOfToday = getStartOfDay(new Date(), { utc: true });
-const last180Days = nDaysBefore(startOfToday, DATE_RANGE_LIMIT, { utc: true });
+const lastXDays = __('Last %{days} days');
+const lastWeek = nWeeksBefore(TOMORROW, 1, { utc: true });
+const last90Days = getDateInPast(TOMORROW, 90, { utc: true });
+const last180Days = getDateInPast(TOMORROW, DATE_RANGE_LIMIT, { utc: true });
+const mrThroughputStartDate = nDaysBefore(startOfToday, DATE_RANGE_LIMIT, { utc: true });
const formatDateParam = (d) => dateFormat(d, dateFormats.isoDate, true);
+export const DATE_RANGE_CUSTOM_VALUE = 'custom';
+export const DATE_RANGE_LAST_30_DAYS_VALUE = 'last_30_days';
+
+export const DEFAULT_DATE_RANGE_OPTIONS = [
+ {
+ text: __('Last week'),
+ value: 'last_week',
+ startDate: lastWeek,
+ endDate: TODAY,
+ },
+ {
+ text: sprintf(lastXDays, { days: 30 }),
+ value: DATE_RANGE_LAST_30_DAYS_VALUE,
+ startDate: LAST_30_DAYS,
+ endDate: TODAY,
+ },
+ {
+ text: sprintf(lastXDays, { days: 90 }),
+ value: 'last_90_days',
+ startDate: last90Days,
+ endDate: TODAY,
+ },
+ {
+ text: sprintf(lastXDays, { days: 180 }),
+ value: 'last_180_days',
+ startDate: last180Days,
+ endDate: TODAY,
+ },
+];
+
+export const MAX_DATE_RANGE_TEXT = (maxDateRange) => {
+ return sprintf(
+ __(
+ 'Showing data for workflow items completed in this date range. Date range limited to %{maxDateRange} days.',
+ ),
+ {
+ maxDateRange,
+ },
+ );
+};
+
+export const NUMBER_OF_DAYS_SELECTED = (numDays) => {
+ return n__('1 day selected', '%d days selected', numDays);
+};
+
export const METRIC_POPOVER_LABEL = s__('ValueStreamAnalytics|View details');
export const ISSUES_COMPLETED_TYPE = 'issues_completed';
@@ -147,7 +208,7 @@ export const METRIC_TOOLTIPS = {
description: s__('ValueStreamAnalytics|The number of merge requests merged by month.'),
groupLink: '-/analytics/productivity_analytics',
projectLink: `-/analytics/merge_request_analytics?start_date=${formatDateParam(
- last180Days,
+ mrThroughputStartDate,
)}&end_date=${formatDateParam(startOfToday)}`,
docsLink: helpPagePath('user/analytics/merge_request_analytics', {
anchor: 'view-the-number-of-merge-requests-in-a-date-range',
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 6dfc1c609de..185cdaa1c99 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -33,7 +33,6 @@ const Api = {
forkedProjectsPath: '/api/:version/projects/:id/forks',
projectLabelsPath: '/:namespace_path/:project_path/-/labels',
projectFileSchemaPath: '/:namespace_path/:project_path/-/schema/:ref/:filename',
- projectGroupsPath: '/api/:version/projects/:id/groups.json',
projectUsersPath: '/api/:version/projects/:id/users',
projectInvitationsPath: '/api/:version/projects/:id/invitations',
projectMembersPath: '/api/:version/projects/:id/members',
@@ -178,19 +177,6 @@ const Api = {
});
},
- projectGroups(id, options) {
- const url = Api.buildUrl(this.projectGroupsPath).replace(':id', encodeURIComponent(id));
-
- return axios
- .get(url, {
- params: {
- ...options,
- },
- })
- .then(({ data }) => {
- return data;
- });
- },
/**
* @deprecated This method will be removed soon. Use the
* `getGroups` method in `~/rest_api` instead.
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
index 31531c90b94..1cd5854740e 100644
--- a/app/assets/javascripts/badges/components/badge.vue
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -80,7 +80,7 @@ export default {
:href="linkUrl"
target="_blank"
rel="noopener noreferrer"
- data-qa-selector="badge_image_link"
+ data-testid="badge-image-link"
:data-qa-link-url="linkUrl"
>
<img
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue
index b69890572eb..12c9662b30d 100644
--- a/app/assets/javascripts/badges/components/badge_list.vue
+++ b/app/assets/javascripts/badges/components/badge_list.vue
@@ -100,7 +100,7 @@ export default {
<template>
<div>
<gl-loading-icon v-show="isLoading" size="md" />
- <div data-qa-selector="badge_list_content">
+ <div data-testid="badge-list-content">
<gl-table
:empty-text="emptyMessage"
:fields="fields"
@@ -109,7 +109,7 @@ export default {
:current-page="currentPage"
stacked="md"
show-empty
- data-qa-selector="badge_list"
+ data-testid="badge-list"
>
<template #cell(name)="{ item }">
<label class="label-bold str-truncated mb-0">{{ item.name }}</label>
diff --git a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
index 42fc85cc5fb..2745ccb4682 100644
--- a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
@@ -31,14 +31,14 @@ export default {
},
},
methods: {
- ...mapActions('diffs', ['setCurrentFileHash']),
+ ...mapActions('diffs', ['goToFile']),
...mapActions('batchComments', ['scrollToDraft']),
isOnLatestDiff(draft) {
return draft.position?.head_sha === this.getNoteableData.diff_head_sha;
},
async onClickDraft(draft) {
- if (this.viewDiffsFileByFile && draft.file_hash) {
- await this.setCurrentFileHash(draft.file_hash);
+ if (this.viewDiffsFileByFile) {
+ await this.goToFile({ path: draft.file_path });
}
if (draft.position && !this.isOnLatestDiff(draft)) {
@@ -54,7 +54,7 @@ export default {
</script>
<template>
- <gl-disclosure-dropdown :items="listItems" dropup data-qa-selector="review_preview_dropdown">
+ <gl-disclosure-dropdown :items="listItems" dropup data-testid="review-preview-dropdown">
<template #toggle>
<gl-button>
{{ __('Pending comments') }}
diff --git a/app/assets/javascripts/batch_comments/components/review_bar.vue b/app/assets/javascripts/batch_comments/components/review_bar.vue
index 00bb9250403..365b7930dd3 100644
--- a/app/assets/javascripts/batch_comments/components/review_bar.vue
+++ b/app/assets/javascripts/batch_comments/components/review_bar.vue
@@ -40,7 +40,7 @@ export default {
<nav class="review-bar-component js-review-bar" data-testid="review_bar_component">
<div
class="review-bar-content d-flex gl-justify-content-end"
- data-qa-selector="review_bar_content"
+ data-testid="review-bar-content"
>
<preview-dropdown />
<submit-dropdown />
diff --git a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
index 72116b1eb7f..fac45f32464 100644
--- a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
@@ -6,7 +6,6 @@ import { __ } from '~/locale';
import { createAlert } from '~/alert';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { CLEAR_AUTOSAVE_ENTRY_EVENT } from '~/vue_shared/constants';
import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub';
import { trackSavedUsingEditor } from '~/vue_shared/components/markdown/tracking';
@@ -23,7 +22,6 @@ export default {
SummarizeMyReview: () =>
import('ee_component/batch_comments/components/summarize_my_review.vue'),
},
- mixins: [glFeatureFlagsMixin()],
inject: {
canSummarize: { default: false },
},
@@ -127,7 +125,7 @@ export default {
dropup
class="submit-review-dropdown"
:class="{ 'submit-review-dropdown-animated': shouldAnimateReviewButton }"
- data-qa-selector="submit_review_dropdown"
+ data-testid="submit-review-dropdown"
variant="info"
category="primary"
>
@@ -151,7 +149,6 @@ export default {
<markdown-editor
ref="markdownEditor"
v-model="noteData.note"
- :enable-content-editor="Boolean(glFeatures.contentEditorOnIssues)"
class="js-no-autosize"
:is-submitting="isSubmitting"
:render-markdown-path="getNoteableData.preview_note_path"
@@ -192,7 +189,6 @@ export default {
type="submit"
class="js-no-auto-disable"
data-testid="submit-review-button"
- data-qa-selector="submit_review_button"
>
{{ __('Submit review') }}
</gl-button>
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index 181d841a068..6787efbeafa 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,11 +1,6 @@
import Autosize from 'autosize';
-import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
-waitForCSSLoaded(() => {
- const autosizeEls = document.querySelectorAll('.js-autosize');
+const autosizeEls = document.querySelectorAll('.js-autosize');
- Autosize(autosizeEls);
- Autosize.update(autosizeEls);
-
- autosizeEls.forEach((el) => el.classList.add('js-autosize-initialized'));
-});
+Autosize(autosizeEls);
+Autosize.update(autosizeEls);
diff --git a/app/assets/javascripts/behaviors/components/global_alerts.vue b/app/assets/javascripts/behaviors/components/global_alerts.vue
new file mode 100644
index 00000000000..d7333619110
--- /dev/null
+++ b/app/assets/javascripts/behaviors/components/global_alerts.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+
+import { getGlobalAlerts, setGlobalAlerts, removeGlobalAlertById } from '~/lib/utils/global_alerts';
+
+export default {
+ name: 'GlobalAlerts',
+ components: { GlAlert },
+ data() {
+ return {
+ alerts: [],
+ };
+ },
+ mounted() {
+ const { page } = document.body.dataset;
+ const alerts = getGlobalAlerts();
+
+ const alertsToPersist = alerts.filter((alert) => alert.persistOnPages.length);
+ const alertsToRender = alerts.filter(
+ (alert) => alert.persistOnPages.length === 0 || alert.persistOnPages.includes(page),
+ );
+
+ this.alerts = alertsToRender;
+
+ // Once we render the global alerts, we re-set the global alerts to only store persistent alerts for the next load.
+ setGlobalAlerts(alertsToPersist);
+ },
+ methods: {
+ onDismiss(index) {
+ const alert = this.alerts[index];
+ this.alerts.splice(index, 1);
+ removeGlobalAlertById(alert.id);
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="alerts.length">
+ <gl-alert
+ v-for="(alert, index) in alerts"
+ :key="alert.id"
+ :variant="alert.variant"
+ :title="alert.title"
+ :dismissible="alert.dismissible"
+ @dismiss="onDismiss(index)"
+ >{{ alert.message }}</gl-alert
+ >
+ </div>
+</template>
diff --git a/app/assets/javascripts/behaviors/global_alerts.js b/app/assets/javascripts/behaviors/global_alerts.js
new file mode 100644
index 00000000000..476291e6b47
--- /dev/null
+++ b/app/assets/javascripts/behaviors/global_alerts.js
@@ -0,0 +1,17 @@
+import Vue from 'vue';
+
+import GlobalAlerts from './components/global_alerts.vue';
+
+export const initGlobalAlerts = () => {
+ const el = document.getElementById('js-global-alerts');
+
+ if (!el) return false;
+
+ return new Vue({
+ el,
+ name: 'GlobalAlertsRoot',
+ render(createElement) {
+ return createElement(GlobalAlerts);
+ },
+ });
+};
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 871b1279ce6..dc9153e61f7 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -9,6 +9,7 @@ import './quick_submit';
import './requires_input';
import initPageShortcuts from './shortcuts';
import { initToastMessages } from './toasts';
+import { initGlobalAlerts } from './global_alerts';
import './toggler_behavior';
import './preview_markdown';
@@ -24,6 +25,8 @@ initCollapseSidebarOnWindowResize();
initToastMessages();
+initGlobalAlerts();
+
window.requestIdleCallback(
() => {
// Check if we have to Load GFM Input
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index 333858f717c..58b08772337 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -3,7 +3,6 @@ import highlightCurrentUser from './highlight_current_user';
import { renderKroki } from './render_kroki';
import renderMath from './render_math';
import renderSandboxedMermaid from './render_sandboxed_mermaid';
-import renderObservability from './render_observability';
import { renderJSONTable } from './render_json_table';
function initPopovers(elements) {
@@ -21,16 +20,7 @@ export function renderGFM(element) {
return;
}
- const [
- highlightEls,
- krokiEls,
- mathEls,
- mermaidEls,
- tableEls,
- userEls,
- popoverEls,
- observabilityEls,
- ] = [
+ const [highlightEls, krokiEls, mathEls, mermaidEls, tableEls, userEls, popoverEls] = [
'.js-syntax-highlight',
'.js-render-kroki[hidden]',
'.js-render-math',
@@ -38,7 +28,6 @@ export function renderGFM(element) {
'[lang="json"][data-lang-params="table"]',
'.gfm-project_member',
'.gfm-issue, .gfm-work_item, .gfm-merge_request, .gfm-epic',
- '.js-render-observability',
].map((selector) => Array.from(element.querySelectorAll(selector)));
syntaxHighlight(highlightEls);
@@ -47,6 +36,5 @@ export function renderGFM(element) {
renderSandboxedMermaid(mermaidEls);
renderJSONTable(tableEls.map((e) => e.parentNode));
highlightCurrentUser(userEls);
- renderObservability(observabilityEls);
initPopovers(popoverEls);
}
diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index 7525fc76d16..4cba3eccb45 100644
--- a/app/assets/javascripts/behaviors/markdown/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -22,6 +22,31 @@ const waitForReflow = (fn) => {
window.requestIdleCallback(fn);
};
+const katexOptions = (el) => {
+ const options = {
+ displayMode: el.dataset.mathStyle === 'display',
+ throwOnError: true,
+ trust: (context) =>
+ // this config option restores the KaTeX pre-v0.11.0
+ // behavior of allowing certain commands and protocols
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ ['\\url', '\\href'].includes(context.command) &&
+ ['http', 'https', 'mailto', '_relative'].includes(context.protocol),
+ };
+
+ if (gon.math_rendering_limits_enabled) {
+ options.maxSize = MAX_USER_SPECIFIED_EMS;
+ // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111107 for
+ // reasoning behind this value
+ options.maxExpand = MAX_MACRO_EXPANSIONS;
+ } else {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ options.maxExpand = 'Infinity';
+ }
+
+ return options;
+};
+
/**
* Renders math blocks sequentially while protecting against DoS attacks. Math blocks have a maximum character limit of MAX_MATH_CHARS. If rendering math takes longer than MAX_RENDER_TIME_MS, all subsequent math blocks are skipped and an error message is shown.
*/
@@ -60,7 +85,10 @@ class SafeMathRenderer {
}
const el = chosenEl || this.queue.shift();
- const forceRender = Boolean(chosenEl) || unrestrictedPages.includes(this.pageName);
+ const forceRender =
+ Boolean(chosenEl) ||
+ unrestrictedPages.includes(this.pageName) ||
+ !gon.math_rendering_limits_enabled;
const text = el.textContent;
el.removeAttribute('style');
@@ -128,20 +156,7 @@ class SafeMathRenderer {
}
// eslint-disable-next-line no-unsanitized/property
- displayContainer.innerHTML = this.katex.renderToString(text, {
- displayMode: el.dataset.mathStyle === 'display',
- throwOnError: true,
- maxSize: MAX_USER_SPECIFIED_EMS,
- // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111107 for
- // reasoning behind this value
- maxExpand: MAX_MACRO_EXPANSIONS,
- trust: (context) =>
- // this config option restores the KaTeX pre-v0.11.0
- // behavior of allowing certain commands and protocols
- // eslint-disable-next-line @gitlab/require-i18n-strings
- ['\\url', '\\href'].includes(context.command) &&
- ['http', 'https', 'mailto', '_relative'].includes(context.protocol),
- });
+ displayContainer.innerHTML = this.katex.renderToString(text, katexOptions(el));
} catch (e) {
// Don't show a flash for now because it would override an existing flash message
if (e.message.match(/Too many expansions/)) {
diff --git a/app/assets/javascripts/behaviors/markdown/render_observability.js b/app/assets/javascripts/behaviors/markdown/render_observability.js
deleted file mode 100644
index 6346fb8ab48..00000000000
--- a/app/assets/javascripts/behaviors/markdown/render_observability.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import Vue from 'vue';
-import ObservabilityApp from '~/observability/components/observability_app.vue';
-import { SKELETON_VARIANT_EMBED, INLINE_EMBED_DIMENSIONS } from '~/observability/constants';
-
-const mountVueComponent = (element) => {
- const url = element.dataset.frameUrl;
- return new Vue({
- el: element,
- render(h) {
- return h(ObservabilityApp, {
- props: {
- observabilityIframeSrc: url,
- inlineEmbed: true,
- skeletonVariant: SKELETON_VARIANT_EMBED,
- height: INLINE_EMBED_DIMENSIONS.HEIGHT,
- width: INLINE_EMBED_DIMENSIONS.WIDTH,
- },
- });
- },
- });
-};
-
-export default function renderObservability(elements) {
- return elements.map(mountVueComponent);
-}
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index ce77ede9fe4..6e0b1250479 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -131,10 +131,13 @@ $(document).on('markdown-preview:show', (e, $form) => {
lastTextareaPreviewed = $form.find('textarea.markdown-area');
lastTextareaHeight = lastTextareaPreviewed.height();
- // toggle tabs
- $form.find(previewButtonSelector).val('edit');
- $form.find(previewButtonSelector).children('span.gl-button-text').text(__('Continue editing'));
- $form.find(previewButtonSelector).addClass('gl-shadow-none! gl-bg-transparent!');
+ const $previewButton = $form.find(previewButtonSelector);
+
+ if (!$previewButton.parents('.js-vue-markdown-field').length) {
+ $previewButton.val('edit');
+ $previewButton.children('span.gl-button-text').text(__('Continue editing'));
+ $previewButton.addClass('gl-shadow-none! gl-bg-transparent!');
+ }
// toggle content
$form.find('.md-write-holder').hide();
@@ -154,9 +157,12 @@ $(document).on('markdown-preview:hide', (e, $form) => {
$form.find('textarea.markdown-area').height(lastTextareaHeight);
}
- // toggle tabs
- $form.find(previewButtonSelector).val('preview');
- $form.find(previewButtonSelector).children('span.gl-button-text').text(__('Preview'));
+ const $previewButton = $form.find(previewButtonSelector);
+
+ if (!$previewButton.parents('.js-vue-markdown-field').length) {
+ $previewButton.val('preview');
+ $previewButton.children('span.gl-button-text').text(__('Preview'));
+ }
// toggle content
$form.find('.md-write-holder').show();
diff --git a/app/assets/javascripts/blob/components/blob_content.vue b/app/assets/javascripts/blob/components/blob_content.vue
index cb9997b7c54..5592a75a4d2 100644
--- a/app/assets/javascripts/blob/components/blob_content.vue
+++ b/app/assets/javascripts/blob/components/blob_content.vue
@@ -98,7 +98,7 @@ export default {
:file-name="blob.name"
:type="activeViewer.fileType"
:hide-line-numbers="hideLineNumbers"
- data-qa-selector="blob_viewer_file_content"
+ data-testid="blob-viewer-file-content"
@richContentLoaded="richContentLoaded = true"
/>
</template>
diff --git a/app/assets/javascripts/blob/components/blob_edit_header.vue b/app/assets/javascripts/blob/components/blob_edit_header.vue
index 8fd3f03ff71..cd2872026c1 100644
--- a/app/assets/javascripts/blob/components/blob_edit_header.vue
+++ b/app/assets/javascripts/blob/components/blob_edit_header.vue
@@ -48,7 +48,7 @@ export default {
variant="danger"
category="secondary"
:disabled="!canDelete"
- data-qa-selector="delete_file_button"
+ data-testid="delete-file-button"
@click="$emit('delete')"
>{{ s__('Snippets|Delete file') }}</gl-button
>
diff --git a/app/assets/javascripts/blob/components/blob_header_default_actions.vue b/app/assets/javascripts/blob/components/blob_header_default_actions.vue
index 12a198f78ea..ddc135e2de7 100644
--- a/app/assets/javascripts/blob/components/blob_header_default_actions.vue
+++ b/app/assets/javascripts/blob/components/blob_header_default_actions.vue
@@ -96,7 +96,7 @@ export default {
};
</script>
<template>
- <gl-button-group data-qa-selector="default_actions_container">
+ <gl-button-group data-testid="default-actions-container">
<gl-button
v-if="showCopyButton"
v-gl-tooltip.hover
@@ -104,8 +104,7 @@ export default {
:title="$options.BTN_COPY_CONTENTS_TITLE"
:disabled="copyDisabled"
:data-clipboard-target="getBlobHashTarget"
- data-testid="copyContentsButton"
- data-qa-selector="copy_contents_button"
+ data-testid="copy-contents-button"
icon="copy-to-clipboard"
category="primary"
variant="default"
diff --git a/app/assets/javascripts/blob/components/blob_header_filepath.vue b/app/assets/javascripts/blob/components/blob_header_filepath.vue
index 95b88937c32..9187b45788a 100644
--- a/app/assets/javascripts/blob/components/blob_header_filepath.vue
+++ b/app/assets/javascripts/blob/components/blob_header_filepath.vue
@@ -49,7 +49,7 @@ export default {
<file-icon :file-name="fileName" :size="16" aria-hidden="true" css-classes="gl-mr-3" />
<strong
class="file-title-name mr-1 js-blob-header-filepath"
- data-qa-selector="file_title_content"
+ data-testid="file-title-content"
>{{ fileName }}</strong
>
</template>
diff --git a/app/assets/javascripts/blob/csv/constants.js b/app/assets/javascripts/blob/csv/constants.js
new file mode 100644
index 00000000000..7445b653d28
--- /dev/null
+++ b/app/assets/javascripts/blob/csv/constants.js
@@ -0,0 +1 @@
+export const MAX_ROWS_TO_RENDER = 2000;
diff --git a/app/assets/javascripts/blob/csv/csv_viewer.vue b/app/assets/javascripts/blob/csv/csv_viewer.vue
index 169167625e0..7231d023024 100644
--- a/app/assets/javascripts/blob/csv/csv_viewer.vue
+++ b/app/assets/javascripts/blob/csv/csv_viewer.vue
@@ -1,12 +1,15 @@
<script>
-import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { GlLoadingIcon, GlTable, GlButton } from '@gitlab/ui';
import Papa from 'papaparse';
+import { setUrlParams } from '~/lib/utils/url_utility';
import PapaParseAlert from '~/vue_shared/components/papa_parse_alert.vue';
+import { MAX_ROWS_TO_RENDER } from './constants';
export default {
components: {
PapaParseAlert,
GlTable,
+ GlButton,
GlLoadingIcon,
},
props: {
@@ -25,8 +28,14 @@ export default {
items: [],
papaParseErrors: [],
loading: true,
+ isTooLarge: false,
};
},
+ computed: {
+ pathToRawFile() {
+ return setUrlParams({ plain: 1 });
+ },
+ },
mounted() {
if (!this.remoteFile) {
const parsed = Papa.parse(this.csv, { skipEmptyLines: true });
@@ -43,7 +52,11 @@ export default {
},
methods: {
handleParsedData(parsed) {
- this.items = parsed.data;
+ if (parsed.data.length > MAX_ROWS_TO_RENDER) {
+ this.isTooLarge = true;
+ }
+
+ this.items = parsed.data.slice(0, MAX_ROWS_TO_RENDER);
if (parsed.errors.length) {
this.papaParseErrors = parsed.errors;
@@ -63,12 +76,28 @@ export default {
<div v-else>
<papa-parse-alert v-if="papaParseErrors.length" :papa-parse-errors="papaParseErrors" />
<gl-table
- :empty-text="__('No CSV data to display.')"
+ :empty-text="s__('CsvViewer|No CSV data to display.')"
:items="items"
:fields="$options.fields"
show-empty
thead-class="gl-display-none"
/>
+ <div
+ v-if="isTooLarge"
+ class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-p-5"
+ >
+ <p data-testid="large-csv-text">
+ {{
+ s__(
+ 'CsvViewer|The file is too large to render all the rows. To see the entire file, switch to the raw view.',
+ )
+ }}
+ </p>
+
+ <gl-button category="secondary" variant="confirm" :href="pathToRawFile">{{
+ s__('CsvViewer|View raw data')
+ }}</gl-button>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_app.vue b/app/assets/javascripts/boards/components/board_app.vue
index 1cfa35ffd91..4d915ff341a 100644
--- a/app/assets/javascripts/boards/components/board_app.vue
+++ b/app/assets/javascripts/boards/components/board_app.vue
@@ -1,6 +1,7 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapGetters } from 'vuex';
+import { omit } from 'lodash';
import { refreshCurrentPage, queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import BoardContent from '~/boards/components/board_content.vue';
@@ -115,9 +116,8 @@ export default {
return this.activeListId ? this.boardListsApollo[this.activeListId] : undefined;
},
formattedFilterParams() {
- if (this.filterParams.groupBy) delete this.filterParams.groupBy;
return filterVariables({
- filters: this.filterParams,
+ filters: omit(this.filterParams, 'groupBy'),
issuableType: this.issuableType,
filterInfo: FiltersInfo,
filterFields: FilterFields,
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 05865dc7305..fd45d2d31c3 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -2,6 +2,9 @@
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState } from 'vuex';
import Tracking from '~/tracking';
+import setSelectedBoardItemsMutation from '~/boards/graphql/client/set_selected_board_items.mutation.graphql';
+import unsetSelectedBoardItemsMutation from '~/boards/graphql/client/unset_selected_board_items.mutation.graphql';
+import selectedBoardItemsQuery from '~/boards/graphql/client/selected_board_items.query.graphql';
import setActiveBoardItemMutation from 'ee_else_ce/boards/graphql/client/set_active_board_item.mutation.graphql';
import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
import BoardCardInner from './board_card_inner.vue';
@@ -52,9 +55,12 @@ export default {
return !this.isApolloBoard;
},
},
+ selectedBoardItems: {
+ query: selectedBoardItemsQuery,
+ },
},
computed: {
- ...mapState(['selectedBoardItems', 'activeId']),
+ ...mapState(['activeId']),
activeItemId() {
return this.isApolloBoard ? this.activeBoardItem?.id : this.activeId;
},
@@ -62,10 +68,7 @@ export default {
return this.item.id === this.activeItemId;
},
multiSelectVisible() {
- return (
- !this.activeItemId &&
- this.selectedBoardItems.findIndex((boardItem) => boardItem.id === this.item.id) > -1
- );
+ return !this.activeItemId && this.selectedBoardItems?.includes(this.item.id);
},
isDisabled() {
return this.disabled || !this.item.id || this.item.isLoading || !this.canAdmin;
@@ -93,7 +96,7 @@ export default {
},
},
methods: {
- ...mapActions(['toggleBoardItemMultiSelection', 'toggleBoardItem']),
+ ...mapActions(['toggleBoardItem']),
toggleIssue(e) {
// Don't do anything if this happened on a no trigger element
if (e.target.closest('.js-no-trigger')) return;
@@ -110,7 +113,10 @@ export default {
this.track('click_card', { label: 'right_sidebar' });
}
},
- toggleItem() {
+ async toggleItem() {
+ await this.$apollo.mutate({
+ mutation: unsetSelectedBoardItemsMutation,
+ });
this.$apollo.mutate({
mutation: setActiveBoardItemMutation,
variables: {
@@ -119,13 +125,32 @@ export default {
},
});
},
+ async toggleBoardItemMultiSelection(item) {
+ if (this.activeItemId) {
+ await this.$apollo.mutate({
+ mutation: setSelectedBoardItemsMutation,
+ variables: {
+ itemId: this.activeItemId,
+ },
+ });
+ await this.$apollo.mutate({
+ mutation: setActiveBoardItemMutation,
+ variables: { boardItem: null },
+ });
+ }
+ this.$apollo.mutate({
+ mutation: setSelectedBoardItemsMutation,
+ variables: {
+ itemId: item.id,
+ },
+ });
+ },
},
};
</script>
<template>
<li
- data-qa-selector="board_card"
:class="[
{
'multi-select gl-bg-blue-50 gl-border-blue-200': multiSelectVisible,
@@ -141,7 +166,7 @@ export default {
:data-item-iid="item.iid"
:data-item-path="item.referencePath"
:style="cardStyle"
- data-testid="board_card"
+ data-testid="board-card"
class="board-card gl-p-5 gl-rounded-base gl-line-height-normal gl-relative gl-mb-3"
@click="toggleIssue($event)"
>
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index c441a718dd8..c10ff2e08da 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -9,11 +9,12 @@ import {
} from '@gitlab/ui';
import { sortBy } from 'lodash';
// eslint-disable-next-line no-restricted-imports
-import { mapActions, mapState } from 'vuex';
+import { mapActions } from 'vuex';
import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
import { sprintf, __, n__ } from '~/locale';
+import isShowingLabelsQuery from '~/graphql_shared/client/is_showing_labels.query.graphql';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
@@ -86,8 +87,13 @@ export default {
maxCounter: 99,
};
},
+ apollo: {
+ isShowingLabels: {
+ query: isShowingLabelsQuery,
+ update: (data) => data.isShowingLabels,
+ },
+ },
computed: {
- ...mapState(['isShowingLabels']),
isLoading() {
return this.item.isLoading || this.item.iid === '-1';
},
@@ -252,7 +258,7 @@ export default {
v-if="item.hidden"
v-gl-tooltip
name="spam"
- :title="__('This issue is hidden because its author has been banned')"
+ :title="__('This issue is hidden because its author has been banned.')"
class="gl-mr-2 hidden-icon gl-text-orange-500 gl-cursor-help"
data-testid="hidden-icon"
/>
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index bcd7db8dcb4..67a4c5eba45 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -93,7 +93,7 @@ export default {
}"
:data-list-id="list.id"
class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal is-expandable"
- data-qa-selector="board_list"
+ data-testid="board-list"
>
<div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base gl-bg-gray-50"
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 3c2659b00c9..554f3bfa416 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -219,7 +219,7 @@ export default {
<template>
<div
v-cloak
- data-qa-selector="boards_list"
+ data-testid="boards-list"
class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-min-h-0"
>
<gl-alert v-if="errorToDisplay" variant="danger" :dismissible="true" @dismiss="dismissError">
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index d12478b42d8..a3d55ac8306 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -1,13 +1,15 @@
<script>
import { GlModal, GlAlert } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
-import { mapActions, mapState } from 'vuex';
+import { mapActions } from 'vuex';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { visitUrl, updateHistory, getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import eventHub from '~/boards/eventhub';
import { formType } from '../constants';
+import { setError } from '../graphql/cache_updates';
+import errorQuery from '../graphql/client/error.query.graphql';
import createBoardMutation from '../graphql/board_create.mutation.graphql';
import destroyBoardMutation from '../graphql/board_destroy.mutation.graphql';
import updateBoardMutation from '../graphql/board_update.mutation.graphql';
@@ -93,8 +95,13 @@ export default {
isLoading: false,
};
},
+ apollo: {
+ error: {
+ query: errorQuery,
+ update: (data) => data.boardsAppError,
+ },
+ },
computed: {
- ...mapState(['error']),
isNewForm() {
return this.currentPage === formType.new;
},
@@ -133,7 +140,7 @@ export default {
variant: this.buttonKind,
disabled: this.submitDisabled,
loading: this.isLoading,
- 'data-qa-selector': 'save_changes_button',
+ 'data-testid': 'save-changes-button',
},
};
},
@@ -177,7 +184,8 @@ export default {
}
},
methods: {
- ...mapActions(['setError', 'unsetError', 'setBoard']),
+ ...mapActions(['setBoard']),
+ setError,
isFocusMode() {
return Boolean(document.querySelector('.content-wrapper > .js-focus-mode-board.is-focused'));
},
@@ -211,8 +219,8 @@ export default {
try {
await this.deleteBoard();
visitUrl(this.boardBaseUrl);
- } catch {
- this.setError({ message: this.$options.i18n.deleteErrorMessage });
+ } catch (error) {
+ setError({ error, message: this.$options.i18n.deleteErrorMessage });
} finally {
this.isLoading = false;
}
@@ -236,8 +244,8 @@ export default {
: '';
updateHistory({ url: `${this.boardBaseUrl}/${getIdFromGraphQLId(board.id)}${param}` });
}
- } catch {
- this.setError({ message: this.$options.i18n.saveErrorMessage });
+ } catch (error) {
+ setError({ error, message: this.$options.i18n.saveErrorMessage });
} finally {
this.isLoading = false;
}
@@ -295,11 +303,11 @@ export default {
@hide.prevent
>
<gl-alert
- v-if="!isApolloBoard && error"
+ v-if="error"
class="gl-mb-3"
variant="danger"
:dismissible="true"
- @dismiss="unsetError"
+ @dismiss="() => setError({ message: null, captureError: false })"
>
{{ error }}
</gl-alert>
@@ -316,7 +324,7 @@ export default {
ref="name"
v-model="board.name"
class="form-control"
- data-qa-selector="board_name_field"
+ data-testid="board-name-field"
type="text"
:placeholder="$options.i18n.titleFieldPlaceholder"
@keyup.enter="submit"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 1bb7e88122a..2693a6bb5ea 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -653,7 +653,7 @@ export default {
<div
v-show="!list.collapsed"
class="board-list-component gl-relative gl-h-full gl-display-flex gl-flex-direction-column gl-min-h-0"
- data-qa-selector="board_list_cards_area"
+ data-testid="board-list-cards-area"
>
<div
v-if="loading"
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 42c30dc8245..0235edd69ac 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -201,8 +201,8 @@ export default {
})
);
},
- totalWeight() {
- return this.boardList?.totalWeight;
+ totalIssueWeight() {
+ return this.boardList?.totalIssueWeight;
},
canShowTotalWeight() {
return this.weightFeatureAvailable && !this.isLoading;
@@ -365,7 +365,6 @@ export default {
}"
:style="headerStyle"
class="board-header gl-relative"
- data-qa-selector="board_list_header"
data-testid="board-list-header"
>
<h3
@@ -473,8 +472,8 @@ export default {
<div v-else>• {{ itemsTooltipLabel }}</div>
<div v-if="weightFeatureAvailable && !isLoading">
•
- <gl-sprintf :message="__('%{totalWeight} total weight')">
- <template #totalWeight>{{ totalWeight }}</template>
+ <gl-sprintf :message="__('%{totalIssueWeight} total weight')">
+ <template #totalIssueWeight>{{ totalIssueWeight }}</template>
</gl-sprintf>
</div>
</gl-tooltip>
@@ -507,7 +506,7 @@ export default {
<gl-tooltip :target="() => $refs.weightTooltip" :title="weightCountToolTip" />
<span ref="weightTooltip" class="gl-display-inline-flex gl-ml-3" data-testid="weight">
<gl-icon class="gl-mr-2" name="weight" :size="14" />
- {{ totalWeight }}
+ {{ totalIssueWeight }}
</span>
</template>
<!-- EE end -->
diff --git a/app/assets/javascripts/boards/components/board_top_bar.vue b/app/assets/javascripts/boards/components/board_top_bar.vue
index 7fd1a934381..31664c28831 100644
--- a/app/assets/javascripts/boards/components/board_top_bar.vue
+++ b/app/assets/javascripts/boards/components/board_top_bar.vue
@@ -4,6 +4,7 @@ import { s__ } from '~/locale';
import BoardsSelector from 'ee_else_ce/boards/components/boards_selector.vue';
import IssueBoardFilteredSearch from 'ee_else_ce/boards/components/issue_board_filtered_search.vue';
import { getBoardQuery } from 'ee_else_ce/boards/boards_util';
+import ToggleLabels from '~/vue_shared/components/toggle_labels.vue';
import { setError } from '../graphql/cache_updates';
import ConfigToggle from './config_toggle.vue';
import NewBoardButton from './new_board_button.vue';
@@ -17,7 +18,7 @@ export default {
ConfigToggle,
NewBoardButton,
ToggleFocus,
- ToggleLabels: () => import('ee_component/boards/components/toggle_labels.vue'),
+ ToggleLabels,
ToggleEpicsSwimlanes: () => import('ee_component/boards/components/toggle_epics_swimlanes.vue'),
EpicBoardFilteredSearch: () =>
import('ee_component/boards/components/epic_filtered_search.vue'),
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index cc6fde92f9b..cd2a4a02b2e 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -1,15 +1,7 @@
<script>
-import {
- GlLoadingIcon,
- GlSearchBoxByType,
- GlDropdown,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlDropdownItem,
- GlModalDirective,
-} from '@gitlab/ui';
+import { GlButton, GlCollapsibleListbox, GlModalDirective } from '@gitlab/ui';
import { produce } from 'immer';
-import { throttle } from 'lodash';
+import { differenceBy, debounce } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState } from 'vuex';
@@ -18,7 +10,8 @@ import BoardForm from 'ee_else_ce/boards/components/board_form.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isMetaKey } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
-import { s__ } from '~/locale';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { s__, __ } from '~/locale';
import eventHub from '../eventhub';
import groupBoardsQuery from '../graphql/group_boards.query.graphql';
@@ -34,15 +27,16 @@ export default {
name: 'BoardsSelector',
i18n: {
fetchBoardsError: s__('Boards|An error occurred while fetching boards. Please try again.'),
+ headerText: s__('IssueBoards|Switch board'),
+ noResultsText: s__('IssueBoards|No matching boards found'),
+ hiddenBoardsText: s__(
+ 'IssueBoards|Some of your boards are hidden, add a license to see them again.',
+ ),
},
components: {
BoardForm,
- GlLoadingIcon,
- GlSearchBoxByType,
- GlDropdown,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlDropdownItem,
+ GlButton,
+ GlCollapsibleListbox,
},
directives: {
GlModalDirective,
@@ -60,11 +54,6 @@ export default {
'isApolloBoard',
],
props: {
- throttleDuration: {
- type: Number,
- default: 200,
- required: false,
- },
boardApollo: {
type: Object,
required: false,
@@ -78,13 +67,10 @@ export default {
},
data() {
return {
- hasScrollFade: false,
- scrollFadeInitialized: false,
boards: [],
recentBoards: [],
loadingBoards: false,
loadingRecentBoards: false,
- throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
contentClientHeight: 0,
maxPosition: 0,
filterTerm: '',
@@ -97,6 +83,12 @@ export default {
boardToUse() {
return this.isApolloBoard ? this.boardApollo : this.board;
},
+ boardToUseName() {
+ return this.boardToUse?.name || s__('IssueBoards|Select board');
+ },
+ boardToUseId() {
+ return getIdFromGraphQLId(this.boardToUse.id) || '';
+ },
isBoardToUseLoading() {
return this.isApolloBoard ? this.isCurrentBoardLoading : this.isBoardLoading;
},
@@ -112,6 +104,26 @@ export default {
loading() {
return this.loadingRecentBoards || this.loadingBoards;
},
+ listBoxItems() {
+ const mapItems = ({ id, name }) => ({ text: name, value: id });
+
+ if (this.showRecentSection) {
+ const notRecent = differenceBy(this.filteredBoards, this.recentBoards, 'id');
+
+ return [
+ {
+ text: __('Recent'),
+ options: this.recentBoards.map(mapItems),
+ },
+ {
+ text: __('All'),
+ options: notRecent.map(mapItems),
+ },
+ ];
+ }
+
+ return this.filteredBoards.map(mapItems);
+ },
filteredBoards() {
return this.boards.filter((board) =>
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
@@ -126,34 +138,25 @@ export default {
showDropdown() {
return this.showCreate || this.hasMissingBoards;
},
- scrollFadeClass() {
- return {
- 'fade-out': !this.hasScrollFade,
- };
- },
showRecentSection() {
return (
- this.recentBoards.length &&
+ this.recentBoards.length > 0 &&
this.boards.length > MIN_BOARDS_TO_VIEW_RECENT &&
!this.filterTerm.length
);
},
},
watch: {
- filteredBoards() {
- this.scrollFadeInitialized = false;
- this.$nextTick(this.setScrollFade);
- },
- recentBoards() {
- this.scrollFadeInitialized = false;
- this.$nextTick(this.setScrollFade);
- },
boardToUse(newBoard) {
document.title = newBoard.name;
},
},
created() {
eventHub.$on('showBoardModal', this.showPage);
+ this.handleSearch = debounce(this.setFilterTerm, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ destroyed() {
+ this.handleSearch.cancel();
},
beforeDestroy() {
eventHub.$off('showBoardModal', this.showPage);
@@ -248,34 +251,6 @@ export default {
this.$emit('switchBoard', board.id);
},
- isScrolledUp() {
- const { content } = this.$refs;
-
- if (!content) {
- return false;
- }
-
- const currentPosition = this.contentClientHeight + content.scrollTop;
-
- return currentPosition < this.maxPosition;
- },
- initScrollFade() {
- const { content } = this.$refs;
-
- if (!content) {
- return;
- }
-
- this.scrollFadeInitialized = true;
-
- this.contentClientHeight = content.clientHeight;
- this.maxPosition = content.scrollHeight;
- },
- setScrollFade() {
- if (!this.scrollFadeInitialized) this.initScrollFade();
-
- this.hasScrollFade = this.isScrolledUp();
- },
fetchCurrentBoard(boardId) {
this.fetchBoard({
fullPath: this.fullPath,
@@ -283,17 +258,24 @@ export default {
boardType: this.boardType,
});
},
- async switchBoard(boardId, e) {
+ setFilterTerm(value) {
+ this.filterTerm = value;
+ },
+ async switchBoardKeyEvent(boardId, e) {
if (isMetaKey(e)) {
+ e.stopPropagation();
window.open(`${this.boardBaseUrl}/${boardId}`, '_blank');
- } else if (this.isApolloBoard) {
+ }
+ },
+ switchBoardGroup(value) {
+ if (this.isApolloBoard) {
// Epic board ID is supported in EE version of this file
- this.$emit('switchBoard', this.fullBoardId(boardId));
- updateHistory({ url: `${this.boardBaseUrl}/${boardId}` });
+ this.$emit('switchBoard', this.fullBoardId(value));
+ updateHistory({ url: `${this.boardBaseUrl}/${value}` });
} else {
this.unsetActiveId();
- this.fetchCurrentBoard(boardId);
- updateHistory({ url: `${this.boardBaseUrl}/${boardId}` });
+ this.fetchCurrentBoard(value);
+ updateHistory({ url: `${this.boardBaseUrl}/${value}` });
}
},
},
@@ -303,105 +285,65 @@ export default {
<template>
<div class="boards-switcher gl-mr-3" data-testid="boards-selector">
<span class="boards-selector-wrapper">
- <gl-dropdown
+ <gl-collapsible-listbox
v-if="showDropdown"
+ block
data-testid="boards-dropdown"
- data-qa-selector="boards_dropdown"
- toggle-class="dropdown-menu-toggle"
- menu-class="flex-column dropdown-extended-height"
+ searchable
+ :searching="loading"
+ toggle-class="gl-min-w-20"
+ :header-text="$options.i18n.headerText"
+ :no-results-text="$options.i18n.noResultsText"
:loading="isBoardToUseLoading"
- :text="boardToUse.name"
- @show="loadBoards"
+ :items="listBoxItems"
+ :toggle-text="boardToUseName"
+ :selected="boardToUseId"
+ @search="handleSearch"
+ @select="switchBoardGroup"
+ @shown="loadBoards"
>
- <p class="gl-dropdown-header-top" @mousedown.prevent>
- {{ s__('IssueBoards|Switch board') }}
- </p>
- <gl-search-box-by-type ref="searchBox" v-model="filterTerm" class="m-2" />
-
- <div
- v-if="!loading"
- ref="content"
- data-qa-selector="boards_dropdown_content"
- class="dropdown-content flex-fill"
- @scroll.passive="throttledSetScrollFade"
- >
- <gl-dropdown-item
- v-show="filteredBoards.length === 0"
- class="gl-pointer-events-none text-secondary"
- >
- {{ s__('IssueBoards|No matching boards found') }}
- </gl-dropdown-item>
-
- <gl-dropdown-section-header v-if="showRecentSection">
- {{ __('Recent') }}
- </gl-dropdown-section-header>
-
- <template v-if="showRecentSection">
- <gl-dropdown-item
- v-for="recentBoard in recentBoards"
- :key="`recent-${recentBoard.id}`"
- data-testid="dropdown-item"
- @click.prevent="switchBoard(recentBoard.id, $event)"
- >
- {{ recentBoard.name }}
- </gl-dropdown-item>
- </template>
-
- <gl-dropdown-divider v-if="showRecentSection" />
-
- <gl-dropdown-section-header v-if="showRecentSection">
- {{ __('All') }}
- </gl-dropdown-section-header>
-
- <gl-dropdown-item
- v-for="otherBoard in filteredBoards"
- :key="otherBoard.id"
- data-testid="dropdown-item"
- @click.prevent="switchBoard(otherBoard.id, $event)"
- >
- {{ otherBoard.name }}
- </gl-dropdown-item>
-
- <gl-dropdown-item v-if="hasMissingBoards" class="no-pointer-events">
+ <template #list-item="{ item }">
+ <div data-testid="dropdown-item-recent" @click="switchBoardKeyEvent(item.value, $event)">
+ {{ item.text }}
+ </div>
+ </template>
+
+ <template #footer>
+ <div v-if="hasMissingBoards" class="gl-border-t gl-font-sm gl-px-4 gl-pt-4 gl-pb-3">
{{
s__('IssueBoards|Some of your boards are hidden, add a license to see them again.')
}}
- </gl-dropdown-item>
- </div>
-
- <div
- v-show="filteredBoards.length > 0"
- class="dropdown-content-faded-mask"
- :class="scrollFadeClass"
- ></div>
-
- <gl-loading-icon v-if="loading" size="sm" />
-
- <div v-if="canAdminBoard">
- <gl-dropdown-divider />
-
- <gl-dropdown-item
- v-if="showCreate"
- v-gl-modal-directive="'board-config-modal'"
- data-qa-selector="create_new_board_button"
- data-track-action="click_button"
- data-track-label="create_new_board"
- data-track-property="dropdown"
- @click.prevent="showPage('new')"
- >
- {{ s__('IssueBoards|Create new board') }}
- </gl-dropdown-item>
-
- <gl-dropdown-item
- v-if="showDelete"
- v-gl-modal-directive="'board-config-modal'"
- class="text-danger"
- @click.prevent="showPage('delete')"
- >
- {{ s__('IssueBoards|Delete board') }}
- </gl-dropdown-item>
- </div>
- </gl-dropdown>
+ </div>
+ <div v-if="canAdminBoard" class="gl-border-t gl-py-2 gl-px-2">
+ <gl-button
+ v-if="showCreate"
+ v-gl-modal-directive="'board-config-modal'"
+ block
+ class="gl-justify-content-start!"
+ category="tertiary"
+ data-testid="create-new-board-button"
+ data-track-action="click_button"
+ data-track-label="create_new_board"
+ data-track-property="dropdown"
+ @click="showPage('new')"
+ >
+ {{ s__('IssueBoards|Create new board') }}
+ </gl-button>
+
+ <gl-button
+ v-if="showDelete"
+ v-gl-modal-directive="'board-config-modal'"
+ block
+ category="tertiary"
+ variant="danger"
+ class="gl-mt-0! gl-justify-content-start!"
+ @click="showPage('delete')"
+ >
+ {{ s__('IssueBoards|Delete board') }}
+ </gl-button>
+ </div>
+ </template>
+ </gl-collapsible-listbox>
<board-form
v-if="currentPage"
diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue
index bc896932ffc..69e6cc870d2 100644
--- a/app/assets/javascripts/boards/components/config_toggle.vue
+++ b/app/assets/javascripts/boards/components/config_toggle.vue
@@ -49,7 +49,7 @@ export default {
v-gl-tooltip
:title="tooltipTitle"
:class="{ 'dot-highlight': hasScope || boardHasScope }"
- data-qa-selector="boards_config_button"
+ data-testid="boards-config-button"
@click.prevent="showPage"
>
{{ buttonText }}
diff --git a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
index a7b3f5536a4..c28415de620 100644
--- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -62,11 +62,7 @@ export default {
tokensCE() {
const { issue, incident } = this.$options.i18n;
const { types } = this.$options;
- const { fetchUsers, fetchLabels } = issueBoardFilters(
- this.$apollo,
- this.fullPath,
- this.isGroupBoard,
- );
+ const { fetchLabels } = issueBoardFilters(this.$apollo, this.fullPath, this.isGroupBoard);
const tokens = [
{
@@ -77,7 +73,8 @@ export default {
token: UserToken,
dataType: 'user',
unique: true,
- fetchUsers,
+ isProject: !this.isGroupBoard,
+ fullPath: this.fullPath,
preloadedUsers: this.preloadedUsers(),
},
{
@@ -89,7 +86,8 @@ export default {
token: UserToken,
dataType: 'user',
unique: true,
- fetchUsers,
+ isProject: !this.isGroupBoard,
+ fullPath: this.fullPath,
preloadedUsers: this.preloadedUsers(),
},
{
diff --git a/app/assets/javascripts/boards/components/toggle_focus.vue b/app/assets/javascripts/boards/components/toggle_focus.vue
index 990a6fa63d4..a886abf9e61 100644
--- a/app/assets/javascripts/boards/components/toggle_focus.vue
+++ b/app/assets/javascripts/boards/components/toggle_focus.vue
@@ -38,7 +38,7 @@ export default {
v-gl-tooltip
category="tertiary"
:icon="isFullscreen ? 'minimize' : 'maximize'"
- data-qa-selector="focus_mode_button"
+ data-testid="focus-mode-button"
:title="$options.i18n.toggleFocusMode"
:aria-label="$options.i18n.toggleFocusMode"
@click="toggleFocusMode"
diff --git a/app/assets/javascripts/boards/graphql/cache_updates.js b/app/assets/javascripts/boards/graphql/cache_updates.js
index 3551c3ed982..ea099e02181 100644
--- a/app/assets/javascripts/boards/graphql/cache_updates.js
+++ b/app/assets/javascripts/boards/graphql/cache_updates.js
@@ -68,7 +68,7 @@ export function updateIssueCountAndWeight({
boardList: {
...boardList,
issuesCount: boardList.issuesCount - 1,
- totalWeight: boardList.totalWeight - issue.weight,
+ totalIssueWeight: boardList.totalIssueWeight - issue.weight,
},
}),
);
@@ -83,7 +83,7 @@ export function updateIssueCountAndWeight({
boardList: {
...boardList,
issuesCount: boardList.issuesCount + 1,
- totalWeight: boardList.totalWeight + issue.weight,
+ ...(issue.weight ? { totalIssueWeight: boardList.totalIssueWeight + issue.weight } : {}),
},
}),
);
diff --git a/app/assets/javascripts/boards/graphql/client/selected_board_items.query.graphql b/app/assets/javascripts/boards/graphql/client/selected_board_items.query.graphql
new file mode 100644
index 00000000000..88006750221
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/client/selected_board_items.query.graphql
@@ -0,0 +1,3 @@
+query selectedBoardItems {
+ selectedBoardItems @client
+}
diff --git a/app/assets/javascripts/boards/graphql/client/set_selected_board_items.mutation.graphql b/app/assets/javascripts/boards/graphql/client/set_selected_board_items.mutation.graphql
new file mode 100644
index 00000000000..28274de6c3f
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/client/set_selected_board_items.mutation.graphql
@@ -0,0 +1,3 @@
+mutation setSelectedBoardItems($itemId: ID!) {
+ setSelectedBoardItems(itemId: $itemId) @client
+}
diff --git a/app/assets/javascripts/boards/graphql/client/unset_selected_board_items.mutation.graphql b/app/assets/javascripts/boards/graphql/client/unset_selected_board_items.mutation.graphql
new file mode 100644
index 00000000000..ab34cf48609
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/client/unset_selected_board_items.mutation.graphql
@@ -0,0 +1,3 @@
+mutation unsetSelectedBoardItems {
+ unsetSelectedBoardItems @client
+}
diff --git a/app/assets/javascripts/boards/issue_board_filters.js b/app/assets/javascripts/boards/issue_board_filters.js
index ba5da70c6ec..0a6580dd49b 100644
--- a/app/assets/javascripts/boards/issue_board_filters.js
+++ b/app/assets/javascripts/boards/issue_board_filters.js
@@ -1,5 +1,3 @@
-import { BoardType } from 'ee_else_ce/boards/constants';
-import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql';
import boardLabels from './graphql/board_labels.query.graphql';
export default function issueBoardFilters(apollo, fullPath, isGroupBoard) {
@@ -7,17 +5,6 @@ export default function issueBoardFilters(apollo, fullPath, isGroupBoard) {
return isGroupBoard ? data.group?.labels.nodes || [] : data.project?.labels.nodes || [];
};
- const fetchUsers = (usersSearchTerm) => {
- const namespace = isGroupBoard ? BoardType.group : BoardType.project;
-
- return apollo
- .query({
- query: usersAutocompleteQuery,
- variables: { fullPath, search: usersSearchTerm, isProject: !isGroupBoard },
- })
- .then(({ data }) => data[namespace]?.autocompleteUsers);
- };
-
const fetchLabels = (labelSearchTerm) => {
return apollo
.query({
@@ -34,6 +21,5 @@ export default function issueBoardFilters(apollo, fullPath, isGroupBoard) {
return {
fetchLabels,
- fetchUsers,
};
}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index e044283534a..3e7d7a7a8d3 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -621,7 +621,7 @@ export default {
__typename: 'BoardList',
id: fromList.boardList.id,
issuesCount: fromList.boardList.issuesCount - 1,
- totalWeight: fromList.boardList.totalWeight - Number(weight),
+ totalIssueWeight: fromList.boardList.totalIssueWeight - Number(weight),
},
};
@@ -645,7 +645,7 @@ export default {
__typename: 'BoardList',
id: toList.boardList.id,
issuesCount: toList.boardList.issuesCount + 1,
- totalWeight: toList.boardList.totalWeight + Number(weight),
+ totalIssueWeight: toList.boardList.totalIssueWeight + Number(weight),
},
};
@@ -731,7 +731,7 @@ export default {
__typename: 'BoardList',
id: fromList.boardList.id,
issuesCount: fromList.boardList.issuesCount + 1,
- totalWeight: fromList.boardList.totalWeight,
+ totalIssueWeight: fromList.boardList.totalIssueWeight,
},
};
diff --git a/app/assets/javascripts/branches/components/delete_merged_branches.vue b/app/assets/javascripts/branches/components/delete_merged_branches.vue
index 50fe610d335..24ae9b83b9c 100644
--- a/app/assets/javascripts/branches/components/delete_merged_branches.vue
+++ b/app/assets/javascripts/branches/components/delete_merged_branches.vue
@@ -1,5 +1,12 @@
<script>
-import { GlDisclosureDropdown, GlButton, GlFormInput, GlModal, GlSprintf } from '@gitlab/ui';
+import {
+ GlDisclosureDropdown,
+ GlButton,
+ GlFormInput,
+ GlModal,
+ GlSprintf,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { sprintf, s__, __ } from '~/locale';
@@ -22,6 +29,7 @@ export const i18n = {
'Branches|Plese type the following to confirm: %{codeStart}delete%{codeEnd}.',
),
cancelButtonText: __('Cancel'),
+ actionsToggleText: __('More actions'),
};
export default {
@@ -33,6 +41,9 @@ export default {
GlFormInput,
GlSprintf,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
formPath: {
type: String,
@@ -96,6 +107,10 @@ export default {
<template>
<div>
<gl-disclosure-dropdown
+ v-gl-tooltip.hover.top="{
+ title: $options.i18n.actionsToggleText,
+ boundary: 'viewport',
+ }"
:toggle-text="$options.i18n.actionsToggleText"
text-sr-only
icon="ellipsis_v"
@@ -153,7 +168,7 @@ export default {
<gl-form-input
v-model="enteredText"
type="text"
- size="sm"
+ width="sm"
class="gl-mt-2"
aria-labelledby="input-label"
autocomplete="off"
diff --git a/app/assets/javascripts/branches/components/sort_dropdown.vue b/app/assets/javascripts/branches/components/sort_dropdown.vue
index 4866d506988..f4a5b25c4f2 100644
--- a/app/assets/javascripts/branches/components/sort_dropdown.vue
+++ b/app/assets/javascripts/branches/components/sort_dropdown.vue
@@ -1,6 +1,6 @@
<script>
import { GlCollapsibleListbox, GlSearchBoxByClick } from '@gitlab/ui';
-import { mergeUrlParams, visitUrl } from '~/lib/utils/url_utility';
+import { mergeUrlParams, visitUrl, getParameterValues } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
export default {
@@ -21,7 +21,7 @@ export default {
// own attributes, also in created()
data() {
return {
- searchTerm: '',
+ searchTerm: getParameterValues('search')[0] || '',
};
},
computed: {
diff --git a/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue
index cbb80a5175f..9d516fc267d 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue
@@ -23,6 +23,6 @@ export default {
</script>
<template>
<div class="gl-text-truncate">
- <gl-link :href="projectUrl"> {{ projectName }}</gl-link>
+ <gl-link :href="projectUrl" data-testid="job-project-link">{{ projectName }}</gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue
index a76829aa129..e44f756a5c5 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue
@@ -1,5 +1,6 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { GlLink, GlTooltipDirective } from '@gitlab/ui';
+import RunnerTypeIcon from '~/ci/runner/components/runner_type_icon.vue';
import { RUNNER_EMPTY_TEXT, RUNNER_NO_DESCRIPTION } from '../../constants';
export default {
@@ -9,6 +10,10 @@ export default {
},
components: {
GlLink,
+ RunnerTypeIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
props: {
job: {
@@ -25,15 +30,19 @@ export default {
? this.job.runner.description
: this.$options.i18n.noRunnerDescription;
},
+ runnerType() {
+ return this.job.runner?.runnerType;
+ },
},
};
</script>
<template>
<div class="gl-text-truncate">
- <gl-link v-if="adminUrl" :href="adminUrl">
- {{ description }}
- </gl-link>
+ <span v-if="adminUrl">
+ <runner-type-icon :type="runnerType" class="gl-vertical-align-middle" />
+ <gl-link :href="adminUrl" data-testid="job-runner-link"> {{ description }} </gl-link>
+ </span>
<span v-else data-testid="empty-runner-text"> {{ $options.i18n.emptyRunnerText }}</span>
</div>
</template>
diff --git a/app/assets/javascripts/ci/admin/jobs_table/constants.js b/app/assets/javascripts/ci/admin/jobs_table/constants.js
index ff0efdb1f5b..86c9ab53e75 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/constants.js
+++ b/app/assets/javascripts/ci/admin/jobs_table/constants.js
@@ -26,9 +26,6 @@ export const DEFAULT_FIELDS_ADMIN = [
{ key: 'project', label: __('Project'), columnClass: 'gl-w-20p' },
{ key: 'runner', label: __('Runner'), columnClass: 'gl-w-15p' },
{ key: 'pipeline', label: __('Pipeline'), columnClass: 'gl-w-10p' },
- { key: 'stage', label: __('Stage'), columnClass: 'gl-w-10p' },
- { key: 'name', label: __('Name'), columnClass: 'gl-w-15p' },
- { key: 'duration', label: __('Duration'), columnClass: 'gl-w-15p' },
{ key: 'actions', label: '', columnClass: 'gl-w-10p' },
];
diff --git a/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql
index 89fb1782e46..2e77f4db907 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql
+++ b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql
@@ -16,6 +16,7 @@ query getAllJobs(
id
description
adminUrl
+ runnerType
}
artifacts {
nodes {
diff --git a/app/assets/javascripts/ci/artifacts/components/bulk_delete_modal.vue b/app/assets/javascripts/ci/artifacts/components/bulk_delete_modal.vue
index 00f5b2eab7d..c27ec0dd500 100644
--- a/app/assets/javascripts/ci/artifacts/components/bulk_delete_modal.vue
+++ b/app/assets/javascripts/ci/artifacts/components/bulk_delete_modal.vue
@@ -65,6 +65,7 @@ export default {
:title="$options.i18n.modalTitle(checkedCount)"
:action-primary="modalActionPrimary"
:action-cancel="modalActionCancel"
+ data-testid="artifacts-bulk-delete-modal"
v-bind="$attrs"
v-on="$listeners"
>
diff --git a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
index e08470c62be..d8f9eb65236 100644
--- a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
+++ b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
@@ -5,16 +5,15 @@ import {
GlLink,
GlButtonGroup,
GlButton,
- GlBadge,
GlIcon,
GlPagination,
GlFormCheckbox,
GlTooltipDirective,
} from '@gitlab/ui';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { createAlert } from '~/alert';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import getJobArtifactsQuery from '../graphql/queries/get_job_artifacts.query.graphql';
import { totalArtifactsSizeForJob, mapArchivesToJobNodes, mapBooleansToJobNodes } from '../utils';
@@ -65,12 +64,11 @@ export default {
GlLink,
GlButtonGroup,
GlButton,
- GlBadge,
GlIcon,
GlPagination,
GlFormCheckbox,
- CiIcon,
TimeAgo,
+ CiBadgeLink,
JobCheckbox,
ArtifactsBulkDelete,
BulkDeleteModal,
@@ -328,7 +326,7 @@ export default {
{
key: 'artifacts',
label: I18N_ARTIFACTS,
- thClass: 'gl-w-quarter',
+ thClass: 'gl-w-eighth',
},
{
key: 'job',
@@ -350,7 +348,7 @@ export default {
{
key: 'actions',
label: '',
- thClass: 'gl-w-eighth',
+ thClass: 'gl-w-20p',
tdClass: 'gl-text-right',
},
],
@@ -403,6 +401,7 @@ export default {
:checked="isAnyVisibleArtifactSelected"
:indeterminate="isAnyVisibleArtifactSelected && !areAllVisibleArtifactsSelected"
:disabled="isSelectedArtifactsLimitReached && !isAnyVisibleArtifactSelected"
+ data-testid="select-all-artifacts-checkbox"
@change="handleSelectAllChecked"
/>
</template>
@@ -441,45 +440,37 @@ export default {
</span>
</template>
<template #cell(job)="{ item }">
- <span class="gl-display-inline-flex gl-align-items-center gl-w-full gl-mb-4">
+ <div class="gl-display-inline-flex gl-align-items-center gl-mb-3 gl-gap-3">
<span data-testid="job-artifacts-job-status">
- <ci-icon v-if="item.succeeded" :status="item.detailedStatus" class="gl-mr-3" />
- <gl-badge
- v-else
- :icon="item.detailedStatus.icon"
- :variant="$options.STATUS_BADGE_VARIANTS[item.detailedStatus.group]"
- class="gl-mr-3"
- >
- {{ item.detailedStatus.label }}
- </gl-badge>
+ <ci-badge-link :status="item.detailedStatus" size="sm" :show-text="false" />
</span>
- <gl-link :href="item.webPath" class="gl-font-weight-bold">
+ <gl-link :href="item.webPath">
{{ item.name }}
</gl-link>
- </span>
- <span class="gl-display-inline-flex">
+ </div>
+ <div class="gl-mb-1">
<gl-icon name="pipeline" class="gl-mr-2" />
- <gl-link
- :href="item.pipeline.path"
- class="gl-text-black-normal gl-text-decoration-underline gl-mr-4"
- >
+ <gl-link :href="item.pipeline.path" class="gl-mr-2">
{{ pipelineId(item) }}
</gl-link>
- <gl-icon name="branch" class="gl-mr-2" />
- <gl-link
- :href="item.refPath"
- class="gl-text-black-normal gl-text-decoration-underline gl-mr-4"
- >
- {{ item.refName }}
- </gl-link>
- <gl-icon name="commit" class="gl-mr-2" />
- <gl-link
- :href="item.commitPath"
- class="gl-text-black-normal gl-text-decoration-underline gl-mr-4"
- >
- {{ item.shortSha }}
- </gl-link>
- </span>
+ <span class="gl-display-inline-block gl-rounded-base gl-px-2 gl-bg-gray-50">
+ <gl-icon name="commit" :size="12" class="gl-mr-2" />
+ <gl-link
+ :href="item.commitPath"
+ class="gl-text-black-normal gl-font-sm gl-font-monospace"
+ >
+ {{ item.shortSha }}
+ </gl-link>
+ </span>
+ </div>
+ <div>
+ <span class="gl-display-inline-block gl-rounded-base gl-px-2 gl-bg-gray-50">
+ <gl-icon name="branch" :size="12" class="gl-mr-1" />
+ <gl-link :href="item.refPath" class="gl-text-black-normal gl-font-sm gl-font-monospace">
+ {{ item.refName }}
+ </gl-link>
+ </span>
+ </div>
</template>
<template #cell(size)="{ item }">
<span data-testid="job-artifacts-size">{{ artifactsSize(item) }}</span>
diff --git a/app/assets/javascripts/ci/catalog/components/ci_catalog_home.vue b/app/assets/javascripts/ci/catalog/components/ci_catalog_home.vue
new file mode 100644
index 00000000000..5fe7e8286ec
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/ci_catalog_home.vue
@@ -0,0 +1,8 @@
+<script>
+export default {};
+</script>
+<template>
+ <div>
+ <router-view />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue
new file mode 100644
index 00000000000..572a8183730
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue
@@ -0,0 +1,120 @@
+<script>
+import { GlIcon, GlLink } from '@gitlab/ui';
+import { n__, s__, sprintf } from '~/locale';
+import { formatDate } from '~/lib/utils/datetime_utility';
+
+export default {
+ components: {
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ isLoadingDetails: {
+ type: Boolean,
+ required: true,
+ },
+ isLoadingSharedData: {
+ type: Boolean,
+ required: true,
+ },
+ openIssuesCount: {
+ required: false,
+ type: Number,
+ default: 0,
+ },
+ openMergeRequestsCount: {
+ required: false,
+ type: Number,
+ default: 0,
+ },
+ latestVersion: {
+ required: false,
+ type: Object,
+ default: () => ({}),
+ },
+ webPath: {
+ required: false,
+ type: String,
+ default: '',
+ },
+ },
+ computed: {
+ hasVersion() {
+ return this.latestVersion;
+ },
+ lastReleaseText() {
+ if (this.hasVersion) {
+ return sprintf(this.$options.i18n.lastRelease, {
+ date: this.releasedAt,
+ });
+ }
+
+ return this.$options.i18n.lastReleaseMissing;
+ },
+ openIssuesText() {
+ return n__('%d issue', '%d issues', this.openIssuesCount);
+ },
+ openMergeRequestText() {
+ return n__('%d merge request', '%d merge requests', this.openMergeRequestsCount);
+ },
+ releasedAt() {
+ return this.hasVersion && formatDate(this.latestVersion.releasedAt, 'yyyy-mm-dd');
+ },
+ projectInfoItems() {
+ return [
+ {
+ icon: 'project',
+ link: `${this.webPath}`,
+ text: this.$options.i18n.projectLink,
+ isLoading: this.isLoadingSharedData,
+ },
+ {
+ icon: 'issues',
+ link: `${this.webPath}/issues`,
+ text: this.openIssuesText,
+ isLoading: this.isLoadingDetails,
+ },
+ {
+ icon: 'merge-request',
+ link: `${this.webPath}/merge_requests`,
+ text: this.openMergeRequestText,
+ isLoading: this.isLoadingDetails,
+ },
+ {
+ icon: 'clock',
+ text: this.lastReleaseText,
+ isLoading: this.isLoadingSharedData,
+ },
+ ];
+ },
+ },
+ i18n: {
+ projectLink: s__('CiCatalog|Go to the project'),
+ lastRelease: s__('CiCatalog|Last release at %{date}'),
+ lastReleaseMissing: s__('CiCatalog|No release available'),
+ },
+};
+</script>
+
+<template>
+ <div class="gl-py-2 gl-sm-display-flex gl-gap-5">
+ <span
+ v-for="item in projectInfoItems"
+ :key="`${item.icon}`"
+ class="gl-display-flex gl-align-items-center gl-xs-mb-3"
+ >
+ <gl-icon class="gl-text-primary gl-mr-2" :name="item.icon" />
+ <div
+ v-if="item.isLoading"
+ class="gl-animate-skeleton-loader gl-h-4 gl-rounded-base gl-w-15"
+ data-testid="skeleton-loading-line"
+ ></div>
+ <template v-else>
+ <gl-link v-if="item.link" :href="item.link"> {{ item.text }} </gl-link>
+ <span v-else class="gl-text-secondary">
+ {{ item.text }}
+ </span>
+ </template>
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
new file mode 100644
index 00000000000..85dfa12c756
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
@@ -0,0 +1,103 @@
+<script>
+import { GlLoadingIcon, GlTableLite } from '@gitlab/ui';
+import { createAlert } from '~/alert';
+import { __, s__ } from '~/locale';
+import getCiCatalogResourceComponents from '../../graphql/queries/get_ci_catalog_resource_components.query.graphql';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlTableLite,
+ },
+ props: {
+ resourceId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ components: [],
+ };
+ },
+ apollo: {
+ components: {
+ query: getCiCatalogResourceComponents,
+ variables() {
+ return {
+ id: this.resourceId,
+ };
+ },
+ update(data) {
+ return data?.ciCatalogResource?.components?.nodes || [];
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.fetchError });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.components.loading;
+ },
+ },
+ methods: {
+ generateSnippet(componentPath) {
+ // This is not to be translated because it is our CI yaml syntax.
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `include:
+ - component: ${componentPath}`;
+ },
+ humanizeBoolean(bool) {
+ return bool ? __('Yes') : __('No');
+ },
+ },
+ fields: [
+ {
+ key: 'name',
+ label: s__('CiCatalogComponent|Parameters'),
+ thClass: 'gl-w-40p',
+ },
+ {
+ key: 'defaultValue',
+ label: s__('CiCatalogComponent|Default Value'),
+ thClass: 'gl-w-40p',
+ },
+ {
+ key: 'required',
+ label: s__('CiCatalogComponent|Mandatory'),
+ thClass: 'gl-w-20p',
+ },
+ ],
+ i18n: {
+ inputTitle: s__('CiCatalogComponent|Inputs'),
+ fetchError: s__("CiCatalogComponent|There was an error fetching this resource's components"),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon v-if="isLoading" size="lg" />
+ <template v-else>
+ <div
+ v-for="component in components"
+ :key="component.id"
+ class="gl-mb-8"
+ data-testid="component-section"
+ >
+ <h3 class="gl-font-size-h2" data-testid="component-name">{{ component.name }}</h3>
+ <p class="gl-mt-5">{{ component.description }}</p>
+ <pre class="gl-w-85p gl-py-4">{{ generateSnippet(component.path) }}</pre>
+ <div class="gl-mt-5">
+ <b class="gl-display-block gl-mb-4"> {{ $options.i18n.inputTitle }}</b>
+ <gl-table-lite :items="component.inputs.nodes" :fields="$options.fields">
+ <template #cell(required)="{ item }">
+ {{ humanizeBoolean(item.required) }}
+ </template>
+ </gl-table-lite>
+ </div>
+ </div>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
new file mode 100644
index 00000000000..c0feb52c185
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
@@ -0,0 +1,41 @@
+<script>
+import { GlTab, GlTabs } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import CiResourceComponents from './ci_resource_components.vue';
+import CiResourceReadme from './ci_resource_readme.vue';
+
+export default {
+ components: {
+ CiResourceReadme,
+ CiResourceComponents,
+ GlTab,
+ GlTabs,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ props: {
+ resourceId: {
+ type: String,
+ required: true,
+ },
+ },
+ i18n: {
+ tabs: {
+ components: s__('CiCatalog|Components'),
+ readme: s__('CiCatalog|Readme'),
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-tabs>
+ <gl-tab v-if="glFeatures.ciCatalogComponentsTab" :title="$options.i18n.tabs.components" lazy>
+ <ci-resource-components :resource-id="resourceId"
+ /></gl-tab>
+ <gl-tab :title="$options.i18n.tabs.readme" lazy>
+ <ci-resource-readme :resource-id="resourceId" />
+ </gl-tab>
+ </gl-tabs>
+</template>
+<style></style>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
new file mode 100644
index 00000000000..6673785ffd2
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
@@ -0,0 +1,130 @@
+<script>
+import { GlAvatar, GlAvatarLink, GlBadge } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { isNumeric } from '~/lib/utils/number_utils';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiResourceAbout from './ci_resource_about.vue';
+import CiResourceHeaderSkeletonLoader from './ci_resource_header_skeleton_loader.vue';
+
+export default {
+ components: {
+ CiBadgeLink,
+ CiResourceAbout,
+ CiResourceHeaderSkeletonLoader,
+ GlAvatar,
+ GlAvatarLink,
+ GlBadge,
+ },
+ props: {
+ isLoadingDetails: {
+ type: Boolean,
+ required: true,
+ },
+ isLoadingSharedData: {
+ type: Boolean,
+ required: true,
+ },
+ openIssuesCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ openMergeRequestsCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ pipelineStatus: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ resource: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ entityId() {
+ return getIdFromGraphQLId(this.resource.id);
+ },
+ fullPath() {
+ return `${this.rootNamespace.fullPath}/${this.rootNamespace.name}`;
+ },
+ hasLatestVersion() {
+ return this.latestVersion?.tagName;
+ },
+ hasPipelineStatus() {
+ return this.pipelineStatus?.text;
+ },
+ latestVersion() {
+ return this.resource.latestVersion;
+ },
+ rootNamespace() {
+ return this.resource.rootNamespace;
+ },
+ versionBadgeText() {
+ return isNumeric(this.latestVersion.tagName)
+ ? `v${this.latestVersion.tagName}`
+ : this.latestVersion.tagName;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <ci-resource-header-skeleton-loader v-if="isLoadingSharedData" class="gl-py-5" />
+ <div v-else class="gl-display-flex gl-py-5">
+ <gl-avatar-link :href="resource.webPath">
+ <gl-avatar
+ class="gl-mr-4"
+ :entity-id="entityId"
+ :entity-name="resource.name"
+ shape="rect"
+ :size="64"
+ :src="resource.icon"
+ />
+ </gl-avatar-link>
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-align-items-flex-start gl-justify-content-center"
+ >
+ <div class="gl-font-sm gl-text-secondary">
+ {{ fullPath }}
+ </div>
+ <span class="gl-display-flex">
+ <div class="gl-font-lg gl-font-weight-bold">{{ resource.name }}</div>
+ <gl-badge
+ v-if="hasLatestVersion"
+ size="sm"
+ class="gl-ml-3 gl-my-1"
+ :href="latestVersion.tagPath"
+ >
+ {{ versionBadgeText }}
+ </gl-badge>
+ </span>
+ <ci-badge-link
+ v-if="hasPipelineStatus"
+ class="gl-mt-2"
+ :status="pipelineStatus"
+ size="sm"
+ show-text
+ />
+ </div>
+ </div>
+ <ci-resource-about
+ :is-loading-details="isLoadingDetails"
+ :is-loading-shared-data="isLoadingSharedData"
+ :open-issues-count="openIssuesCount"
+ :open-merge-requests-count="openMergeRequestsCount"
+ :latest-version="latestVersion"
+ :web-path="resource.webPath"
+ />
+ <div
+ v-if="isLoadingSharedData"
+ class="gl-animate-skeleton-loader gl-h-4 gl-rounded-base gl-my-3 gl-max-w-20!"
+ ></div>
+ <p v-else class="gl-mt-3">
+ {{ resource.description }}
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header_skeleton_loader.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header_skeleton_loader.vue
new file mode 100644
index 00000000000..83ea224d772
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header_skeleton_loader.vue
@@ -0,0 +1,13 @@
+<script>
+export default {};
+</script>
+
+<template>
+ <div class="gl-display-flex">
+ <div class="gl-animate-skeleton-loader gl-h-11 gl-rounded-base gl-w-11"></div>
+ <div class="gl-pl-4 gl--flex-center gl-flex-direction-column">
+ <div class="gl-animate-skeleton-loader gl-h-4 gl-rounded-base gl-mb-3 gl-w-20"></div>
+ <div class="gl-animate-skeleton-loader gl-h-4 gl-rounded-base gl-w-20"></div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue
new file mode 100644
index 00000000000..d473833869d
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import { createAlert } from '~/alert';
+import { __ } from '~/locale';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import getCiCatalogResourceReadme from '../../graphql/queries/get_ci_catalog_resource_readme.query.graphql';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ },
+ directives: { SafeHtml },
+ props: {
+ resourceId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ readmeHtml: null,
+ };
+ },
+ apollo: {
+ readmeHtml: {
+ query: getCiCatalogResourceReadme,
+ variables() {
+ return {
+ id: this.resourceId,
+ };
+ },
+ update(data) {
+ return data?.ciCatalogResource?.readmeHtml || null;
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.loadingError });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.readmeHtml.loading;
+ },
+ },
+ i18n: {
+ loadingError: __("There was a problem loading this project's readme content."),
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
+ <div v-else v-safe-html="readmeHtml"></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
new file mode 100644
index 00000000000..487215875c0
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
@@ -0,0 +1,59 @@
+<script>
+import { GlBanner, GlLink } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { CATALOG_FEEDBACK_DISMISSED_KEY } from '../../constants';
+
+export default {
+ components: {
+ GlBanner,
+ GlLink,
+ },
+ inject: ['pageTitle', 'pageDescription'],
+ data() {
+ return {
+ isFeedbackBannerDismissed: localStorage.getItem(CATALOG_FEEDBACK_DISMISSED_KEY) === 'true',
+ };
+ },
+ methods: {
+ handleDismissBanner() {
+ localStorage.setItem(CATALOG_FEEDBACK_DISMISSED_KEY, 'true');
+ this.isFeedbackBannerDismissed = true;
+ },
+ },
+ i18n: {
+ banner: {
+ title: __('Your feedback is important to us 👋'),
+ description: s__(
+ "CiCatalog|We want to help you create and manage pipeline component repositories, while also making it easier to reuse pipeline configurations. Let us know how we're doing!",
+ ),
+ btnText: __('Give us some feedback'),
+ },
+ learnMore: __('Learn more'),
+ },
+ learnMorePath: helpPagePath('ci/components/index'),
+};
+</script>
+<template>
+ <div class="gl-border-b-1 gl-border-gray-100 gl-border-b-solid">
+ <gl-banner
+ v-if="!isFeedbackBannerDismissed"
+ class="gl-mt-5"
+ :title="$options.i18n.banner.title"
+ :button-text="$options.i18n.banner.btnText"
+ button-link="https://gitlab.com/gitlab-org/gitlab/-/issues/407556"
+ @close="handleDismissBanner"
+ >
+ <p>
+ {{ $options.i18n.banner.description }}
+ </p>
+ </gl-banner>
+ <h1 class="gl-font-size-h-display">{{ pageTitle }}</h1>
+ <p>
+ <span>{{ pageDescription }}</span>
+ <gl-link :href="$options.learnMorePath" target="_blank">{{
+ $options.i18n.learnMore
+ }}</gl-link>
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue
new file mode 100644
index 00000000000..3722b8e6c59
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue
@@ -0,0 +1,57 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+ data() {
+ return {
+ coordinates: {
+ statsX: 0,
+ releaseDateX: 0,
+ },
+ width: 0,
+ };
+ },
+ mounted() {
+ this.setSvgSize();
+ },
+ methods: {
+ setSvgSize() {
+ this.width = this.$el.offsetWidth;
+ this.coordinates.releaseDateX = this.width - 200;
+ this.coordinates.statsX = this.width - 90;
+ },
+ },
+ skeletonLoadItems: new Array(5),
+};
+</script>
+<template>
+ <div class="gl-w-full">
+ <gl-skeleton-loader
+ v-for="(_, index) in $options.skeletonLoadItems"
+ :key="index"
+ :height="60"
+ :width="width"
+ >
+ <!-- Catalog project avatar -->
+ <rect x="0" y="0" width="48" height="48" rx="4" ry="4" />
+ <!-- namespace path -->
+ <rect x="60" y="4" width="400" height="16" rx="2" ry="2" />
+ <!-- Project description -->
+ <rect x="60" y="30" width="500" height="12" rx="2" ry="2" />
+
+ <!-- Release date line -->
+ <rect :x="coordinates.releaseDateX" y="30" width="200" height="12" rx="2" ry="2" />
+
+ <!-- Favorites -->
+ <rect :x="coordinates.statsX" y="4" width="16" height="16" rx="2" ry="2" />
+ <rect :x="coordinates.statsX + 18" y="7" width="18" height="10" rx="2" ry="2" />
+
+ <!-- Forks -->
+ <rect :x="coordinates.statsX + 50" y="4" width="16" height="16" rx="2" ry="2" />
+ <rect :x="coordinates.statsX + 68" y="7" width="18" height="10" rx="2" ry="2" />
+ </gl-skeleton-loader>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list.vue b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list.vue
new file mode 100644
index 00000000000..d1fd9fe977b
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list.vue
@@ -0,0 +1,74 @@
+<script>
+import { GlKeysetPagination } from '@gitlab/ui';
+
+import { s__, sprintf } from '~/locale';
+import { ciCatalogResourcesItemsCount } from '../../graphql/settings';
+import CiResourcesListItem from './ci_resources_list_item.vue';
+
+export default {
+ components: {
+ CiResourcesListItem,
+ GlKeysetPagination,
+ },
+ props: {
+ currentPage: {
+ type: Number,
+ required: true,
+ },
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ resources: {
+ type: Array,
+ required: true,
+ },
+ totalCount: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ showPageCount() {
+ return typeof this.totalPageCount === 'number' && this.totalPageCount > 0;
+ },
+ totalPageCount() {
+ return Math.ceil(this.totalCount / ciCatalogResourcesItemsCount);
+ },
+ pageText() {
+ return sprintf(this.$options.i18n.pageText, {
+ currentPage: this.currentPage,
+ totalPage: this.totalPageCount,
+ });
+ },
+ },
+ i18n: {
+ pageText: s__('CiCatalog|Page %{currentPage} of %{totalPage}'),
+ },
+};
+</script>
+<template>
+ <div>
+ <ul class="gl-p-0" data-testId="catalog-list-container">
+ <ci-resources-list-item
+ v-for="resource in resources"
+ :key="resource.id"
+ :resource="resource"
+ />
+ </ul>
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-keyset-pagination
+ v-bind="pageInfo"
+ @prev="$emit('onPrevPage')"
+ @next="$emit('onNextPage')"
+ />
+ </div>
+ <div
+ v-if="showPageCount"
+ class="gl-display-flex gl-justify-content-center gl-mt-3"
+ data-testid="pageCount"
+ >
+ {{ pageText }}
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
new file mode 100644
index 00000000000..63243539575
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
@@ -0,0 +1,144 @@
+<script>
+import {
+ GlAvatar,
+ GlBadge,
+ GlButton,
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
+import { CI_RESOURCE_DETAILS_PAGE_NAME } from '../../router/constants';
+
+export default {
+ i18n: {
+ unreleased: s__('CiCatalog|Unreleased'),
+ releasedMessage: s__('CiCatalog|Released %{timeAgo} by %{author}'),
+ },
+ components: {
+ GlAvatar,
+ GlBadge,
+ GlButton,
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ resource: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ authorName() {
+ return this.latestVersion.author.name;
+ },
+ authorProfileUrl() {
+ return this.latestVersion.author.webUrl;
+ },
+ entityId() {
+ return getIdFromGraphQLId(this.resource.id);
+ },
+ starCount() {
+ return this.resource?.starCount || 0;
+ },
+ forksCount() {
+ return this.resource?.forksCount || 0;
+ },
+ hasReleasedVersion() {
+ return Boolean(this.latestVersion?.releasedAt);
+ },
+ formattedDate() {
+ return formatDate(this.latestVersion?.releasedAt);
+ },
+ latestVersion() {
+ return this.resource?.latestVersion || {};
+ },
+ releasedAt() {
+ return getTimeago().format(this.latestVersion?.releasedAt);
+ },
+ resourcePath() {
+ return `${this.resource.rootNamespace?.name} / ${this.resource.rootNamespace?.fullPath} / `;
+ },
+ tagName() {
+ return this.latestVersion?.tagName || this.$options.i18n.unreleased;
+ },
+ },
+ methods: {
+ navigateToDetailsPage() {
+ this.$router.push({
+ name: CI_RESOURCE_DETAILS_PAGE_NAME,
+ params: { id: this.entityId },
+ });
+ },
+ },
+};
+</script>
+<template>
+ <li
+ class="gl-display-flex gl-display-flex-wrap gl-border-b-1 gl-border-gray-100 gl-border-b-solid gl-text-gray-500 gl-py-3"
+ data-testid="catalog-resource-item"
+ >
+ <gl-avatar
+ class="gl-mr-4"
+ :entity-id="entityId"
+ :entity-name="resource.name"
+ shape="rect"
+ :size="48"
+ :src="resource.icon"
+ @click="navigateToDetailsPage"
+ />
+ <div class="gl-display-flex gl-flex-direction-column gl-flex-grow-1">
+ <div class="gl-display-flex gl-flex-wrap gl-gap-2 gl-mb-2">
+ <gl-button
+ variant="link"
+ class="gl-text-gray-900! gl-mr-1"
+ data-testid="ci-resource-link"
+ @click="navigateToDetailsPage"
+ >
+ {{ resourcePath }} <b> {{ resource.name }}</b>
+ </gl-button>
+ <div class="gl-display-flex gl-flex-grow-1 gl-md-justify-content-space-between">
+ <gl-badge size="sm">{{ tagName }}</gl-badge>
+ <span class="gl-display-flex gl-align-items-center gl-ml-5">
+ <span class="gl--flex-center" data-testid="stats-favorites">
+ <gl-icon name="star" :size="14" class="gl-mr-1" />
+ <span class="gl-mr-3">{{ starCount }}</span>
+ </span>
+ <span class="gl--flex-center" data-testid="stats-forks">
+ <gl-icon name="fork" :size="14" class="gl-mr-1" />
+ <span>{{ forksCount }}</span>
+ </span>
+ </span>
+ </div>
+ </div>
+ <div class="gl-display-flex gl-sm-flex-direction-column gl-justify-content-space-between">
+ <span class="gl-display-flex gl-flex-basis-two-thirds gl-font-sm">{{
+ resource.description
+ }}</span>
+ <div class="gl-display-flex gl-justify-content-end">
+ <span v-if="hasReleasedVersion">
+ <gl-sprintf :message="$options.i18n.releasedMessage">
+ <template #timeAgo>
+ <span v-gl-tooltip.bottom :title="formattedDate">
+ {{ releasedAt }}
+ </span>
+ </template>
+ <template #author>
+ <gl-link :href="authorProfileUrl" data-testid="user-link">
+ <span>{{ authorName }}</span>
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </div>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/empty_state.vue b/app/assets/javascripts/ci/catalog/components/list/empty_state.vue
new file mode 100644
index 00000000000..a53ddefaa50
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/empty_state.vue
@@ -0,0 +1,22 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ title: s__('CiCatalog|Get started with the CI/CD Catalog'),
+ description: s__(
+ 'CiCatalog|Create a pipeline component repository and make reusing pipeline configurations faster and easier.',
+ ),
+ },
+ name: 'CiCatalogEmptyState',
+ components: {
+ GlEmptyState,
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-empty-state :title="$options.i18n.title" :description="$options.i18n.description" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue b/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue
new file mode 100644
index 00000000000..da2c73be900
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue
@@ -0,0 +1,109 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { createAlert } from '~/alert';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { CI_CATALOG_RESOURCE_TYPE } from '../../graphql/settings';
+import getCatalogCiResourceDetails from '../../graphql/queries/get_ci_catalog_resource_details.query.graphql';
+import getCatalogCiResourceSharedData from '../../graphql/queries/get_ci_catalog_resource_shared_data.query.graphql';
+import CiResourceDetails from '../details/ci_resource_details.vue';
+import CiResourceHeader from '../details/ci_resource_header.vue';
+
+export default {
+ components: {
+ CiResourceDetails,
+ CiResourceHeader,
+ GlEmptyState,
+ },
+ inject: ['ciCatalogPath'],
+ data() {
+ return {
+ isEmpty: false,
+ resourceSharedData: {},
+ resourceAdditionalDetails: {},
+ };
+ },
+ apollo: {
+ resourceSharedData: {
+ query: getCatalogCiResourceSharedData,
+ variables() {
+ return {
+ id: this.graphQLId,
+ };
+ },
+ update(data) {
+ return data.ciCatalogResource;
+ },
+ error(e) {
+ this.isEmpty = true;
+ createAlert({ message: e.message });
+ },
+ },
+ resourceAdditionalDetails: {
+ query: getCatalogCiResourceDetails,
+ variables() {
+ return {
+ id: this.graphQLId,
+ };
+ },
+ update(data) {
+ return data.ciCatalogResource;
+ },
+ error(e) {
+ this.isEmpty = true;
+ createAlert({ message: e.message });
+ },
+ },
+ },
+ computed: {
+ graphQLId() {
+ return convertToGraphQLId(CI_CATALOG_RESOURCE_TYPE, this.$route.params.id);
+ },
+ isLoadingDetails() {
+ return this.$apollo.queries.resourceAdditionalDetails.loading;
+ },
+ isLoadingSharedData() {
+ return this.$apollo.queries.resourceSharedData.loading;
+ },
+ versions() {
+ return this.resourceAdditionalDetails?.versions?.nodes || [];
+ },
+ pipelineStatus() {
+ return (
+ this.resourceAdditionalDetails?.versions?.nodes[0]?.commit?.pipelines?.nodes[0]
+ ?.detailedStatus || null
+ );
+ },
+ },
+ i18n: {
+ emptyStateTitle: s__('CiCatalog|No component available'),
+ emptyStateDescription: s__(
+ 'CiCatalog|Component ID not found, or you do not have permission to access component.',
+ ),
+ emptyStateButtonText: s__('CiCatalog|Back to the CI/CD Catalog'),
+ },
+};
+</script>
+<template>
+ <div>
+ <div v-if="isEmpty" class="gl-display-flex">
+ <gl-empty-state
+ :title="$options.i18n.emptyStateTitle"
+ :description="$options.i18n.emptyStateDescription"
+ :primary-button-text="$options.i18n.emptyStateButtonText"
+ :primary-button-link="ciCatalogPath"
+ />
+ </div>
+ <div v-else>
+ <ci-resource-header
+ :open-issues-count="resourceAdditionalDetails.openIssuesCount"
+ :open-merge-requests-count="resourceAdditionalDetails.openMergeRequestsCount"
+ :is-loading-details="isLoadingDetails"
+ :is-loading-shared-data="isLoadingSharedData"
+ :pipeline-status="pipelineStatus"
+ :resource="resourceSharedData"
+ />
+ <ci-resource-details :resource-id="graphQLId" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/constants.js b/app/assets/javascripts/ci/catalog/constants.js
new file mode 100644
index 00000000000..ab067f991cd
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/constants.js
@@ -0,0 +1,35 @@
+// We disable this for the entire file until the mock data is cleanup
+/* eslint-disable @gitlab/require-i18n-strings */
+export const CATALOG_FEEDBACK_DISMISSED_KEY = 'catalog_feedback_dismissed';
+
+export const componentsMockData = {
+ __typename: 'CiComponentConnection',
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Component/1',
+ name: 'Ruby gal',
+ description: 'This is a pretty amazing component that does EVERYTHING ruby.',
+ path: 'gitlab.com/gitlab-org/ruby-gal@~latest',
+ inputs: { nodes: [{ name: 'version', defaultValue: '1.0.0', required: true }] },
+ },
+ {
+ id: 'gid://gitlab/Ci::Component/2',
+ name: 'Javascript madness',
+ description: 'Adds some spice to your life.',
+ path: 'gitlab.com/gitlab-org/javascript-madness@~latest',
+ inputs: {
+ nodes: [
+ { name: 'isFun', defaultValue: 'true', required: true },
+ { name: 'RandomNumber', defaultValue: '10', required: false },
+ ],
+ },
+ },
+ {
+ id: 'gid://gitlab/Ci::Component/3',
+ name: 'Go go go',
+ description: 'When you write Go, you gotta go go go.',
+ path: 'gitlab.com/gitlab-org/go-go-go@~latest',
+ inputs: { nodes: [{ name: 'version', defaultValue: '1.0.0', required: true }] },
+ },
+ ],
+};
diff --git a/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
new file mode 100644
index 00000000000..f4d1bb0eaaf
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
@@ -0,0 +1,25 @@
+fragment CatalogResourceFields on CiCatalogResource {
+ id
+ icon
+ name
+ description
+ starCount
+ forksCount
+ latestVersion {
+ id
+ tagName
+ tagPath
+ releasedAt
+ author {
+ id
+ name
+ webUrl
+ }
+ }
+ rootNamespace {
+ id
+ fullPath
+ name
+ }
+ webPath
+}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql
new file mode 100644
index 00000000000..6aef5dcc4e7
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql
@@ -0,0 +1,20 @@
+query getCiCatalogResourceComponents($id: CiCatalogResourceID!) {
+ ciCatalogResource(id: $id) {
+ id
+ components @client {
+ nodes {
+ id
+ name
+ description
+ path
+ inputs {
+ nodes {
+ name
+ defaultValue
+ required
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql
new file mode 100644
index 00000000000..382d3866795
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql
@@ -0,0 +1,29 @@
+query getCiCatalogResourceDetails($id: CiCatalogResourceID!) {
+ ciCatalogResource(id: $id) {
+ id
+ openIssuesCount
+ openMergeRequestsCount
+ versions(first: 1) {
+ nodes {
+ id
+ commit {
+ id
+ pipelines(first: 1) {
+ nodes {
+ id
+ detailedStatus {
+ id
+ detailsPath
+ icon
+ text
+ group
+ }
+ }
+ }
+ }
+ tagName
+ releasedAt
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql
new file mode 100644
index 00000000000..6b3d0cdcfc7
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql
@@ -0,0 +1,6 @@
+query getCiCatalogResourceReadme($id: CiCatalogResourceID!) {
+ ciCatalogResource(id: $id) {
+ id
+ readmeHtml
+ }
+}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql
new file mode 100644
index 00000000000..4ac4cb0e394
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql
@@ -0,0 +1,7 @@
+#import "../fragments/catalog_resource.fragment.graphql"
+
+query getCiCatalogResourceSharedData($id: CiCatalogResourceID!) {
+ ciCatalogResource(id: $id) {
+ ...CatalogResourceFields
+ }
+}
diff --git a/app/assets/javascripts/ci/catalog/graphql/settings.js b/app/assets/javascripts/ci/catalog/graphql/settings.js
new file mode 100644
index 00000000000..a87b26ca4fc
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/settings.js
@@ -0,0 +1,32 @@
+import { componentsMockData } from '../constants';
+
+export const ciCatalogResourcesItemsCount = 20;
+export const CI_CATALOG_RESOURCE_TYPE = 'Ci::Catalog::Resource';
+
+export const cacheConfig = {
+ cacheConfig: {
+ typePolicies: {
+ Query: {
+ fields: {
+ ciCatalogResource(_, { args, toReference }) {
+ return toReference({
+ __typename: 'CiCatalogResource',
+ id: args.id,
+ });
+ },
+ ciCatalogResources: {
+ keyArgs: false,
+ },
+ },
+ },
+ },
+ },
+};
+
+export const resolvers = {
+ CiCatalogResource: {
+ components() {
+ return componentsMockData;
+ },
+ },
+};
diff --git a/app/assets/javascripts/ci/catalog/router/constants.js b/app/assets/javascripts/ci/catalog/router/constants.js
new file mode 100644
index 00000000000..2d9462ef403
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/router/constants.js
@@ -0,0 +1,2 @@
+export const CI_RESOURCES_PAGE_NAME = 'ci_resources';
+export const CI_RESOURCE_DETAILS_PAGE_NAME = 'ci_resources_details';
diff --git a/app/assets/javascripts/ci/catalog/router/index.js b/app/assets/javascripts/ci/catalog/router/index.js
new file mode 100644
index 00000000000..0b2b3dd3aa3
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/router/index.js
@@ -0,0 +1,13 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import { createRoutes } from './routes';
+
+Vue.use(VueRouter);
+
+export const createRouter = (base, listComponent) => {
+ return new VueRouter({
+ base,
+ mode: 'history',
+ routes: createRoutes(listComponent),
+ });
+};
diff --git a/app/assets/javascripts/ci/catalog/router/routes.js b/app/assets/javascripts/ci/catalog/router/routes.js
new file mode 100644
index 00000000000..ccfb0673c83
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/router/routes.js
@@ -0,0 +1,9 @@
+import CiResourceDetailsPage from '../components/pages/ci_resource_details_page.vue';
+import { CI_RESOURCES_PAGE_NAME, CI_RESOURCE_DETAILS_PAGE_NAME } from './constants';
+
+export const createRoutes = (listComponent) => {
+ return [
+ { name: CI_RESOURCES_PAGE_NAME, path: '', component: listComponent },
+ { name: CI_RESOURCE_DETAILS_PAGE_NAME, path: '/:id', component: CiResourceDetailsPage },
+ ];
+};
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue
index a25f871ac92..77af643cbb3 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue
@@ -24,10 +24,6 @@ export default {
type: Array,
required: true,
},
- hasEnvScopeQuery: {
- type: Boolean,
- required: true,
- },
selectedEnvironmentScope: {
type: String,
required: false,
@@ -36,6 +32,7 @@ export default {
},
data() {
return {
+ customEnvScope: null,
isDropdownShown: false,
selectedEnvironment: '',
searchTerm: '',
@@ -45,42 +42,38 @@ export default {
composedCreateButtonLabel() {
return sprintf(__('Create wildcard: %{searchTerm}'), { searchTerm: this.searchTerm });
},
- filteredEnvironments() {
- const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
- return this.environments.filter((environment) => {
- return environment.toLowerCase().includes(lowerCasedSearchTerm);
- });
- },
isDropdownLoading() {
- return this.areEnvironmentsLoading && this.hasEnvScopeQuery && !this.isDropdownShown;
+ return this.areEnvironmentsLoading && !this.isDropdownShown;
},
isDropdownSearching() {
- return this.areEnvironmentsLoading && this.hasEnvScopeQuery && this.isDropdownShown;
+ return this.areEnvironmentsLoading && this.isDropdownShown;
},
searchedEnvironments() {
- // If hasEnvScopeQuery (applies only to projects for now), search query will be fired so this
- // component will already receive filtered environments during the refetch.
- // Otherwise (applies to groups), search the existing list of environments in the frontend
- let filtered = this.hasEnvScopeQuery ? this.environments : this.filteredEnvironments;
+ let filtered = this.environments;
// If there is no search term, make sure to include *
- if (this.hasEnvScopeQuery && !this.searchTerm) {
+ if (!this.searchTerm) {
filtered = uniq([...filtered, '*']);
}
+ // add custom env scope if it matches the search term
+ if (this.customEnvScope && this.customEnvScope.startsWith(this.searchTerm)) {
+ filtered = uniq([...filtered, this.customEnvScope]);
+ }
+
return filtered.sort().map((environment) => ({
value: environment,
text: environment,
}));
},
shouldRenderCreateButton() {
- return this.searchTerm && !this.environments.includes(this.searchTerm);
- },
- shouldRenderDivider() {
return (
- (this.hasEnvScopeQuery || this.shouldRenderCreateButton) && !this.areEnvironmentsLoading
+ this.searchTerm && ![...this.environments, this.customEnvScope].includes(this.searchTerm)
);
},
+ shouldRenderDivider() {
+ return !this.areEnvironmentsLoading;
+ },
environmentScopeLabel() {
return convertEnvironmentScope(this.selectedEnvironmentScope);
},
@@ -89,16 +82,14 @@ export default {
debouncedSearch: debounce(function debouncedSearch(searchTerm) {
const newSearchTerm = searchTerm.trim();
this.searchTerm = newSearchTerm;
- if (this.hasEnvScopeQuery) {
- this.$emit('search-environment-scope', newSearchTerm);
- }
+ this.$emit('search-environment-scope', newSearchTerm);
}, 500),
selectEnvironment(selected) {
this.$emit('select-environment', selected);
this.selectedEnvironment = selected;
},
createEnvironmentScope() {
- this.$emit('create-environment-scope', this.searchTerm);
+ this.customEnvScope = this.searchTerm;
this.selectEnvironment(this.searchTerm);
},
toggleDropdownShown(isShown) {
@@ -129,7 +120,7 @@ export default {
>
<template #footer>
<gl-dropdown-divider v-if="shouldRenderDivider" />
- <div v-if="hasEnvScopeQuery" data-testid="max-envs-notice">
+ <div data-testid="max-envs-notice">
<gl-dropdown-item class="gl-list-style-none" disabled>
<gl-sprintf :message="$options.i18n.maxEnvsNote" class="gl-font-sm">
<template #limit>
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
index c609e05bbb7..a32c5f476fb 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
@@ -11,9 +11,11 @@ import {
GlFormTextarea,
GlIcon,
GlLink,
+ GlModal,
+ GlModalDirective,
GlSprintf,
} from '@gitlab/ui';
-import { __, s__ } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -36,10 +38,11 @@ import { awsTokenList } from './ci_variable_autocomplete_tokens';
const trackingMixin = Tracking.mixin({ label: DRAWER_EVENT_LABEL });
export const i18n = {
- addVariable: s__('CiVariables|Add Variable'),
+ addVariable: s__('CiVariables|Add variable'),
cancel: __('Cancel'),
defaultScope: allEnvironments.text,
- editVariable: s__('CiVariables|Edit Variable'),
+ deleteVariable: s__('CiVariables|Delete variable'),
+ editVariable: s__('CiVariables|Edit variable'),
environments: __('Environments'),
environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
expandedField: s__('CiVariables|Expand variable reference'),
@@ -51,6 +54,7 @@ export const i18n = {
maskedDescription: s__(
'CiVariables|Variable will be masked in job logs. Requires values to meet regular expression requirements.',
),
+ modalDeleteMessage: s__('CiVariables|Do you want to delete the variable %{key}?'),
protectedField: s__('CiVariables|Protect variable'),
protectedDescription: s__(
'CiVariables|Export variable to pipelines running on protected branches and tags only.',
@@ -86,8 +90,12 @@ export default {
GlFormTextarea,
GlIcon,
GlLink,
+ GlModal,
GlSprintf,
},
+ directives: {
+ GlModalDirective,
+ },
mixins: [trackingMixin],
inject: ['environmentScopeLink', 'isProtectedByDefault', 'maskableRawRegex', 'maskableRegex'],
props: {
@@ -170,6 +178,9 @@ export default {
modalActionText() {
return this.isEditing ? this.$options.i18n.editVariable : this.$options.i18n.addVariable;
},
+ removeVariableMessage() {
+ return sprintf(this.$options.i18n.modalDeleteMessage, { key: this.variable.key });
+ },
},
watch: {
variable: {
@@ -188,6 +199,13 @@ export default {
close() {
this.$emit('close-form');
},
+ deleteVariable() {
+ this.$emit('delete-variable', this.variable);
+ this.close();
+ },
+ setEnvironmentScope(scope) {
+ this.variable = { ...this.variable, environmentScope: scope };
+ },
getTrackingErrorProperty() {
if (this.isValueEmpty) {
return null;
@@ -225,164 +243,206 @@ export default {
}),
i18n,
variableOptions,
+ deleteModal: {
+ actionPrimary: {
+ text: __('Delete'),
+ attributes: {
+ variant: 'danger',
+ },
+ },
+ actionSecondary: {
+ text: __('Cancel'),
+ attributes: {
+ variant: 'default',
+ },
+ },
+ },
};
</script>
<template>
- <gl-drawer
- open
- data-testid="ci-variable-drawer"
- :header-height="getDrawerHeaderHeight"
- :z-index="$options.DRAWER_Z_INDEX"
- @close="close"
- >
- <template #title>
- <h2 class="gl-m-0">{{ modalActionText }}</h2>
- </template>
- <gl-form-group
- :label="$options.i18n.type"
- label-for="ci-variable-type"
- class="gl-border-none"
- :class="{
- 'gl-mb-n5': !hideEnvironmentScope,
- 'gl-mb-n1': hideEnvironmentScope,
- }"
+ <div>
+ <gl-drawer
+ open
+ data-testid="ci-variable-drawer"
+ :header-height="getDrawerHeaderHeight"
+ :z-index="$options.DRAWER_Z_INDEX"
+ @close="close"
>
- <gl-form-select
- id="ci-variable-type"
- v-model="variable.variableType"
- :options="$options.variableOptions"
- />
- </gl-form-group>
- <gl-form-group
- v-if="!hideEnvironmentScope"
- class="gl-border-none gl-mb-n5"
- label-for="ci-variable-env"
- data-testid="environment-scope"
- >
- <template #label>
- <div class="gl-display-flex gl-align-items-center">
- <span class="gl-mr-2">
- {{ $options.i18n.environments }}
- </span>
- <gl-link
- class="gl-display-flex"
- :title="$options.i18n.environmentScopeLinkTitle"
- :href="environmentScopeLink"
- target="_blank"
- data-testid="environment-scope-link"
- >
- <gl-icon name="question-o" :size="14" />
- </gl-link>
- </div>
+ <template #title>
+ <h2 class="gl-m-0">{{ modalActionText }}</h2>
</template>
- <ci-environments-dropdown
- v-if="areScopedVariablesAvailable"
- class="gl-mb-5"
- has-env-scope-query
- :are-environments-loading="areEnvironmentsLoading"
- :environments="environments"
- :selected-environment-scope="variable.environmentScope"
- />
- <gl-form-input
- v-else
- :value="$options.i18n.defaultScope"
- class="gl-w-full gl-mb-5"
- readonly
+ <gl-form-group
+ :label="$options.i18n.type"
+ label-for="ci-variable-type"
+ class="gl-border-none"
+ :class="{
+ 'gl-mb-n5': !hideEnvironmentScope,
+ 'gl-mb-n1': hideEnvironmentScope,
+ }"
+ >
+ <gl-form-select
+ id="ci-variable-type"
+ v-model="variable.variableType"
+ :options="$options.variableOptions"
+ />
+ </gl-form-group>
+ <gl-form-group
+ v-if="!hideEnvironmentScope"
+ class="gl-border-none gl-mb-n5"
+ label-for="ci-variable-env"
+ data-testid="environment-scope"
+ >
+ <template #label>
+ <div class="gl-display-flex gl-align-items-center">
+ <span class="gl-mr-2">
+ {{ $options.i18n.environments }}
+ </span>
+ <gl-link
+ class="gl-display-flex"
+ :title="$options.i18n.environmentScopeLinkTitle"
+ :href="environmentScopeLink"
+ target="_blank"
+ data-testid="environment-scope-link"
+ >
+ <gl-icon name="question-o" :size="14" />
+ </gl-link>
+ </div>
+ </template>
+ <ci-environments-dropdown
+ v-if="areScopedVariablesAvailable"
+ class="gl-mb-5"
+ :are-environments-loading="areEnvironmentsLoading"
+ :environments="environments"
+ :selected-environment-scope="variable.environmentScope"
+ @select-environment="setEnvironmentScope"
+ @search-environment-scope="$emit('search-environment-scope', $event)"
+ />
+ <gl-form-input
+ v-else
+ :value="$options.i18n.defaultScope"
+ class="gl-w-full gl-mb-5"
+ readonly
+ />
+ </gl-form-group>
+ <gl-form-group class="gl-border-none gl-mb-n8">
+ <template #label>
+ <div class="gl-display-flex gl-align-items-center gl-mb-n3">
+ <span class="gl-mr-2">
+ {{ $options.i18n.flags }}
+ </span>
+ <gl-link
+ class="gl-display-flex"
+ :title="$options.i18n.flagsLinkTitle"
+ :href="$options.flagLink"
+ target="_blank"
+ >
+ <gl-icon name="question-o" :size="14" />
+ </gl-link>
+ </div>
+ </template>
+ <gl-form-checkbox v-model="variable.protected" data-testid="ci-variable-protected-checkbox">
+ {{ $options.i18n.protectedField }}
+ <p class="gl-text-secondary">
+ {{ $options.i18n.protectedDescription }}
+ </p>
+ </gl-form-checkbox>
+ <gl-form-checkbox v-model="variable.masked" data-testid="ci-variable-masked-checkbox">
+ {{ $options.i18n.maskedField }}
+ <p class="gl-text-secondary">{{ $options.i18n.maskedDescription }}</p>
+ </gl-form-checkbox>
+ <gl-form-checkbox
+ data-testid="ci-variable-expanded-checkbox"
+ :checked="isExpanded"
+ @change="setRaw"
+ >
+ {{ $options.i18n.expandedField }}
+ <p class="gl-text-secondary">
+ <gl-sprintf :message="$options.i18n.expandedDescription" class="gl-text-secondary">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-form-checkbox>
+ </gl-form-group>
+ <gl-form-combobox
+ v-model="variable.key"
+ :token-list="$options.awsTokenList"
+ :label-text="$options.i18n.key"
+ class="gl-border-none gl-pb-0! gl-mb-n5"
+ data-testid="ci-variable-key"
+ data-qa-selector="ci_variable_key_field"
/>
- </gl-form-group>
- <gl-form-group class="gl-border-none gl-mb-n8">
- <template #label>
- <div class="gl-display-flex gl-align-items-center gl-mb-n3">
- <span class="gl-mr-2">
- {{ $options.i18n.flags }}
- </span>
- <gl-link
- class="gl-display-flex"
- :title="$options.i18n.flagsLinkTitle"
- :href="$options.flagLink"
- target="_blank"
- >
- <gl-icon name="question-o" :size="14" />
- </gl-link>
- </div>
- </template>
- <gl-form-checkbox v-model="variable.protected" data-testid="ci-variable-protected-checkbox">
- {{ $options.i18n.protectedField }}
- <p class="gl-text-secondary">
- {{ $options.i18n.protectedDescription }}
- </p>
- </gl-form-checkbox>
- <gl-form-checkbox v-model="variable.masked" data-testid="ci-variable-masked-checkbox">
- {{ $options.i18n.maskedField }}
- <p class="gl-text-secondary">{{ $options.i18n.maskedDescription }}</p>
- </gl-form-checkbox>
- <gl-form-checkbox
- data-testid="ci-variable-expanded-checkbox"
- :checked="isExpanded"
- @change="setRaw"
+ <gl-form-group
+ :label="$options.i18n.value"
+ label-for="ci-variable-value"
+ class="gl-border-none gl-mb-n2"
+ data-testid="ci-variable-value-label"
+ :invalid-feedback="maskedReqsNotMetText"
+ :state="isValueValid"
>
- {{ $options.i18n.expandedField }}
- <p class="gl-text-secondary">
- <gl-sprintf :message="$options.i18n.expandedDescription" class="gl-text-secondary">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- </gl-sprintf>
+ <gl-form-textarea
+ id="ci-variable-value"
+ v-model="variable.value"
+ class="gl-border-none gl-font-monospace!"
+ rows="3"
+ max-rows="10"
+ data-testid="ci-variable-value"
+ data-qa-selector="ci_variable_value_field"
+ spellcheck="false"
+ />
+ <p
+ v-if="variable.raw"
+ class="gl-mt-2 gl-mb-0 text-secondary"
+ data-testid="raw-variable-tip"
+ >
+ {{ $options.i18n.valueFeedback.rawHelpText }}
</p>
- </gl-form-checkbox>
- </gl-form-group>
- <gl-form-combobox
- v-model="variable.key"
- :token-list="$options.awsTokenList"
- :label-text="$options.i18n.key"
- class="gl-border-none gl-pb-0! gl-mb-n5"
- data-testid="ci-variable-key"
- data-qa-selector="ci_variable_key_field"
- />
- <gl-form-group
- :label="$options.i18n.value"
- label-for="ci-variable-value"
- class="gl-border-none gl-mb-n2"
- data-testid="ci-variable-value-label"
- :invalid-feedback="maskedReqsNotMetText"
- :state="isValueValid"
- >
- <gl-form-textarea
- id="ci-variable-value"
- v-model="variable.value"
- class="gl-border-none gl-font-monospace!"
- rows="3"
- max-rows="10"
- data-testid="ci-variable-value"
- data-qa-selector="ci_variable_value_field"
- spellcheck="false"
- />
- <p v-if="variable.raw" class="gl-mt-2 gl-mb-0 text-secondary" data-testid="raw-variable-tip">
- {{ $options.i18n.valueFeedback.rawHelpText }}
- </p>
- </gl-form-group>
- <gl-alert
- v-if="hasVariableReference"
- :title="$options.i18n.variableReferenceTitle"
- :dismissible="false"
- variant="warning"
- class="gl-mx-4 gl-pl-9! gl-border-bottom-0"
- data-testid="has-variable-reference-alert"
+ </gl-form-group>
+ <gl-alert
+ v-if="hasVariableReference"
+ :title="$options.i18n.variableReferenceTitle"
+ :dismissible="false"
+ variant="warning"
+ class="gl-mx-4 gl-pl-9! gl-border-bottom-0"
+ data-testid="has-variable-reference-alert"
+ >
+ {{ $options.i18n.variableReferenceDescription }}
+ </gl-alert>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button category="secondary" class="gl-mr-3" data-testid="cancel-button" @click="close"
+ >{{ $options.i18n.cancel }}
+ </gl-button>
+ <gl-button
+ v-if="isEditing"
+ v-gl-modal-directive="`delete-variable-${variable.key}`"
+ variant="danger"
+ category="secondary"
+ class="gl-mr-3"
+ data-testid="ci-variable-delete-btn"
+ >{{ $options.i18n.deleteVariable }}</gl-button
+ >
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :disabled="!canSubmit"
+ data-testid="ci-variable-confirm-btn"
+ data-qa-selector="ci_variable_save_button"
+ @click="submit"
+ >{{ modalActionText }}
+ </gl-button>
+ </div>
+ </gl-drawer>
+ <gl-modal
+ ref="modal"
+ :modal-id="`delete-variable-${variable.key}`"
+ :title="$options.i18n.deleteVariable"
+ :action-primary="$options.deleteModal.actionPrimary"
+ :action-secondary="$options.deleteModal.actionSecondary"
+ data-testid="ci-variable-drawer-confirm-delete-modal"
+ @primary="deleteVariable"
>
- {{ $options.i18n.variableReferenceDescription }}
- </gl-alert>
- <div class="gl-display-flex gl-justify-content-end">
- <gl-button category="secondary" class="gl-mr-3" data-testid="cancel-button" @click="close"
- >{{ $options.i18n.cancel }}
- </gl-button>
- <gl-button
- category="primary"
- variant="confirm"
- :disabled="!canSubmit"
- data-testid="ci-variable-confirm-btn"
- @click="submit"
- >{{ modalActionText }}
- </gl-button>
- </div>
- </gl-drawer>
+ {{ removeVariableMessage }}
+ </gl-modal>
+ </div>
</template>
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
index 86c0f34215e..cc664d76267 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
@@ -38,7 +38,6 @@ import {
VARIABLE_ACTIONS,
variableOptions,
} from '../constants';
-import { createJoinedEnvironments } from '../utils';
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
@@ -90,10 +89,6 @@ export default {
required: false,
default: false,
},
- hasEnvScopeQuery: {
- type: Boolean,
- required: true,
- },
mode: {
type: String,
required: true,
@@ -147,13 +142,6 @@ export default {
isTipVisible() {
return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
},
- environmentsList() {
- if (this.hasEnvScopeQuery) {
- return this.environments;
- }
-
- return createJoinedEnvironments(this.variables, this.environments, this.newEnvironments);
- },
maskedFeedback() {
return this.displayMaskedError
? __('This variable value does not meet the masking requirements.')
@@ -211,9 +199,6 @@ export default {
addVariable() {
this.$emit('add-variable', this.variable);
},
- createEnvironmentScope(env) {
- this.newEnvironments.push(env);
- },
deleteVariable() {
this.$emit('delete-variable', this.variable);
},
@@ -407,11 +392,9 @@ export default {
<ci-environments-dropdown
v-if="areScopedVariablesAvailable"
:are-environments-loading="areEnvironmentsLoading"
- :has-env-scope-query="hasEnvScopeQuery"
:selected-environment-scope="variable.environmentScope"
- :environments="environmentsList"
+ :environments="environments"
@select-environment="setEnvironmentScope"
- @create-environment-scope="createEnvironmentScope"
@search-environment-scope="$emit('search-environment-scope', $event)"
/>
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
index 482f6da5617..f2d81b3f271 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
@@ -37,10 +37,6 @@ export default {
required: false,
default: false,
},
- hasEnvScopeQuery: {
- type: Boolean,
- required: true,
- },
isLoading: {
type: Boolean,
required: false,
@@ -125,7 +121,6 @@ export default {
:are-environments-loading="areEnvironmentsLoading"
:are-scoped-variables-available="areScopedVariablesAvailable"
:environments="environments"
- :has-env-scope-query="hasEnvScopeQuery"
:hide-environment-scope="hideEnvironmentScope"
:variables="variables"
:mode="mode"
@@ -144,8 +139,11 @@ export default {
:hide-environment-scope="hideEnvironmentScope"
:selected-variable="selectedVariable"
:mode="mode"
- v-on="$listeners"
+ @add-variable="addVariable"
+ @delete-variable="deleteVariable"
@close-form="closeForm"
+ @update-variable="updateVariable"
+ @search-environment-scope="$emit('search-environment-scope', $event)"
/>
</div>
</div>
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
index 3d5ed327dc7..011a424b6c2 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
@@ -2,7 +2,7 @@
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { reportMessageToSentry } from '~/ci/utils';
+import { reportToSentry } from '~/ci/utils';
import { mapEnvironmentNames } from '../utils';
import {
ADD_MUTATION_ACTION,
@@ -140,7 +140,7 @@ export default {
this.loadingCounter += 1;
} else {
createAlert({ message: this.$options.tooManyCallsError });
- reportMessageToSentry(this.componentName, this.$options.tooManyCallsError, {});
+ reportToSentry(this.componentName, new Error(this.$options.tooManyCallsError));
}
}
},
@@ -285,7 +285,6 @@ export default {
:are-scoped-variables-available="areScopedVariablesAvailable"
:entity="entity"
:environments="environments"
- :has-env-scope-query="hasEnvScopeQuery"
:hide-environment-scope="hideEnvironmentScope"
:is-loading="isLoading"
:max-variable-limit="maxVariableLimit"
diff --git a/app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql
index a28ca4eebc9..f243a1cb30b 100644
--- a/app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql
@@ -1,5 +1,4 @@
fragment BaseCiVariable on CiVariable {
- __typename
id
key
value
diff --git a/app/assets/javascripts/ci/ci_variable_list/utils.js b/app/assets/javascripts/ci/ci_variable_list/utils.js
index 1faa97a5f73..a7e020206ea 100644
--- a/app/assets/javascripts/ci/ci_variable_list/utils.js
+++ b/app/assets/javascripts/ci/ci_variable_list/utils.js
@@ -1,29 +1,6 @@
-import { uniq } from 'lodash';
import { allEnvironments } from './constants';
/**
- * This function takes a list of variable, environments and
- * new environments added through the scope dropdown
- * and create a new Array that concatenate the environment list
- * with the environment scopes find in the variable list. This is
- * useful for variable settings so that we can render a list of all
- * environment scopes available based on the list of envs, the ones the user
- * added explictly and what is found under each variable.
- * @param {Array} variables
- * @param {Array} environments
- * @returns {Array} - Array of environments
- */
-
-export const createJoinedEnvironments = (
- variables = [],
- environments = [],
- newEnvironments = [],
-) => {
- const scopesFromVariables = variables.map((variable) => variable.environmentScope);
- return uniq([...environments, ...newEnvironments, ...scopesFromVariables]).sort();
-};
-
-/**
* This function job is to convert the * wildcard to text when applicable
* in the UI. It uses a constants to compare the incoming value to that
* of the * and then apply the corresponding label if applicable. If there
diff --git a/app/assets/javascripts/ci/common/pipelines_table.vue b/app/assets/javascripts/ci/common/pipelines_table.vue
index 807128d2341..13b5120654a 100644
--- a/app/assets/javascripts/ci/common/pipelines_table.vue
+++ b/app/assets/javascripts/ci/common/pipelines_table.vue
@@ -3,39 +3,52 @@ import { GlTableLite, GlTooltipDirective } from '@gitlab/ui';
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { TRACKING_CATEGORIES } from '~/ci/constants';
+import { PIPELINE_ID_KEY, PIPELINE_IID_KEY, TRACKING_CATEGORIES } from '~/ci/constants';
import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
import PipelineFailedJobsWidget from '~/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue';
-import eventHub from '~/ci/event_hub';
import PipelineOperations from '../pipelines_page/components/pipeline_operations.vue';
-import PipelineStopModal from '../pipelines_page/components/pipeline_stop_modal.vue';
import PipelineTriggerer from '../pipelines_page/components/pipeline_triggerer.vue';
import PipelineUrl from '../pipelines_page/components/pipeline_url.vue';
-import PipelinesStatusBadge from '../pipelines_page/components/pipelines_status_badge.vue';
+import PipelineStatusBadge from '../pipelines_page/components/pipeline_status_badge.vue';
const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!';
const DEFAULT_TH_CLASSES =
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!';
+/**
+ * Pipelines Table
+ *
+ * Presentational component of a table of pipelines. This component does not
+ * fetch the list of pipelines and instead expects it as a prop.
+ * GraphQL actions for pipelines, such as retrying, canceling, etc.
+ * are handled within this component.
+ *
+ * Use this `legacy_pipelines_table_wrapper` if you need a fully functional REST component.
+ *
+ * IMPORTANT: When using this component, make sure to handle the following events:
+ * 1- @refresh-pipeline-table
+ * 2- @cancel-pipeline
+ * 3- @retry-pipeline
+ *
+ */
+
export default {
components: {
GlTableLite,
LegacyPipelineMiniGraph,
PipelineFailedJobsWidget,
PipelineOperations,
- PipelinesStatusBadge,
- PipelineStopModal,
+ PipelineStatusBadge,
PipelineTriggerer,
PipelineUrl,
},
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [Tracking.mixin(), glFeatureFlagMixin()],
+ mixins: [Tracking.mixin()],
inject: {
- withFailedJobsDetails: {
+ useFailedJobsWidget: {
default: false,
},
},
@@ -44,37 +57,21 @@ export default {
type: Array,
required: true,
},
- pipelineScheduleUrl: {
- type: String,
- required: false,
- default: '',
- },
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
- viewType: {
+ pipelineIdType: {
type: String,
- required: true,
- },
- pipelineKeyOption: {
- type: Object,
- required: true,
+ required: false,
+ default: PIPELINE_ID_KEY,
+ validator(value) {
+ return value === PIPELINE_IID_KEY || value === PIPELINE_ID_KEY;
+ },
},
},
- data() {
- return {
- pipelineId: 0,
- pipeline: {},
- endpoint: '',
- cancelingPipeline: null,
- };
- },
computed: {
- showFailedJobsWidget() {
- return this.glFeatures.ciJobFailuresInMr;
- },
tableFields() {
return [
{
@@ -119,10 +116,10 @@ export default {
];
},
tdClasses() {
- return this.withFailedJobsDetails ? 'gl-pb-0! gl-border-none!' : 'pl-p-5!';
+ return this.useFailedJobsWidget ? 'gl-pb-0! gl-border-none!' : 'pl-p-5!';
},
pipelinesWithDetails() {
- if (this.withFailedJobsDetails) {
+ if (this.useFailedJobsWidget) {
return this.pipelines.map((p) => {
return { ...p, _showDetails: true };
});
@@ -131,17 +128,6 @@ export default {
return this.pipelines;
},
},
- watch: {
- pipelines() {
- this.cancelingPipeline = null;
- },
- },
- created() {
- eventHub.$on('openConfirmationModal', this.setModalData);
- },
- beforeDestroy() {
- eventHub.$off('openConfirmationModal', this.setModalData);
- },
methods: {
getDownstreamPipelines(pipeline) {
const downstream = pipeline.triggered;
@@ -151,16 +137,19 @@ export default {
return cleanLeadingSeparator(item.project.full_path);
},
failedJobsCount(pipeline) {
- return pipeline?.failed_builds?.length || 0;
+ // Remove `pipeline?.failed_builds?.length` when we remove `ci_fix_performance_pipelines_json_endpoint`.
+ return pipeline?.failed_builds_count || pipeline?.failed_builds?.length || 0;
},
- setModalData(data) {
- this.pipelineId = data.pipeline.id;
- this.pipeline = data.pipeline;
- this.endpoint = data.endpoint;
+ onRefreshPipelinesTable() {
+ this.$emit('refresh-pipelines-table');
},
- onSubmit() {
- eventHub.$emit('postAction', this.endpoint);
- this.cancelingPipeline = this.pipelineId;
+ onRetryPipeline(pipeline) {
+ // This emit is only used by the `legacy_pipelines_table_wrapper`.
+ this.$emit('retry-pipeline', pipeline);
+ },
+ onCancelPipeline(pipeline) {
+ // This emit is only used by the `legacy_pipelines_table_wrapper`.
+ this.$emit('cancel-pipeline', pipeline);
},
trackPipelineMiniGraph() {
this.track('click_minigraph', { label: TRACKING_CATEGORIES.table });
@@ -168,7 +157,6 @@ export default {
},
TBODY_TR_ATTR: {
'data-testid': 'pipeline-table-row',
- 'data-qa-selector': 'pipeline_row_container',
},
};
</script>
@@ -191,14 +179,13 @@ export default {
</template>
<template #cell(status)="{ item }">
- <pipelines-status-badge :pipeline="item" :view-type="viewType" />
+ <pipeline-status-badge :pipeline="item" />
</template>
<template #cell(pipeline)="{ item }">
<pipeline-url
:pipeline="item"
- :pipeline-schedule-url="pipelineScheduleUrl"
- :pipeline-key="pipelineKeyOption.value"
+ :pipeline-id-type="pipelineIdType"
ref-color="gl-text-black-normal"
/>
</template>
@@ -219,12 +206,17 @@ export default {
</template>
<template #cell(actions)="{ item }">
- <pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" />
+ <pipeline-operations
+ :pipeline="item"
+ @cancel-pipeline="onCancelPipeline"
+ @refresh-pipelines-table="onRefreshPipelinesTable"
+ @retry-pipeline="onRetryPipeline"
+ />
</template>
<template #row-details="{ item }">
<pipeline-failed-jobs-widget
- v-if="showFailedJobsWidget"
+ v-if="useFailedJobsWidget"
:failed-jobs-count="failedJobsCount(item)"
:is-pipeline-active="item.active"
:pipeline-iid="item.iid"
@@ -234,7 +226,5 @@ export default {
/>
</template>
</gl-table-lite>
-
- <pipeline-stop-modal :pipeline="pipeline" @submit="onSubmit" />
</div>
</template>
diff --git a/app/assets/javascripts/ci/common/private/job_action_component.vue b/app/assets/javascripts/ci/common/private/job_action_component.vue
index f649750ce8a..b0fa724d450 100644
--- a/app/assets/javascripts/ci/common/private/job_action_component.vue
+++ b/app/assets/javascripts/ci/common/private/job_action_component.vue
@@ -120,7 +120,7 @@ export default {
:class="cssClass"
:disabled="isDisabled"
class="js-ci-action gl-ci-action-icon-container ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center"
- data-testid="ci-action-component"
+ data-testid="ci-action-button"
@click.stop="onClickAction"
>
<div
diff --git a/app/assets/javascripts/ci/constants.js b/app/assets/javascripts/ci/constants.js
index 93c2504dd5d..5b60528f521 100644
--- a/app/assets/javascripts/ci/constants.js
+++ b/app/assets/javascripts/ci/constants.js
@@ -24,19 +24,8 @@ export const SUCCESS_STATUS = 'SUCCESS';
export const PASSED_STATUS = 'passed';
export const MANUAL_STATUS = 'manual';
-// Constants for the ID and IID selection dropdown
-export const PipelineKeyOptions = [
- {
- text: __('Show Pipeline ID'),
- label: __('Pipeline ID'),
- value: 'id',
- },
- {
- text: __('Show Pipeline IID'),
- label: __('Pipeline IID'),
- value: 'iid',
- },
-];
+export const PIPELINE_ID_KEY = 'id';
+export const PIPELINE_IID_KEY = 'iid';
export const RAW_TEXT_WARNING = s__(
'Pipeline|Raw text search is not currently supported. Please use the available search tokens.',
diff --git a/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue b/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue
index f02d59af1d9..0b079ccb64f 100644
--- a/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue
+++ b/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue
@@ -3,7 +3,7 @@ import { produce } from 'immer';
import { s__ } from '~/locale';
import { createAlert } from '~/alert';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { reportMessageToSentry } from '~/ci/utils';
+import { reportToSentry } from '~/ci/utils';
import CiVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue';
import getInheritedCiVariables from '../graphql/queries/inherited_ci_variables.query.graphql';
@@ -51,7 +51,7 @@ export default {
this.loadingCounter += 1;
} else {
createAlert({ message: this.$options.i18n.tooManyCallsError });
- reportMessageToSentry(this.$options.name, this.$options.i18n.tooManyCallsError, {});
+ reportToSentry(this.$options.name, new Error(this.$options.i18n.tooManyCallsError));
}
},
error() {
diff --git a/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql b/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql
index b25768632e1..9fac461a47d 100644
--- a/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql
+++ b/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql
@@ -8,7 +8,6 @@ query getInheritedCiVariables($after: String, $first: Int, $fullPath: ID!) {
...PageInfo
}
nodes {
- __typename
id
key
variableType
diff --git a/app/assets/javascripts/ci/job_details/components/job_header.vue b/app/assets/javascripts/ci/job_details/components/job_header.vue
index 13f3eebd447..00d15f87064 100644
--- a/app/assets/javascripts/ci/job_details/components/job_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/job_header.vue
@@ -89,18 +89,36 @@ export default {
<template>
<header
- class="page-content-header gl-md-display-flex gl-min-h-7"
+ class="page-content-header gl-md-display-flex gl-flex-wrap gl-min-h-7 gl-pb-2! gl-w-full"
data-testid="job-header-content"
>
- <section class="header-main-content gl-mr-3">
- <ci-badge-link class="gl-mr-3" :status="status" />
+ <div
+ v-if="name"
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-w-full"
+ >
+ <h1 class="gl-font-size-h-display gl-my-0 gl-display-inline-block" data-testid="job-name">
+ {{ name }}
+ </h1>
- <strong data-testid="job-name">{{ name }}</strong>
+ <div class="gl-display-flex gl-align-self-start gl-mt-n2">
+ <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right">
+ <gl-button
+ :aria-label="__('Toggle sidebar')"
+ category="secondary"
+ class="gl-lg-display-none gl-ml-2"
+ icon="chevron-double-lg-left"
+ @click="onClickSidebarButton"
+ />
+ </div>
+ </div>
+ </div>
+ <section class="header-main-content gl-display-flex gl-align-items-center gl-mr-3">
+ <ci-badge-link class="gl-mr-3" :status="status" />
- <template v-if="shouldRenderTriggeredLabel">{{ __('started') }}</template>
- <template v-else>{{ __('created') }}</template>
+ <template v-if="shouldRenderTriggeredLabel">{{ __('Started') }}</template>
+ <template v-else>{{ __('Created') }}</template>
- <timeago-tooltip :time="time" />
+ <timeago-tooltip :time="time" class="gl-mx-2" />
{{ __('by') }}
@@ -133,16 +151,5 @@ export default {
</gl-avatar-link>
</template>
</section>
-
- <!-- eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots -->
- <section v-if="$slots.default" data-testid="job-header-action-buttons" class="gl-display-flex">
- <slot></slot>
- </section>
- <gl-button
- class="gl-md-display-none gl-ml-auto gl-align-self-start js-sidebar-build-toggle"
- icon="chevron-double-lg-left"
- :aria-label="__('Toggle sidebar')"
- @click="onClickSidebarButton"
- />
</header>
</template>
diff --git a/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue b/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue
index 419efcba46d..4a30878bec5 100644
--- a/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue
+++ b/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue
@@ -146,7 +146,7 @@ export default {
// BE returns zero based index, we need to add one to match the line numbers in the DOM
const firstSearchResult = `#L${this.searchResults[0].lineNumber + 1}`;
- const logLine = document.querySelector(`.log-line ${firstSearchResult}`);
+ const logLine = document.querySelector(`.js-log-line ${firstSearchResult}`);
if (logLine) {
setTimeout(() => scrollToElement(logLine));
diff --git a/app/assets/javascripts/ci/job_details/components/log/line.vue b/app/assets/javascripts/ci/job_details/components/log/line.vue
index fa4a12b3dd3..416f75372f9 100644
--- a/app/assets/javascripts/ci/job_details/components/log/line.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line.vue
@@ -56,7 +56,7 @@ export default {
if (window.location.hash) {
const hash = getLocationHash();
- const lineToMatch = `L${line.lineNumber + 1}`;
+ const lineToMatch = `L${line.lineNumber}`;
if (hash === lineToMatch) {
applyHashHighlight = true;
@@ -66,7 +66,11 @@ export default {
return h(
'div',
{
- class: ['js-line', 'log-line', { 'gl-bg-gray-700': isHighlighted || applyHashHighlight }],
+ class: [
+ 'js-log-line',
+ 'log-line',
+ { 'gl-bg-gray-700': isHighlighted || applyHashHighlight },
+ ],
},
[
h(LineNumber, {
diff --git a/app/assets/javascripts/ci/job_details/components/log/line_header.vue b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
index e647ab4ac0b..658a94e6af4 100644
--- a/app/assets/javascripts/ci/job_details/components/log/line_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
@@ -46,7 +46,7 @@ export default {
},
mounted() {
const hash = getLocationHash();
- const lineToMatch = `L${this.line.lineNumber + 1}`;
+ const lineToMatch = `L${this.line.lineNumber}`;
if (hash === lineToMatch) {
this.applyHashHighlight = true;
@@ -62,7 +62,7 @@ export default {
<template>
<div
- class="log-line collapsible-line d-flex justify-content-between ws-normal gl-align-items-flex-start gl-relative"
+ class="js-log-line log-line collapsible-line d-flex justify-content-between ws-normal gl-align-items-flex-start gl-relative"
:class="{ 'gl-bg-gray-700': isHighlighted || applyHashHighlight }"
role="button"
@click="handleOnClick"
diff --git a/app/assets/javascripts/ci/job_details/components/log/line_number.vue b/app/assets/javascripts/ci/job_details/components/log/line_number.vue
index 7ca9154d2fe..30b4c80f3fa 100644
--- a/app/assets/javascripts/ci/job_details/components/log/line_number.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line_number.vue
@@ -14,8 +14,7 @@ export default {
render(h, { props }) {
const { lineNumber, path } = props;
- const parsedLineNumber = lineNumber + 1;
- const lineId = `L${parsedLineNumber}`;
+ const lineId = `L${lineNumber}`;
const lineHref = `${path}#${lineId}`;
return h(
@@ -27,7 +26,7 @@ export default {
href: lineHref,
},
},
- parsedLineNumber,
+ lineNumber,
);
},
};
diff --git a/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue b/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue
index 1232ffffb57..7f419a249cf 100644
--- a/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue
+++ b/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue
@@ -18,7 +18,7 @@ import { JOB_GRAPHQL_ERRORS } from '~/ci/constants';
import { helpPagePath } from '~/helpers/help_page_helper';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { s__ } from '~/locale';
-import { reportMessageToSentry } from '~/ci/utils';
+import { reportToSentry } from '~/ci/utils';
import GetJob from '../graphql/queries/get_job.query.graphql';
import playJobWithVariablesMutation from '../graphql/mutations/job_play_with_variables.mutation.graphql';
import retryJobWithVariablesMutation from '../graphql/mutations/job_retry_with_variables.mutation.graphql';
@@ -57,7 +57,7 @@ export default {
},
error(error) {
createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText });
- reportMessageToSentry(this.$options.name, error, {});
+ reportToSentry(this.$options.name, error);
},
},
},
@@ -141,7 +141,7 @@ export default {
}
} catch (error) {
createAlert({ message: JOB_GRAPHQL_ERRORS.jobMutationErrorText });
- reportMessageToSentry(this.$options.name, error, {});
+ reportToSentry(this.$options.name, error);
}
},
async retryJob() {
@@ -157,7 +157,7 @@ export default {
}
} catch (error) {
createAlert({ message: JOB_GRAPHQL_ERRORS.jobMutationErrorText });
- reportMessageToSentry(this.$options.name, error, {});
+ reportToSentry(this.$options.name, error);
}
},
addEmptyVariable() {
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue b/app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue
index 4c81a9bd033..f6d39e8e4ac 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue
@@ -78,7 +78,7 @@ export default {
<span v-if="willExpire" data-testid="artifacts-unlocked-message-content">
{{ $options.i18n.willExpireText }}
</span>
- <timeago-tooltip v-if="artifact.expire_at" :time="artifact.expire_at" />
+ <timeago-tooltip v-if="artifact.expireAt" :time="artifact.expireAt" />
<gl-link
:href="helpUrl"
target="_blank"
@@ -95,23 +95,23 @@ export default {
</p>
<gl-button-group class="gl-display-flex gl-mt-3">
<gl-button
- v-if="artifact.keep_path"
- :href="artifact.keep_path"
+ v-if="artifact.keepPath"
+ :href="artifact.keepPath"
data-method="post"
data-testid="keep-artifacts"
>{{ $options.i18n.keepText }}</gl-button
>
<gl-button
- v-if="artifact.download_path"
- :href="artifact.download_path"
+ v-if="artifact.downloadPath"
+ :href="artifact.downloadPath"
rel="nofollow"
data-testid="download-artifacts"
download
>{{ $options.i18n.downloadText }}</gl-button
>
<gl-button
- v-if="artifact.browse_path"
- :href="artifact.browse_path"
+ v-if="artifact.browsePath"
+ :href="artifact.browsePath"
data-testid="browse-artifacts-button"
>{{ $options.i18n.browseText }}</gl-button
>
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue b/app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue
index 95616a4c706..5e826efbefb 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue
@@ -25,11 +25,7 @@ export default {
<p class="gl-display-flex gl-flex-wrap gl-align-items-baseline gl-gap-2 gl-mb-0">
<span class="gl-display-flex gl-font-weight-bold">{{ __('Commit') }}</span>
- <gl-link
- :href="commit.commit_path"
- class="gl-text-blue-500! gl-font-monospace"
- data-testid="commit-sha"
- >
+ <gl-link :href="commit.commit_path" class="commit-sha-container" data-testid="commit-sha">
{{ commit.short_id }}
</gl-link>
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
index 7f2f4fc0331..231f45d7ae6 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
@@ -4,6 +4,8 @@ import { isEmpty } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import { forwardDeploymentFailureModalId } from '~/ci/constants';
import { filterAnnotations } from '~/ci/job_details/utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
import ArtifactsBlock from './artifacts_block.vue';
import CommitBlock from './commit_block.vue';
import ExternalLinksBlock from './external_links_block.vue';
@@ -15,6 +17,9 @@ import StagesDropdown from './stages_dropdown.vue';
import TriggerBlock from './trigger_block.vue';
export default {
+ i18n: {
+ toggleSidebar: __('Toggle Sidebar'),
+ },
name: 'JobSidebar',
forwardDeploymentFailureModalId,
components: {
@@ -42,6 +47,9 @@ export default {
// the artifact object will always have a locked property
return Object.keys(this.job.artifact).length > 1;
},
+ artifact() {
+ return convertObjectPropsToCamelCase(this.job.artifact, { deep: true });
+ },
hasExternalLinks() {
return this.externalLinks.length > 0;
},
@@ -79,36 +87,44 @@ export default {
<template>
<aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix">
<div class="sidebar-container">
- <div class="blocks-container gl-p-4">
+ <div class="blocks-container gl-p-4 gl-pt-0">
<sidebar-header
- class="block gl-pb-4! gl-mb-2"
+ class="gl-py-4 gl-border-b gl-border-gray-50"
:rest-job="job"
:job-id="job.id"
@updateVariables="$emit('updateVariables')"
/>
- <job-sidebar-details-container class="block gl-mb-2" />
+ <job-sidebar-details-container class="gl-py-4 gl-border-b gl-border-gray-50" />
<artifacts-block
v-if="hasArtifact"
- class="block gl-mb-2"
- :artifact="job.artifact"
+ class="gl-py-4 gl-border-b gl-border-gray-50"
+ :artifact="artifact"
:help-url="artifactHelpUrl"
/>
<external-links-block
v-if="hasExternalLinks"
- class="block gl-mb-2"
+ class="gl-py-4 gl-border-b gl-border-gray-50"
:external-links="externalLinks"
/>
- <trigger-block v-if="hasTriggers" class="block gl-mb-2" :trigger="job.trigger" />
+ <trigger-block
+ v-if="hasTriggers"
+ class="gl-py-4 gl-border-b gl-border-gray-50"
+ :trigger="job.trigger"
+ />
- <commit-block class="block gl-mb-2" :commit="commit" :merge-request="job.merge_request" />
+ <commit-block
+ class="gl-py-4 gl-border-b gl-border-gray-50"
+ :commit="commit"
+ :merge-request="job.merge_request"
+ />
<stages-dropdown
v-if="job.pipeline"
- class="block gl-mb-2"
+ class="gl-py-4 gl-border-b gl-border-gray-50"
:pipeline="job.pipeline"
:selected-stage="selectedStage"
:stages="stages"
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue
index 5b1bf354fd4..d7726b952de 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue
@@ -39,8 +39,8 @@ export default {
};
</script>
<template>
- <p class="build-sidebar-item gl-mb-2">
- <b v-if="hasTitle" class="gl-display-flex">{{ title }}:</b>
+ <p class="build-sidebar-item gl-line-height-normal gl-display-flex gl-mb-3">
+ <b v-if="hasTitle" class="gl-mr-3">{{ title }}:</b>
<gl-link
v-if="path"
:href="path"
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue
index 77e3ecb9b3c..f757a3bcf00 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue
@@ -6,7 +6,6 @@ import { createAlert } from '~/alert';
import { TYPENAME_COMMIT_STATUS } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { JOB_GRAPHQL_ERRORS, forwardDeploymentFailureModalId, PASSED_STATUS } from '~/ci/constants';
import GetJob from '../../graphql/queries/get_job.query.graphql';
import JobSidebarRetryButton from './job_sidebar_retry_button.vue';
@@ -20,7 +19,6 @@ export default {
eraseLogConfirmText: s__('Job|Are you sure you want to erase this job log and artifacts?'),
newIssue: __('New issue'),
retryJobLabel: s__('Job|Retry'),
- toggleSidebar: __('Toggle Sidebar'),
runAgainJobButtonLabel: s__('Job|Run again'),
},
forwardDeploymentFailureModalId,
@@ -30,7 +28,6 @@ export default {
components: {
GlButton,
JobSidebarRetryButton,
- TooltipOnTruncate,
},
inject: ['projectPath'],
apollo: {
@@ -85,6 +82,15 @@ export default {
retryButtonCategory() {
return this.restJob.status && this.restJob.recoverable ? 'primary' : 'secondary';
},
+ jobHasPath() {
+ return Boolean(
+ this.restJob.erase_path ||
+ this.restJob.new_issue_path ||
+ this.restJob.terminal_path ||
+ this.restJob.retry_path ||
+ this.restJob.cancel_path,
+ );
+ },
},
methods: {
...mapActions(['toggleSidebar']),
@@ -93,73 +99,74 @@ export default {
</script>
<template>
- <div>
- <tooltip-on-truncate :title="job.name" truncate-target="child"
- ><h4 class="gl-mt-0 gl-mb-3 gl-text-truncate" data-testid="job-name">{{ job.name }}</h4>
- </tooltip-on-truncate>
- <div class="gl-display-flex gl-gap-3">
- <gl-button
- v-if="restJob.erase_path"
- v-gl-tooltip.bottom
- :title="$options.i18n.eraseLogButtonLabel"
- :aria-label="$options.i18n.eraseLogButtonLabel"
- :href="restJob.erase_path"
- :data-confirm="$options.i18n.eraseLogConfirmText"
- data-testid="job-log-erase-link"
- data-confirm-btn-variant="danger"
- data-method="post"
- icon="remove"
- />
- <gl-button
- v-if="restJob.new_issue_path"
- v-gl-tooltip.bottom
- :href="restJob.new_issue_path"
- :title="$options.i18n.newIssue"
- :aria-label="$options.i18n.newIssue"
- category="secondary"
- variant="confirm"
- data-testid="job-new-issue"
- icon="issue-new"
- />
- <gl-button
- v-if="restJob.terminal_path"
- v-gl-tooltip.bottom
- :href="restJob.terminal_path"
- :title="$options.i18n.debug"
- :aria-label="$options.i18n.debug"
- target="_blank"
- icon="external-link"
- data-testid="terminal-link"
- />
- <job-sidebar-retry-button
- v-if="canShowJobRetryButton"
- v-gl-tooltip.bottom
- :title="buttonTitle"
- :aria-label="buttonTitle"
- :is-manual-job="isManualJob"
- :category="retryButtonCategory"
- :href="restJob.retry_path"
- :modal-id="$options.forwardDeploymentFailureModalId"
- variant="confirm"
- data-testid="retry-button"
- @updateVariablesClicked="$emit('updateVariables')"
- />
- <gl-button
- v-if="restJob.cancel_path"
- v-gl-tooltip.bottom
- :title="$options.i18n.cancelJobButtonLabel"
- :aria-label="$options.i18n.cancelJobButtonLabel"
- :href="restJob.cancel_path"
- variant="danger"
- icon="cancel"
- data-method="post"
- data-testid="cancel-button"
- rel="nofollow"
- />
+ <div class="gl-py-3!">
+ <div class="gl-display-flex gl-justify-content-space-between gl-gap-3">
+ <div class="gl-display-flex gl-gap-3">
+ <template v-if="jobHasPath">
+ <gl-button
+ v-if="restJob.erase_path"
+ v-gl-tooltip.bottom
+ :title="$options.i18n.eraseLogButtonLabel"
+ :aria-label="$options.i18n.eraseLogButtonLabel"
+ :href="restJob.erase_path"
+ :data-confirm="$options.i18n.eraseLogConfirmText"
+ data-testid="job-log-erase-link"
+ data-confirm-btn-variant="danger"
+ data-method="post"
+ icon="remove"
+ />
+ <gl-button
+ v-if="restJob.new_issue_path"
+ v-gl-tooltip.bottom
+ :href="restJob.new_issue_path"
+ :title="$options.i18n.newIssue"
+ :aria-label="$options.i18n.newIssue"
+ category="secondary"
+ variant="confirm"
+ data-testid="job-new-issue"
+ icon="issue-new"
+ />
+ <gl-button
+ v-if="restJob.terminal_path"
+ v-gl-tooltip.bottom
+ :href="restJob.terminal_path"
+ :title="$options.i18n.debug"
+ :aria-label="$options.i18n.debug"
+ target="_blank"
+ icon="external-link"
+ data-testid="terminal-link"
+ />
+ <job-sidebar-retry-button
+ v-if="canShowJobRetryButton"
+ v-gl-tooltip.bottom
+ :title="buttonTitle"
+ :aria-label="buttonTitle"
+ :is-manual-job="isManualJob"
+ :category="retryButtonCategory"
+ :href="restJob.retry_path"
+ :modal-id="$options.forwardDeploymentFailureModalId"
+ variant="confirm"
+ data-testid="retry-button"
+ @updateVariablesClicked="$emit('updateVariables')"
+ />
+ <gl-button
+ v-if="restJob.cancel_path"
+ v-gl-tooltip.bottom
+ :title="$options.i18n.cancelJobButtonLabel"
+ :aria-label="$options.i18n.cancelJobButtonLabel"
+ :href="restJob.cancel_path"
+ variant="danger"
+ icon="cancel"
+ data-method="post"
+ data-testid="cancel-button"
+ rel="nofollow"
+ />
+ </template>
+ </div>
<gl-button
:aria-label="$options.i18n.toggleSidebar"
category="secondary"
- class="gl-md-display-none gl-ml-2"
+ class="gl-lg-display-none"
icon="chevron-double-lg-right"
@click="toggleSidebar"
/>
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue
index ebef3ecaa3f..f04987a87b5 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue
@@ -44,14 +44,10 @@ export default {
this.job.finished_at ||
this.job.erased_at ||
this.job.queued_duration ||
- this.job.id ||
this.job.runner ||
this.job.coverage,
);
},
- jobId() {
- return this.job?.id ? `#${this.job.id}` : '';
- },
runnerId() {
const { id, short_sha: token, description } = this.job.runner;
@@ -87,7 +83,6 @@ export default {
RUNNER: __('Runner'),
TAGS: __('Tags'),
TIMEOUT: __('Timeout'),
- ID: __('Job ID'),
},
TIMEOUT_HELP_URL: helpPagePath('/ci/pipelines/settings.md', {
anchor: 'set-a-limit-for-how-long-jobs-can-run',
@@ -113,7 +108,6 @@ export default {
data-testid="job-timeout"
:title="$options.i18n.TIMEOUT"
/>
- <detail-row v-if="job.id" :value="jobId" :title="$options.i18n.ID" />
<detail-row
v-if="job.runner"
:value="runnerId"
diff --git a/app/assets/javascripts/ci/job_details/components/stuck_block.vue b/app/assets/javascripts/ci/job_details/components/stuck_block.vue
index 8c73f09daea..b8ff0b032cc 100644
--- a/app/assets/javascripts/ci/job_details/components/stuck_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/stuck_block.vue
@@ -78,7 +78,7 @@ export default {
</template>
</gl-sprintf>
<template v-if="stuckData.showTags">
- <gl-badge v-for="tag in tags" :key="tag" variant="info">
+ <gl-badge v-for="tag in tags" :key="tag" size="sm" variant="info">
{{ tag }}
</gl-badge>
</template>
diff --git a/app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql
index 7fb887b2dd4..3a27a9a62a3 100644
--- a/app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql
@@ -7,5 +7,4 @@ fragment BaseCiJob on CiJob {
...ManualCiVariable
}
}
- __typename
}
diff --git a/app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql
index 0479df7bc4c..e560a2f29b6 100644
--- a/app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql
@@ -1,5 +1,4 @@
fragment ManualCiVariable on CiVariable {
- __typename
id
key
value
diff --git a/app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql b/app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql
index cd66a30ce63..b7c93c2830a 100644
--- a/app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql
@@ -1,6 +1,6 @@
#import "~/ci/job_details/graphql/fragments/ci_job.fragment.graphql"
-mutation retryJobWithVariables($id: CiBuildID!, $variables: [CiVariableInput!]) {
+mutation retryJobWithVariables($id: CiProcessableID!, $variables: [CiVariableInput!]) {
jobRetry(input: { id: $id, variables: $variables }) {
job {
...BaseCiJob
diff --git a/app/assets/javascripts/ci/job_details/index.js b/app/assets/javascripts/ci/job_details/index.js
index 5a1ecf2fff3..20235015ce6 100644
--- a/app/assets/javascripts/ci/job_details/index.js
+++ b/app/assets/javascripts/ci/job_details/index.js
@@ -13,11 +13,11 @@ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
-const initializeJobPage = (element) => {
- const store = createStore();
-
- // Let's start initializing the store (i.e. fetching data) right away
- store.dispatch('init', element.dataset);
+export const initJobDetails = () => {
+ const el = document.getElementById('js-job-page');
+ if (!el) {
+ return null;
+ }
const {
artifactHelpUrl,
@@ -26,27 +26,27 @@ const initializeJobPage = (element) => {
subscriptionsMoreMinutesUrl,
endpoint,
pagePath,
- logState,
buildStatus,
projectPath,
retryOutdatedJobDocsUrl,
aiRootCauseAnalysisAvailable,
- } = element.dataset;
+ } = el.dataset;
+
+ // init store to start fetching log
+ const store = createStore();
+ store.dispatch('init', { endpoint, pagePath });
return new Vue({
- el: element,
+ el,
apolloProvider,
store,
- components: {
- JobApp,
- },
provide: {
projectPath,
retryOutdatedJobDocsUrl,
aiRootCauseAnalysisAvailable: parseBoolean(aiRootCauseAnalysisAvailable),
},
- render(createElement) {
- return createElement('job-app', {
+ render(h) {
+ return h(JobApp, {
props: {
artifactHelpUrl,
deploymentHelpUrl,
@@ -54,7 +54,6 @@ const initializeJobPage = (element) => {
subscriptionsMoreMinutesUrl,
endpoint,
pagePath,
- logState,
buildStatus,
projectPath,
},
@@ -62,8 +61,3 @@ const initializeJobPage = (element) => {
},
});
};
-
-export default () => {
- const jobElement = document.getElementById('js-job-page');
- initializeJobPage(jobElement);
-};
diff --git a/app/assets/javascripts/ci/job_details/job_app.vue b/app/assets/javascripts/ci/job_details/job_app.vue
index 5137ebfeaa8..119f8259be7 100644
--- a/app/assets/javascripts/ci/job_details/job_app.vue
+++ b/app/assets/javascripts/ci/job_details/job_app.vue
@@ -130,7 +130,7 @@ export default {
},
jobName() {
- return sprintf(__('Job %{jobName}'), { jobName: this.job.name });
+ return sprintf(__('%{jobName}'), { jobName: this.job.name });
},
},
watch: {
@@ -195,7 +195,7 @@ export default {
},
updateSidebar() {
const breakpoint = bp.getBreakpointSize();
- if (breakpoint === 'xs' || breakpoint === 'sm') {
+ if (breakpoint === 'xs' || breakpoint === 'sm' || breakpoint === 'md') {
this.hideSidebar();
} else if (!this.isSidebarOpen) {
this.showSidebar();
@@ -224,7 +224,7 @@ export default {
<div class="build-page" data-testid="job-content">
<!-- Header Section -->
<header>
- <div class="build-header top-area">
+ <div class="build-header gl-display-flex">
<job-header
:status="job.status"
:time="headerTime"
@@ -290,11 +290,7 @@ export default {
{{ __('This job is archived. Only the complete pipeline can be retried.') }}
</div>
<!-- job log -->
- <div
- v-if="hasJobLog && !showUpdateVariablesState"
- class="build-log-container gl-relative"
- :class="{ 'gl-mt-3': !job.archived }"
- >
+ <div v-if="hasJobLog && !showUpdateVariablesState" class="build-log-container gl-relative">
<log-top-bar
:class="{
'has-archived-block': job.archived,
@@ -332,18 +328,17 @@ export default {
<!-- EO empty state -->
<!-- EO Body Section -->
+
+ <sidebar
+ :class="{
+ 'right-sidebar-expanded': isSidebarOpen,
+ 'right-sidebar-collapsed': !isSidebarOpen,
+ }"
+ :artifact-help-url="artifactHelpUrl"
+ data-testid="job-sidebar"
+ @updateVariables="onUpdateVariables()"
+ />
</div>
</template>
-
- <sidebar
- v-if="shouldRenderContent"
- :class="{
- 'right-sidebar-expanded': isSidebarOpen,
- 'right-sidebar-collapsed': !isSidebarOpen,
- }"
- :artifact-help-url="artifactHelpUrl"
- data-testid="job-sidebar"
- @updateVariables="onUpdateVariables()"
- />
</div>
</template>
diff --git a/app/assets/javascripts/ci/job_details/store/actions.js b/app/assets/javascripts/ci/job_details/store/actions.js
index 33d83689e61..fa23589f7d6 100644
--- a/app/assets/javascripts/ci/job_details/store/actions.js
+++ b/app/assets/javascripts/ci/job_details/store/actions.js
@@ -15,17 +15,15 @@ import { __ } from '~/locale';
import { reportToSentry } from '~/ci/utils';
import * as types from './mutation_types';
-export const init = ({ dispatch }, { endpoint, logState, pagePath }) => {
- dispatch('setJobEndpoint', endpoint);
+export const init = ({ dispatch }, { endpoint, pagePath }) => {
dispatch('setJobLogOptions', {
- logState,
+ endpoint,
pagePath,
});
return dispatch('fetchJob');
};
-export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint);
export const setJobLogOptions = ({ commit }, options) => commit(types.SET_JOB_LOG_OPTIONS, options);
export const hideSidebar = ({ commit }) => commit(types.HIDE_SIDEBAR);
diff --git a/app/assets/javascripts/ci/job_details/store/mutation_types.js b/app/assets/javascripts/ci/job_details/store/mutation_types.js
index 4915a826b84..e125538317d 100644
--- a/app/assets/javascripts/ci/job_details/store/mutation_types.js
+++ b/app/assets/javascripts/ci/job_details/store/mutation_types.js
@@ -1,4 +1,3 @@
-export const SET_JOB_ENDPOINT = 'SET_JOB_ENDPOINT';
export const SET_JOB_LOG_OPTIONS = 'SET_JOB_LOG_OPTIONS';
export const HIDE_SIDEBAR = 'HIDE_SIDEBAR';
diff --git a/app/assets/javascripts/ci/job_details/store/mutations.js b/app/assets/javascripts/ci/job_details/store/mutations.js
index b7d7006ee61..fe6506bf8a5 100644
--- a/app/assets/javascripts/ci/job_details/store/mutations.js
+++ b/app/assets/javascripts/ci/job_details/store/mutations.js
@@ -3,13 +3,9 @@ import * as types from './mutation_types';
import { logLinesParser, updateIncrementalJobLog } from './utils';
export default {
- [types.SET_JOB_ENDPOINT](state, endpoint) {
- state.jobEndpoint = endpoint;
- },
-
[types.SET_JOB_LOG_OPTIONS](state, options = {}) {
state.jobLogEndpoint = options.pagePath;
- state.jobLogState = options.logState;
+ state.jobEndpoint = options.endpoint;
},
[types.HIDE_SIDEBAR](state) {
diff --git a/app/assets/javascripts/ci/job_details/store/utils.js b/app/assets/javascripts/ci/job_details/store/utils.js
index bc76901026d..b18a3fa162d 100644
--- a/app/assets/javascripts/ci/job_details/store/utils.js
+++ b/app/assets/javascripts/ci/job_details/store/utils.js
@@ -19,20 +19,17 @@ export const parseLine = (line = {}, lineNumber) => ({
* @param Number lineNumber
*/
export const parseHeaderLine = (line = {}, lineNumber, hash) => {
+ let isClosed = parseBoolean(line.section_options?.collapsed);
+
// if a hash is present in the URL then we ensure
// all sections are visible so we can scroll to the hash
// in the DOM
if (hash) {
- return {
- isClosed: false,
- isHeader: true,
- line: parseLine(line, lineNumber),
- lines: [],
- };
+ isClosed = false;
}
return {
- isClosed: parseBoolean(line.section_options?.collapsed),
+ isClosed,
isHeader: true,
line: parseLine(line, lineNumber),
lines: [],
@@ -80,27 +77,28 @@ export const isCollapsibleSection = (acc = [], last = {}, section = {}) =>
section.section === last.line.section;
/**
- * Returns the lineNumber of the last line in
- * a parsed log
+ * Returns the next line number in the parsed log
*
* @param Array acc
* @returns Number
*/
-export const getIncrementalLineNumber = (acc) => {
- let lineNumberValue;
- const lastIndex = acc.length - 1;
- const lastElement = acc[lastIndex];
+export const getNextLineNumber = (acc) => {
+ if (!acc?.length) {
+ return 1;
+ }
+
+ const lastElement = acc[acc.length - 1];
const nestedLines = lastElement.lines;
if (lastElement.isHeader && !nestedLines.length && lastElement.line) {
- lineNumberValue = lastElement.line.lineNumber;
- } else if (lastElement.isHeader && nestedLines.length) {
- lineNumberValue = nestedLines[nestedLines.length - 1].lineNumber;
- } else {
- lineNumberValue = lastElement.lineNumber;
+ return lastElement.line.lineNumber + 1;
}
- return lineNumberValue === 0 ? 1 : lineNumberValue + 1;
+ if (lastElement.isHeader && nestedLines.length) {
+ return nestedLines[nestedLines.length - 1].lineNumber + 1;
+ }
+
+ return lastElement.lineNumber + 1;
};
/**
@@ -118,32 +116,29 @@ export const getIncrementalLineNumber = (acc) => {
* @param Array accumulator
* @returns Array parsed log lines
*/
-export const logLinesParser = (lines = [], accumulator = [], hash = '') =>
- lines.reduce(
- (acc, line, index) => {
- const lineNumber = accumulator.length > 0 ? getIncrementalLineNumber(acc) : index;
-
- const last = acc[acc.length - 1];
-
- // If the object is an header, we parse it into another structure
- if (line.section_header) {
- acc.push(parseHeaderLine(line, lineNumber, hash));
- } else if (isCollapsibleSection(acc, last, line)) {
- // if the object belongs to a nested section, we append it to the new `lines` array of the
- // previously formatted header
- last.lines.push(parseLine(line, lineNumber));
- } else if (line.section_duration) {
- // if the line has section_duration, we look for the correct header to add it
- addDurationToHeader(acc, line);
- } else {
- // otherwise it's a regular line
- acc.push(parseLine(line, lineNumber));
- }
+export const logLinesParser = (lines = [], prevLogLines = [], hash = '') =>
+ lines.reduce((acc, line) => {
+ const lineNumber = getNextLineNumber(acc);
+
+ const last = acc[acc.length - 1];
+
+ // If the object is an header, we parse it into another structure
+ if (line.section_header) {
+ acc.push(parseHeaderLine(line, lineNumber, hash));
+ } else if (isCollapsibleSection(acc, last, line)) {
+ // if the object belongs to a nested section, we append it to the new `lines` array of the
+ // previously formatted header
+ last.lines.push(parseLine(line, lineNumber));
+ } else if (line.section_duration) {
+ // if the line has section_duration, we look for the correct header to add it
+ addDurationToHeader(acc, line);
+ } else {
+ // otherwise it's a regular line
+ acc.push(parseLine(line, lineNumber));
+ }
- return acc;
- },
- [...accumulator],
- );
+ return acc;
+ }, prevLogLines);
/**
* Finds the repeated offset, removes the old one
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
index 609f2790869..3ad2582e36b 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
@@ -7,7 +7,7 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
-import { reportMessageToSentry } from '~/ci/utils';
+import { reportToSentry } from '~/ci/utils';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import {
@@ -133,7 +133,7 @@ export default {
variables: { id: this.job.id },
});
if (errors.length > 0) {
- reportMessageToSentry(this.$options.name, errors.join(', '), {});
+ reportToSentry(this.$options.name, new Error(errors.join(', ')));
this.showToastMessage();
} else if (redirect) {
// Retry and Play actions redirect to job detail view
@@ -143,7 +143,7 @@ export default {
eventHub.$emit('jobActionPerformed');
}
} catch (failure) {
- reportMessageToSentry(this.$options.name, failure, {});
+ reportToSentry(this.$options.name, failure);
this.showToastMessage();
}
},
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
index b435eb283fd..fbdfc7c9c6a 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
@@ -35,9 +35,6 @@ export default {
jobRef() {
return this.job?.refName;
},
- jobRefPath() {
- return this.job?.refPath;
- },
jobTags() {
return this.job.tags;
},
@@ -72,61 +69,60 @@ export default {
<template>
<div>
<div class="gl-text-truncate gl-p-3 gl-mt-n3 gl-mx-n3 gl-mb-n2">
- <gl-link
- v-if="canReadJob"
- class="gl-text-blue-600!"
- :href="jobPath"
- data-testid="job-id-link"
- >
- {{ jobId }}
- </gl-link>
-
- <span v-else data-testid="job-id-limited-access">{{ jobId }}</span>
-
<gl-icon
v-if="jobStuck"
v-gl-tooltip="$options.i18n.stuckText"
name="warning"
:size="$options.iconSize"
+ class="gl-mr-2"
data-testid="stuck-icon"
/>
- <div
- class="gl-display-flex gl-text-gray-700 gl-align-items-center gl-lg-justify-content-start gl-justify-content-end gl-mt-2"
+ <gl-link
+ v-if="canReadJob"
+ class="gl-text-blue-600!"
+ :href="jobPath"
+ data-testid="job-id-link"
>
- <div
- v-if="jobRef"
- class="gl-p-2 gl-rounded-base gl-bg-gray-50 gl-max-w-15 gl-text-truncate"
- >
- <gl-icon
- v-if="createdByTag"
- name="label"
- :size="$options.iconSize"
- data-testid="label-icon"
- />
- <gl-icon v-else name="fork" :size="$options.iconSize" data-testid="fork-icon" />
- <gl-link
- class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
- :href="job.refPath"
- data-testid="job-ref"
- >{{ job.refName }}</gl-link
- >
- </div>
+ <span class="gl-text-truncate">
+ <span data-testid="job-name">{{ jobId }}: {{ job.name }}</span>
+ </span>
+ </gl-link>
- <span v-else>{{ __('none') }}</span>
- <div class="gl-ml-2 gl-p-2 gl-rounded-base gl-bg-gray-50">
- <gl-icon class="gl-mx-2" name="commit" :size="$options.iconSize" />
- <gl-link
- class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
- :href="job.commitPath"
- data-testid="job-sha"
- >{{ job.shortSha }}</gl-link
- >
- </div>
+ <span v-else data-testid="job-id-limited-access">{{ jobId }}: {{ job.name }}</span>
+ </div>
+
+ <div
+ class="gl-display-flex gl-text-gray-700 gl-align-items-center gl-lg-justify-content-start gl-justify-content-end gl-mt-1"
+ >
+ <div v-if="jobRef" class="gl-p-2 gl-rounded-base gl-bg-gray-50 gl-max-w-26 gl-text-truncate">
+ <gl-icon
+ v-if="createdByTag"
+ name="label"
+ :size="$options.iconSize"
+ data-testid="label-icon"
+ />
+ <gl-icon v-else name="fork" :size="$options.iconSize" data-testid="fork-icon" />
+ <gl-link
+ class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
+ :href="job.refPath"
+ data-testid="job-ref"
+ >{{ job.refName }}</gl-link
+ >
+ </div>
+ <span v-else>{{ __('none') }}</span>
+ <div class="gl-ml-2 gl-p-2 gl-rounded-base gl-bg-gray-50">
+ <gl-icon class="gl-mx-2" name="commit" :size="$options.iconSize" />
+ <gl-link
+ class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
+ :href="job.commitPath"
+ data-testid="job-sha"
+ >{{ job.shortSha }}</gl-link
+ >
</div>
</div>
- <div>
+ <div class="gl-mt-2">
<gl-badge
v-for="tag in jobTags"
:key="tag"
@@ -136,7 +132,6 @@ export default {
>
{{ tag }}
</gl-badge>
-
<gl-badge
v-if="triggered"
variant="info"
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
index 18d68ee8a29..945674153c4 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
@@ -1,8 +1,12 @@
<script>
import { GlAvatar, GlLink } from '@gitlab/ui';
+import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
export default {
+ i18n: {
+ stageLabel: s__('Jobs|Stage'),
+ },
components: {
GlAvatar,
GlLink,
@@ -36,21 +40,22 @@ export default {
<template>
<div>
- <div class="gl-p-3 gl-mt-n3">
- <gl-link
- class="gl-text-truncate gl-ml-n3 gl-text-gray-500!"
- :href="pipelinePath"
- data-testid="pipeline-id"
- >
+ <div class="gl-p-3 gl-mt-n3 gl-mx-n3">
+ <gl-link class="gl-text-truncate" :href="pipelinePath" data-testid="pipeline-id">
{{ pipelineId }}
</gl-link>
+
+ <span class="gl-text-secondary">
+ <span>{{ __('created by') }}</span>
+ <gl-link v-if="showAvatar" :href="userPath" data-testid="pipeline-user-link">
+ <gl-avatar :src="pipelineUserAvatar" :size="16" />
+ </gl-link>
+ <span v-else>{{ __('API') }}</span>
+ </span>
</div>
- <div class="gl-font-sm gl-text-secondary gl-mt-n2">
- <span>{{ __('created by') }}</span>
- <gl-link v-if="showAvatar" :href="userPath" data-testid="pipeline-user-link">
- <gl-avatar :src="pipelineUserAvatar" :size="16" />
- </gl-link>
- <span v-else>{{ __('API') }}</span>
+
+ <div v-if="job.stage" class="gl-text-truncate gl-font-sm gl-text-secondary gl-mt-1">
+ <span data-testid="job-stage-name">{{ $options.i18n.stageLabel }}: {{ job.stage.name }}</span>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
index dbf1dfe7a29..a2b6a430138 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
@@ -1,12 +1,14 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { formatTime } from '~/lib/utils/datetime_utility';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
iconSize: 12,
components: {
+ CiBadgeLink,
GlIcon,
TimeAgoTooltip,
},
@@ -36,17 +38,16 @@ export default {
<template>
<div>
- <div v-if="duration" data-testid="job-duration">
- <gl-icon name="timer" :size="$options.iconSize" data-testid="duration-icon" />
- {{ durationFormatted }}
- </div>
- <div
- v-if="finishedTime"
- :class="{ 'gl-mt-2': hasDurationAndFinishedTime }"
- data-testid="job-finished-time"
- >
- <gl-icon name="calendar" :size="$options.iconSize" data-testid="finished-time-icon" />
- <time-ago-tooltip :time="finishedTime" />
+ <ci-badge-link :status="job.detailedStatus" />
+ <div class="gl-font-sm gl-text-secondary gl-mt-2 gl-ml-3">
+ <div v-if="duration" data-testid="job-duration">
+ <gl-icon name="timer" :size="$options.iconSize" data-testid="duration-icon" />
+ {{ durationFormatted }}
+ </div>
+ <div v-if="finishedTime" data-testid="job-finished-time">
+ <gl-icon name="calendar" :size="$options.iconSize" data-testid="finished-time-icon" />
+ <time-ago-tooltip :time="finishedTime" />
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue b/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
index 23100a3f3db..d81d19cfd52 100644
--- a/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
@@ -1,12 +1,11 @@
<script>
import { GlTable } from '@gitlab/ui';
import { s__ } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import ProjectCell from '~/ci/admin/jobs_table/components/cells/project_cell.vue';
import RunnerCell from '~/ci/admin/jobs_table/components/cells/runner_cell.vue';
-import { DEFAULT_FIELDS } from '../constants';
+import { JOBS_DEFAULT_FIELDS } from '../constants';
import ActionsCell from './job_cells/actions_cell.vue';
-import DurationCell from './job_cells/duration_cell.vue';
+import StatusCell from './job_cells/status_cell.vue';
import JobCell from './job_cells/job_cell.vue';
import PipelineCell from './job_cells/pipeline_cell.vue';
@@ -16,13 +15,12 @@ export default {
},
components: {
ActionsCell,
- CiBadgeLink,
- DurationCell,
- GlTable,
+ StatusCell,
JobCell,
PipelineCell,
ProjectCell,
RunnerCell,
+ GlTable,
},
props: {
jobs: {
@@ -32,7 +30,7 @@ export default {
tableFields: {
type: Array,
required: false,
- default: () => DEFAULT_FIELDS,
+ default: () => JOBS_DEFAULT_FIELDS,
},
admin: {
type: Boolean,
@@ -64,7 +62,7 @@ export default {
</template>
<template #cell(status)="{ item }">
- <ci-badge-link :status="item.detailedStatus" />
+ <status-cell :job="item" />
</template>
<template #cell(job)="{ item }">
@@ -75,28 +73,20 @@ export default {
<pipeline-cell :job="item" />
</template>
- <template v-if="admin" #cell(project)="{ item }">
- <project-cell :job="item" />
- </template>
-
- <template v-if="admin" #cell(runner)="{ item }">
- <runner-cell :job="item" />
- </template>
-
<template #cell(stage)="{ item }">
<div class="gl-text-truncate">
- <span v-if="item.stage" data-testid="job-stage-name">{{ item.stage.name }}</span>
+ <span v-if="item.stage" data-testid="job-stage-name" class="gl-text-secondary">{{
+ item.stage.name
+ }}</span>
</div>
</template>
- <template #cell(name)="{ item }">
- <div class="gl-text-truncate">
- <span data-testid="job-name">{{ item.name }}</span>
- </div>
+ <template v-if="admin" #cell(project)="{ item }">
+ <project-cell :job="item" />
</template>
- <template #cell(duration)="{ item }">
- <duration-cell :job="item" />
+ <template v-if="admin" #cell(runner)="{ item }">
+ <runner-cell :job="item" />
</template>
<template #cell(coverage)="{ item }">
diff --git a/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue b/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue
index d2cd27be034..7effb8fe239 100644
--- a/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue
@@ -29,6 +29,7 @@ export default {
:title="$options.i18n.title"
:description="$options.i18n.description"
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
:primary-button-link="pipelineEditorPath"
:primary-button-text="$options.i18n.buttonText"
data-testid="jobs-empty-state"
diff --git a/app/assets/javascripts/ci/jobs_page/constants.js b/app/assets/javascripts/ci/jobs_page/constants.js
index 1b572e60c58..dec355ddff6 100644
--- a/app/assets/javascripts/ci/jobs_page/constants.js
+++ b/app/assets/javascripts/ci/jobs_page/constants.js
@@ -29,6 +29,7 @@ export const PLAY_JOB_CONFIRMATION_MESSAGE = s__(
export const RUN_JOB_NOW_HEADER_TITLE = s__('DelayedJobs|Run the delayed job now?');
/* Table constants */
+/* There is another field list based on this one in app/assets/javascripts/ci/admin/jobs_table/constants.js */
export const DEFAULT_FIELDS = [
{
key: 'status',
@@ -38,7 +39,7 @@ export const DEFAULT_FIELDS = [
{
key: 'job',
label: __('Job'),
- columnClass: 'gl-w-20p',
+ columnClass: 'gl-w-quarter',
},
{
key: 'pipeline',
@@ -51,16 +52,6 @@ export const DEFAULT_FIELDS = [
columnClass: 'gl-w-10p',
},
{
- key: 'name',
- label: __('Name'),
- columnClass: 'gl-w-15p',
- },
- {
- key: 'duration',
- label: __('Duration'),
- columnClass: 'gl-w-15p',
- },
- {
key: 'coverage',
label: __('Coverage'),
tdClass: 'gl-display-none! gl-lg-display-table-cell!',
@@ -69,8 +60,10 @@ export const DEFAULT_FIELDS = [
{
key: 'actions',
label: '',
+ tdClass: 'gl-text-right',
columnClass: 'gl-w-10p',
},
];
+export const JOBS_DEFAULT_FIELDS = DEFAULT_FIELDS.filter((field) => field.key !== 'stage');
export const JOBS_TAB_FIELDS = DEFAULT_FIELDS.filter((field) => field.key !== 'pipeline');
diff --git a/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql
index 6e51f9a20fa..077c8e31749 100644
--- a/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql
@@ -1,6 +1,6 @@
#import "../fragments/job.fragment.graphql"
-mutation retryJob($id: CiBuildID!) {
+mutation retryJob($id: CiProcessableID!) {
jobRetry(input: { id: $id }) {
job {
...Job
diff --git a/app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql b/app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql
index 022d461dbec..f6de6cde9d0 100644
--- a/app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql
+++ b/app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql
@@ -1,4 +1,4 @@
-mutation retryMrFailedJob($id: CiBuildID!) {
+mutation retryMrFailedJob($id: CiProcessableID!) {
jobRetry(input: { id: $id }) {
errors
}
diff --git a/app/assets/javascripts/ci/pipeline_details/constants.js b/app/assets/javascripts/ci/pipeline_details/constants.js
index bf312e66144..70b758ae6b0 100644
--- a/app/assets/javascripts/ci/pipeline_details/constants.js
+++ b/app/assets/javascripts/ci/pipeline_details/constants.js
@@ -23,8 +23,6 @@ export const PARSE_FAILURE = 'parse_failure';
export const POST_FAILURE = 'post_failure';
export const UNSUPPORTED_DATA = 'unsupported_data';
-export const CHILD_VIEW = 'child';
-
// Pipeline tabs
export const pipelineTabName = 'graph';
diff --git a/app/assets/javascripts/ci/pipeline_details/dag/dag.vue b/app/assets/javascripts/ci/pipeline_details/dag/dag.vue
index 5415340c956..fb8e5d679b7 100644
--- a/app/assets/javascripts/ci/pipeline_details/dag/dag.vue
+++ b/app/assets/javascripts/ci/pipeline_details/dag/dag.vue
@@ -220,6 +220,7 @@ export default {
<gl-empty-state
v-else-if="hasNoDependentJobs"
:svg-path="emptyDagSvgPath"
+ :svg-height="null"
:title="$options.emptyStateTexts.title"
>
<template #description>
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue
index 7538ad87af8..ec8f30e94b4 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue
@@ -65,7 +65,7 @@ export default {
<div
:id="computedJobId"
class="ci-job-dropdown-container dropdown dropright"
- data-qa-selector="job_dropdown_container"
+ data-testid="job-dropdown-container"
>
<button
type="button"
@@ -90,7 +90,7 @@ export default {
<ul
class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown"
- data-qa-selector="jobs_dropdown_menu"
+ data-testid="jobs-dropdown-menu"
>
<li class="scrollable-menu">
<ul>
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
index 4298052d1c0..bb36ac8b6ab 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
@@ -5,7 +5,7 @@ import delayedJobMixin from '~/ci/mixins/delayed_job_mixin';
import { helpPagePath } from '~/helpers/help_page_helper';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import ActionComponent from '../../../common/private/job_action_component.vue';
import JobNameComponent from '../../../common/private/job_name_component.vue';
import { BRIDGE_KIND, RETRY_ACTION_TITLE, SINGLE_JOB, SKIP_RETRY_MODAL_KEY } from '../constants';
@@ -58,7 +58,7 @@ export default {
hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500',
components: {
ActionComponent,
- CiIcon,
+ CiBadgeLink,
GlBadge,
GlForm,
GlFormCheckbox,
@@ -312,7 +312,6 @@ export default {
<div
:id="computedJobId"
class="ci-job-component gl-display-flex gl-justify-content-space-between gl-pipeline-job-width"
- data-qa-selector="job_item_container"
>
<component
:is="nameComponent"
@@ -326,12 +325,11 @@ export default {
:href="detailsPath"
class="js-pipeline-graph-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none gl-w-full"
:data-testid="testId"
- data-qa-selector="job_link"
@click="jobItemClick"
@mouseout="hideTooltips"
>
<div class="gl-display-flex gl-align-items-center gl-flex-grow-1">
- <ci-icon :size="24" :status="job.status" class="gl-line-height-0" />
+ <ci-badge-link :status="job.status" size="md" :show-text="false" :use-link="false" />
<div class="gl-pl-3 gl-pr-3 gl-display-flex gl-flex-direction-column gl-pipeline-job-width">
<div class="gl-text-truncate gl-pr-9 gl-line-height-normal">{{ job.name }}</div>
<div
@@ -343,7 +341,13 @@ export default {
</div>
</div>
</div>
- <gl-badge v-if="isBridge" class="gl-mt-3" variant="info" size="sm">
+ <gl-badge
+ v-if="isBridge"
+ class="gl-mt-3"
+ variant="info"
+ size="sm"
+ data-testid="job-bridge-badge"
+ >
{{ $options.i18n.bridgeBadgeText }}
</gl-badge>
</component>
@@ -356,7 +360,6 @@ export default {
class="gl-mr-1"
:should-trigger-click="shouldTriggerActionClick"
:with-confirmation-modal="withConfirmationModal"
- data-qa-selector="job_action_button"
@actionButtonClicked="handleConfirmationModalPreferences"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
@showActionConfirmationModal="showActionConfirmationModal"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
index d6adaf78da4..5960eea5b4f 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
@@ -13,7 +13,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
import CancelPipelineMutation from '~/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql';
import RetryPipelineMutation from '~/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { reportToSentry } from '~/ci/utils';
import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from '../constants';
@@ -22,7 +22,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- CiIcon,
+ CiBadgeLink,
GlBadge,
GlButton,
GlLink,
@@ -233,7 +233,7 @@ export default {
ref="linkedPipeline"
class="gl-h-full gl-display-flex! gl-px-2"
:class="flexDirection"
- data-qa-selector="linked_pipeline_container"
+ data-testid="linked-pipeline-container"
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
>
@@ -242,16 +242,19 @@ export default {
</gl-tooltip>
<div class="gl-bg-white gl-border gl-p-3 gl-rounded-lg gl-w-full" :class="cardClasses">
<div class="gl-display-flex gl-gap-x-3">
- <ci-icon v-if="!pipelineIsLoading" :status="pipelineStatus" :size="24" />
+ <ci-badge-link
+ v-if="!pipelineIsLoading"
+ :status="pipelineStatus"
+ size="md"
+ :show-text="false"
+ :use-link="false"
+ class="gl-align-self-start"
+ />
<div v-else class="gl-pr-3"><gl-loading-icon size="sm" inline /></div>
<div
class="gl-display-flex gl-downstream-pipeline-job-width gl-flex-direction-column gl-line-height-normal"
>
- <span
- class="gl-text-truncate"
- data-testid="downstream-title"
- data-qa-selector="downstream_title_content"
- >
+ <span class="gl-text-truncate" data-testid="downstream-title-content">
{{ downstreamTitle }}
</span>
<div class="gl-text-truncate">
@@ -294,7 +297,6 @@ export default {
:icon="expandedIcon"
:aria-label="expandBtnText"
data-testid="expand-pipeline-button"
- data-qa-selector="expand_linked_pipeline_button"
@mouseover="setExpandBtnActiveState(true)"
@mouseout="setExpandBtnActiveState(false)"
@focus="setExpandBtnActiveState(true)"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
index 1401bdba5ca..6030adc96ad 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
@@ -179,6 +179,7 @@ export default {
{ 'gl-opacity-3': isFadedOut(group.name) },
'gl-transition-duration-slow gl-transition-timing-function-ease',
]"
+ data-testid="job-item-container"
@pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
@setSkipRetryModal="$emit('setSkipRetryModal')"
/>
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue b/app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue
index bd7325f7925..a6e7a645442 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue
@@ -6,7 +6,7 @@ import { __, s__ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '~/ci/pipeline_details/constants';
import getPipelineQuery from '~/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql';
-import { reportToSentry, reportMessageToSentry } from '~/ci/utils';
+import { reportToSentry } from '~/ci/utils';
import DismissPipelineGraphCallout from './graphql/mutations/dismiss_pipeline_notification.graphql';
import {
ACTION_FAILURE,
@@ -156,17 +156,7 @@ export default {
error(err) {
this.reportFailure({ type: LOAD_FAILURE, skipSentry: true });
- reportMessageToSentry(
- this.$options.name,
- `| type: ${LOAD_FAILURE} , info: ${JSON.stringify(err)}`,
- {
- graphViewType: this.graphViewType,
- graphqlResourceEtag: this.graphqlResourceEtag,
- metricsPath: this.metricsPath,
- projectPath: this.pipelineProjectPath,
- pipelineIid: this.pipelineIid,
- },
- );
+ reportToSentry(this.$options.name, new Error(err));
},
result({ data, error }) {
const stages = data?.project?.pipeline?.stages?.nodes || [];
diff --git a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
index 3a6a655bfa6..51a68f6619a 100644
--- a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
+++ b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
@@ -396,18 +396,14 @@ export default {
</div>
</gl-alert>
<gl-loading-icon v-if="loading" class="gl-text-left" size="lg" />
- <div
- v-else
- class="gl-display-flex gl-justify-content-space-between gl-flex-wrap"
- data-qa-selector="pipeline_details_header"
- >
+ <div v-else class="gl-display-flex gl-justify-content-space-between gl-flex-wrap">
<div>
<h3 v-if="name" class="gl-mt-0 gl-mb-3" data-testid="pipeline-name">{{ name }}</h3>
<h3 v-else class="gl-mt-0 gl-mb-3" data-testid="pipeline-commit-title">
{{ commitTitle }}
</h3>
<div>
- <ci-badge-link :status="detailedStatus" />
+ <ci-badge-link :status="detailedStatus" class="gl-display-inline-block gl-mb-3" />
<div class="gl-ml-2 gl-mb-3 gl-display-inline-block gl-h-6">
<gl-link
v-if="user"
@@ -423,7 +419,7 @@ export default {
<template #link="{ content }">
<gl-link
:href="commitPath"
- class="gl-bg-blue-50 gl-rounded-base gl-px-2 gl-mx-2"
+ class="commit-sha-container"
data-testid="commit-link"
target="_blank"
>
@@ -431,6 +427,8 @@ export default {
</gl-link>
</template>
</gl-sprintf>
+ </div>
+ <div class="gl-display-inline-block gl-mb-3">
<clipboard-button
:text="shortId"
category="tertiary"
@@ -449,123 +447,127 @@ export default {
</div>
<div v-safe-html="refText" class="gl-mb-3" data-testid="pipeline-ref-text"></div>
<div>
- <gl-badge
- v-if="badges.schedule"
- v-gl-tooltip
- :title="$options.i18n.scheduleBadgeTooltip"
- variant="info"
- size="sm"
- >
- {{ $options.i18n.scheduleBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.child"
- v-gl-tooltip
- :title="$options.i18n.childBadgeTooltip"
- variant="info"
- size="sm"
- >
- <gl-sprintf :message="$options.i18n.childBadgeText">
- <template #link="{ content }">
- <gl-link :href="paths.triggeredByPath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </gl-badge>
- <gl-badge
- v-if="badges.latest"
- v-gl-tooltip
- :title="$options.i18n.latestBadgeTooltip"
- variant="success"
- size="sm"
- >
- {{ $options.i18n.latestBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.mergeTrainPipeline"
- v-gl-tooltip
- :title="$options.i18n.mergeTrainBadgeTooltip"
- variant="info"
- size="sm"
- >
- {{ $options.i18n.mergeTrainBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.invalid"
- v-gl-tooltip
- :title="yamlErrors"
- variant="danger"
- size="sm"
- >
- {{ $options.i18n.invalidBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.failed"
- v-gl-tooltip
- :title="failureReason"
- variant="danger"
- size="sm"
- >
- {{ $options.i18n.failedBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.autoDevops"
- v-gl-tooltip
- :title="$options.i18n.autoDevopsBadgeTooltip"
- variant="info"
- size="sm"
- >
- {{ $options.i18n.autoDevopsBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.detached"
- v-gl-tooltip
- :title="$options.i18n.detachedBadgeTooltip"
- variant="info"
- size="sm"
- >
- {{ $options.i18n.detachedBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.stuck"
- v-gl-tooltip
- :title="$options.i18n.stuckBadgeTooltip"
- variant="warning"
- size="sm"
- >
- {{ $options.i18n.stuckBadgeText }}
- </gl-badge>
- <span
- v-gl-tooltip
- :title="$options.i18n.totalJobsTooltip"
- class="gl-ml-2"
- data-testid="total-jobs"
- >
- <gl-icon name="pipeline" />
- {{ totalJobsText }}
- </span>
- <span
- v-if="showComputeMinutes"
- v-gl-tooltip
- :title="$options.i18n.computeMinutesTooltip"
- class="gl-ml-2"
- data-testid="compute-minutes"
- >
- <gl-icon name="quota" />
- {{ computeMinutes }}
- </span>
- <span v-if="inProgress" class="gl-ml-2" data-testid="pipeline-running-text">
- <gl-icon name="timer" />
- {{ inProgressText }}
- </span>
- <span v-if="showDuration" class="gl-ml-2" data-testid="pipeline-duration-text">
- <gl-icon name="timer" />
- {{ durationText }}
- </span>
+ <div class="gl-display-inline-block gl-mb-3">
+ <gl-badge
+ v-if="badges.schedule"
+ v-gl-tooltip
+ :title="$options.i18n.scheduleBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.scheduleBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.child"
+ v-gl-tooltip
+ :title="$options.i18n.childBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ <gl-sprintf :message="$options.i18n.childBadgeText">
+ <template #link="{ content }">
+ <gl-link :href="paths.triggeredByPath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-badge>
+ <gl-badge
+ v-if="badges.latest"
+ v-gl-tooltip
+ :title="$options.i18n.latestBadgeTooltip"
+ variant="success"
+ size="sm"
+ >
+ {{ $options.i18n.latestBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.mergeTrainPipeline"
+ v-gl-tooltip
+ :title="$options.i18n.mergeTrainBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.mergeTrainBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.invalid"
+ v-gl-tooltip
+ :title="yamlErrors"
+ variant="danger"
+ size="sm"
+ >
+ {{ $options.i18n.invalidBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.failed"
+ v-gl-tooltip
+ :title="failureReason"
+ variant="danger"
+ size="sm"
+ >
+ {{ $options.i18n.failedBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.autoDevops"
+ v-gl-tooltip
+ :title="$options.i18n.autoDevopsBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.autoDevopsBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.detached"
+ v-gl-tooltip
+ :title="$options.i18n.detachedBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.detachedBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.stuck"
+ v-gl-tooltip
+ :title="$options.i18n.stuckBadgeTooltip"
+ variant="warning"
+ size="sm"
+ >
+ {{ $options.i18n.stuckBadgeText }}
+ </gl-badge>
+ </div>
+ <div class="gl-display-inline-block">
+ <span
+ v-gl-tooltip
+ :title="$options.i18n.totalJobsTooltip"
+ class="gl-ml-2"
+ data-testid="total-jobs"
+ >
+ <gl-icon name="pipeline" />
+ {{ totalJobsText }}
+ </span>
+ <span
+ v-if="showComputeMinutes"
+ v-gl-tooltip
+ :title="$options.i18n.computeMinutesTooltip"
+ class="gl-ml-2"
+ data-testid="compute-minutes"
+ >
+ <gl-icon name="quota" />
+ {{ computeMinutes }}
+ </span>
+ <span v-if="inProgress" class="gl-ml-2" data-testid="pipeline-running-text">
+ <gl-icon name="timer" />
+ {{ inProgressText }}
+ </span>
+ <span v-if="showDuration" class="gl-ml-2" data-testid="pipeline-duration-text">
+ <gl-icon name="timer" />
+ {{ durationText }}
+ </span>
+ </div>
</div>
</div>
- <div class="gl-mt-5 gl-lg-mt-0">
+ <div class="gl-mt-5 gl-lg-mt-0 gl-display-flex gl-align-items-flex-start gl-gap-3">
<gl-button
v-if="canRetryPipeline"
v-gl-tooltip
@@ -588,7 +590,6 @@ export default {
:title="$options.BUTTON_TOOLTIP_CANCEL"
:loading="isCanceling"
:disabled="isCanceling"
- class="gl-ml-3"
variant="danger"
data-testid="cancel-pipeline"
@click="cancelPipeline()"
@@ -601,7 +602,6 @@ export default {
v-gl-modal="$options.modal.id"
:loading="isDeleting"
:disabled="isDeleting"
- class="gl-ml-3"
variant="danger"
category="secondary"
data-testid="delete-pipeline"
diff --git a/app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql
index 1955cc9b0ac..b60afe51dd2 100644
--- a/app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql
@@ -1,4 +1,4 @@
-mutation retryFailedJob($id: CiBuildID!) {
+mutation retryFailedJob($id: CiProcessableID!) {
jobRetry(input: { id: $id }) {
job {
id
diff --git a/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js b/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js
index 53f755fda37..5d1f1ac770c 100644
--- a/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js
+++ b/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js
@@ -52,14 +52,12 @@ export default {
});
eventHub.$on('postAction', this.postAction);
- eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable);
eventHub.$on('updateTable', this.updateTable);
eventHub.$on('runMergeRequestPipeline', this.runMergeRequestPipeline);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
- eventHub.$off('retryPipeline', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable);
eventHub.$off('updateTable', this.updateTable);
eventHub.$off('runMergeRequestPipeline', this.runMergeRequestPipeline);
@@ -68,6 +66,15 @@ export default {
this.poll.stop();
},
methods: {
+ onCancelPipeline(pipeline) {
+ this.postAction(pipeline.cancel_path);
+ },
+ onRefreshPipelinesTable() {
+ this.updateTable();
+ },
+ onRetryPipeline(pipeline) {
+ this.postAction(pipeline.retry_path);
+ },
updateInternalState(parameters) {
this.poll.stop();
diff --git a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
index d38397e7479..8a7c3367fc1 100644
--- a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
@@ -31,10 +31,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
endpoint,
artifactsEndpoint,
artifactsEndpointPlaceholder,
- pipelineScheduleUrl,
- emptyStateSvgPath,
- errorStateSvgPath,
- noPipelinesSvgPath,
+ pipelineSchedulesPath,
newPipelinePath,
pipelineEditorPath,
suggestedCiTemplates,
@@ -55,13 +52,14 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
el,
apolloProvider,
provide: {
- pipelineEditorPath,
artifactsEndpoint,
artifactsEndpointPlaceholder,
- suggestedCiTemplates: JSON.parse(suggestedCiTemplates),
- iosRunnersAvailable: parseBoolean(iosRunnersAvailable),
fullPath,
+ iosRunnersAvailable: parseBoolean(iosRunnersAvailable),
manualActionsLimit: 50,
+ pipelineEditorPath,
+ pipelineSchedulesPath,
+ suggestedCiTemplates: JSON.parse(suggestedCiTemplates),
},
data() {
return {
@@ -77,22 +75,18 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
render(createElement) {
return createElement(Pipelines, {
props: {
- store: this.store,
- endpoint,
- pipelineScheduleUrl,
- emptyStateSvgPath,
- errorStateSvgPath,
- noPipelinesSvgPath,
- newPipelinePath,
canCreatePipeline: parseBoolean(canCreatePipeline),
- hasGitlabCi: parseBoolean(hasGitlabCi),
ciLintPath,
- resetCachePath,
- projectId,
defaultBranchName,
+ defaultVisibilityPipelineIdType: visibilityPipelineIdType,
+ endpoint,
+ hasGitlabCi: parseBoolean(hasGitlabCi),
+ newPipelinePath,
params: JSON.parse(params),
+ projectId,
registrationToken,
- defaultVisibilityPipelineIdType: visibilityPipelineIdType,
+ resetCachePath,
+ store: this.store,
},
});
},
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
index 58b5c0004e0..44cf11acfe2 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
@@ -7,7 +7,7 @@ import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.quer
import getPipelineEtag from '~/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import PipelineMiniGraph from '~/ci/pipeline_mini_graph/pipeline_mini_graph.vue';
import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
@@ -25,7 +25,7 @@ export const i18n = {
export default {
i18n,
components: {
- CiIcon,
+ CiBadgeLink,
GlButton,
GlIcon,
GlLink,
@@ -156,7 +156,12 @@ export default {
<template v-else>
<div class="gl-text-truncate gl-md-max-w-50p gl-mr-1">
<a :href="status.detailsPath" class="gl-mr-auto">
- <ci-icon :status="status" :size="16" data-testid="pipeline-status-icon" class="gl-mr-2" />
+ <ci-badge-link
+ :status="status"
+ size="md"
+ :show-text="false"
+ data-testid="pipeline-status-icon"
+ />
</a>
<span class="gl-font-weight-bold">
<gl-sprintf :message="$options.i18n.pipelineInfo">
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
index bbe0f1fbefc..34640d49b80 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
@@ -13,7 +13,7 @@
*/
import { GlDropdown, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { createAlert } from '~/alert';
import eventHub from '~/ci/event_hub';
import axios from '~/lib/utils/axios_utils';
@@ -33,7 +33,7 @@ export default {
positionFixed: true,
},
components: {
- CiIcon,
+ CiBadgeLink,
GlLoadingIcon,
GlDropdown,
LegacyJobItem,
@@ -126,14 +126,13 @@ export default {
@show="onShowDropdown"
>
<template #button-content>
- <ci-icon
- is-borderless
- is-interactive
- css-classes="gl-rounded-full"
- :is-active="isDropdownOpen"
- :size="24"
+ <ci-badge-link
:status="stage.status"
- class="gl-display-inline-flex gl-align-items-center gl-border gl-z-index-1"
+ size="md"
+ :show-text="false"
+ :show-tooltip="false"
+ :use-link="false"
+ class="gl-mb-0!"
/>
</template>
<div v-if="isLoading" class="gl--flex-center gl-p-2" data-testid="pipeline-stage-loading-state">
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
index 8567654a89e..cc703d29e23 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { accessValue } from './accessors/linked_pipelines_accessors';
/**
* Renders the upstream/downstream portions of the pipeline mini graph.
@@ -11,7 +11,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- CiIcon,
+ CiBadgeLink,
},
inject: {
dataMethod: {
@@ -99,24 +99,18 @@ export default {
}"
class="linked-pipeline-mini-list gl-display-inline gl-vertical-align-middle"
>
- <a
+ <ci-badge-link
v-for="pipeline in linkedPipelinesTrimmed"
:key="pipeline.id"
v-gl-tooltip="{ title: pipelineTooltipText(pipeline) }"
- :href="pipeline.path"
+ :status="pipelineStatus(pipeline)"
+ size="md"
+ :show-text="false"
+ :show-tooltip="false"
:class="triggerButtonClass(pipeline)"
- class="linked-pipeline-mini-item gl-display-inline-flex gl-mr-2 gl-my-2 gl-rounded-full gl-vertical-align-middle"
+ class="linked-pipeline-mini-item gl-mb-0!"
data-testid="linked-pipeline-mini-item"
- >
- <ci-icon
- is-borderless
- is-interactive
- css-classes="gl-rounded-full"
- :size="24"
- :status="pipelineStatus(pipeline)"
- class="gl-align-items-center gl-border gl-display-inline-flex"
- />
- </a>
+ />
<a
v-if="shouldRenderCounter"
diff --git a/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
index cc7d9bd2340..2f06b82bac0 100644
--- a/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
@@ -438,8 +438,7 @@ export default {
v-for="(variable, index) in variables"
:key="variable.uniqueId"
class="gl-mb-3 gl-pb-2"
- data-testid="ci-variable-row"
- data-qa-selector="ci_variable_row_container"
+ data-testid="ci-variable-row-container"
>
<div
class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row"
@@ -461,8 +460,7 @@ export default {
v-model="variable.key"
:placeholder="s__('CiVariables|Input variable key')"
:class="$options.formElementClasses"
- data-testid="pipeline-form-ci-variable-key"
- data-qa-selector="ci_variable_key_field"
+ data-testid="pipeline-form-ci-variable-key-field"
@change="addEmptyVariable(refFullName)"
/>
<gl-dropdown
@@ -471,12 +469,11 @@ export default {
:class="$options.formElementClasses"
class="gl-flex-grow-1 gl-mr-0!"
data-testid="pipeline-form-ci-variable-value-dropdown"
- data-qa-selector="ci_variable_value_dropdown"
>
<gl-dropdown-item
v-for="option in configVariablesWithDescription.options[variable.key]"
:key="option"
- data-qa-selector="ci_variable_value_dropdown_item"
+ data-testid="ci-variable-value-dropdown-item"
@click="setVariableAttribute(variable.key, 'value', option)"
>
{{ option }}
@@ -489,8 +486,7 @@ export default {
class="gl-mb-3"
:style="$options.textAreaStyle"
:no-resize="false"
- data-testid="pipeline-form-ci-variable-value"
- data-qa-selector="ci_variable_value_field"
+ data-testid="pipeline-form-ci-variable-value-field"
/>
<template v-if="variables.length > 1">
@@ -542,8 +538,7 @@ export default {
category="primary"
variant="confirm"
class="js-no-auto-disable gl-mr-3"
- data-qa-selector="run_pipeline_button"
- data-testid="run_pipeline_button"
+ data-testid="run-pipeline-button"
:disabled="submitted"
>{{ s__('Pipeline|Run pipeline') }}</gl-button
>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
index c993b65f6c0..386835d21d4 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
@@ -4,6 +4,7 @@ import {
GlBadge,
GlButton,
GlLoadingIcon,
+ GlPagination,
GlTabs,
GlTab,
GlSprintf,
@@ -16,12 +17,20 @@ import deletePipelineScheduleMutation from '../graphql/mutations/delete_pipeline
import playPipelineScheduleMutation from '../graphql/mutations/play_pipeline_schedule.mutation.graphql';
import takeOwnershipMutation from '../graphql/mutations/take_ownership.mutation.graphql';
import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql';
-import { ALL_SCOPE } from '../constants';
+import { ALL_SCOPE, SCHEDULES_PER_PAGE } from '../constants';
import PipelineSchedulesTable from './table/pipeline_schedules_table.vue';
import TakeOwnershipModal from './take_ownership_modal.vue';
import DeletePipelineScheduleModal from './delete_pipeline_schedule_modal.vue';
import PipelineScheduleEmptyState from './pipeline_schedules_empty_state.vue';
+const defaultPagination = {
+ first: SCHEDULES_PER_PAGE,
+ last: null,
+ prevPageCursor: '',
+ nextPageCursor: '',
+ currentPage: 1,
+};
+
export default {
i18n: {
schedulesFetchError: s__('PipelineSchedules|There was a problem fetching pipeline schedules.'),
@@ -44,6 +53,7 @@ export default {
GlBadge,
GlButton,
GlLoadingIcon,
+ GlPagination,
GlTabs,
GlTab,
GlSprintf,
@@ -72,16 +82,22 @@ export default {
// we need to ensure we send null to the API when
// the scope is 'ALL'
status: this.scope === ALL_SCOPE ? null : this.scope,
+ first: this.pagination.first,
+ last: this.pagination.last,
+ prevPageCursor: this.pagination.prevPageCursor,
+ nextPageCursor: this.pagination.nextPageCursor,
};
},
update(data) {
- const { pipelineSchedules: { nodes: list = [], count } = {} } = data.project || {};
+ const { pipelineSchedules: { nodes: list = [], count, pageInfo = {} } = {} } =
+ data.project || {};
const currentUser = data.currentUser || {};
return {
list,
count,
currentUser,
+ pageInfo,
};
},
error() {
@@ -104,6 +120,9 @@ export default {
showDeleteModal: false,
showTakeOwnershipModal: false,
count: 0,
+ pagination: {
+ ...defaultPagination,
+ },
};
},
computed: {
@@ -144,6 +163,15 @@ export default {
showEmptyState() {
return !this.isLoading && this.schedulesCount === 0 && this.onAllTab;
},
+ showPagination() {
+ return this.schedules?.pageInfo?.hasNextPage || this.schedules?.pageInfo?.hasPreviousPage;
+ },
+ prevPage() {
+ return Number(this.schedules?.pageInfo?.hasPreviousPage);
+ },
+ nextPage() {
+ return Number(this.schedules?.pageInfo?.hasNextPage);
+ },
},
watch: {
// this watcher ensures that the count on the all tab
@@ -245,10 +273,36 @@ export default {
this.reportError(this.$options.i18n.schedulePlayError);
}
},
+ resetPagination() {
+ this.pagination = {
+ ...defaultPagination,
+ };
+ },
fetchPipelineSchedulesByStatus(scope) {
this.scope = scope;
+ this.resetPagination();
this.$apollo.queries.schedules.refetch();
},
+ handlePageChange(page) {
+ const { startCursor, endCursor } = this.schedules.pageInfo;
+
+ if (page > this.pagination.currentPage) {
+ this.pagination = {
+ first: SCHEDULES_PER_PAGE,
+ last: null,
+ prevPageCursor: '',
+ nextPageCursor: endCursor,
+ currentPage: page,
+ };
+ } else {
+ this.pagination = {
+ last: SCHEDULES_PER_PAGE,
+ first: null,
+ prevPageCursor: startCursor,
+ currentPage: page,
+ };
+ }
+ },
},
};
</script>
@@ -296,14 +350,25 @@ export default {
<gl-loading-icon v-if="isLoading" size="lg" />
- <pipeline-schedules-table
- v-else
- :schedules="schedules.list"
- :current-user="schedules.currentUser"
- @showTakeOwnershipModal="setTakeOwnershipModal"
- @showDeleteModal="setDeleteModal"
- @playPipelineSchedule="playPipelineSchedule"
- />
+ <template v-else>
+ <pipeline-schedules-table
+ :schedules="schedules.list"
+ :current-user="schedules.currentUser"
+ @showTakeOwnershipModal="setTakeOwnershipModal"
+ @showDeleteModal="setDeleteModal"
+ @playPipelineSchedule="playPipelineSchedule"
+ />
+
+ <gl-pagination
+ v-if="showPagination"
+ :value="pagination.currentPage"
+ :prev-page="prevPage"
+ :next-page="nextPage"
+ align="center"
+ class="gl-mt-5"
+ @input="handlePageChange"
+ />
+ </template>
</gl-tab>
<template #tabs-end>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
index 0c3ede47015..cd1d9a97ef3 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
@@ -370,7 +370,7 @@ export default {
/>
</gl-form-group>
<!--Variable List-->
- <gl-form-group class="gl-mb-2" :label="$options.i18n.variables">
+ <gl-form-group class="gl-mb-0" :label="$options.i18n.variables">
<div
v-for="(variable, index) in variables"
:key="`var-${index}`"
@@ -456,13 +456,23 @@ export default {
<gl-form-checkbox id="schedule-active" v-model="activated" class="gl-mb-3">
{{ $options.i18n.activated }}
</gl-form-checkbox>
-
- <gl-button variant="confirm" data-testid="schedule-submit-button" @click="scheduleHandler">
- {{ buttonText }}
- </gl-button>
- <gl-button :href="schedulesPath" data-testid="schedule-cancel-button">
- {{ $options.i18n.cancel }}
- </gl-button>
+ <div class="gl-display-flex gl-gap-3 gl-flex-wrap">
+ <gl-button
+ variant="confirm"
+ data-testid="schedule-submit-button"
+ class="gl-w-full gl-sm-w-auto"
+ @click="scheduleHandler"
+ >
+ {{ buttonText }}
+ </gl-button>
+ <gl-button
+ :href="schedulesPath"
+ data-testid="schedule-cancel-button"
+ class="gl-w-full gl-sm-w-auto"
+ >
+ {{ $options.i18n.cancel }}
+ </gl-button>
+ </div>
</gl-form>
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/constants.js b/app/assets/javascripts/ci/pipeline_schedules/constants.js
index 16dab33ce29..be3feeb6623 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/constants.js
+++ b/app/assets/javascripts/ci/pipeline_schedules/constants.js
@@ -1,3 +1,4 @@
export const VARIABLE_TYPE = 'ENV_VAR';
export const FILE_TYPE = 'FILE';
export const ALL_SCOPE = 'ALL';
+export const SCHEDULES_PER_PAGE = 50;
diff --git a/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql b/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
index 29a26be0344..8fe9fbc5e24 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
@@ -1,7 +1,13 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+
query getPipelineSchedulesQuery(
$projectPath: ID!
$status: PipelineScheduleStatus
$ids: [ID!] = null
+ $first: Int
+ $last: Int
+ $prevPageCursor: String = ""
+ $nextPageCursor: String = ""
) {
currentUser {
id
@@ -9,7 +15,14 @@ query getPipelineSchedulesQuery(
}
project(fullPath: $projectPath) {
id
- pipelineSchedules(status: $status, ids: $ids) {
+ pipelineSchedules(
+ status: $status
+ ids: $ids
+ first: $first
+ last: $last
+ after: $nextPageCursor
+ before: $prevPageCursor
+ ) {
count
nodes {
id
@@ -56,6 +69,9 @@ query getPipelineSchedulesQuery(
adminPipelineSchedule
}
}
+ pageInfo {
+ ...PageInfo
+ }
}
}
}
diff --git a/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
index 6e7d6908cd9..728e8541ae3 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
@@ -47,6 +47,7 @@ export default {
v-else
title=""
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
:description="$options.i18n.noCiDescription"
/>
</div>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue b/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue
index 235126fea0c..0165bbfe69d 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue
@@ -7,28 +7,25 @@ export default {
GlButton,
},
props: {
- newPipelinePath: {
+ ciLintPath: {
type: String,
required: false,
default: null,
},
-
- resetCachePath: {
- type: String,
+ isResetCacheButtonLoading: {
+ type: Boolean,
required: false,
- default: null,
+ default: false,
},
-
- ciLintPath: {
+ newPipelinePath: {
type: String,
required: false,
default: null,
},
-
- isResetCacheButtonLoading: {
- type: Boolean,
+ resetCachePath: {
+ type: String,
required: false,
- default: false,
+ default: null,
},
},
methods: {
@@ -61,7 +58,6 @@ export default {
category="primary"
class="js-run-pipeline"
data-testid="run-pipeline-button"
- data-qa-selector="run_pipeline_button"
>
{{ s__('Pipeline|Run pipeline') }}
</gl-button>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
index 082ede60244..8f45094eb74 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
@@ -17,16 +17,15 @@ export default {
targetProjectFullPath: {
default: '',
},
+ pipelineSchedulesPath: {
+ default: '',
+ },
},
props: {
pipeline: {
type: Object,
required: true,
},
- pipelineScheduleUrl: {
- type: String,
- required: true,
- },
},
computed: {
isScheduled() {
@@ -38,6 +37,13 @@ export default {
this.pipeline?.project?.full_path !== `/${this.targetProjectFullPath}`,
);
},
+ showMergedResultsBadge() {
+ // A merge train pipeline is technically also a merged results pipeline,
+ // but we want the badges to be mutually exclusive.
+ return (
+ this.pipeline.flags.merged_result_pipeline && !this.pipeline.flags.merge_train_pipeline
+ );
+ },
autoDevopsTagId() {
return `pipeline-url-autodevops-${this.pipeline.id}`;
},
@@ -52,7 +58,7 @@ export default {
<gl-badge
v-if="isScheduled"
v-gl-tooltip
- :href="pipelineScheduleUrl"
+ :href="pipelineSchedulesPath"
target="__blank"
:title="__('This pipeline was created by a schedule.')"
variant="info"
@@ -74,7 +80,7 @@ export default {
v-gl-tooltip
:title="
s__(
- 'Pipeline|This pipeline ran on the contents of this merge request combined with the contents of all other merge requests queued for merging into the target branch.',
+ 'Pipeline|This pipeline ran on the contents of the merge request combined with the contents of all other merge requests queued for merging into the target branch.',
)
"
variant="info"
@@ -149,7 +155,7 @@ export default {
v-gl-tooltip
:title="
s__(
- `Pipeline|This pipeline ran on the contents of this merge request's source branch, not the target branch.`,
+ `Pipeline|This pipeline ran on the contents of the merge request's source branch, not the target branch.`,
)
"
variant="info"
@@ -158,6 +164,19 @@ export default {
>{{ s__('Pipeline|merge request') }}</gl-badge
>
<gl-badge
+ v-if="showMergedResultsBadge"
+ v-gl-tooltip
+ :title="
+ s__(
+ `Pipeline|This pipeline ran on the contents of the merge request combined with the contents of the target branch.`,
+ )
+ "
+ variant="info"
+ size="sm"
+ data-testid="pipeline-url-merged-results"
+ >{{ s__('Pipeline|merged results') }}</gl-badge
+ >
+ <gl-badge
v-if="isInFork"
v-gl-tooltip
:title="__('Pipeline ran in fork of project')"
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue
index b05bdae65c4..8945bb06862 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue
@@ -1,22 +1,22 @@
<script>
-import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Tracking from '~/tracking';
import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL, TRACKING_CATEGORIES } from '~/ci/constants';
-import eventHub from '../../event_hub';
import PipelineMultiActions from './pipeline_multi_actions.vue';
import PipelinesManualActions from './pipelines_manual_actions.vue';
+import PipelineStopModal from './pipeline_stop_modal.vue';
export default {
BUTTON_TOOLTIP_RETRY,
BUTTON_TOOLTIP_CANCEL,
directives: {
GlTooltip: GlTooltipDirective,
- GlModalDirective,
},
components: {
GlButton,
PipelineMultiActions,
PipelinesManualActions,
+ PipelineStopModal,
},
mixins: [Tracking.mixin()],
props: {
@@ -24,15 +24,12 @@ export default {
type: Object,
required: true,
},
- cancelingPipeline: {
- type: Number,
- required: false,
- default: null,
- },
},
data() {
return {
+ isCanceling: false,
isRetrying: false,
+ showConfirmationModal: false,
};
},
computed: {
@@ -41,27 +38,36 @@ export default {
this.pipeline?.details?.has_manual_actions || this.pipeline?.details?.has_scheduled_actions
);
},
- isCancelling() {
- return this.cancelingPipeline === this.pipeline.id;
- },
},
watch: {
pipeline() {
- this.isRetrying = false;
+ if (this.isCanceling || this.isRetrying) {
+ this.isCanceling = false;
+ this.isRetrying = false;
+ }
},
},
methods: {
+ onCloseModal() {
+ this.showConfirmationModal = false;
+ },
+ onConfirmCancelPipeline() {
+ this.isCanceling = true;
+ this.showConfirmationModal = false;
+
+ this.$emit('cancel-pipeline', this.pipeline);
+ },
handleCancelClick() {
+ this.showConfirmationModal = true;
+
this.trackClick('click_cancel_button');
- eventHub.$emit('openConfirmationModal', {
- pipeline: this.pipeline,
- endpoint: this.pipeline.cancel_path,
- });
},
handleRetryClick() {
this.isRetrying = true;
+
this.trackClick('click_retry_button');
- eventHub.$emit('retryPipeline', this.pipeline.retry_path);
+
+ this.$emit('retry-pipeline', this.pipeline);
},
trackClick(action) {
this.track(action, { label: TRACKING_CATEGORIES.table });
@@ -72,8 +78,19 @@ export default {
<template>
<div class="gl-text-right">
+ <pipeline-stop-modal
+ :pipeline="pipeline"
+ :show-confirmation-modal="showConfirmationModal"
+ @submit="onConfirmCancelPipeline"
+ @close-modal="onCloseModal"
+ />
+
<div class="btn-group">
- <pipelines-manual-actions v-if="hasActions" :iid="pipeline.iid" />
+ <pipelines-manual-actions
+ v-if="hasActions"
+ :iid="pipeline.iid"
+ @refresh-pipeline-table="$emit('refresh-pipelines-table')"
+ />
<gl-button
v-if="pipeline.flags.retryable"
@@ -83,7 +100,6 @@ export default {
:disabled="isRetrying"
:loading="isRetrying"
class="js-pipelines-retry-button"
- data-qa-selector="pipeline_retry_button"
data-testid="pipelines-retry-button"
icon="retry"
variant="default"
@@ -94,11 +110,10 @@ export default {
<gl-button
v-if="pipeline.flags.cancelable"
v-gl-tooltip.hover
- v-gl-modal-directive="'confirmation-modal'"
:aria-label="$options.BUTTON_TOOLTIP_CANCEL"
:title="$options.BUTTON_TOOLTIP_CANCEL"
- :loading="isCancelling"
- :disabled="isCancelling"
+ :loading="isCanceling"
+ :disabled="isCanceling"
icon="cancel"
variant="danger"
category="primary"
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipelines_status_badge.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
index 2da9141df8e..20e2c7e9dce 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipelines_status_badge.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
@@ -1,6 +1,5 @@
<script>
import { TRACKING_CATEGORIES } from '~/ci/constants';
-import { CHILD_VIEW } from '~/ci/pipeline_details/constants';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import Tracking from '~/tracking';
import PipelinesTimeago from './time_ago.vue';
@@ -16,18 +15,11 @@ export default {
type: Object,
required: true,
},
- viewType: {
- type: String,
- required: true,
- },
},
computed: {
pipelineStatus() {
return this.pipeline?.details?.status ?? {};
},
- isChildView() {
- return this.viewType === CHILD_VIEW;
- },
},
methods: {
trackClick() {
@@ -39,13 +31,7 @@ export default {
<template>
<div>
- <ci-badge-link
- class="gl-mb-3"
- :status="pipelineStatus"
- :show-text="!isChildView"
- data-qa-selector="pipeline_commit_status"
- @ciStatusBadgeClick="trackClick"
- />
+ <ci-badge-link class="gl-mb-3" :status="pipelineStatus" @ciStatusBadgeClick="trackClick" />
<pipelines-timeago :pipeline="pipeline" />
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
index 9f38be668f2..d62a68f0dcc 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
@@ -7,7 +7,7 @@ import CiIcon from '~/vue_shared/components/ci_icon.vue';
/**
* Pipeline Stop Modal.
*
- * Renders the modal used to confirm stopping a pipeline.
+ * Renders the modal used to confirm cancelling a pipeline.
*/
export default {
components: {
@@ -22,8 +22,15 @@ export default {
required: true,
deep: true,
},
+ showConfirmationModal: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
+ hasRef() {
+ return !isEmpty(this.pipeline.ref);
+ },
modalTitle() {
return sprintf(
s__('Pipeline|Stop pipeline #%{pipelineId}?'),
@@ -34,10 +41,7 @@ export default {
);
},
modalText() {
- return s__(`Pipeline|You’re about to stop pipeline #%{pipelineId}.`);
- },
- hasRef() {
- return !isEmpty(this.pipeline.ref);
+ return s__(`Pipeline|You're about to stop pipeline #%{pipelineId}.`);
},
primaryProps() {
return {
@@ -45,10 +49,13 @@ export default {
attributes: { variant: 'danger' },
};
},
- cancelProps() {
- return {
- text: __('Cancel'),
- };
+ showModal: {
+ get() {
+ return this.showConfirmationModal;
+ },
+ set() {
+ this.$emit('close-modal');
+ },
},
},
methods: {
@@ -56,14 +63,16 @@ export default {
this.$emit('submit', event);
},
},
+ cancelProps: { text: __('Cancel') },
};
</script>
<template>
<gl-modal
+ v-model="showModal"
modal-id="confirmation-modal"
:title="modalTitle"
:action-primary="primaryProps"
- :action-cancel="cancelProps"
+ :action-cancel="$options.cancelProps"
@primary="emitSubmit($event)"
>
<p>
@@ -74,7 +83,7 @@ export default {
</gl-sprintf>
</p>
- <p v-if="pipeline">
+ <p>
<ci-icon
v-if="pipeline.details"
:status="pipeline.details.status"
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue
index edaeb481d7b..9a49eefbf98 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue
@@ -4,7 +4,7 @@ import { __ } from '~/locale';
import Tracking from '~/tracking';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import { ICONS, TRACKING_CATEGORIES } from '~/ci/constants';
+import { ICONS, PIPELINE_ID_KEY, PIPELINE_IID_KEY, TRACKING_CATEGORIES } from '~/ci/constants';
import PipelineLabels from './pipeline_labels.vue';
export default {
@@ -24,13 +24,13 @@ export default {
type: Object,
required: true,
},
- pipelineScheduleUrl: {
+ pipelineIdType: {
type: String,
- required: true,
- },
- pipelineKey: {
- type: String,
- required: true,
+ required: false,
+ default: PIPELINE_ID_KEY,
+ validator(value) {
+ return value === PIPELINE_IID_KEY || value === PIPELINE_ID_KEY;
+ },
},
refClass: {
type: String,
@@ -173,9 +173,8 @@ export default {
:href="pipeline.path"
class="gl-mr-1 gl-text-blue-500!"
data-testid="pipeline-url-link"
- data-qa-selector="pipeline_url_link"
@click="trackClick('click_pipeline_id')"
- >#{{ pipeline[pipelineKey] }}</gl-link
+ >#{{ pipeline[pipelineIdType] }}</gl-link
>
<!--Commit row-->
<div class="gl-display-inline-flex gl-rounded-base gl-px-2 gl-bg-gray-50 gl-text-gray-700">
@@ -237,6 +236,6 @@ export default {
/>
<!--End of commit row-->
</div>
- <pipeline-labels :pipeline-schedule-url="pipelineScheduleUrl" :pipeline="pipeline" />
+ <pipeline-labels :pipeline="pipeline" />
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue b/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue
index 4dacd474bde..ebf1744aee2 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue
@@ -6,7 +6,6 @@ import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_m
import { s__, __, sprintf } from '~/locale';
import Tracking from '~/tracking';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
-import eventHub from '../../event_hub';
import { TRACKING_CATEGORIES } from '../../constants';
import getPipelineActionsQuery from '../graphql/queries/get_pipeline_actions.query.graphql';
@@ -94,7 +93,7 @@ export default {
.post(`${action.playPath}.json`)
.then(() => {
this.isLoading = false;
- eventHub.$emit('updateTable');
+ this.$emit('refresh-pipeline-table');
})
.catch(() => {
this.isLoading = false;
diff --git a/app/assets/javascripts/ci/pipelines_page/pipelines.vue b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
index 87ee5463bb0..faa013079be 100644
--- a/app/assets/javascripts/ci/pipelines_page/pipelines.vue
+++ b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
@@ -1,5 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
+import NO_PIPELINES_SVG from '@gitlab/svgs/dist/illustrations/empty-state/empty-pipeline-md.svg?url';
+import ERROR_STATE_SVG from '@gitlab/svgs/dist/illustrations/pipelines_failed.svg?url';
import { GlEmptyState, GlIcon, GlLoadingIcon, GlCollapsibleListbox } from '@gitlab/ui';
import { isEqual } from 'lodash';
import * as Sentry from '@sentry/browser';
@@ -9,11 +11,12 @@ import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
import {
FILTER_TAG_IDENTIFIER,
- PipelineKeyOptions,
+ PIPELINE_ID_KEY,
+ PIPELINE_IID_KEY,
RAW_TEXT_WARNING,
TRACKING_CATEGORIES,
} from '~/ci/constants';
-import PipelinesTableComponent from '~/ci/common/pipelines_table.vue';
+import PipelinesTable from '~/ci/common/pipelines_table.vue';
import PipelinesMixin from '~/ci/pipeline_details/mixins/pipelines_mixin';
import { validateParams } from '~/ci/pipeline_details/utils';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
@@ -27,7 +30,6 @@ import NavigationControls from './components/nav_controls.vue';
import PipelinesFilteredSearch from './components/pipelines_filtered_search.vue';
export default {
- PipelineKeyOptions,
components: {
NoCiEmptyState,
GlCollapsibleListbox,
@@ -37,7 +39,7 @@ export default {
NavigationTabs,
NavigationControls,
PipelinesFilteredSearch,
- PipelinesTableComponent,
+ PipelinesTable,
TablePagination,
},
mixins: [PipelinesMixin, Tracking.mixin()],
@@ -46,36 +48,10 @@ export default {
type: Object,
required: true,
},
- // Can be rendered in 3 different places, with some visual differences
- // Accepts root | child
- // `root` -> main view
- // `child` -> rendered inside MR or Commit View
- viewType: {
- type: String,
- required: false,
- default: 'root',
- },
endpoint: {
type: String,
required: true,
},
- pipelineScheduleUrl: {
- type: String,
- required: false,
- default: '',
- },
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- errorStateSvgPath: {
- type: String,
- required: true,
- },
- noPipelinesSvgPath: {
- type: String,
- required: true,
- },
hasGitlabCi: {
type: Boolean,
required: true,
@@ -243,8 +219,9 @@ export default {
},
selectedPipelineKeyOption() {
return (
- this.$options.PipelineKeyOptions.find((e) => this.visibilityPipelineIdType === e.value) ||
- this.$options.PipelineKeyOptions[0]
+ this.$options.pipelineKeyOptions.find(
+ (option) => this.visibilityPipelineIdType === option.value,
+ ) || this.$options.pipelineKeyOptions[0]
);
},
},
@@ -334,11 +311,12 @@ export default {
},
changeVisibilityPipelineIDType(idType) {
this.visibilityPipelineIdType = idType;
- this.saveVisibilityPipelineIDType(idType);
+
+ if (isLoggedIn()) {
+ this.saveVisibilityPipelineIDType(idType);
+ }
},
saveVisibilityPipelineIDType(idType) {
- if (!isLoggedIn()) return;
-
this.$apollo
.mutate({
mutation: setSortPreferenceMutation,
@@ -354,6 +332,20 @@ export default {
});
},
},
+ errorStateSvgPath: ERROR_STATE_SVG,
+ noPipelinesSvgPath: NO_PIPELINES_SVG,
+ pipelineKeyOptions: [
+ {
+ text: __('Show Pipeline ID'),
+ label: __('Pipeline ID'),
+ value: PIPELINE_ID_KEY,
+ },
+ {
+ text: __('Show Pipeline IID'),
+ label: __('Pipeline IID'),
+ value: PIPELINE_IID_KEY,
+ },
+ ],
};
</script>
<template>
@@ -393,9 +385,8 @@ export default {
/>
<gl-collapsible-listbox
v-model="visibilityPipelineIdType"
- data-testid="pipeline-key-collapsible-box"
:toggle-text="selectedPipelineKeyOption.text"
- :items="$options.PipelineKeyOptions"
+ :items="$options.pipelineKeyOptions"
@select="changeVisibilityPipelineIDType"
/>
</div>
@@ -411,32 +402,34 @@ export default {
<no-ci-empty-state
v-else-if="stateToRender === $options.stateMap.emptyState"
- :empty-state-svg-path="emptyStateSvgPath"
+ :empty-state-svg-path="$options.noPipelinesSvgPath"
:can-set-ci="canCreatePipeline"
:registration-token="registrationToken"
/>
<gl-empty-state
v-else-if="stateToRender === $options.stateMap.error"
- :svg-path="errorStateSvgPath"
+ :svg-path="$options.errorStateSvgPath"
+ :svg-height="null"
:title="s__('Pipelines|There was an error fetching the pipelines.')"
:description="s__('Pipelines|Try again in a few moments or contact your support team.')"
/>
<gl-empty-state
v-else-if="stateToRender === $options.stateMap.emptyTab"
- :svg-path="noPipelinesSvgPath"
+ :svg-path="$options.noPipelinesSvgPath"
:svg-height="150"
:title="emptyTabMessage"
/>
<div v-else-if="stateToRender === $options.stateMap.tableList">
- <pipelines-table-component
+ <pipelines-table
:pipelines="state.pipelines"
- :pipeline-schedule-url="pipelineScheduleUrl"
:update-graph-dropdown="updateGraphDropdown"
- :view-type="viewType"
- :pipeline-key-option="selectedPipelineKeyOption"
+ :pipeline-id-type="selectedPipelineKeyOption.value"
+ @cancel-pipeline="onCancelPipeline"
+ @refresh-pipelines-table="onRefreshPipelinesTable"
+ @retry-pipeline="onRetryPipeline"
/>
</div>
diff --git a/app/assets/javascripts/ci/runner/components/registration/utils.js b/app/assets/javascripts/ci/runner/components/registration/utils.js
index c8a75506c9c..c1885be9585 100644
--- a/app/assets/javascripts/ci/runner/components/registration/utils.js
+++ b/app/assets/javascripts/ci/runner/components/registration/utils.js
@@ -3,8 +3,8 @@ import {
LINUX_PLATFORM,
MACOS_PLATFORM,
WINDOWS_PLATFORM,
- DOWNLOAD_LOCATIONS,
-} from '../../constants';
+ RUNNER_PACKAGE_HOST,
+} from 'jh_else_ce/ci/runner/constants';
import linuxInstall from './scripts/linux/install.sh?raw';
import osxInstall from './scripts/osx/install.sh?raw';
import windowsInstall from './scripts/windows/install.ps1?raw';
@@ -27,6 +27,47 @@ const OS = {
},
};
+export const DOWNLOAD_LOCATIONS = {
+ [LINUX_PLATFORM]: [
+ {
+ arch: 'amd64',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-linux-amd64`,
+ },
+ {
+ arch: '386',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-linux-386`,
+ },
+ {
+ arch: 'arm',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-linux-arm`,
+ },
+ {
+ arch: 'arm64',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-linux-arm64`,
+ },
+ ],
+ [MACOS_PLATFORM]: [
+ {
+ arch: 'amd64',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-darwin-amd64`,
+ },
+ {
+ arch: 'arm64',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-darwin-arm64`,
+ },
+ ],
+ [WINDOWS_PLATFORM]: [
+ {
+ arch: 'amd64',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-windows-amd64.exe`,
+ },
+ {
+ arch: '386',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-windows-386.exe`,
+ },
+ ],
+};
+
export const commandPrompt = ({ platform }) => {
return (OS[platform] || OS[DEFAULT_PLATFORM]).commandPrompt;
};
diff --git a/app/assets/javascripts/ci/runner/components/runner_details.vue b/app/assets/javascripts/ci/runner/components/runner_details.vue
index fac90fb0370..0ec2ef30c20 100644
--- a/app/assets/javascripts/ci/runner/components/runner_details.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_details.vue
@@ -29,10 +29,6 @@ export default {
import('ee_component/ci/runner/components/runner_maintenance_note_detail.vue'),
RunnerGroups,
RunnerProjects,
- RunnerUpgradeStatusBadge: () =>
- import('ee_component/ci/runner/components/runner_upgrade_status_badge.vue'),
- RunnerUpgradeStatusAlert: () =>
- import('ee_component/ci/runner/components/runner_upgrade_status_alert.vue'),
RunnerTags,
RunnerManagersDetail,
TimeAgo,
@@ -92,7 +88,6 @@ export default {
<template>
<div>
- <runner-upgrade-status-alert class="gl-my-4" :runner="runner" />
<div class="gl-pt-4">
<dl class="gl-mb-0 gl-display-grid runner-details-grid-template">
<runner-detail :label="s__('Runners|Description')" :value="runner.description" />
@@ -104,16 +99,6 @@ export default {
<time-ago :time="runner.contactedAt" />
</template>
</runner-detail>
- <runner-detail :label="s__('Runners|Version')">
- <template v-if="runner.version" #value>
- {{ runner.version }}
- <runner-upgrade-status-badge size="sm" :runner="runner" />
- </template>
- </runner-detail>
- <runner-detail :label="s__('Runners|IP Address')" :value="runner.ipAddress" />
- <runner-detail :label="s__('Runners|Executor')" :value="runner.executorName" />
- <runner-detail :label="s__('Runners|Architecture')" :value="runner.architectureName" />
- <runner-detail :label="s__('Runners|Platform')" :value="runner.platformName" />
<runner-detail :label="s__('Runners|Configuration')">
<template v-if="configTextProtected || configTextUntagged" #value>
<gl-intersperse>
diff --git a/app/assets/javascripts/ci/runner/components/runner_form_fields.vue b/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
index 38e36733045..b8c80986fbc 100644
--- a/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
@@ -92,9 +92,7 @@ export default {
<gl-form-group :label="__('Tags')" label-for="runner-tags">
<template #description>
<gl-sprintf
- :message="
- s__('Runners|Multiple tags must be separated by a comma. For example, %{example}.')
- "
+ :message="s__('Runners|Separate multiple tags with a comma. For example, %{example}.')"
>
<template #example>
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
@@ -106,7 +104,7 @@ export default {
<gl-sprintf
:message="
s__(
- 'Runners|Add tags for the types of jobs the runner processes to ensure that the runner only runs jobs that you intend it to. %{helpLinkStart}Learn more.%{helpLinkEnd}',
+ 'Runners|Add tags to specify jobs that the runner can run. %{helpLinkStart}Learn more.%{helpLinkEnd}',
)
"
>
@@ -191,7 +189,9 @@ export default {
)
"
label-for="runner-max-timeout"
- :description="s__('Runners|Enter the number of seconds.')"
+ :description="
+ s__('Runners|Enter the job timeout in seconds. Must be a minimum of 600 seconds.')
+ "
>
<gl-form-input
id="runner-max-timeout"
diff --git a/app/assets/javascripts/ci/runner/components/runner_header.vue b/app/assets/javascripts/ci/runner/components/runner_header.vue
index 55a33ef2074..0fa06537ed6 100644
--- a/app/assets/javascripts/ci/runner/components/runner_header.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_header.vue
@@ -13,6 +13,8 @@ export default {
TimeAgo,
RunnerTypeBadge,
RunnerStatusBadge,
+ RunnerUpgradeStatusBadge: () =>
+ import('ee_component/ci/runner/components/runner_upgrade_status_badge.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -40,6 +42,7 @@ export default {
<div class="gl-display-flex gl-align-items-flex-start gl-gap-3 gl-flex-wrap gl-mt-3">
<runner-status-badge :contacted-at="runner.contactedAt" :status="runner.status" />
<runner-type-badge :type="runner.runnerType" />
+ <runner-upgrade-status-badge :runner="runner" />
<span v-if="runner.createdAt">
<gl-sprintf :message="__('%{locked} created %{timeago}')">
<template #locked>
diff --git a/app/assets/javascripts/ci/runner/components/runner_type_icon.vue b/app/assets/javascripts/ci/runner/components/runner_type_icon.vue
new file mode 100644
index 00000000000..c56f28e10a3
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_type_icon.vue
@@ -0,0 +1,62 @@
+<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import {
+ INSTANCE_TYPE,
+ GROUP_TYPE,
+ PROJECT_TYPE,
+ I18N_INSTANCE_TYPE,
+ I18N_INSTANCE_RUNNER_DESCRIPTION,
+ I18N_GROUP_TYPE,
+ I18N_GROUP_RUNNER_DESCRIPTION,
+ I18N_PROJECT_TYPE,
+ I18N_PROJECT_RUNNER_DESCRIPTION,
+} from '../constants';
+
+const ICON_DATA = {
+ [INSTANCE_TYPE]: {
+ name: 'users',
+ tooltip: `${I18N_INSTANCE_TYPE}: ${I18N_INSTANCE_RUNNER_DESCRIPTION}`,
+ },
+ [GROUP_TYPE]: {
+ name: 'group',
+ tooltip: `${I18N_GROUP_TYPE}: ${I18N_GROUP_RUNNER_DESCRIPTION}`,
+ },
+ [PROJECT_TYPE]: {
+ name: 'project',
+ tooltip: `${I18N_PROJECT_TYPE}: ${I18N_PROJECT_RUNNER_DESCRIPTION}`,
+ },
+};
+
+export default {
+ components: {
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ type: {
+ type: String,
+ required: false,
+ default: null,
+ validator(type) {
+ return Boolean(ICON_DATA[type]);
+ },
+ },
+ },
+ computed: {
+ icon() {
+ return ICON_DATA[this.type];
+ },
+ },
+};
+</script>
+<template>
+ <gl-icon
+ v-if="icon"
+ v-gl-tooltip="icon.tooltip"
+ :aria-label="icon.tooltip"
+ :name="icon.name"
+ v-bind="$attrs"
+ />
+</template>
diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js
index 3293c68ddb8..b3cc295f8e4 100644
--- a/app/assets/javascripts/ci/runner/constants.js
+++ b/app/assets/javascripts/ci/runner/constants.js
@@ -216,54 +216,8 @@ export const LINUX_PLATFORM = 'linux';
export const MACOS_PLATFORM = 'osx';
export const WINDOWS_PLATFORM = 'windows';
-export const DOWNLOAD_LOCATIONS = {
- [LINUX_PLATFORM]: [
- {
- arch: 'amd64',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64',
- },
- {
- arch: '386',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-386',
- },
- {
- arch: 'arm',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm',
- },
- {
- arch: 'arm64',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64',
- },
- ],
- [MACOS_PLATFORM]: [
- {
- arch: 'amd64',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64',
- },
- {
- arch: 'arm64',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-arm64',
- },
- ],
- [WINDOWS_PLATFORM]: [
- {
- arch: 'amd64',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe',
- },
- {
- arch: '386',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-386.exe',
- },
- ],
-};
+// About Gitlab Runner Package host
+export const RUNNER_PACKAGE_HOST = 'gitlab-runner-downloads.s3.amazonaws.com';
export const DEFAULT_PLATFORM = LINUX_PLATFORM;
diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql
index 1a2ad59650e..e2c890b3834 100644
--- a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql
@@ -6,10 +6,6 @@ fragment RunnerDetailsShared on CiRunner {
accessLevel
runUntagged
locked
- ipAddress
- executorName
- architectureName
- platformName
description
maximumTimeout
jobCount
diff --git a/app/assets/javascripts/ci/runner/sentry_utils.js b/app/assets/javascripts/ci/runner/sentry_utils.js
index 29de1f9adae..25fecdcfa7d 100644
--- a/app/assets/javascripts/ci/runner/sentry_utils.js
+++ b/app/assets/javascripts/ci/runner/sentry_utils.js
@@ -6,15 +6,16 @@ const COMPONENT_TAG = 'vue_component';
* Captures an error in a Vue component and sends it
* to Sentry
*
- * @param {Object} options
- * @param {Error} options.error - Exception or error
- * @param {String} options.component - Component name in CamelCase format
+ * @param {Object} options Exception details
+ * @param {Object} options.error An exception-like object
+ * @param {string} [options.component=] Component name in CamelCase format
*/
export const captureException = ({ error, component }) => {
- Sentry.withScope((scope) => {
- if (component) {
- scope.setTag(COMPONENT_TAG, component);
- }
+ if (component) {
+ Sentry.captureException(error, {
+ tags: { [COMPONENT_TAG]: component },
+ });
+ } else {
Sentry.captureException(error);
- });
+ }
};
diff --git a/app/assets/javascripts/ci/utils.js b/app/assets/javascripts/ci/utils.js
index eb9e9538b75..8a4f28404c6 100644
--- a/app/assets/javascripts/ci/utils.js
+++ b/app/assets/javascripts/ci/utils.js
@@ -1,17 +1,9 @@
import * as Sentry from '@sentry/browser';
export const reportToSentry = (component, failureType) => {
- Sentry.withScope((scope) => {
- scope.setTag('component', component);
- Sentry.captureException(failureType);
- });
-};
-
-export const reportMessageToSentry = (component, message, context) => {
- Sentry.withScope((scope) => {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- scope.setContext('Vue data', context);
- scope.setTag('component', component);
- Sentry.captureMessage(message);
+ Sentry.captureException(failureType, {
+ tags: {
+ component,
+ },
});
};
diff --git a/app/assets/javascripts/clusters_list/components/agent_empty_state.vue b/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
index 2f45ef8a862..4a20f9ec10d 100644
--- a/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
+++ b/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
@@ -16,7 +16,11 @@ export default {
</script>
<template>
- <gl-empty-state :svg-path="emptyStateImage" :svg-height="100">
+ <gl-empty-state
+ :svg-path="emptyStateImage"
+ :svg-height="100"
+ data-testid="cluster-agent-empty-state"
+ >
<template #title>
<gl-sprintf :message="$options.i18n.introText">
<template #link="{ content }">
diff --git a/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue b/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue
index f4134ab5072..339ea3b7c0d 100644
--- a/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue
@@ -22,7 +22,11 @@ export default {
<template>
<div>
- <gl-empty-state :svg-path="clustersEmptyStateImage" :svg-height="100">
+ <gl-empty-state
+ :svg-path="clustersEmptyStateImage"
+ :svg-height="100"
+ data-testid="clusters-empty-state"
+ >
<template #title>
<p>
<gl-sprintf :message="$options.i18n.introText">
diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js
index 1ea18dcc97d..4537fd51fcf 100644
--- a/app/assets/javascripts/clusters_list/store/actions.js
+++ b/app/assets/javascripts/clusters_list/store/actions.js
@@ -17,9 +17,10 @@ const allNodesPresent = (clusters, retryCount) => {
};
export const reportSentryError = (_store, { error, tag }) => {
- Sentry.withScope((scope) => {
- scope.setTag('javascript_clusters_list', tag);
- Sentry.captureException(error);
+ Sentry.captureException(error, {
+ tags: {
+ javascript_clusters_list: tag,
+ },
});
};
diff --git a/app/assets/javascripts/comment_templates/components/form.vue b/app/assets/javascripts/comment_templates/components/form.vue
index c29482eab7a..5a5d221591a 100644
--- a/app/assets/javascripts/comment_templates/components/form.vue
+++ b/app/assets/javascripts/comment_templates/components/form.vue
@@ -93,7 +93,7 @@ export default {
this.$emit('saved');
this.updateCommentTemplate = { name: '', content: '' };
this.showValidation = false;
- this.track_event('i_code_review_saved_replies_create');
+ this.trackEvent('i_code_review_saved_replies_create');
}
},
})
diff --git a/app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue b/app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue
index 5e84dcbe48e..1954f9f8f35 100644
--- a/app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue
+++ b/app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue
@@ -2,8 +2,8 @@
import { GlButton, GlEmptyState, GlLoadingIcon, GlModal, GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { getParameterByName } from '~/lib/utils/url_utility';
-import PipelinesTableComponent from '~/ci/common/pipelines_table.vue';
-import { PipelineKeyOptions } from '~/ci/constants';
+import PipelinesTable from '~/ci/common/pipelines_table.vue';
+import { PIPELINE_ID_KEY } from '~/ci/constants';
import eventHub from '~/ci/event_hub';
import PipelinesMixin from '~/ci/pipeline_details/mixins/pipelines_mixin';
import PipelinesService from '~/ci/pipelines_page/services/pipelines_service';
@@ -13,7 +13,6 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__, __ } from '~/locale';
export default {
- PipelineKeyOptions,
components: {
GlButton,
GlEmptyState,
@@ -21,7 +20,7 @@ export default {
GlLoadingIcon,
GlModal,
GlSprintf,
- PipelinesTableComponent,
+ PipelinesTable,
TablePagination,
},
mixins: [PipelinesMixin, glFeatureFlagMixin()],
@@ -180,6 +179,7 @@ export default {
}
},
},
+ pipelineIdKey: PIPELINE_ID_KEY,
modal: {
actionPrimary: {
text: s__('Pipeline|Run pipeline'),
@@ -225,6 +225,7 @@ export default {
<gl-empty-state
v-else-if="shouldRenderErrorState"
:svg-path="errorStateSvgPath"
+ :svg-height="null"
:title="
s__(`Pipelines|There was an error fetching the pipelines.
Try again in a few moments or contact your support team.`)
@@ -279,11 +280,14 @@ export default {
{{ $options.i18n.runPipelineText }}
</gl-button>
- <pipelines-table-component
+ <pipelines-table
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:view-type="viewType"
- :pipeline-key-option="$options.PipelineKeyOptions[0]"
+ :pipeline-id-type="$options.pipelineIdKey"
+ @cancel-pipeline="onCancelPipeline"
+ @refresh-pipelines-table="onRefreshPipelinesTable"
+ @retry-pipeline="onRetryPipeline"
>
<template #table-header-actions>
<div v-if="canRenderPipelineButton" class="gl-text-right">
@@ -296,7 +300,7 @@ export default {
</gl-button>
</div>
</template>
- </pipelines-table-component>
+ </pipelines-table>
</div>
<gl-modal
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index beeb9b9ada4..6ca59f634a2 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -11,7 +11,7 @@ const apolloProvider = new VueApollo({
/**
* Used in:
- * - Project Pipelines List (projects:pipelines:index)
+ * - Project Pipelines List (projects:pipelines)
* - Commit details View > Pipelines Tab > Pipelines Table (projects:commit:pipelines)
* - Merge request details View > Pipelines Tab > Pipelines Table (projects:merge_requests:show)
* - New merge request View > Pipelines Tab > Pipelines Table (projects:merge_requests:creations:new)
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 25c03496a76..2e9388c1e20 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -4,6 +4,8 @@ import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
import { __ } from '~/locale';
import { VARIANT_DANGER } from '~/alert';
import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
+import { CONTENT_EDITOR_READY_EVENT } from '~/vue_shared/constants';
+import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub';
import { createContentEditor } from '../services/create_content_editor';
import { ALERT_EVENT, TIPTAP_AUTOFOCUS_OPTIONS } from '../constants';
import ContentEditorAlert from './content_editor_alert.vue';
@@ -161,9 +163,10 @@ export default {
},
});
},
- mounted() {
+ async mounted() {
this.$emit('initialized');
- this.setSerializedContent(this.markdown);
+ await this.setSerializedContent(this.markdown);
+ markdownEditorEventHub.$emit(CONTENT_EDITOR_READY_EVENT);
},
beforeDestroy() {
this.contentEditor.dispose();
@@ -238,11 +241,7 @@ export default {
@keydown="$emit('keydown', $event)"
/>
<content-editor-alert />
- <div
- data-testid="content-editor"
- data-qa-selector="content_editor_container"
- :class="{ 'is-focused': focused }"
- >
+ <div data-testid="content-editor" :class="{ 'is-focused': focused }">
<formatting-toolbar
ref="toolbar"
:supports-quick-actions="supportsQuickActions"
@@ -275,7 +274,8 @@ export default {
target="_blank"
category="tertiary"
size="small"
- title="Markdown is supported"
+ :title="__('Markdown is supported')"
+ :aria-label="__('Markdown is supported')"
class="gl-px-3!"
/>
</div>
diff --git a/app/assets/javascripts/content_editor/components/content_editor_provider.vue b/app/assets/javascripts/content_editor/components/content_editor_provider.vue
index fa842f23cc3..955fa129ce7 100644
--- a/app/assets/javascripts/content_editor/components/content_editor_provider.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor_provider.vue
@@ -1,13 +1,7 @@
<script>
export default {
provide() {
- // We can't use this.contentEditor due to bug in vue-apollo when
- // provide is called in beforeCreate
- // See https://github.com/vuejs/vue-apollo/pull/1153 for details
-
- // @vue-compat does not care to normalize propsData fields
- const contentEditor =
- this.$options.propsData.contentEditor || this.$options.propsData['content-editor'];
+ const { contentEditor } = this;
return {
contentEditor,
diff --git a/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue b/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue
index 4cf150dd948..78a01693f14 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue
@@ -58,7 +58,7 @@ export default {
name="content_editor_image"
class="gl-display-none"
:aria-label="$options.i18n.inputLabel"
- data-qa-selector="file_upload_field"
+ data-testid="file-upload-field"
@change="onFileSelect"
/>
</span>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue b/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue
index bd30bdcea0c..4b1e14665de 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue
@@ -75,7 +75,7 @@ export default {
:selected="activeItemLabel"
:disabled="!activeItem"
:data-qa-text-style="activeItemLabel"
- data-qa-selector="text_style_dropdown"
+ data-testid="text-style-dropdown"
size="small"
toggle-class="btn-default-tertiary"
@select="execute"
diff --git a/app/assets/javascripts/content_editor/extensions/selection.js b/app/assets/javascripts/content_editor/extensions/selection.js
index 2e0bb29e5a1..0c24207b395 100644
--- a/app/assets/javascripts/content_editor/extensions/selection.js
+++ b/app/assets/javascripts/content_editor/extensions/selection.js
@@ -6,12 +6,22 @@ export default Extension.create({
name: 'selection',
addProseMirrorPlugins() {
+ let contextMenuVisible = false;
+
return [
new Plugin({
key: new PluginKey('selection'),
props: {
+ handleDOMEvents: {
+ contextmenu() {
+ contextMenuVisible = true;
+ setTimeout(() => {
+ contextMenuVisible = false;
+ });
+ },
+ },
decorations(state) {
- if (state.selection.empty) return null;
+ if (state.selection.empty || contextMenuVisible) return null;
return DecorationSet.create(state.doc, [
Decoration.inline(state.selection.from, state.selection.to, {
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index 17e650644b3..0897232cf89 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -561,7 +561,14 @@ const linkType = (sourceMarkdown) => {
return LINK_HTML;
};
-const normalizeUrl = (url) => decodeURIComponent(removeLastSlashInUrlPath(removeUrlProtocol(url)));
+const normalizeUrl = (url) => {
+ const processedUrl = removeLastSlashInUrlPath(removeUrlProtocol(url));
+ try {
+ return decodeURIComponent(processedUrl);
+ } catch {
+ return processedUrl;
+ }
+};
/**
* Validates that the provided URL is a valid GFM autolink
diff --git a/app/assets/javascripts/crm/organizations/components/graphql/create_organization.mutation.graphql b/app/assets/javascripts/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql
index 2cc7e53ee9b..8e019420eb7 100644
--- a/app/assets/javascripts/crm/organizations/components/graphql/create_organization.mutation.graphql
+++ b/app/assets/javascripts/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql
@@ -1,6 +1,6 @@
#import "./crm_organization_fields.fragment.graphql"
-mutation createOrganization($input: CustomerRelationsOrganizationCreateInput!) {
+mutation createCustomerRelationsOrganization($input: CustomerRelationsOrganizationCreateInput!) {
customerRelationsOrganizationCreate(input: $input) {
organization {
...OrganizationFragment
diff --git a/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue b/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
index 4d2a038458d..fb056e4fa2c 100644
--- a/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
+++ b/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
@@ -4,7 +4,7 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_CRM_ORGANIZATION, TYPENAME_GROUP } from '~/graphql_shared/constants';
import CrmForm from '../../components/crm_form.vue';
import getGroupOrganizationsQuery from './graphql/get_group_organizations.query.graphql';
-import createOrganizationMutation from './graphql/create_organization.mutation.graphql';
+import createCustomerRelationsOrganizationMutation from './graphql/create_customer_relations_organization.mutation.graphql';
import updateOrganizationMutation from './graphql/update_organization.mutation.graphql';
export default {
@@ -31,7 +31,7 @@ export default {
mutation() {
if (this.isEditMode) return updateOrganizationMutation;
- return createOrganizationMutation;
+ return createCustomerRelationsOrganizationMutation;
},
getQuery() {
return {
diff --git a/app/assets/javascripts/custom_emoji/components/app.vue b/app/assets/javascripts/custom_emoji/components/app.vue
index 405a296397f..00b904fbea4 100644
--- a/app/assets/javascripts/custom_emoji/components/app.vue
+++ b/app/assets/javascripts/custom_emoji/components/app.vue
@@ -8,7 +8,7 @@ export default {};
<h4 class="gl-mt-0">
{{ __('Custom emoji') }}
</h4>
- <p>{{ __('Custom emoji will be available to use in every project in group.') }}</p>
+ <p>{{ __('Custom emoji will be available to use in every project in the group.') }}</p>
<router-view />
</div>
</div>
diff --git a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
index ccf4b064fa4..f21086185fb 100644
--- a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
+++ b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
@@ -283,7 +283,7 @@ export default {
</template>
</gl-sprintf>
</template>
- <gl-form-input id="deploy_token_username" v-model="username" class="gl-form-input-xl" />
+ <gl-form-input id="deploy_token_username" v-model="username" width="xl" />
</gl-form-group>
<gl-form-group
:label="$options.translations.addTokenScopesLabel"
diff --git a/app/assets/javascripts/design_management/components/design_description/description_form.vue b/app/assets/javascripts/design_management/components/design_description/description_form.vue
index 413442074f0..6be643e88dc 100644
--- a/app/assets/javascripts/design_management/components/design_description/description_form.vue
+++ b/app/assets/javascripts/design_management/components/design_description/description_form.vue
@@ -4,7 +4,6 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import glFeaturesFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { toggleMarkCheckboxes } from '~/behaviors/markdown/utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -36,7 +35,6 @@ export default {
placeholder: s__('DesignManagement|Write a comment or drag your files here…'),
'aria-label': s__('DesignManagement|Design description'),
},
- mixins: [glFeaturesFlagMixin()],
markdownDocsPath: helpPagePath('user/markdown'),
quickActionsDocsPath: helpPagePath('user/project/quick_actions'),
props: {
@@ -174,7 +172,6 @@ export default {
:render-markdown-path="markdownPreviewPath"
:markdown-docs-path="$options.markdownDocsPath"
:form-field-props="$options.formFieldProps"
- :enable-content-editor="Boolean(glFeatures.contentEditorOnIssues)"
:quick-actions-docs-path="$options.quickActionsDocsPath"
:autosave-key="autosaveKey"
enable-autocomplete
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 08306312c2e..924c515ee2d 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -3,6 +3,7 @@ import { GlLoadingIcon, GlPagination, GlSprintf, GlAlert } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapGetters, mapActions } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import api from '~/api';
import {
@@ -22,6 +23,7 @@ import { __ } from '~/locale';
import notesEventHub from '~/notes/event_hub';
import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
+import { sortFindingsByFile } from '../utils/sort_findings_by_file';
import {
MR_TREE_SHOW_KEY,
ALERT_OVERFLOW_HIDDEN,
@@ -42,7 +44,7 @@ import {
import diffsEventHub from '../event_hub';
import { reviewStatuses } from '../utils/file_reviews';
import { diffsApp } from '../utils/performance';
-import { updateChangesTabCount } from '../utils/merge_request';
+import { updateChangesTabCount, extractFileHash } from '../utils/merge_request';
import { queueRedisHllEvents } from '../utils/queue_events';
import FindingsDrawer from './shared/findings_drawer.vue';
import CollapsedFilesWarning from './collapsed_files_warning.vue';
@@ -53,6 +55,7 @@ import HiddenFilesWarning from './hidden_files_warning.vue';
import NoChanges from './no_changes.vue';
import VirtualScrollerScrollSync from './virtual_scroller_scroll_sync';
import DiffsFileTree from './diffs_file_tree.vue';
+import getMRCodequalityReports from './graphql/get_mr_codequality_reports.query.graphql';
export default {
name: 'DiffsApp',
@@ -75,6 +78,7 @@ export default {
GenerateTestFileDrawer: () =>
import('ee_component/ai/components/generate_test_file_drawer.vue'),
},
+ mixins: [glFeatureFlagsMixin()],
alerts: {
ALERT_OVERFLOW_HIDDEN,
ALERT_MERGE_CONFLICT,
@@ -86,6 +90,16 @@ export default {
required: false,
default: '',
},
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ iid: {
+ type: String,
+ required: false,
+ default: '',
+ },
endpointSast: {
type: String,
required: false,
@@ -123,6 +137,32 @@ export default {
subscribedToVirtualScrollingEvents: false,
};
},
+ apollo: {
+ getMRCodequalityReports: {
+ query: getMRCodequalityReports,
+ variables() {
+ return { fullPath: this.projectPath, iid: this.iid };
+ },
+ skip() {
+ return !this.endpointCodequality || !this.sastReportsInInlineDiff;
+ },
+ update(data) {
+ if (data?.project?.mergeRequest?.codequalityReportsComparer?.report?.newErrors) {
+ this.$store.commit(
+ 'diffs/SET_CODEQUALITY_DATA',
+ sortFindingsByFile(
+ data.project.mergeRequest.codequalityReportsComparer.report.newErrors,
+ ),
+ );
+ }
+ },
+ error() {
+ createAlert({
+ message: __('Something went wrong fetching the CodeQuality Findings. Please try again!'),
+ });
+ },
+ },
+ },
computed: {
...mapState('diffs', {
numTotalFiles: 'realSize',
@@ -220,6 +260,9 @@ export default {
resourceId() {
return convertToGraphQLId('MergeRequest', this.getNoteableData.id);
},
+ sastReportsInInlineDiff() {
+ return this.glFeatures.sastReportsInInlineDiff;
+ },
},
watch: {
commit(newCommit, oldCommit) {
@@ -344,12 +387,13 @@ export default {
...mapActions(['startTaskList']),
...mapActions('diffs', [
'moveToNeighboringCommit',
- 'setBaseConfig',
'setCodequalityEndpoint',
'setSastEndpoint',
'fetchDiffFilesMeta',
'fetchDiffFilesBatch',
'fetchFileByFile',
+ 'loadCollapsedDiff',
+ 'setFileForcedOpen',
'fetchCoverageFiles',
'fetchCodequality',
'fetchSast',
@@ -373,15 +417,34 @@ export default {
notesEventHub.$on('refetchDiffData', this.refetchDiffData);
notesEventHub.$on('fetchedNotesData', this.rereadNoteHash);
diffsEventHub.$on('diffFilesModified', this.setDiscussions);
+ diffsEventHub.$on('doneLoadingBatches', this.autoScroll);
diffsEventHub.$on(EVT_MR_PREPARED, this.fetchData);
},
unsubscribeFromEvents() {
diffsEventHub.$off(EVT_MR_PREPARED, this.fetchData);
+ diffsEventHub.$off('doneLoadingBatches', this.autoScroll);
diffsEventHub.$off('diffFilesModified', this.setDiscussions);
notesEventHub.$off('fetchedNotesData', this.rereadNoteHash);
notesEventHub.$off('refetchDiffData', this.refetchDiffData);
notesEventHub.$off('fetchDiffData', this.fetchData);
},
+ autoScroll() {
+ const lineCode = window.location.hash;
+ const sha1InHash = extractFileHash({ input: lineCode });
+
+ if (sha1InHash) {
+ const idx = this.diffs.findIndex((diffFile) => diffFile.file_hash === sha1InHash);
+ const file = this.diffs[idx];
+
+ this.loadCollapsedDiff({ file })
+ .then(() => {
+ this.setDiscussions();
+ this.scrollVirtualScrollerToIndex(idx);
+ this.setFileForcedOpen({ filePath: file.new_path });
+ })
+ .catch(() => {});
+ }
+ },
navigateToDiffFileNumber(number) {
this.navigateToDiffFileIndex(number - 1);
},
@@ -445,7 +508,7 @@ export default {
this.fetchCoverageFiles();
}
- if (this.endpointCodequality) {
+ if (this.endpointCodequality && !this.sastReportsInInlineDiff) {
this.fetchCodequality();
}
@@ -623,9 +686,13 @@ export default {
page-mode
>
<template #default="{ item, index, active }">
- <dynamic-scroller-item :item="item" :active="active" :class="{ active }">
+ <dynamic-scroller-item
+ v-if="active"
+ :item="item"
+ :active="active"
+ :class="{ active }"
+ >
<diff-file
- v-if="active"
:file="item"
:reviewed="fileReviews[item.id]"
:is-first-file="index === 0"
diff --git a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
index 4501988ee4f..74b872d8bc4 100644
--- a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
+++ b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
@@ -26,7 +26,7 @@ export default {
<template>
<gl-dropdown
:text="selectedVersionName"
- data-qa-selector="dropdown_content"
+ data-testid="version-dropdown-content"
size="small"
category="tertiary"
>
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index bc2376fec09..13388307955 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -90,7 +90,7 @@ export default {
variant="default"
icon="file-tree"
class="gl-mr-3 js-toggle-tree-list btn-icon"
- data-qa-selector="file_tree_button"
+ data-testid="file-tree-button"
:title="toggleFileBrowserTitle"
:aria-label="toggleFileBrowserTitle"
:selected="showTreeList"
@@ -141,7 +141,7 @@ export default {
<compare-dropdown-layout
:versions="diffCompareDropdownTargetVersions"
class="mr-version-compare-dropdown"
- data-qa-selector="target_version_dropdown"
+ data-testid="target-version-dropdown"
/>
</template>
<template #source>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index f99edced361..c74a4b47fcb 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -161,6 +161,9 @@ export default {
manuallyCollapsed() {
return collapsedType(this.file) === DIFF_FILE_MANUAL_COLLAPSE;
},
+ forcedOpen() {
+ return this.file.viewer.forceOpen;
+ },
showBody() {
return !this.isCollapsed || this.automaticallyCollapsed;
},
@@ -174,6 +177,10 @@ export default {
return Boolean(gon.current_user_id);
},
isCollapsed() {
+ if (this.forcedOpen) {
+ return false;
+ }
+
if (collapsedType(this.file) !== DIFF_FILE_MANUAL_COLLAPSE) {
return this.viewDiffsFileByFile ? false : this.file.viewer?.automaticallyCollapsed;
}
@@ -201,6 +208,11 @@ export default {
this.manageViewedEffects();
},
},
+ 'file.viewer.forceOpen': {
+ handler: function fileForcedOpenHandler() {
+ this.handleToggle();
+ },
+ },
'file.file_hash': {
handler: function hashChangeWatch(newHash, oldHash) {
if (
@@ -390,23 +402,23 @@ export default {
<div
v-if="idState.forkMessageVisible"
- class="js-file-fork-suggestion-section file-fork-suggestion"
+ class="js-file-fork-suggestion-section file-fork-suggestion gl-border-1 gl-border-solid gl-border-gray-100 gl-border-top-0"
>
<span v-safe-html="forkMessage" class="file-fork-suggestion-note"></span>
<gl-button
:href="file.fork_path"
- class="js-fork-suggestion-button"
+ class="js-fork-suggestion-button gl-mr-3"
category="secondary"
variant="confirm"
>{{ $options.i18n.fork }}</gl-button
>
- <button
- class="js-cancel-fork-suggestion-button btn btn-grouped"
- type="button"
+ <gl-button
+ class="js-cancel-fork-suggestion-button"
+ category="secondary"
@click="hideForkMessage"
>
{{ $options.i18n.cancel }}
- </button>
+ </gl-button>
</div>
<template v-else>
<div
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index a9e63ad53bb..20f82500a02 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -232,10 +232,15 @@ export default {
'setCurrentFileHash',
'reviewFile',
'setFileCollapsedByUser',
+ 'setFileForcedOpen',
'setGenerateTestFilePath',
'toggleFileCommentForm',
]),
handleToggleFile() {
+ this.setFileForcedOpen({
+ filePath: this.diffFile.file_path,
+ forced: false,
+ });
this.$emit('toggleFile');
},
showForkMessage(e) {
@@ -278,6 +283,10 @@ export default {
}
if ((open && reviewed) || (closed && !reviewed)) {
+ this.setFileForcedOpen({
+ filePath: this.diffFile.file_path,
+ forced: false,
+ });
this.$emit('toggleFile');
}
},
@@ -293,7 +302,7 @@ export default {
'is-sidebar-moved': glFeatures.movedMrSidebar,
}"
class="js-file-title file-title file-title-flex-parent gl-border"
- data-qa-selector="file_title_container"
+ data-testid="file-title-container"
:data-qa-file-name="filePath"
@click.self="handleToggleFile"
>
@@ -423,7 +432,7 @@ export default {
right
toggle-class="btn-icon js-diff-more-actions"
class="gl-pt-0!"
- data-qa-selector="dropdown_button"
+ data-testid="options-dropdown-button"
lazy
@show="setMoreActionsShown(true)"
@hidden="setMoreActionsShown(false)"
@@ -450,7 +459,7 @@ export default {
ref="ideEditButton"
:href="diffFile.ide_edit_path"
class="js-ide-edit-blob"
- data-qa-selector="edit_in_ide_button"
+ data-testid="edit-in-ide-button"
target="_blank"
>
{{ __('Open in Web IDE') }}
@@ -482,7 +491,7 @@ export default {
<gl-dropdown-item
v-if="diffHasDiscussions(diffFile)"
ref="toggleDiscussionsButton"
- data-qa-selector="toggle_comments_button"
+ data-testid="toggle-comments-button"
@click="toggleFileDiscussionWrappers(diffFile)"
>
<template v-if="diffHasExpandedDiscussions(diffFile)">
diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue
index ee6e9a2fc94..3dad7a1a8e4 100644
--- a/app/assets/javascripts/diffs/components/diff_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_row.vue
@@ -248,7 +248,6 @@ export default {
:class="$options.classNameMapCellLeft(props)"
data-testid="left-line-number"
class="diff-td diff-line-num"
- data-qa-selector="new_diff_line_link"
>
<span
v-if="
@@ -266,7 +265,6 @@ export default {
:draggable="!props.line.left.commentsDisabled"
type="button"
class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button"
- data-qa-selector="diff_comment_button"
:disabled="props.line.left.commentsDisabled"
:aria-disabled="props.line.left.commentsDisabled"
@click="
diff --git a/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql b/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql
new file mode 100644
index 00000000000..b6920d0f6ec
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql
@@ -0,0 +1,46 @@
+query getMRCodequalityReports($fullPath: ID!, $iid: String!) {
+ project(fullPath: $fullPath) {
+ id
+ mergeRequest(iid: $iid) {
+ id
+ title
+ codequalityReportsComparer {
+ report {
+ status
+ newErrors {
+ description
+ fingerprint
+ severity
+ filePath
+ line
+ webUrl
+ engineName
+ }
+ resolvedErrors {
+ description
+ fingerprint
+ severity
+ filePath
+ line
+ webUrl
+ engineName
+ }
+ existingErrors {
+ description
+ fingerprint
+ severity
+ filePath
+ line
+ webUrl
+ engineName
+ }
+ summary {
+ errored
+ resolved
+ total
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 7a661d51c9b..f4715c591b2 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -164,11 +164,7 @@ export default {
</script>
<template>
- <div
- ref="wrapper"
- class="tree-list-holder d-flex flex-column"
- data-qa-selector="file_tree_container"
- >
+ <div ref="wrapper" class="tree-list-holder d-flex flex-column" data-testid="file-tree-container">
<div class="gl-pb-3 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex">
<gl-icon name="search" class="gl-absolute gl-top-3 gl-left-3 tree-list-icon" />
@@ -181,7 +177,6 @@ export default {
name="diff-tree-search"
class="form-control"
data-testid="diff-tree-search"
- data-qa-selector="diff_tree_search"
/>
<button
v-show="search"
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index 49f25416585..c0b6c8159dc 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState, mapGetters } from 'vuex';
+import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import { getCookie, parseBoolean, removeCookie } from '~/lib/utils/common_utils';
import notesStore from '~/mr_notes/stores';
@@ -31,6 +32,8 @@ export default function initDiffsApp(store = notesStore) {
},
data() {
return {
+ projectPath: dataset.projectPath || '',
+ iid: dataset.iid || '',
endpointCoverage: dataset.endpointCoverage || '',
endpointCodequality: dataset.endpointCodequality || '',
endpointSast: dataset.endpointSast || '',
@@ -79,6 +82,8 @@ export default function initDiffsApp(store = notesStore) {
render(createElement) {
return createElement('diffs-app', {
props: {
+ projectPath: cleanLeadingSeparator(this.projectPath),
+ iid: this.iid,
endpointCoverage: this.endpointCoverage,
endpointCodequality: this.endpointCodequality,
endpointSast: this.endpointSast,
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 7c68b5f69f1..ed8ae795bda 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -254,6 +254,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
if (totalLoaded === pagination.total_pages || pagination.total_pages === null) {
commit(types.SET_RETRIEVING_BATCHES, false);
+ eventHub.$emit('doneLoadingBatches');
// We need to check that the currentDiffFileId points to a file that exists
if (
@@ -879,6 +880,7 @@ export function switchToFullDiffFromRenamedFile({ commit }, { diffFile }) {
...diffFile.alternate_viewer,
automaticallyCollapsed: false,
manuallyCollapsed: false,
+ forceOpen: false,
},
});
commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: diffFile.file_path, lines });
@@ -893,6 +895,10 @@ export const setFileCollapsedAutomatically = ({ commit }, { filePath, collapsed
commit(types.SET_FILE_COLLAPSED, { filePath, collapsed, trigger: DIFF_FILE_AUTOMATIC_COLLAPSE });
};
+export function setFileForcedOpen({ commit }, { filePath, forced }) {
+ commit(types.SET_FILE_FORCED_OPEN, { filePath, forced });
+}
+
export const setSuggestPopoverDismissed = ({ commit, state }) =>
axios
.post(state.dismissEndpoint, {
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 3df491503a4..c2177bacbcc 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -39,6 +39,7 @@ export const REQUEST_FULL_DIFF = 'REQUEST_FULL_DIFF';
export const RECEIVE_FULL_DIFF_SUCCESS = 'RECEIVE_FULL_DIFF_SUCCESS';
export const RECEIVE_FULL_DIFF_ERROR = 'RECEIVE_FULL_DIFF_ERROR';
export const SET_FILE_COLLAPSED = 'SET_FILE_COLLAPSED';
+export const SET_FILE_FORCED_OPEN = 'SET_FILE_FORCED_OPEN';
export const SET_CURRENT_VIEW_DIFF_FILE_LINES = 'SET_CURRENT_VIEW_DIFF_FILE_LINES';
export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINES';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 3af2d6ee6b1..31369b169f5 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -349,6 +349,11 @@ export default {
}
}
},
+ [types.SET_FILE_FORCED_OPEN](state, { filePath, forced = true }) {
+ const file = state.diffFiles.find((f) => f.file_path === filePath);
+
+ Vue.set(file.viewer, 'forceOpen', forced);
+ },
[types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) {
const file = state.diffFiles.find((f) => f.file_path === filePath);
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 307c41a98f8..15d2ab71bc8 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -18,8 +18,7 @@ import {
EXPANDED_LINE_TYPE,
} from '../constants';
import { prepareRawDiffFile } from '../utils/diff_file';
-
-const SHA1 = /\b([a-f0-9]{40})\b/;
+import { extractFileHash } from '../utils/merge_request';
export const isAdded = (line) => ['new', 'new-nonewline'].includes(line.type);
export const isRemoved = (line) => ['old', 'old-nonewline'].includes(line.type);
@@ -571,14 +570,16 @@ export function isUrlHashFileHeader(urlHash = '') {
}
export function parseUrlHashAsFileHash(urlHash = '', currentDiffFileId = '') {
- const isNoteLink = isUrlHashNoteLink(urlHash);
- let id = urlHash.replace(/^#/, '');
+ const hashless = urlHash.replace(/^#/, '');
+ const isNoteLink = isUrlHashNoteLink(hashless);
+ const extractedSha1 = extractFileHash({ input: hashless });
+ let id = extractedSha1;
if (isNoteLink && currentDiffFileId) {
id = currentDiffFileId;
- } else if (isUrlHashFileHeader(urlHash)) {
- id = id.replace('diff-content-', '');
- } else if (!SHA1.test(id) || isNoteLink) {
+ } else if (isUrlHashFileHeader(hashless)) {
+ id = hashless.replace('diff-content-', '');
+ } else if (!extractedSha1 || isNoteLink) {
id = null;
}
diff --git a/app/assets/javascripts/diffs/utils/diff_file.js b/app/assets/javascripts/diffs/utils/diff_file.js
index 98e1c1cc849..f20ae6464ae 100644
--- a/app/assets/javascripts/diffs/utils/diff_file.js
+++ b/app/assets/javascripts/diffs/utils/diff_file.js
@@ -35,6 +35,7 @@ function collapsed(file) {
return {
automaticallyCollapsed: viewer.automaticallyCollapsed || viewer.collapsed || false,
manuallyCollapsed: null,
+ forceOpen: false,
};
}
diff --git a/app/assets/javascripts/diffs/utils/merge_request.js b/app/assets/javascripts/diffs/utils/merge_request.js
index bc81c0b0a05..a74c9fe7fac 100644
--- a/app/assets/javascripts/diffs/utils/merge_request.js
+++ b/app/assets/javascripts/diffs/utils/merge_request.js
@@ -1,6 +1,7 @@
import { ZERO_CHANGES_ALT_DISPLAY } from '../constants';
const endpointRE = /^(\/?(.+\/)+(.+)\/-\/merge_requests\/(\d+)).*$/i;
+const SHA1RE = /([a-f0-9]{40})/g;
function getVersionInfo({ endpoint } = {}) {
const dummyRoot = 'https://gitlab.com';
@@ -51,3 +52,9 @@ export function getDerivedMergeRequestInformation({ endpoint } = {}) {
startSha,
};
}
+
+export function extractFileHash({ input = '' } = {}) {
+ const matches = input.match(SHA1RE);
+
+ return matches?.[0];
+}
diff --git a/app/assets/javascripts/diffs/utils/sort_findings_by_file.js b/app/assets/javascripts/diffs/utils/sort_findings_by_file.js
new file mode 100644
index 00000000000..3a285e80ace
--- /dev/null
+++ b/app/assets/javascripts/diffs/utils/sort_findings_by_file.js
@@ -0,0 +1,17 @@
+export function sortFindingsByFile(newErrors = []) {
+ const files = {};
+ newErrors.forEach(({ filePath, line, description, severity }) => {
+ if (!files[filePath]) {
+ files[filePath] = [];
+ }
+ files[filePath].push({ line, description, severity: severity.toLowerCase() });
+ });
+
+ const sortedFiles = Object.keys(files)
+ .sort()
+ .reduce((acc, key) => {
+ acc[key] = files[key];
+ return acc;
+ }, {});
+ return { files: sortedFiles };
+}
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 2dba919cf58..0420ffb82f5 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -9,22 +9,27 @@
"format": "uri"
},
"image": {
- "$ref": "#/definitions/image"
+ "$ref": "#/definitions/image",
+ "markdownDescription": "Defining `image` globally is deprecated. Use [`default`](https://docs.gitlab.com/ee/ci/yaml/#default) instead. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#globally-defined-image-services-cache-before_script-after_script)."
},
"services": {
- "$ref": "#/definitions/services"
+ "$ref": "#/definitions/services",
+ "markdownDescription": "Defining `services` globally is deprecated. Use [`default`](https://docs.gitlab.com/ee/ci/yaml/#default) instead. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#globally-defined-image-services-cache-before_script-after_script)."
},
"before_script": {
- "$ref": "#/definitions/before_script"
+ "$ref": "#/definitions/before_script",
+ "markdownDescription": "Defining `before_script` globally is deprecated. Use [`default`](https://docs.gitlab.com/ee/ci/yaml/#default) instead. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#globally-defined-image-services-cache-before_script-after_script)."
},
"after_script": {
- "$ref": "#/definitions/after_script"
+ "$ref": "#/definitions/after_script",
+ "markdownDescription": "Defining `after_script` globally is deprecated. Use [`default`](https://docs.gitlab.com/ee/ci/yaml/#default) instead. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#globally-defined-image-services-cache-before_script-after_script)."
},
"variables": {
"$ref": "#/definitions/globalVariables"
},
"cache": {
- "$ref": "#/definitions/cache"
+ "$ref": "#/definitions/cache",
+ "markdownDescription": "Defining `cache` globally is deprecated. Use [`default`](https://docs.gitlab.com/ee/ci/yaml/#default) instead. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#globally-defined-image-services-cache-before_script-after_script)."
},
"!reference": {
"$ref": "#/definitions/!reference"
@@ -744,39 +749,61 @@
}
}
},
- "before_script": {
- "type": "array",
- "markdownDescription": "Defines scripts that should run *before* the job. Can be set globally or per job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#before_script).",
- "items": {
- "anyOf": [
- {
- "type": "string"
+ "script": {
+ "oneOf": [
+ {
+ "type": "string",
+ "minLength": 1
+ },
+ {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
},
- {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "minItems": 1
+ }
+ ]
+ },
+ "optional_script": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
}
- ]
- }
+ }
+ ]
+ },
+ "before_script": {
+ "$ref": "#/definitions/optional_script",
+ "markdownDescription": "Defines scripts that should run *before* the job. Can be set globally or per job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#before_script)."
},
"after_script": {
- "type": "array",
- "markdownDescription": "Defines scripts that should run *after* the job. Can be set globally or per job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#after_script).",
- "items": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- ]
- }
+ "$ref": "#/definitions/optional_script",
+ "markdownDescription": "Defines scripts that should run *after* the job. Can be set globally or per job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#after_script)."
},
"rules": {
"type": [
@@ -1508,30 +1535,8 @@
"$ref": "#/definitions/secrets"
},
"script": {
- "markdownDescription": "Shell scripts executed by the Runner. The only required property of jobs. Be careful with special characters (e.g. `:`, `{`, `}`, `&`) and use single or double quotes to avoid issues. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#script)",
- "oneOf": [
- {
- "type": "string",
- "minLength": 1
- },
- {
- "type": "array",
- "items": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- ]
- },
- "minItems": 1
- }
- ]
+ "$ref": "#/definitions/script",
+ "markdownDescription": "Shell scripts executed by the Runner. The only required property of jobs. Be careful with special characters (e.g. `:`, `{`, `}`, `&`) and use single or double quotes to avoid issues. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#script)"
},
"stage": {
"description": "Define what stage the job will run in.",
@@ -2145,30 +2150,8 @@
"markdownDescription": "Specifies lists of commands to execute on the runner at certain stages of job execution. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#hooks).",
"properties": {
"pre_get_sources_script": {
- "markdownDescription": "Specifies a list of commands to execute on the runner before updating the Git repository and any submodules. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#hookspre_get_sources_script).",
- "oneOf": [
- {
- "type": "string",
- "minLength": 1
- },
- {
- "type": "array",
- "items": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- ]
- },
- "minItems": 1
- }
- ]
+ "$ref": "#/definitions/optional_script",
+ "markdownDescription": "Specifies a list of commands to execute on the runner before updating the Git repository and any submodules. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#hookspre_get_sources_script)."
}
},
"additionalProperties": false
diff --git a/app/assets/javascripts/editor/source_editor.js b/app/assets/javascripts/editor/source_editor.js
index d585dc009e6..0c9315701eb 100644
--- a/app/assets/javascripts/editor/source_editor.js
+++ b/app/assets/javascripts/editor/source_editor.js
@@ -1,5 +1,4 @@
import { editor as monacoEditor, Uri } from 'monaco-editor';
-import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
import languages from '~/ide/lib/languages';
import { registerLanguages } from '~/ide/utils';
@@ -128,9 +127,7 @@ export default class SourceEditor {
this.extensionsStore,
);
- waitForCSSLoaded(() => {
- instance.layout();
- });
+ instance.layout();
let model;
const language = instanceOptions.language || getBlobLanguage(blobPath);
diff --git a/app/assets/javascripts/emoji/awards_app/index.js b/app/assets/javascripts/emoji/awards_app/index.js
index f3d72c2dba5..965cb4f421a 100644
--- a/app/assets/javascripts/emoji/awards_app/index.js
+++ b/app/assets/javascripts/emoji/awards_app/index.js
@@ -9,14 +9,18 @@ export default (el) => {
if (!el) return null;
const {
- dataset: { path },
+ dataset: { path, newCustomEmojiPath },
} = el;
const canAwardEmoji = parseBoolean(el.dataset.canAwardEmoji);
+ const showDefaultAwardEmojis = parseBoolean(el.dataset.showDefaultAwardEmojis);
return new Vue({
el,
name: 'AwardsListRoot',
store: createstore(),
+ provide: {
+ newCustomEmojiPath,
+ },
computed: {
...mapState(['currentUserId', 'canAwardEmoji', 'awards']),
},
@@ -35,7 +39,7 @@ export default (el) => {
awards: this.awards,
canAwardEmoji: this.canAwardEmoji,
currentUserId: this.currentUserId,
- defaultAwards: ['thumbsup', 'thumbsdown'],
+ defaultAwards: showDefaultAwardEmojis ? ['thumbsup', 'thumbsdown'] : [],
selectedClass: 'selected',
},
on: {
diff --git a/app/assets/javascripts/emoji/components/picker.vue b/app/assets/javascripts/emoji/components/picker.vue
index fcc54f17466..238f0d81b22 100644
--- a/app/assets/javascripts/emoji/components/picker.vue
+++ b/app/assets/javascripts/emoji/components/picker.vue
@@ -1,6 +1,6 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
-import { GlIcon, GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
+import { GlIcon, GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { findLastIndex } from 'lodash';
import VirtualList from 'vue-virtual-scroll-list';
import { CATEGORY_NAMES, getEmojiCategoryMap, state } from '~/emoji';
@@ -13,11 +13,17 @@ export default {
components: {
GlIcon,
GlDropdown,
+ GlDropdownItem,
GlSearchBoxByType,
VirtualList,
Category,
EmojiList,
},
+ inject: {
+ newCustomEmojiPath: {
+ default: '',
+ },
+ },
props: {
toggleClass: {
type: [Array, String, Object],
@@ -167,6 +173,11 @@ export default {
</virtual-list>
</template>
</emoji-list>
+ <template v-if="newCustomEmojiPath" #footer>
+ <gl-dropdown-item :href="newCustomEmojiPath">
+ {{ __('Create new emoji') }}
+ </gl-dropdown-item>
+ </template>
</gl-dropdown>
</div>
</template>
diff --git a/app/assets/javascripts/environments/components/canary_ingress.vue b/app/assets/javascripts/environments/components/canary_ingress.vue
index 30f3f9dfc75..ef3c6210ce1 100644
--- a/app/assets/javascripts/environments/components/canary_ingress.vue
+++ b/app/assets/javascripts/environments/components/canary_ingress.vue
@@ -1,16 +1,12 @@
<script>
-import { GlDropdown, GlDropdownItem, GlModalDirective as GlModal } from '@gitlab/ui';
-import { uniqueId } from 'lodash';
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import uniqueId from 'lodash/uniqueId';
import { s__ } from '~/locale';
import { CANARY_UPDATE_MODAL } from '../constants';
export default {
components: {
- GlDropdown,
- GlDropdownItem,
- },
- directives: {
- GlModal,
+ GlCollapsibleListbox,
},
props: {
canaryIngress: {
@@ -25,8 +21,10 @@ export default {
},
ingressOptions: Array(100 / 5 + 1)
.fill(0)
- .map((_, i) => i * 5),
-
+ .map((_, i) => {
+ const value = i * 5;
+ return { value, text: value.toString() };
+ }),
translations: {
stableLabel: s__('CanaryIngress|Stable'),
canaryLabel: s__('CanaryIngress|Canary'),
@@ -59,17 +57,19 @@ export default {
return this.canaryIngress.canary_weight;
},
stableWeight() {
- return (100 - this.weight).toString();
+ return 100 - this.weight;
},
canaryWeight() {
- return this.weight.toString();
+ return this.weight;
},
},
methods: {
changeCanary(weight) {
+ this.$root.$emit('bv::show::modal', CANARY_UPDATE_MODAL);
this.$emit('change', weight);
},
changeStable(weight) {
+ this.$root.$emit('bv::show::modal', CANARY_UPDATE_MODAL);
this.$emit('change', 100 - weight);
},
},
@@ -81,40 +81,27 @@ export default {
<label :for="stableWeightId" :class="$options.css.label" class="gl-rounded-top-left-base">
{{ $options.translations.stableLabel }}
</label>
- <gl-dropdown
+ <gl-collapsible-listbox
:id="stableWeightId"
- :text="stableWeight"
- data-testid="stable-weight"
- class="gl-w-full"
- toggle-class="gl-rounded-top-left-none! gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
- >
- <gl-dropdown-item
- v-for="option in $options.ingressOptions"
- :key="option"
- v-gl-modal="$options.CANARY_UPDATE_MODAL"
- @click="changeStable(option)"
- >{{ option }}</gl-dropdown-item
- >
- </gl-dropdown>
+ :selected="stableWeight"
+ :items="$options.ingressOptions"
+ class="gl-min-w-full gl-text-black-normal"
+ toggle-class="gl-min-w-full gl-rounded-top-left-none! gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
+ @select="changeStable"
+ />
</div>
<div class="gl-display-flex gl-display-flex gl-flex-direction-column">
<label :for="canaryWeightId" :class="$options.css.label" class="gl-rounded-top-right-base">{{
$options.translations.canaryLabel
}}</label>
- <gl-dropdown
+ <gl-collapsible-listbox
:id="canaryWeightId"
- :text="canaryWeight"
- data-testid="canary-weight"
- toggle-class="gl-rounded-top-left-none! gl-rounded-top-right-none! gl-rounded-bottom-left-none! gl-border-l-none!"
- >
- <gl-dropdown-item
- v-for="option in $options.ingressOptions"
- :key="option"
- v-gl-modal="$options.CANARY_UPDATE_MODAL"
- @click="changeCanary(option)"
- >{{ option }}</gl-dropdown-item
- >
- </gl-dropdown>
+ :selected="canaryWeight"
+ :items="$options.ingressOptions"
+ class="gl-min-w-full"
+ toggle-class="gl-min-w-full gl-rounded-top-left-none! gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
+ @select="changeCanary"
+ />
</div>
</section>
</template>
diff --git a/app/assets/javascripts/environments/components/empty_state.vue b/app/assets/javascripts/environments/components/empty_state.vue
index 3ac32f0d045..5b9cc2f3d21 100644
--- a/app/assets/javascripts/environments/components/empty_state.vue
+++ b/app/assets/javascripts/environments/components/empty_state.vue
@@ -42,7 +42,7 @@ export default {
};
</script>
<template>
- <gl-empty-state class="gl-layout-w-limited" :title="title">
+ <gl-empty-state class="gl-layout-w-limited gl-mx-auto" :title="title">
<template #description>
<gl-sprintf :message="content">
<template #link="{ content: contentToDisplay }">
@@ -51,10 +51,10 @@ export default {
</gl-sprintf>
</template>
<template v-if="!hasTerm" #actions>
- <gl-button :href="newEnvironmentPath" variant="confirm">
+ <gl-button class="gl-mx-2 gl-mb-3" :href="newEnvironmentPath" variant="confirm">
{{ $options.i18n.newEnvironmentButtonLabel }}
</gl-button>
- <gl-button @click="$emit('enable-review')">
+ <gl-button class="gl-mx-2 gl-mb-3" @click="$emit('enable-review')">
{{ $options.i18n.enablingReviewButtonLabel }}
</gl-button>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue
index 846f2cf73b2..8ebba0e27bb 100644
--- a/app/assets/javascripts/environments/components/environment_form.vue
+++ b/app/assets/javascripts/environments/components/environment_form.vue
@@ -190,13 +190,12 @@ export default {
}
return {
basePath: this.kasTunnelUrl,
- baseOptions: {
- headers: {
- 'GitLab-Agent-Id': getIdFromGraphQLId(this.selectedAgentId),
- ...csrf.headers,
- },
- withCredentials: true,
+ headers: {
+ 'GitLab-Agent-Id': getIdFromGraphQLId(this.selectedAgentId),
+ 'Content-Type': 'application/json',
+ ...csrf.headers,
},
+ credentials: 'include',
};
},
},
diff --git a/app/assets/javascripts/environments/components/kubernetes_overview.vue b/app/assets/javascripts/environments/components/kubernetes_overview.vue
index 0e52a80c2c5..252ced6391d 100644
--- a/app/assets/javascripts/environments/components/kubernetes_overview.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_overview.vue
@@ -61,10 +61,12 @@ export default {
k8sAccessConfiguration() {
return {
basePath: this.kasTunnelUrl,
- baseOptions: {
- headers: { 'GitLab-Agent-Id': this.gitlabAgentId, ...csrf.headers },
- withCredentials: true,
+ headers: {
+ 'GitLab-Agent-Id': this.gitlabAgentId,
+ 'Content-Type': 'application/json',
+ ...csrf.headers,
},
+ credentials: 'include',
};
},
clusterHealthStatus() {
diff --git a/app/assets/javascripts/environments/components/kubernetes_pods.vue b/app/assets/javascripts/environments/components/kubernetes_pods.vue
index aded3a4d0c4..3f040f1f40a 100644
--- a/app/assets/javascripts/environments/components/kubernetes_pods.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_pods.vue
@@ -23,7 +23,7 @@ export default {
return data?.k8sPods || [];
},
error(error) {
- this.error = error;
+ this.error = error.message;
this.$emit('cluster-error', this.error);
},
watchLoading(isLoading) {
diff --git a/app/assets/javascripts/environments/components/kubernetes_summary.vue b/app/assets/javascripts/environments/components/kubernetes_summary.vue
index b00e82809f6..e2fbc6fd2e7 100644
--- a/app/assets/javascripts/environments/components/kubernetes_summary.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_summary.vue
@@ -30,7 +30,7 @@ export default {
return data?.k8sWorkloads || {};
},
error(error) {
- this.$emit('cluster-error', error);
+ this.$emit('cluster-error', error.message);
},
result() {
this.checkFailed();
diff --git a/app/assets/javascripts/environments/components/kubernetes_tabs.vue b/app/assets/javascripts/environments/components/kubernetes_tabs.vue
index 4492d209e3b..7c699eec412 100644
--- a/app/assets/javascripts/environments/components/kubernetes_tabs.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_tabs.vue
@@ -24,13 +24,14 @@ export default {
variables() {
return {
configuration: this.configuration,
+ namespace: this.namespace,
};
},
update(data) {
return data?.k8sServices || [];
},
error(error) {
- this.$emit('cluster-error', error);
+ this.$emit('cluster-error', error.message);
},
},
},
@@ -139,6 +140,7 @@ export default {
:configuration="configuration"
@loading="$emit('loading', $event)"
@failed="$emit('failed')"
+ @cluster-error="$emit('cluster-error', $event)"
/>
<gl-tab>
diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue
index 149cab21acd..aacb460a817 100644
--- a/app/assets/javascripts/environments/components/new_environment_item.vue
+++ b/app/assets/javascripts/environments/components/new_environment_item.vue
@@ -185,7 +185,7 @@ export default {
},
update(data) {
this.clusterAgent = data?.project?.environment?.clusterAgent;
- this.kubernetesNamespace = data?.project?.environment?.kubernetesNamespace;
+ this.kubernetesNamespace = data?.project?.environment?.kubernetesNamespace || '';
this.fluxResourcePath = data?.project?.environment?.fluxResourcePath || '';
},
});
diff --git a/app/assets/javascripts/environments/environment_details/deployments_table.vue b/app/assets/javascripts/environments/environment_details/deployments_table.vue
index 261d8106438..e46411f4d2c 100644
--- a/app/assets/javascripts/environments/environment_details/deployments_table.vue
+++ b/app/assets/javascripts/environments/environment_details/deployments_table.vue
@@ -48,12 +48,17 @@ export default {
<deployment-job :job="item.job" />
</template>
<template #cell(created)="{ item }">
- <time-ago-tooltip :time="item.created" data-testid="deployment-created-at" />
+ <time-ago-tooltip
+ :time="item.created"
+ enable-truncation
+ data-testid="deployment-created-at"
+ />
</template>
<template #cell(deployed)="{ item }">
<time-ago-tooltip
v-if="item.deployed"
:time="item.deployed"
+ enable-truncation
data-testid="deployment-deployed-at"
/>
</template>
diff --git a/app/assets/javascripts/environments/environment_details/index.vue b/app/assets/javascripts/environments/environment_details/index.vue
index aa836299bcc..6e3ec04ba3b 100644
--- a/app/assets/javascripts/environments/environment_details/index.vue
+++ b/app/assets/javascripts/environments/environment_details/index.vue
@@ -150,10 +150,10 @@ export default {
},
},
errorCaptured(error) {
- Sentry.withScope((scope) => {
- scope.setTag('vue_component', 'EnvironmentDetailsIndex');
-
- Sentry.captureException(error);
+ Sentry.captureException(error, {
+ tags: {
+ vue_component: 'EnvironmentDetailsIndex',
+ },
});
},
mounted() {
diff --git a/app/assets/javascripts/environments/graphql/queries/k8s_services.query.graphql b/app/assets/javascripts/environments/graphql/queries/k8s_services.query.graphql
index d97849eecc1..8fc4a54b08b 100644
--- a/app/assets/javascripts/environments/graphql/queries/k8s_services.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/k8s_services.query.graphql
@@ -1,5 +1,5 @@
-query getK8sServices($configuration: LocalConfiguration) {
- k8sServices(configuration: $configuration) @client {
+query getK8sServices($configuration: LocalConfiguration, $namespace: String) {
+ k8sServices(configuration: $configuration, namespace: $namespace) @client {
metadata {
name
namespace
diff --git a/app/assets/javascripts/environments/graphql/resolvers/flux.js b/app/assets/javascripts/environments/graphql/resolvers/flux.js
index f9ca35a3165..d39b1bed7b6 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/flux.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/flux.js
@@ -23,7 +23,7 @@ const buildFluxResourceUrl = ({
};
const getFluxResourceStatus = (configuration, url) => {
- const { headers } = configuration.baseOptions;
+ const { headers } = configuration;
const withCredentials = true;
return axios
@@ -37,7 +37,7 @@ const getFluxResourceStatus = (configuration, url) => {
};
const getFluxResources = (configuration, url) => {
- const { headers } = configuration.baseOptions;
+ const { headers } = configuration;
const withCredentials = true;
return axios
diff --git a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
index 9ab65d0bb7f..67a472dac93 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
@@ -44,30 +44,41 @@ const mapWorkloadItems = (items, kind) => {
});
};
-const handleClusterError = (err) => {
- const error = err?.response?.data?.message ? new Error(err.response.data.message) : err;
- throw error;
+const handleClusterError = async (err) => {
+ if (!err.response) {
+ throw err;
+ }
+
+ const errorData = await err.response.json();
+ throw errorData;
};
export default {
k8sPods(_, { configuration, namespace }) {
const coreV1Api = new CoreV1Api(new Configuration(configuration));
const podsApi = namespace
- ? coreV1Api.listCoreV1NamespacedPod(namespace)
+ ? coreV1Api.listCoreV1NamespacedPod({ namespace })
: coreV1Api.listCoreV1PodForAllNamespaces();
return podsApi
- .then((res) => res?.data?.items || [])
- .catch((err) => {
- handleClusterError(err);
+ .then((res) => res?.items || [])
+ .catch(async (err) => {
+ try {
+ await handleClusterError(err);
+ } catch (error) {
+ throw new Error(error.message);
+ }
});
},
- k8sServices(_, { configuration }) {
+ k8sServices(_, { configuration, namespace }) {
const coreV1Api = new CoreV1Api(new Configuration(configuration));
- return coreV1Api
- .listCoreV1ServiceForAllNamespaces()
+ const servicesApi = namespace
+ ? coreV1Api.listCoreV1NamespacedService({ namespace })
+ : coreV1Api.listCoreV1ServiceForAllNamespaces();
+
+ return servicesApi
.then((res) => {
- const items = res?.data?.items || [];
+ const items = res?.items || [];
return items.map((item) => {
const { type, clusterIP, externalIP, ports } = item.spec;
return {
@@ -81,24 +92,28 @@ export default {
};
});
})
- .catch((err) => {
- handleClusterError(err);
+ .catch(async (err) => {
+ try {
+ await handleClusterError(err);
+ } catch (error) {
+ throw new Error(error.message);
+ }
});
},
k8sWorkloads(_, { configuration, namespace }) {
- const appsV1api = new AppsV1Api(configuration);
- const batchV1api = new BatchV1Api(configuration);
+ const appsV1api = new AppsV1Api(new Configuration(configuration));
+ const batchV1api = new BatchV1Api(new Configuration(configuration));
let promises;
if (namespace) {
promises = [
- appsV1api.listAppsV1NamespacedDeployment(namespace),
- appsV1api.listAppsV1NamespacedDaemonSet(namespace),
- appsV1api.listAppsV1NamespacedStatefulSet(namespace),
- appsV1api.listAppsV1NamespacedReplicaSet(namespace),
- batchV1api.listBatchV1NamespacedJob(namespace),
- batchV1api.listBatchV1NamespacedCronJob(namespace),
+ appsV1api.listAppsV1NamespacedDeployment({ namespace }),
+ appsV1api.listAppsV1NamespacedDaemonSet({ namespace }),
+ appsV1api.listAppsV1NamespacedStatefulSet({ namespace }),
+ appsV1api.listAppsV1NamespacedReplicaSet({ namespace }),
+ batchV1api.listBatchV1NamespacedJob({ namespace }),
+ batchV1api.listBatchV1NamespacedCronJob({ namespace }),
];
} else {
promises = [
@@ -120,15 +135,18 @@ export default {
CronJobList: [],
};
- return Promise.allSettled(promises).then((results) => {
+ return Promise.allSettled(promises).then(async (results) => {
if (results.every((res) => res.status === 'rejected')) {
const error = results[0].reason;
- const errorMessage = error?.response?.data?.message ?? error;
- throw new Error(errorMessage);
+ try {
+ await handleClusterError(error);
+ } catch (err) {
+ throw new Error(err.message);
+ }
}
for (const promiseResult of results) {
- if (promiseResult.status === 'fulfilled' && promiseResult?.value?.data) {
- const { kind, items } = promiseResult.value.data;
+ if (promiseResult.status === 'fulfilled' && promiseResult?.value) {
+ const { kind, items } = promiseResult.value;
if (items?.length > 0) {
summaryList[kind] = mapWorkloadItems(items, kind);
@@ -145,11 +163,14 @@ export default {
return namespacesApi
.then((res) => {
- return res?.data?.items || [];
+ return res?.items || [];
})
- .catch((err) => {
- const error = err?.response?.data?.reason || err;
- throw new Error(humanizeClusterErrors(error));
+ .catch(async (error) => {
+ try {
+ await handleClusterError(error);
+ } catch (err) {
+ throw new Error(humanizeClusterErrors(err.reason));
+ }
});
},
};
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 9b30ec4afbb..4d4bae12570 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -493,7 +493,11 @@ export default {
</div>
<!-- Get Started with ET -->
<div v-else>
- <gl-empty-state :title="__('Get started with error tracking')" :svg-path="illustrationPath">
+ <gl-empty-state
+ :title="__('Get started with error tracking')"
+ :svg-path="illustrationPath"
+ :svg-height="null"
+ >
<template #description>
<div>
<span>{{ __('Monitor your errors directly in GitLab.') }}</span>
diff --git a/app/assets/javascripts/feature_flags/components/empty_state.vue b/app/assets/javascripts/feature_flags/components/empty_state.vue
index 60aeb297700..ccc984ee7a0 100644
--- a/app/assets/javascripts/feature_flags/components/empty_state.vue
+++ b/app/assets/javascripts/feature_flags/components/empty_state.vue
@@ -74,6 +74,7 @@ export default {
:title="errorTitle"
:description="s__('FeatureFlags|Try again in a few moments or contact your support team.')"
:svg-path="errorStateSvgPath"
+ :svg-height="null"
data-testid="error-state"
/>
diff --git a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
index 0fde87dd0ba..7cc87544be9 100644
--- a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
+++ b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
@@ -93,7 +93,7 @@ export default {
type="number"
min="0"
max="100"
- size="xs"
+ width="xs"
@input="onPercentageChange"
/>
<span class="ml-1">%</span>
diff --git a/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue b/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue
index 0acb0d4366c..a46eee7b130 100644
--- a/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue
+++ b/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue
@@ -59,7 +59,7 @@ export default {
type="number"
min="0"
max="100"
- size="xs"
+ width="xs"
@input="onPercentageChange"
/>
<span class="gl-ml-2">%</span>
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 264427f5806..99d22b1330b 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -129,7 +129,7 @@ class GfmAutoComplete {
this.dataSources = dataSources;
this.cachedData = {};
this.isLoadingData = {};
- this.previousQuery = '';
+ this.previousQuery = undefined;
}
setup(input, enableMap = defaultAutocompleteConfig) {
@@ -776,15 +776,19 @@ class GfmAutoComplete {
return $.fn.atwho.default.callbacks.sorter(query, items, searchKey);
},
filter(query, data, searchKey) {
+ if (GfmAutoComplete.isTypeWithBackendFiltering(this.at)) {
+ if (GfmAutoComplete.isLoading(data) || self.previousQuery !== query) {
+ self.previousQuery = query;
+ self.fetchData(this.$inputor, this.at, query);
+ return data;
+ }
+ }
+
if (GfmAutoComplete.isLoading(data)) {
self.fetchData(this.$inputor, this.at);
return data;
}
- if (GfmAutoComplete.isTypeWithBackendFiltering(this.at) && self.previousQuery !== query) {
- self.fetchData(this.$inputor, this.at, query);
- self.previousQuery = query;
- return data;
- }
+
return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
},
beforeInsert(value) {
@@ -828,14 +832,18 @@ class GfmAutoComplete {
const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]];
if (GfmAutoComplete.isTypeWithBackendFiltering(at)) {
- axios
- .get(dataSource, { params: { search } })
- .then(({ data }) => {
- this.loadData($input, at, data);
- })
- .catch(() => {
- this.isLoadingData[at] = false;
- });
+ if (this.cachedData[at]?.[search]) {
+ this.loadData($input, at, this.cachedData[at][search], { search });
+ } else {
+ axios
+ .get(dataSource, { params: { search } })
+ .then(({ data }) => {
+ this.loadData($input, at, data, { search });
+ })
+ .catch(() => {
+ this.isLoadingData[at] = false;
+ });
+ }
} else if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
@@ -853,9 +861,19 @@ class GfmAutoComplete {
}
}
- loadData($input, at, data) {
+ loadData($input, at, data, { search } = {}) {
this.isLoadingData[at] = false;
- this.cachedData[at] = data;
+
+ if (search !== undefined) {
+ if (this.cachedData[at] === undefined) {
+ this.cachedData[at] = {};
+ }
+
+ this.cachedData[at][search] = data;
+ } else {
+ this.cachedData[at] = data;
+ }
+
$input.atwho('load', at, data);
// This trigger at.js again
// otherwise we would be stuck with loading until the user types
diff --git a/app/assets/javascripts/google_cloud/databases/cloudsql/instance_table.vue b/app/assets/javascripts/google_cloud/databases/cloudsql/instance_table.vue
index 823895214df..f97c1e54094 100644
--- a/app/assets/javascripts/google_cloud/databases/cloudsql/instance_table.vue
+++ b/app/assets/javascripts/google_cloud/databases/cloudsql/instance_table.vue
@@ -57,6 +57,7 @@ export default {
:title="$options.i18n.noInstancesTitle"
:description="$options.i18n.noInstancesDescription"
:svg-path="emptyIllustrationUrl"
+ :svg-height="null"
/>
<div v-else>
diff --git a/app/assets/javascripts/google_tag_manager/index.js b/app/assets/javascripts/google_tag_manager/index.js
index a9ae9a5af82..0cb25fbaeb5 100644
--- a/app/assets/javascripts/google_tag_manager/index.js
+++ b/app/assets/javascripts/google_tag_manager/index.js
@@ -1,310 +1 @@
-import { v4 as uuidv4 } from 'uuid';
-import { logError } from '~/lib/logger';
-
-const SKU_PREMIUM = '2c92a00d76f0d5060176f2fb0a5029ff';
-const SKU_ULTIMATE = '2c92a0ff76f0d5250176f2f8c86f305a';
-const PRODUCT_INFO = {
- [SKU_PREMIUM]: {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- name: 'Premium',
- id: '0002',
- price: '228',
- variant: 'SaaS',
- },
- [SKU_ULTIMATE]: {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- name: 'Ultimate',
- id: '0001',
- price: '1188',
- variant: 'SaaS',
- },
-};
-const EMPTY_NAMESPACE_ID_VALUE = 'not available';
-
-const generateProductInfo = (sku, quantity) => {
- const product = PRODUCT_INFO[sku];
-
- if (!product) {
- logError('Unexpected product sku provided to generateProductInfo');
- return {};
- }
-
- const productInfo = {
- ...product,
- brand: 'GitLab',
- category: 'DevOps',
- quantity,
- };
-
- return productInfo;
-};
-
-const isSupported = () => Boolean(window.dataLayer) && gon.features?.gitlabGtmDatalayer;
-// gon.features.gitlabGtmDatalayer is set by writing
-// `push_frontend_feature_flag(:gitlab_gtm_datalayer, type: :ops)`
-// to the appropriate controller
-// window.dataLayer is set by adding partials to the appropriate view found in
-// views/layouts/_google_tag_manager_body.html.haml and _google_tag_manager_head.html.haml
-
-const pushEvent = (event, args = {}) => {
- if (!window.dataLayer) {
- return;
- }
-
- try {
- window.dataLayer.push({
- event,
- ...args,
- });
- } catch (e) {
- logError('Unexpected error while pushing to dataLayer', e);
- }
-};
-
-const pushEnhancedEcommerceEvent = (event, args = {}) => {
- if (!window.dataLayer) {
- return;
- }
-
- try {
- window.dataLayer.push({ ecommerce: null }); // Clear the previous ecommerce object
- window.dataLayer.push({
- event,
- ...args,
- });
- } catch (e) {
- logError('Unexpected error while pushing to dataLayer', e);
- }
-};
-
-const pushAccountSubmit = (accountType, accountMethod) =>
- pushEvent('accountSubmit', { accountType, accountMethod });
-
-const trackFormSubmission = (accountType) => {
- const form = document.getElementById('new_new_user');
- form.addEventListener('submit', () => {
- pushAccountSubmit(accountType, 'form');
- });
-};
-
-const trackOmniAuthSubmission = (accountType) => {
- const links = document.querySelectorAll('.js-oauth-login');
- links.forEach((link) => {
- const { provider } = link.dataset;
- link.addEventListener('click', () => {
- pushAccountSubmit(accountType, provider);
- });
- });
-};
-
-export const trackFreeTrialAccountSubmissions = () => {
- if (!isSupported()) {
- return;
- }
-
- trackFormSubmission('freeThirtyDayTrial');
- trackOmniAuthSubmission('freeThirtyDayTrial');
-};
-
-export const trackNewRegistrations = () => {
- if (!isSupported()) {
- return;
- }
-
- trackFormSubmission('standardSignUp');
- trackOmniAuthSubmission('standardSignUp');
-};
-
-export const trackSaasTrialSubmit = () => {
- if (!isSupported()) {
- return;
- }
-
- pushEvent('saasTrialSubmit');
-};
-
-export const trackSaasTrialGroup = () => {
- if (!isSupported()) {
- return;
- }
-
- const form = document.querySelector('.js-saas-trial-group');
-
- if (!form) return;
-
- form.addEventListener('submit', () => {
- pushEvent('saasTrialGroup');
- });
-};
-
-export const trackProjectImport = () => {
- if (!isSupported()) {
- return;
- }
-
- const importButtons = document.querySelectorAll('.js-import-project-btn');
- importButtons.forEach((button) => {
- button.addEventListener('click', () => {
- const { platform } = button.dataset;
- pushEvent('projectImport', { platform });
- });
- });
-};
-
-export const trackSaasTrialGetStarted = () => {
- if (!isSupported()) {
- return;
- }
-
- const getStartedButton = document.querySelector('.js-get-started-btn');
- getStartedButton.addEventListener('click', () => {
- pushEvent('saasTrialGetStarted');
- });
-};
-
-export const trackTrialAcceptTerms = () => {
- if (!isSupported()) {
- return;
- }
-
- pushEvent('saasTrialAcceptTerms');
-};
-
-export const trackCheckout = (selectedPlan, quantity) => {
- if (!isSupported()) {
- return;
- }
-
- const product = generateProductInfo(selectedPlan, quantity);
-
- if (Object.keys(product).length === 0) {
- return;
- }
-
- const eventData = {
- ecommerce: {
- currencyCode: 'USD',
- checkout: {
- actionField: { step: 1 },
- products: [product],
- },
- },
- };
-
- // eslint-disable-next-line @gitlab/require-i18n-strings
- pushEnhancedEcommerceEvent('EECCheckout', eventData);
-};
-
-export const getNamespaceId = () => {
- return window.gl.snowplowStandardContext?.data?.namespace_id || EMPTY_NAMESPACE_ID_VALUE;
-};
-
-export const trackTransaction = (transactionDetails) => {
- if (!isSupported()) {
- return;
- }
-
- const transactionId = uuidv4();
- const { paymentOption, revenue, tax, selectedPlan, quantity } = transactionDetails;
- const product = generateProductInfo(selectedPlan, quantity);
- const namespaceId = getNamespaceId();
-
- if (Object.keys(product).length === 0) {
- return;
- }
-
- const eventData = {
- ecommerce: {
- currencyCode: 'USD',
- purchase: {
- actionField: {
- id: transactionId,
- affiliation: 'GitLab',
- option: paymentOption,
- revenue: revenue.toString(),
- tax: tax.toString(),
- },
- products: [{ ...product, dimension36: namespaceId }],
- },
- },
- };
-
- pushEnhancedEcommerceEvent('EECtransactionSuccess', eventData);
-};
-
-export const pushEECproductAddToCartEvent = () => {
- if (!isSupported()) {
- return;
- }
-
- window.dataLayer.push({
- event: 'EECproductAddToCart',
- ecommerce: {
- currencyCode: 'USD',
- add: {
- products: [
- {
- name: 'CI/CD Minutes',
- id: '0003',
- price: '10',
- brand: 'GitLab',
- category: 'DevOps',
- variant: 'add-on',
- quantity: 1,
- },
- ],
- },
- },
- });
-};
-
-export const trackAddToCartUsageTab = () => {
- const getStartedButton = document.querySelector('.js-buy-additional-minutes');
- if (!getStartedButton) {
- return;
- }
- getStartedButton.addEventListener('click', pushEECproductAddToCartEvent);
-};
-
-export const trackCombinedGroupProjectForm = () => {
- if (!isSupported()) {
- return;
- }
-
- const form = document.querySelector('.js-groups-projects-form');
- form.addEventListener('submit', () => {
- pushEvent('combinedGroupProjectFormSubmit');
- });
-};
-
-export const trackCompanyForm = (aboutYourCompanyType) => {
- if (!isSupported()) {
- return;
- }
-
- pushEvent('aboutYourCompanyFormSubmit', { aboutYourCompanyType });
-};
-
-export const saasTrialWelcome = () => {
- if (!isSupported()) {
- return;
- }
-
- const saasTrialWelcomeButton = document.querySelector('.js-trial-welcome-btn');
-
- saasTrialWelcomeButton?.addEventListener('click', () => {
- pushEvent('saasTrialWelcome');
- });
-};
-
-export const saasTrialContinuousOnboarding = () => {
- if (!isSupported()) {
- return;
- }
-
- const getStartedButton = document.querySelector('.js-get-started-btn');
-
- getStartedButton?.addEventListener('click', () => {
- pushEvent('saasTrialContinuousOnboarding');
- });
-};
+export const trackTrialAcceptTerms = () => {};
diff --git a/app/assets/javascripts/graphql_shared/client/is_showing_labels.query.graphql b/app/assets/javascripts/graphql_shared/client/is_showing_labels.query.graphql
new file mode 100644
index 00000000000..dc16f7ad313
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/client/is_showing_labels.query.graphql
@@ -0,0 +1,3 @@
+query isShowingLabels {
+ isShowingLabels @client
+}
diff --git a/app/assets/javascripts/graphql_shared/client/set_is_showing_labels.mutation.graphql b/app/assets/javascripts/graphql_shared/client/set_is_showing_labels.mutation.graphql
new file mode 100644
index 00000000000..2f115291977
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/client/set_is_showing_labels.mutation.graphql
@@ -0,0 +1,3 @@
+mutation setIsShowingLabels($isShowingLabels: Boolean!) {
+ setIsShowingLabels(isShowingLabels: $isShowingLabels) @client
+}
diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js
index eb807bc7540..9537c9ef8a6 100644
--- a/app/assets/javascripts/graphql_shared/issuable_client.js
+++ b/app/assets/javascripts/graphql_shared/issuable_client.js
@@ -3,6 +3,8 @@ import VueApollo from 'vue-apollo';
import { defaultDataIdFromObject } from '@apollo/client/core';
import { concatPagination } from '@apollo/client/utilities';
import errorQuery from '~/boards/graphql/client/error.query.graphql';
+import selectedBoardItemsQuery from '~/boards/graphql/client/selected_board_items.query.graphql';
+import isShowingLabelsQuery from '~/graphql_shared/client/is_showing_labels.query.graphql';
import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql';
import createDefaultClient from '~/lib/graphql';
import typeDefs from '~/work_items/graphql/typedefs.graphql';
@@ -20,6 +22,20 @@ export const config = {
: defaultDataIdFromObject(object);
},
typePolicies: {
+ Query: {
+ fields: {
+ isShowingLabels: {
+ read(currentState) {
+ return currentState ?? true;
+ },
+ },
+ selectedBoardItems: {
+ read(currentState) {
+ return currentState ?? [];
+ },
+ },
+ },
+ },
Project: {
fields: {
projectMembers: {
@@ -77,14 +93,6 @@ export const config = {
const incomingWidget = incoming.find(
(w) => w.type && w.type === existingWidget.type,
);
- // We don't want to override existing notes or award emojis with empty widget on work item updates
- if (
- (incomingWidget?.type === WIDGET_TYPE_NOTES ||
- incomingWidget?.type === WIDGET_TYPE_AWARD_EMOJI) &&
- !context.variables.pageSize
- ) {
- return existingWidget;
- }
// we want to concat next page of awardEmoji to the existing ones
if (incomingWidget?.type === WIDGET_TYPE_AWARD_EMOJI && context.variables.after) {
@@ -116,7 +124,7 @@ export const config = {
};
}
- return incomingWidget || existingWidget;
+ return { ...existingWidget, ...incomingWidget };
});
},
},
@@ -211,6 +219,16 @@ export const config = {
epicBoardList: {
keyArgs: ['id'],
},
+ isShowingLabels: {
+ read(currentState) {
+ return currentState ?? true;
+ },
+ },
+ selectedBoardItems: {
+ read(currentState) {
+ return currentState ?? [];
+ },
+ },
},
},
}
@@ -235,6 +253,21 @@ export const resolvers = {
});
return boardItem;
},
+ setSelectedBoardItems(_, { itemId }, { cache }) {
+ const sourceData = cache.readQuery({ query: selectedBoardItemsQuery });
+ cache.writeQuery({
+ query: selectedBoardItemsQuery,
+ data: { selectedBoardItems: [...sourceData.selectedBoardItems, itemId] },
+ });
+ return [...sourceData.selectedBoardItems, itemId];
+ },
+ unsetSelectedBoardItems(_, _variables, { cache }) {
+ cache.writeQuery({
+ query: selectedBoardItemsQuery,
+ data: { selectedBoardItems: [] },
+ });
+ return [];
+ },
setError(_, { error }, { cache }) {
cache.writeQuery({
query: errorQuery,
@@ -258,6 +291,13 @@ export const resolvers = {
},
};
},
+ setIsShowingLabels(_, { isShowingLabels }, { cache }) {
+ cache.writeQuery({
+ query: isShowingLabelsQuery,
+ data: { isShowingLabels },
+ });
+ return isShowingLabels;
+ },
},
};
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 37c1674cc5a..4e0b1413f71 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -3,6 +3,9 @@
"AlertManagementHttpIntegration",
"AlertManagementPrometheusIntegration"
],
+ "AmazonS3ConfigurationInterface": [
+ "AmazonS3ConfigurationType"
+ ],
"BaseHeaderInterface": [
"AuditEventStreamingHeader",
"AuditEventsStreamingInstanceHeader"
diff --git a/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
index 7c88e494a2e..8f45224338f 100644
--- a/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
@@ -1,10 +1,21 @@
#import "../fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
-query groupUsersSearch($search: String!, $fullPath: ID!) {
+query groupUsersSearch($search: String!, $fullPath: ID!, $after: String, $first: Int) {
workspace: group(fullPath: $fullPath) {
id
- users: groupMembers(search: $search, relations: [DIRECT, DESCENDANTS, INHERITED]) {
+ users: groupMembers(
+ search: $search
+ relations: [DIRECT, DESCENDANTS, INHERITED]
+ first: $first
+ after: $after
+ sort: USER_FULL_NAME_ASC
+ ) {
+ pageInfo {
+ hasNextPage
+ endCursor
+ startCursor
+ }
nodes {
id
user {
diff --git a/app/assets/javascripts/groups/components/empty_states/groups_dashboard_empty_state.vue b/app/assets/javascripts/groups/components/empty_states/groups_dashboard_empty_state.vue
index 470ff45f47a..e74d827af9b 100644
--- a/app/assets/javascripts/groups/components/empty_states/groups_dashboard_empty_state.vue
+++ b/app/assets/javascripts/groups/components/empty_states/groups_dashboard_empty_state.vue
@@ -20,5 +20,6 @@ export default {
:title="$options.i18n.title"
:description="$options.i18n.description"
:svg-path="groupsEmptyStateIllustration"
+ :svg-height="null"
/>
</template>
diff --git a/app/assets/javascripts/groups/components/empty_states/groups_explore_empty_state.vue b/app/assets/javascripts/groups/components/empty_states/groups_explore_empty_state.vue
index f524b769802..0068772ff23 100644
--- a/app/assets/javascripts/groups/components/empty_states/groups_explore_empty_state.vue
+++ b/app/assets/javascripts/groups/components/empty_states/groups_explore_empty_state.vue
@@ -13,5 +13,9 @@ export default {
</script>
<template>
- <gl-empty-state :title="$options.i18n.title" :svg-path="groupsEmptyStateIllustration" />
+ <gl-empty-state
+ :title="$options.i18n.title"
+ :svg-path="groupsEmptyStateIllustration"
+ :svg-height="null"
+ />
</template>
diff --git a/app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue b/app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue
index 0bd95d59022..841a80b6ce4 100644
--- a/app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue
+++ b/app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue
@@ -86,6 +86,7 @@ export default {
v-else
:title="$options.i18n.withoutLinks.title"
:svg-path="emptySubgroupIllustration"
+ :svg-height="null"
:description="$options.i18n.withoutLinks.description"
/>
</template>
diff --git a/app/assets/javascripts/groups/components/group_name_and_path.vue b/app/assets/javascripts/groups/components/group_name_and_path.vue
index fd633df3022..853fdd7c55e 100644
--- a/app/assets/javascripts/groups/components/group_name_and_path.vue
+++ b/app/assets/javascripts/groups/components/group_name_and_path.vue
@@ -294,7 +294,7 @@ export default {
:name="fields.name.name"
:placeholder="$options.i18n.inputs.name.placeholder"
data-testid="group-name-field"
- :size="$options.inputSize"
+ :width="$options.inputSize"
:state="nameFeedbackState"
@invalid="handleInvalidName"
/>
@@ -374,7 +374,7 @@ export default {
:maxlength="fields.path.maxLength"
:pattern="fields.path.pattern"
:state="pathFeedbackState"
- :size="pathInputSize"
+ :width="pathInputSize"
required
data-testid="group-path-field"
:data-bind-in="mattermostEnabled ? $options.mattermostDataBindName : null"
@@ -397,7 +397,7 @@ export default {
:id="fields.groupId.id"
:value="fields.groupId.value"
:name="fields.groupId.name"
- size="sm"
+ width="sm"
readonly
/>
</gl-form-group>
diff --git a/app/assets/javascripts/groups/components/transfer_group_form.vue b/app/assets/javascripts/groups/components/transfer_group_form.vue
index 3da417ebf0a..9f968817a3a 100644
--- a/app/assets/javascripts/groups/components/transfer_group_form.vue
+++ b/app/assets/javascripts/groups/components/transfer_group_form.vue
@@ -73,7 +73,7 @@ export default {
:disabled="disableSubmitButton"
:phrase="confirmationPhrase"
:button-text="confirmButtonText"
- button-qa-selector="transfer_group_button"
+ button-testid="transfer-group-button"
@confirm="$emit('confirm')"
/>
</div>
diff --git a/app/assets/javascripts/helpers/startup_css_helper.js b/app/assets/javascripts/helpers/startup_css_helper.js
deleted file mode 100644
index 6e19979721c..00000000000
--- a/app/assets/javascripts/helpers/startup_css_helper.js
+++ /dev/null
@@ -1,36 +0,0 @@
-const CSS_LOADED_EVENT = 'CSSLoaded';
-const STARTUP_LINK_LOADED_EVENT = 'CSSStartupLinkLoaded';
-
-const getAllStartupLinks = (() => {
- let links = null;
- return () => {
- if (!links) {
- links = Array.from(document.querySelectorAll('link[data-startupcss]'));
- }
- return links;
- };
-})();
-const isStartupLinkLoaded = ({ dataset }) => dataset.startupcss === 'loaded';
-const allLinksLoaded = () => getAllStartupLinks().every(isStartupLinkLoaded);
-
-const handleStartupEvents = () => {
- if (allLinksLoaded()) {
- document.dispatchEvent(new CustomEvent(CSS_LOADED_EVENT));
- document.removeEventListener(STARTUP_LINK_LOADED_EVENT, handleStartupEvents);
- }
-};
-
-/* For `waitForCSSLoaded` methods, see docs.gitlab.com/ee/development/fe_guide/performance.html#important-considerations */
-export const waitForCSSLoaded = (action = () => {}) => {
- if (allLinksLoaded()) {
- return new Promise((resolve) => {
- action();
- resolve();
- });
- }
-
- return new Promise((resolve) => {
- document.addEventListener(CSS_LOADED_EVENT, resolve, { once: true });
- document.addEventListener(STARTUP_LINK_LOADED_EVENT, handleStartupEvents);
- }).then(action);
-};
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 741845e3325..ba1258f8b50 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -176,7 +176,6 @@ export default {
type="text"
class="form-control"
data-testid="file-name-field"
- data-qa-selector="file_name_field"
:placeholder="placeholder"
/>
</form>
diff --git a/app/assets/javascripts/ide/init_gitlab_web_ide.js b/app/assets/javascripts/ide/init_gitlab_web_ide.js
index 2e113003f8a..868830c953a 100644
--- a/app/assets/javascripts/ide/init_gitlab_web_ide.js
+++ b/app/assets/javascripts/ide/init_gitlab_web_ide.js
@@ -1,9 +1,11 @@
import { start } from '@gitlab/web-ide';
import { __ } from '~/locale';
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
import { createAndSubmitForm } from '~/lib/utils/create_and_submit_form';
import csrf from '~/lib/utils/csrf';
+import Tracking from '~/tracking';
import { getBaseConfig } from './lib/gitlab_web_ide/get_base_config';
import { setupRootElement } from './lib/gitlab_web_ide/setup_root_element';
import { GITLAB_WEB_IDE_FEEDBACK_ISSUE } from './constants';
@@ -39,13 +41,14 @@ export const initGitlabWebIDE = async (el) => {
filePath,
mergeRequest: mrId,
forkInfo: forkInfoJSON,
- editorFontSrcUrl,
- editorFontFormat,
- editorFontFamily,
+ editorFont: editorFontJSON,
codeSuggestionsEnabled,
} = el.dataset;
const rootEl = setupRootElement(el);
+ const editorFont = editorFontJSON
+ ? convertObjectPropsToCamelCase(JSON.parse(editorFontJSON), { deep: true })
+ : null;
const forkInfo = forkInfoJSON ? JSON.parse(forkInfoJSON) : null;
// See ClientOnlyConfig https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/main/packages/web-ide-types/src/config.ts#L17
@@ -69,13 +72,11 @@ export const initGitlabWebIDE = async (el) => {
userPreferences: el.dataset.userPreferencesPath,
signIn: el.dataset.signInPath,
},
- editorFont: {
- srcUrl: editorFontSrcUrl,
- fontFamily: editorFontFamily,
- format: editorFontFormat,
- },
+ editorFont,
codeSuggestionsEnabled,
handleTracking,
+ // See https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/main/packages/web-ide-types/src/config.ts#L86
+ telemetryEnabled: Tracking.enabled(),
async handleStartRemote({ remoteHost, remotePath, connectionToken }) {
const confirmed = await confirmAction(
__('Are you sure you want to leave the Web IDE? All unsaved changes will be lost.'),
diff --git a/app/assets/javascripts/ide/lib/gitlab_web_ide/handle_tracking_event.js b/app/assets/javascripts/ide/lib/gitlab_web_ide/handle_tracking_event.js
index 615dad02386..5e3e5bfe4c1 100644
--- a/app/assets/javascripts/ide/lib/gitlab_web_ide/handle_tracking_event.js
+++ b/app/assets/javascripts/ide/lib/gitlab_web_ide/handle_tracking_event.js
@@ -8,7 +8,7 @@ export const handleTracking = ({ name, data }) => {
if (data && Object.keys(data).length) {
Tracking.event(undefined, snakeCaseEventName, {
/* See GitLab snowplow schema for a definition of the extra field
- * https://docs.gitlab.com/ee/development/snowplow/schemas.html#gitlab_standard.
+ * https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_standard/jsonschema/1-0-9.
*/
extra: convertObjectPropsToSnakeCase(data, {
deep: true,
diff --git a/app/assets/javascripts/import/constants.js b/app/assets/javascripts/import/constants.js
index b9814b5ca60..ddf69a8fcdf 100644
--- a/app/assets/javascripts/import/constants.js
+++ b/app/assets/javascripts/import/constants.js
@@ -8,7 +8,7 @@ const STATISTIC_ITEMS = {
issue_event: __('Issue events'),
label: __('Labels'),
lfs_object: __('LFS objects'),
- merge_request_attachment: s__('GithubImporter|Merge request links'),
+ merge_request_attachment: s__('GithubImporter|PR attachments'),
milestone: __('Milestones'),
note: __('Notes'),
note_attachment: s__('GithubImporter|Note links'),
@@ -17,7 +17,7 @@ const STATISTIC_ITEMS = {
pull_request: s__('GithubImporter|Pull requests'),
pull_request_merged_by: s__('GithubImporter|PR mergers'),
pull_request_review: s__('GithubImporter|PR reviews'),
- pull_request_review_request: s__('GithubImporter|PR reviews'),
+ pull_request_review_request: s__('GithubImporter|PR reviewers'),
release: __('Releases'),
release_attachment: s__('GithubImporter|Release links'),
};
diff --git a/app/assets/javascripts/import_entities/components/group_dropdown.vue b/app/assets/javascripts/import_entities/components/group_dropdown.vue
deleted file mode 100644
index 68bdcf7ef90..00000000000
--- a/app/assets/javascripts/import_entities/components/group_dropdown.vue
+++ /dev/null
@@ -1,72 +0,0 @@
-<script>
-import { GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
-import { debounce } from 'lodash';
-
-import { s__ } from '~/locale';
-import { createAlert } from '~/alert';
-import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql';
-import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
-import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-
-// This is added outside the component as each dropdown on the page triggers a query,
-// so if multiple queries fail, we only show 1 error.
-const reportNamespaceLoadError = debounce(
- () =>
- createAlert({
- message: s__('ImportProjects|Requesting namespaces failed'),
- }),
- DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
-);
-
-export default {
- components: {
- GlDropdown,
- GlSearchBoxByType,
- },
- inheritAttrs: false,
- data() {
- return { searchTerm: '' };
- },
- apollo: {
- namespaces: {
- query: searchNamespacesWhereUserCanImportProjectsQuery,
- variables() {
- return {
- search: this.searchTerm,
- };
- },
- skip() {
- const hasNotEnoughSearchCharacters =
- this.searchTerm.length > 0 && this.searchTerm.length < MINIMUM_SEARCH_LENGTH;
- return hasNotEnoughSearchCharacters;
- },
- update(data) {
- return data.currentUser.groups.nodes;
- },
- error: reportNamespaceLoadError,
- debounce: DEBOUNCE_DELAY,
- },
- },
- computed: {
- filteredNamespaces() {
- return (this.namespaces ?? []).filter((ns) =>
- ns.fullPath.toLowerCase().includes(this.searchTerm.toLowerCase()),
- );
- },
- },
-};
-</script>
-<template>
- <gl-dropdown
- toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
- class="gl-h-7 gl-flex-fill-1"
- data-qa-selector="target_namespace_selector_dropdown"
- v-bind="$attrs"
- >
- <template #header>
- <gl-search-box-by-type v-model.trim="searchTerm" />
- </template>
- <slot :namespaces="filteredNamespaces"></slot>
- </gl-dropdown>
-</template>
diff --git a/app/assets/javascripts/import_entities/components/import_target_dropdown.vue b/app/assets/javascripts/import_entities/components/import_target_dropdown.vue
index b18a106608a..47c030bf1fc 100644
--- a/app/assets/javascripts/import_entities/components/import_target_dropdown.vue
+++ b/app/assets/javascripts/import_entities/components/import_target_dropdown.vue
@@ -26,13 +26,19 @@ export default {
},
props: {
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
selected: {
type: String,
required: true,
},
userNamespace: {
type: String,
- required: true,
+ required: false,
+ default: undefined,
},
},
@@ -66,6 +72,10 @@ export default {
},
computed: {
+ isProject() {
+ return Boolean(this.userNamespace);
+ },
+
filteredNamespaces() {
return (this.namespaces ?? []).filter((ns) =>
ns.fullPath.toLowerCase().includes(this.searchTerm.toLowerCase()),
@@ -78,14 +88,33 @@ export default {
items() {
return [
- {
- text: __('Users'),
- options: [{ text: this.userNamespace, value: this.userNamespace }],
- },
+ this.isProject
+ ? {
+ text: __('Users'),
+ options: [
+ {
+ text: this.userNamespace,
+ value: this.userNamespace,
+ },
+ ],
+ }
+ : {
+ text: __('Parent'),
+ textSrOnly: true,
+ options: [
+ {
+ text: s__('BulkImport|No parent'),
+ value: '',
+ },
+ ],
+ },
{
text: __('Groups'),
options: this.filteredNamespaces.map((namespace) => {
- return { text: namespace.fullPath, value: namespace.fullPath };
+ return {
+ text: namespace.fullPath,
+ value: namespace.fullPath,
+ };
}),
},
];
@@ -94,7 +123,15 @@ export default {
methods: {
onSelect(value) {
- this.$emit('select', value);
+ if (this.isProject) {
+ this.$emit('select', value);
+ } else if (value === '') {
+ this.$emit('select', { fullPath: '', id: null });
+ } else {
+ const { fullPath, id } = this.filteredNamespaces.find((ns) => ns.fullPath === value);
+
+ this.$emit('select', { fullPath, id });
+ }
},
onSearch(value) {
@@ -107,12 +144,13 @@ export default {
<template>
<gl-collapsible-listbox
:items="items"
+ :disabled="disabled"
:selected="selected"
:toggle-text="toggleText"
searchable
fluid-width
toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
- data-qa-selector="target_namespace_selector_dropdown"
+ data-testid="target-namespace-dropdown"
@select="onSelect"
@search="onSearch"
/>
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index cd07e9fbdd9..24197c680eb 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -393,7 +393,7 @@ export default {
}
},
- importGroup({ group, extraArgs, index }) {
+ async importGroup({ group, extraArgs, index }) {
if (group.flags.isFinished && !this.reimportRequests.includes(group.id)) {
this.validateImportTarget(group.importTarget);
this.reimportRequests.push(group.id);
@@ -402,7 +402,7 @@ export default {
});
} else {
this.reimportRequests = this.reimportRequests.filter((id) => id !== group.id);
- this.requestGroupsImport([
+ await this.requestGroupsImport([
{
sourceGroupId: group.id,
targetNamespace: group.importTarget.targetNamespace.fullPath,
@@ -410,6 +410,16 @@ export default {
...extraArgs,
},
]);
+
+ const updatedGroup = this.groups?.find((g) => g.id === group.id);
+
+ if (
+ updatedGroup.progress &&
+ updatedGroup.progress.status === STATUSES.FAILED &&
+ updatedGroup.progress.message
+ ) {
+ this.reimportRequests.push(group.id);
+ }
}
},
@@ -427,6 +437,7 @@ export default {
},
setPageSize(size) {
+ this.page = 1;
this.perPage = size;
},
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
index 807b084fefb..b4484c89b9f 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
@@ -1,20 +1,12 @@
<script>
-import {
- GlDropdownDivider,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlFormInput,
-} from '@gitlab/ui';
-import { s__ } from '~/locale';
-import ImportGroupDropdown from '../../components/group_dropdown.vue';
+import { GlFormInput } from '@gitlab/ui';
+import ImportTargetDropdown from '../../components/import_target_dropdown.vue';
+
import { getInvalidNameValidationMessage } from '../utils';
export default {
components: {
- ImportGroupDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- GlDropdownSectionHeader,
+ ImportTargetDropdown,
GlFormInput,
},
props: {
@@ -25,8 +17,8 @@ export default {
},
computed: {
- fullPath() {
- return this.group.importTarget.targetNamespace.fullPath || s__('BulkImport|No parent');
+ selectedImportTarget() {
+ return this.group.importTarget.targetNamespace.fullPath || '';
},
validationMessage() {
return (
@@ -47,6 +39,10 @@ export default {
focusNewName() {
this.$refs.newName.$el.focus();
},
+
+ onImportTargetSelect(namespace) {
+ this.$emit('update-target-namespace', namespace);
+ },
},
};
</script>
@@ -54,34 +50,12 @@ export default {
<template>
<div>
<div class="gl-display-flex gl-align-items-stretch">
- <import-group-dropdown
- #default="{ namespaces }"
- :text="fullPath"
+ <import-target-dropdown
+ :selected="selectedImportTarget"
:disabled="!isPathSelectionAvailable"
- toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
- class="gl-h-7 gl-flex-grow-1"
- data-qa-selector="target_namespace_selector_dropdown"
- data-testid="target-namespace-selector"
- >
- <gl-dropdown-item @click="$emit('update-target-namespace', { fullPath: '', id: null })">{{
- s__('BulkImport|No parent')
- }}</gl-dropdown-item>
- <template v-if="namespaces.length">
- <gl-dropdown-divider />
- <gl-dropdown-section-header>
- {{ s__('BulkImport|Existing groups') }}
- </gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="ns in namespaces"
- :key="ns.fullPath"
- data-qa-selector="target_group_dropdown_item"
- :data-qa-group-name="ns.fullPath"
- @click="$emit('update-target-namespace', ns)"
- >
- {{ ns.fullPath }}
- </gl-dropdown-item>
- </template>
- </import-group-dropdown>
+ @select="onImportTargetSelect"
+ />
+
<div
class="gl-h-7 gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1 gl-bg-gray-10"
:class="{
@@ -100,6 +74,7 @@ export default {
'gl-inset-border-1-gray-100!': !isPathSelectionAvailable,
}"
debounce="500"
+ data-testid="target-namespace-input"
:disabled="!isPathSelectionAvailable"
:value="group.importTarget.newName"
:aria-label="__('New name')"
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 782f417a989..0e1afebbe2b 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -500,6 +500,7 @@ export default {
<gl-empty-state
:title="emptyStateData.title"
:svg-path="emptyListSvgPath"
+ :svg-height="null"
:description="emptyStateData.description"
:primary-button-link="emptyStateData.btnLink"
:primary-button-text="emptyStateData.btnText"
diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js
index e803e11bf6d..12f5fa21137 100644
--- a/app/assets/javascripts/integrations/constants.js
+++ b/app/assets/javascripts/integrations/constants.js
@@ -59,6 +59,8 @@ export const integrationTriggerEvents = {
DEPLOYMENT: 'deployment_events',
ALERT: 'alert_events',
INCIDENT: 'incident_events',
+ GROUP_MENTION: 'group_mention_events',
+ GROUP_CONFIDENTIAL_MENTION: 'group_confidential_mention_events',
};
export const integrationTriggerEventTitles = {
@@ -88,6 +90,12 @@ export const integrationTriggerEventTitles = {
[integrationTriggerEvents.INCIDENT]: s__(
'IntegrationEvents|An incident is created, closed, or reopened',
),
+ [integrationTriggerEvents.GROUP_MENTION]: s__(
+ 'IntegrationEvents|A group is mentioned in a public context',
+ ),
+ [integrationTriggerEvents.GROUP_CONFIDENTIAL_MENTION]: s__(
+ 'IntegrationEvents|A group is mentioned in a confidential context',
+ ),
};
export const billingPlans = {
diff --git a/app/assets/javascripts/integrations/gitlab_slack_application/components/gitlab_slack_application.vue b/app/assets/javascripts/integrations/gitlab_slack_application/components/gitlab_slack_application.vue
index bcb199853bd..edfb0af5bbe 100644
--- a/app/assets/javascripts/integrations/gitlab_slack_application/components/gitlab_slack_application.vue
+++ b/app/assets/javascripts/integrations/gitlab_slack_application/components/gitlab_slack_application.vue
@@ -101,7 +101,7 @@ export default {
@project-selected="selectProject"
/>
- <div class="gl-display-flex gl-justify-content-end">
+ <div class="gl-display-flex gl-justify-content-end gl-mt-3">
<gl-button
category="primary"
variant="confirm"
diff --git a/app/assets/javascripts/integrations/gitlab_slack_application/components/projects_dropdown.vue b/app/assets/javascripts/integrations/gitlab_slack_application/components/projects_dropdown.vue
index 26d191cd0bf..7c5287c69d6 100644
--- a/app/assets/javascripts/integrations/gitlab_slack_application/components/projects_dropdown.vue
+++ b/app/assets/javascripts/integrations/gitlab_slack_application/components/projects_dropdown.vue
@@ -1,13 +1,13 @@
<script>
-import { GlDropdown } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlAvatarLabeled } from '@gitlab/ui';
import { __ } from '~/locale';
-
-import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
+import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default {
components: {
- GlDropdown,
- ProjectListItem,
+ GlCollapsibleListbox,
+ GlAvatarLabeled,
},
props: {
projectDropdownText: {
@@ -26,30 +26,61 @@ export default {
default: null,
},
},
+ data() {
+ return {
+ selected: this.selectedProject,
+ };
+ },
computed: {
dropdownText() {
return this.selectedProject
? this.selectedProject.name_with_namespace
: this.projectDropdownText;
},
+ items() {
+ const items = this.projects.map((project) => {
+ return {
+ value: project.id,
+ ...project,
+ };
+ });
+
+ return convertObjectPropsToCamelCase(items, { deep: true });
+ },
},
methods: {
- onClick(project) {
- this.$emit('project-selected', project);
- this.$refs.dropdown.hide(true);
+ getEntityId(project) {
+ return isGid(project.id) ? getIdFromGraphQLId(project.id) : project.id;
+ },
+ selectProject(projectId) {
+ this.$emit(
+ 'project-selected',
+ this.projects.find((project) => project.id === projectId),
+ );
},
},
};
</script>
<template>
- <gl-dropdown ref="dropdown" block :text="dropdownText" menu-class="gl-w-full!">
- <project-list-item
- v-for="project in projects"
- :key="project.id"
- :project="project"
- :selected="false"
- @click="onClick(project)"
- />
- </gl-dropdown>
+ <gl-collapsible-listbox
+ v-model="selected"
+ block
+ fluid-width
+ is-check-centered
+ :toggle-text="dropdownText"
+ :items="items"
+ @select="selectProject"
+ >
+ <template #list-item="{ item }">
+ <gl-avatar-labeled
+ :label="item.nameWithNamespace"
+ :entity-name="item.nameWithNamespace"
+ :entity-id="getEntityId(item)"
+ shape="rect"
+ :size="32"
+ :src="item.avatarUrl"
+ />
+ </template>
+ </gl-collapsible-listbox>
</template>
diff --git a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
index 91dbd86418c..4b492e48095 100644
--- a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
@@ -16,7 +16,7 @@ import GroupSelect from './group_select.vue';
import InviteGroupNotification from './invite_group_notification.vue';
export default {
- name: 'InviteMembersModal',
+ name: 'InviteGroupsModal',
components: {
GroupSelect,
InviteModalBase,
diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
index e9d7acdc913..509efd31dcd 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -1,12 +1,14 @@
<script>
import { GlAlert, GlButton, GlCollapse, GlIcon } from '@gitlab/ui';
import { partition, isString, uniqueId, isEmpty } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_base.vue';
import Api from '~/api';
import Tracking from '~/tracking';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { n__, sprintf } from '~/locale';
import { memberName, triggerExternalAlert } from 'ee_else_ce/invite_members/utils/member_utils';
+import { captureException } from '~/ci/runner/sentry_utils';
import {
USERS_FILTER_ALL,
MEMBER_MODAL_LABELS,
@@ -37,6 +39,9 @@ export default {
ActiveTrialNotification: () =>
import('ee_component/invite_members/components/active_trial_notification.vue'),
},
+ directives: {
+ SafeHtml,
+ },
mixins: [Tracking.mixin({ category: INVITE_MEMBER_MODAL_TRACKING_CATEGORY })],
props: {
id: {
@@ -262,8 +267,9 @@ export default {
} else {
this.onInviteSuccess();
}
- } catch (e) {
- this.showInvalidFeedbackMessage(e);
+ } catch (error) {
+ captureException({ error, component: this.$options.name });
+ this.showInvalidFeedbackMessage(error);
} finally {
this.isLoading = false;
}
@@ -391,7 +397,8 @@ export default {
:key="error.member"
data-testid="errors-limited-item"
>
- <strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
+ <strong>{{ error.displayedMemberName }}:</strong>
+ <span v-safe-html="error.message"></span>
</li>
</ul>
<template v-if="shouldErrorsSectionExpand">
@@ -402,7 +409,8 @@ export default {
:key="error.member"
data-testid="errors-expanded-item"
>
- <strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
+ <strong>{{ error.displayedMemberName }}:</strong>
+ <span v-safe-html="error.message"></span>
</li>
</ul>
</gl-collapse>
diff --git a/app/assets/javascripts/invite_members/components/invite_modal_base.vue b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
index 5a891e23faf..18d22395104 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -253,7 +253,6 @@ export default {
<gl-modal
ref="modal"
:modal-id="modalId"
- data-qa-selector="invite_members_modal_content"
data-testid="invite-modal"
size="sm"
dialog-class="gl-mx-5"
diff --git a/app/assets/javascripts/issuable/components/hidden_badge.vue b/app/assets/javascripts/issuable/components/hidden_badge.vue
new file mode 100644
index 00000000000..a80dc2f62d4
--- /dev/null
+++ b/app/assets/javascripts/issuable/components/hidden_badge.vue
@@ -0,0 +1,36 @@
+<script>
+import { GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { issuableTypeText } from '~/issues/constants';
+import { __, sprintf } from '~/locale';
+
+export default {
+ components: {
+ GlBadge,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ issuableType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ title() {
+ return sprintf(__('This %{issuable} is hidden because its author has been banned.'), {
+ issuable: issuableTypeText[this.issuableType],
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-badge v-gl-tooltip :title="title" variant="warning">
+ <gl-icon name="spam" />
+ <span class="gl-sr-only">{{ __('Hidden') }}</span>
+ </gl-badge>
+</template>
diff --git a/app/assets/javascripts/issuable/components/locked_badge.vue b/app/assets/javascripts/issuable/components/locked_badge.vue
new file mode 100644
index 00000000000..f97ac888417
--- /dev/null
+++ b/app/assets/javascripts/issuable/components/locked_badge.vue
@@ -0,0 +1,36 @@
+<script>
+import { GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { issuableTypeText } from '~/issues/constants';
+import { __, sprintf } from '~/locale';
+
+export default {
+ components: {
+ GlBadge,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ issuableType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ title() {
+ return sprintf(__('This %{issuable} is locked. Only project members can comment.'), {
+ issuable: issuableTypeText[this.issuableType],
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-badge v-gl-tooltip :title="title" variant="warning">
+ <gl-icon name="lock" />
+ <span class="gl-sr-only">{{ __('Locked') }}</span>
+ </gl-badge>
+</template>
diff --git a/app/assets/javascripts/issuable/components/related_issuable_item.vue b/app/assets/javascripts/issuable/components/related_issuable_item.vue
index ff48bfceb29..71bd301162e 100644
--- a/app/assets/javascripts/issuable/components/related_issuable_item.vue
+++ b/app/assets/javascripts/issuable/components/related_issuable_item.vue
@@ -248,7 +248,7 @@ export default {
size="small"
:disabled="removeDisabled"
class="js-issue-item-remove-button gl-mr-2"
- data-testid="remove_related_issue_button"
+ data-testid="remove-related-issue-button"
:title="__('Remove')"
:aria-label="__('Remove')"
@click="onRemoveRequest"
diff --git a/app/assets/javascripts/issuable/issuable_label_selector.js b/app/assets/javascripts/issuable/issuable_label_selector.js
index fc6d850c341..804f7384732 100644
--- a/app/assets/javascripts/issuable/issuable_label_selector.js
+++ b/app/assets/javascripts/issuable/issuable_label_selector.js
@@ -45,7 +45,7 @@ export default () => {
labelsManagePath,
variant: VARIANT_EMBEDDED,
workspaceType: WORKSPACE_PROJECT,
- toggleAttrs: { 'data-testid': 'issuable_label_dropdown' },
+ toggleAttrs: { 'data-testid': 'issuable-label-dropdown' },
},
render(createElement) {
return createElement(IssuableLabelSelector);
diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js
index 80344efc44c..3d8017e6e07 100644
--- a/app/assets/javascripts/issues/constants.js
+++ b/app/assets/javascripts/issues/constants.js
@@ -28,7 +28,7 @@ export const issuableStatusText = {
[STATUS_LOCKED]: __('Open'),
};
-export const IssuableTypeText = {
+export const issuableTypeText = {
[TYPE_ISSUE]: __('issue'),
[TYPE_EPIC]: __('epic'),
[TYPE_MERGE_REQUEST]: __('merge request'),
diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js
index 3bd28c50800..eea5207801c 100644
--- a/app/assets/javascripts/issues/index.js
+++ b/app/assets/javascripts/issues/index.js
@@ -17,17 +17,6 @@ import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initWorkItemLinks from '~/work_items/components/work_item_links';
import ZenMode from '~/zen_mode';
import initAwardsApp from '~/emoji/awards_app';
-import FilteredSearchServiceDesk from './filtered_search_service_desk';
-
-export function initFilteredSearchServiceDesk() {
- if (document.querySelector('.filtered-search')) {
- const supportBotData = JSON.parse(
- document.querySelector('.js-service-desk-issues').dataset.supportBot,
- );
- const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
- filteredSearchManager.setup();
- }
-}
export function initForm() {
new IssuableForm($('.issue-form')); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue b/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue
index 3c58843bcbc..a9ad2db5dd3 100644
--- a/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue
+++ b/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue
@@ -29,6 +29,7 @@ export default {
:title="$options.i18n.noSearchResultsTitle"
:svg-path="emptyStateSvgPath"
:svg-height="150"
+ data-testid="issuable-empty-state"
>
<template #actions>
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
@@ -42,6 +43,8 @@ export default {
:description="$options.i18n.noOpenIssuesDescription"
:title="$options.i18n.noOpenIssuesTitle"
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
+ data-testid="issuable-empty-state"
>
<template #actions>
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
@@ -55,5 +58,6 @@ export default {
:title="$options.i18n.noClosedIssuesTitle"
:svg-path="emptyStateSvgPath"
:svg-height="150"
+ data-testid="issuable-empty-state"
/>
</template>
diff --git a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue
index 3d62ea07f59..6741b39d5ef 100644
--- a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue
+++ b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue
@@ -61,6 +61,7 @@ export default {
:title="$options.i18n.noIssuesTitle"
:svg-path="emptyStateSvgPath"
:svg-height="150"
+ data-testid="issuable-empty-state"
>
<template #description>
<gl-link :href="$options.issuesHelpPagePath">
@@ -71,16 +72,26 @@ export default {
</p>
</template>
<template #actions>
- <gl-button v-if="canCreateProjects" :href="newProjectPath" variant="confirm">
+ <gl-button
+ v-if="canCreateProjects"
+ :href="newProjectPath"
+ variant="confirm"
+ class="gl-mx-2 gl-mb-3"
+ >
{{ $options.i18n.newProjectLabel }}
</gl-button>
- <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+ <gl-button
+ v-if="showNewIssueLink"
+ :href="newIssuePath"
+ variant="confirm"
+ class="gl-mx-2 gl-mb-3"
+ >
{{ $options.i18n.newIssueLabel }}
</gl-button>
<gl-disclosure-dropdown
v-if="showCsvButtons"
- class="gl-w-full gl-sm-w-auto gl-sm-mr-3"
+ class="gl-mx-2 gl-mb-3"
:toggle-text="$options.i18n.importIssues"
data-testid="import-issues-dropdown"
>
@@ -92,7 +103,7 @@ export default {
<new-resource-dropdown
v-if="showNewIssueDropdown"
- class="gl-align-self-center"
+ class="gl-align-self-center gl-mx-2 gl-mb-3"
:query="$options.searchProjectsQuery"
:query-variables="newIssueDropdownQueryVariables"
:extract-projects="extractProjects"
@@ -120,8 +131,10 @@ export default {
v-else
:title="$options.i18n.noIssuesTitle"
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
:primary-button-text="$options.i18n.noIssuesSignedOutButtonText"
:primary-button-link="signInPath"
+ data-testid="issuable-empty-state"
>
<template #description>
<gl-link :href="$options.issuesHelpPagePath">
diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue
index 3d8ed3af816..16e687cff10 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -21,7 +21,6 @@ import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_coun
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { createAlert, VARIANT_INFO } from '~/alert';
import { TYPENAME_USER } from '~/graphql_shared/constants';
-import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
@@ -384,7 +383,8 @@ export default {
dataType: 'user',
defaultUsers: [],
operators: this.hasOrFeature ? OPERATORS_IS_NOT_OR : OPERATORS_IS_NOT,
- fetchUsers: this.fetchUsers,
+ fullPath: this.fullPath,
+ isProject: this.isProject,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-author`,
preloadedUsers,
},
@@ -395,7 +395,8 @@ export default {
token: UserToken,
dataType: 'user',
operators: this.hasOrFeature ? OPERATORS_IS_NOT_OR : OPERATORS_IS_NOT,
- fetchUsers: this.fetchUsers,
+ fullPath: this.fullPath,
+ isProject: this.isProject,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-assignee`,
preloadedUsers,
},
@@ -634,14 +635,6 @@ export default {
fetchLatestLabels(search) {
return this.fetchLabelsWithFetchPolicy(search, fetchPolicies.NETWORK_ONLY);
},
- fetchUsers(search) {
- return this.$apollo
- .query({
- query: usersAutocompleteQuery,
- variables: { fullPath: this.fullPath, search, isProject: this.isProject },
- })
- .then(({ data }) => data[this.namespace]?.autocompleteUsers);
- },
getExportCsvPathWithQuery() {
return `${this.exportCsvPath}${window.location.search}`;
},
diff --git a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
index f3173f0e33a..3b49c0efb14 100644
--- a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
@@ -11,7 +11,6 @@ fragment IssueFragment on Issue {
moved
state
title
- titleHtml
updatedAt
closedAt
upvotes
diff --git a/app/assets/javascripts/issues/service_desk/components/empty_state_with_any_issues.vue b/app/assets/javascripts/issues/service_desk/components/empty_state_with_any_issues.vue
index ab9e70ae223..f5f06e4daef 100644
--- a/app/assets/javascripts/issues/service_desk/components/empty_state_with_any_issues.vue
+++ b/app/assets/javascripts/issues/service_desk/components/empty_state_with_any_issues.vue
@@ -55,5 +55,6 @@ export default {
:title="content.title"
:svg-path="emptyStateSvgPath"
:svg-height="content.svgHeight"
+ data-testid="issuable-empty-state"
/>
</template>
diff --git a/app/assets/javascripts/issues/service_desk/components/empty_state_without_any_issues.vue b/app/assets/javascripts/issues/service_desk/components/empty_state_without_any_issues.vue
index 9dbed2c2579..ea866dfb161 100644
--- a/app/assets/javascripts/issues/service_desk/components/empty_state_without_any_issues.vue
+++ b/app/assets/javascripts/issues/service_desk/components/empty_state_without_any_issues.vue
@@ -42,7 +42,9 @@ export default {
<gl-empty-state
:title="$options.i18n.infoBannerTitle"
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
content-class="gl-max-w-80!"
+ data-testid="issues-service-desk-empty-state"
>
<template #description>
<p v-if="canSeeEmailAddress">
@@ -60,9 +62,11 @@ export default {
v-else
:title="$options.i18n.infoBannerTitle"
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
:primary-button-text="$options.i18n.noIssuesSignedOutButtonText"
:primary-button-link="signInPath"
content-class="gl-max-w-80!"
+ data-testid="issues-service-desk-empty-state"
>
<template #description>
<p>{{ $options.i18n.infoBannerUserNote }}</p>
diff --git a/app/assets/javascripts/issues/filtered_search_service_desk.js b/app/assets/javascripts/issues/service_desk/filtered_search_service_desk.js
index bec207aa439..bec207aa439 100644
--- a/app/assets/javascripts/issues/filtered_search_service_desk.js
+++ b/app/assets/javascripts/issues/service_desk/filtered_search_service_desk.js
diff --git a/app/assets/javascripts/issues/service_desk/index.js b/app/assets/javascripts/issues/service_desk/index.js
index 579cf343477..cc5f6b40a91 100644
--- a/app/assets/javascripts/issues/service_desk/index.js
+++ b/app/assets/javascripts/issues/service_desk/index.js
@@ -3,8 +3,19 @@ import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import { parseBoolean } from '~/lib/utils/common_utils';
import ServiceDeskListApp from 'ee_else_ce/issues/service_desk/components/service_desk_list_app.vue';
+import FilteredSearchServiceDesk from './filtered_search_service_desk';
import { gqlClient } from './graphql';
+export function initFilteredSearchServiceDesk() {
+ if (document.querySelector('.filtered-search')) {
+ const supportBotData = JSON.parse(
+ document.querySelector('.js-service-desk-issues').dataset.supportBot,
+ );
+ const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
+ filteredSearchManager.setup();
+ }
+}
+
export async function mountServiceDeskListApp() {
const el = document.querySelector('.js-service-desk-list');
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index d59692d2a28..756585683c8 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -185,12 +185,12 @@ export default {
default: false,
},
issueId: {
- type: Number,
+ type: String,
required: false,
default: null,
},
issueIid: {
- type: Number,
+ type: String,
required: false,
default: null,
},
@@ -521,7 +521,6 @@ export default {
:project-namespace="projectNamespace"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
- :issue-id="issueId"
:issuable-type="issuableType"
@updateForm="setFormState"
/>
@@ -550,7 +549,6 @@ export default {
:issuable-type="issuableType"
:show="isStickyHeaderShowing"
:title="state.titleText"
- :title-html="state.titleHtml"
@hide="hideStickyHeader"
@show="showStickyHeader"
/>
diff --git a/app/assets/javascripts/issues/show/components/delete_issue_modal.vue b/app/assets/javascripts/issues/show/components/delete_issue_modal.vue
index 26e82f10c3d..23979669453 100644
--- a/app/assets/javascripts/issues/show/components/delete_issue_modal.vue
+++ b/app/assets/javascripts/issues/show/components/delete_issue_modal.vue
@@ -35,7 +35,7 @@ export default {
return {
attributes: {
variant: 'danger',
- 'data-qa-selector': 'confirm_delete_issue_button',
+ 'data-testid': 'confirm-delete-issue-button',
},
text: this.title,
};
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index acbba216601..369aa694739 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -74,12 +74,12 @@ export default {
default: 0,
},
issueId: {
- type: Number,
+ type: String,
required: false,
default: null,
},
issueIid: {
- type: Number,
+ type: String,
required: false,
default: null,
},
@@ -362,7 +362,12 @@ export default {
},
},
update: (cache, { data: { workItemCreate } }) =>
- addHierarchyChild(cache, this.fullPath, String(this.issueIid), workItemCreate.workItem),
+ addHierarchyChild({
+ cache,
+ fullPath: this.fullPath,
+ iid: this.issueIid,
+ workItem: workItemCreate.workItem,
+ }),
});
const { workItem, errors } = data.workItemCreate;
@@ -392,7 +397,12 @@ export default {
mutation: deleteWorkItemMutation,
variables: { input: { id } },
update: (cache) =>
- removeHierarchyChild(cache, this.fullPath, String(this.issueIid), { id }),
+ removeHierarchyChild({
+ cache,
+ fullPath: this.fullPath,
+ iid: this.issueIid,
+ workItem: { id },
+ }),
});
if (data.workItemDelete.errors?.length) {
diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue
index efe1619ed1f..10323b99665 100644
--- a/app/assets/javascripts/issues/show/components/fields/description.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description.vue
@@ -2,7 +2,6 @@
<script>
import { __ } from '~/locale';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import glFeaturesFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { trackSavedUsingEditor } from '~/vue_shared/components/markdown/tracking';
import { ISSUE_NOTEABLE_TYPE } from '~/notes/constants';
import updateMixin from '../../mixins/update';
@@ -11,7 +10,7 @@ export default {
components: {
MarkdownEditor,
},
- mixins: [updateMixin, glFeaturesFlagMixin()],
+ mixins: [updateMixin],
props: {
value: {
type: String,
@@ -71,7 +70,6 @@ export default {
<label class="sr-only" for="issue-description">{{ __('Description') }}</label>
<markdown-editor
ref="markdownEditor"
- :enable-content-editor="Boolean(glFeatures.contentEditorOnIssues)"
class="gl-mt-3"
:value="value"
:render-markdown-path="markdownPreviewPath"
diff --git a/app/assets/javascripts/issues/show/components/form.vue b/app/assets/javascripts/issues/show/components/form.vue
index 047bdcdcefc..c2248d66860 100644
--- a/app/assets/javascripts/issues/show/components/form.vue
+++ b/app/assets/javascripts/issues/show/components/form.vue
@@ -2,10 +2,7 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { getDraft, updateDraft, getLockVersion, clearDraft } from '~/lib/utils/autosave';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { TYPENAME_ISSUE, TYPENAME_USER } from '~/graphql_shared/constants';
import { TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../event_hub';
import EditActions from './edit_actions.vue';
import DescriptionField from './fields/description.vue';
@@ -24,7 +21,6 @@ export default {
IssuableTypeField,
LockedWarning,
},
- mixins: [glFeatureFlagMixin()],
props: {
endpoint: {
type: String,
@@ -78,11 +74,6 @@ export default {
required: false,
default: '',
},
- issueId: {
- type: Number,
- required: false,
- default: null,
- },
},
data() {
const autosaveKey = [document.location.pathname, document.location.search];
@@ -110,12 +101,6 @@ export default {
showTypeField() {
return [TYPE_INCIDENT, TYPE_ISSUE].includes(this.issuableType);
},
- resourceId() {
- return this.issueId && convertToGraphQLId(TYPENAME_ISSUE, this.issueId);
- },
- userId() {
- return convertToGraphQLId(TYPENAME_USER, gon.current_user_id);
- },
},
watch: {
formData: {
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 81e5c30a264..dee4c536afa 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -14,21 +14,16 @@ import * as Sentry from '@sentry/browser';
import { mapActions, mapGetters, mapState } from 'vuex';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
-import { STATUS_CLOSED, TYPE_ISSUE, IssuableTypeText } from '~/issues/constants';
-import {
- ISSUE_STATE_EVENT_CLOSE,
- ISSUE_STATE_EVENT_REOPEN,
- NEW_ACTIONS_POPOVER_KEY,
-} from '~/issues/show/constants';
+import { STATUS_CLOSED, TYPE_ISSUE, issuableTypeText } from '~/issues/constants';
+import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-import { getCookie, parseBoolean, setCookie, isLoggedIn } from '~/lib/utils/common_utils';
+import { isLoggedIn } from '~/lib/utils/common_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import eventHub from '~/notes/event_hub';
import Tracking from '~/tracking';
import toast from '~/vue_shared/plugins/global_toast';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
-import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import IssuableLockForm from '~/sidebar/components/lock/issuable_lock_form.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -70,7 +65,6 @@ export default {
GlLink,
GlModal,
AbuseCategorySelector,
- NewHeaderActionsPopover,
SidebarSubscriptionsWidget,
IssuableLockForm,
},
@@ -138,7 +132,7 @@ export default {
issueTypeText() {
const { issueType } = this;
- return IssuableTypeText[issueType] ?? issueType;
+ return issuableTypeText[issueType] ?? issueType;
},
buttonText() {
return this.isClosed
@@ -278,11 +272,6 @@ export default {
edit() {
issuesEventHub.$emit('open.form');
},
- dismissPopover() {
- if (this.isMrSidebarMoved && !parseBoolean(getCookie(`${NEW_ACTIONS_POPOVER_KEY}`))) {
- setCookie(NEW_ACTIONS_POPOVER_KEY, true);
- }
- },
copyReference() {
toast(__('Reference copied'));
},
@@ -390,17 +379,6 @@ export default {
{{ $options.i18n.edit }}
</gl-button>
- <gl-button
- v-if="showToggleIssueStateButton && !glFeatures.moveCloseIntoDropdown"
- class="gl-display-none gl-sm-display-inline-flex!"
- :data-qa-selector="qaSelector"
- :loading="isToggleStateButtonLoading"
- data-testid="toggle-issue-state-button"
- @click="toggleIssueState"
- >
- {{ buttonText }}
- </gl-button>
-
<gl-dropdown
v-if="hasDesktopDropdown"
id="new-actions-header-dropdown"
@@ -415,9 +393,8 @@ export default {
data-testid="desktop-dropdown"
no-caret
right
- @shown="dismissPopover"
>
- <template v-if="showMovedSidebarOptions">
+ <template v-if="showMovedSidebarOptions && !glFeatures.notificationsTodosButtons">
<sidebar-subscriptions-widget
:iid="String(iid)"
:full-path="fullPath"
@@ -428,7 +405,7 @@ export default {
<gl-dropdown-divider />
</template>
<gl-dropdown-item
- v-if="showToggleIssueStateButton && glFeatures.moveCloseIntoDropdown"
+ v-if="showToggleIssueStateButton"
data-testid="toggle-issue-state-button"
@click="toggleIssueState"
>
@@ -492,7 +469,6 @@ export default {
</template>
</gl-dropdown>
- <new-header-actions-popover v-if="isMrSidebarMoved" :issue-type="issueType" />
<gl-modal
ref="blockedByIssuesModal"
modal-id="blocked-by-issues-modal"
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
index 1905678209f..7d2b371801b 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
@@ -178,7 +178,7 @@ export default {
id="timeline-input-hours"
v-model="hourPickerInput"
data-testid="input-hours"
- size="xs"
+ width="xs"
type="number"
min="00"
max="23"
@@ -189,7 +189,7 @@ export default {
v-model="minutePickerInput"
class="gl-ml-3"
data-testid="input-minutes"
- size="xs"
+ width="xs"
type="number"
min="00"
max="59"
diff --git a/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue b/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue
deleted file mode 100644
index f7a324d9f3f..00000000000
--- a/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-<script>
-import { GlPopover, GlButton } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
-import { getCookie, parseBoolean, setCookie } from '~/lib/utils/common_utils';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { NEW_ACTIONS_POPOVER_KEY } from '~/issues/show/constants';
-import { IssuableTypeText } from '~/issues/constants';
-
-export default {
- name: 'NewHeaderActionsPopover',
- i18n: {
- popoverText: s__(
- 'HeaderAction|Notifications and other %{issueType} actions have moved to this menu.',
- ),
- confirmButtonText: s__('HeaderAction|Okay!'),
- },
- components: {
- GlPopover,
- GlButton,
- },
- mixins: [glFeatureFlagMixin()],
- props: {
- issueType: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- dismissKey: NEW_ACTIONS_POPOVER_KEY,
- popoverDismissed: parseBoolean(getCookie(`${NEW_ACTIONS_POPOVER_KEY}`)),
- };
- },
- computed: {
- popoverText() {
- return sprintf(this.$options.i18n.popoverText, {
- issueType: IssuableTypeText[this.issueType],
- });
- },
- showPopover() {
- return !this.popoverDismissed && this.isMrSidebarMoved;
- },
- isMrSidebarMoved() {
- return this.glFeatures.movedMrSidebar;
- },
- },
- methods: {
- dismissPopover() {
- this.popoverDismissed = true;
- setCookie(this.dismissKey, this.popoverDismissed);
- },
- },
-};
-</script>
-
-<template>
- <gl-popover
- v-if="showPopover"
- target="new-actions-header-dropdown"
- container="viewport"
- placement="left"
- :show="showPopover"
- triggers="manual"
- content="text"
- :css-classes="['gl-p-2 new-header-popover']"
- >
- <template #title>
- <div class="gl-font-base gl-font-weight-normal">
- {{ popoverText }}
- </div>
- </template>
- <gl-button
- data-testid="confirm-button"
- variant="confirm"
- type="submit"
- @click="dismissPopover"
- >{{ $options.i18n.confirmButtonText }}</gl-button
- >
- </gl-popover>
-</template>
diff --git a/app/assets/javascripts/issues/show/components/sticky_header.vue b/app/assets/javascripts/issues/show/components/sticky_header.vue
index bcf10ee92bb..738bb2c2aa0 100644
--- a/app/assets/javascripts/issues/show/components/sticky_header.vue
+++ b/app/assets/javascripts/issues/show/components/sticky_header.vue
@@ -1,12 +1,13 @@
<script>
-import { GlBadge, GlIcon, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
+import { GlBadge, GlIcon, GlIntersectionObserver, GlLink } from '@gitlab/ui';
+import HiddenBadge from '~/issuable/components/hidden_badge.vue';
+import LockedBadge from '~/issuable/components/locked_badge.vue';
import {
issuableStatusText,
STATUS_CLOSED,
TYPE_EPIC,
WORKSPACE_PROJECT,
} from '~/issues/constants';
-import SafeHtml from '~/vue_shared/directives/safe_html';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
export default {
@@ -16,10 +17,9 @@ export default {
GlBadge,
GlIcon,
GlIntersectionObserver,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- SafeHtml,
+ GlLink,
+ HiddenBadge,
+ LockedBadge,
},
props: {
isConfidential: {
@@ -54,10 +54,6 @@ export default {
type: String,
required: true,
},
- titleHtml: {
- type: String,
- required: true,
- },
},
computed: {
isClosed() {
@@ -94,35 +90,20 @@ export default {
<gl-icon :name="statusIcon" />
<span class="gl-display-none gl-sm-display-block gl-ml-2">{{ statusText }}</span>
</gl-badge>
- <span
- v-if="isLocked"
- v-gl-tooltip.bottom
- data-testid="locked"
- class="issuable-warning-icon"
- :title="__('This issue is locked. Only project members can comment.')"
- >
- <gl-icon name="lock" :aria-label="__('Locked')" />
- </span>
<confidentiality-badge
v-if="isConfidential"
:issuable-type="issuableType"
:workspace-type="$options.WORKSPACE_PROJECT"
/>
- <span
- v-if="isHidden"
- v-gl-tooltip.bottom
- :title="__('This issue is hidden because its author has been banned')"
- data-testid="hidden"
- class="issuable-warning-icon"
- >
- <gl-icon name="spam" />
- </span>
- <a
- v-safe-html="titleHtml || title"
+ <locked-badge v-if="isLocked" :issuable-type="issuableType" />
+ <hidden-badge v-if="isHidden" :issuable-type="issuableType" />
+ <gl-link
+ class="gl-font-weight-bold gl-text-black-normal gl-text-truncate"
href="#top"
- class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0 gl-text-black-normal"
+ :title="title"
>
- </a>
+ {{ title }}
+ </gl-link>
</div>
</div>
</transition>
diff --git a/app/assets/javascripts/issues/show/constants.js b/app/assets/javascripts/issues/show/constants.js
index 6320e4ef266..4d8c11f9669 100644
--- a/app/assets/javascripts/issues/show/constants.js
+++ b/app/assets/javascripts/issues/show/constants.js
@@ -17,5 +17,3 @@ export const issueState = {
issueType: undefined,
isDirty: false,
};
-
-export const NEW_ACTIONS_POPOVER_KEY = 'new-actions-popover-viewed';
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index b94f88f690e..cd5c6f4825a 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -131,8 +131,8 @@ export function initIssuableApp(store) {
isLocked: this.getNoteableData?.discussion_locked,
issuableStatus: this.getNoteableData?.state,
issuableType: issueType,
- issueId: this.getNoteableData?.id,
- issueIid: this.getNoteableData?.iid,
+ issueId: this.getNoteableData?.id.toString(),
+ issueIid: this.getNoteableData?.iid.toString(),
showTitleBorder: issueType !== TYPE_INCIDENT,
},
});
diff --git a/app/assets/javascripts/jira_connect/branches/pages/index.vue b/app/assets/javascripts/jira_connect/branches/pages/index.vue
index 3824e2350e8..3b92ace694c 100644
--- a/app/assets/javascripts/jira_connect/branches/pages/index.vue
+++ b/app/assets/javascripts/jira_connect/branches/pages/index.vue
@@ -56,6 +56,7 @@ export default {
:title="$options.i18n.I18N_NEW_BRANCH_SUCCESS_TITLE"
:description="$options.i18n.I18N_NEW_BRANCH_SUCCESS_MESSAGE"
:svg-path="successStateSvgPath"
+ :svg-height="null"
/>
</div>
</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js
index 72fd25a6230..1a10360ed30 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/constants.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js
@@ -37,11 +37,11 @@ export const I18N_OAUTH_FAILED_MESSAGE = s__(
export const INTEGRATIONS_DOC_LINK = helpPagePath('integration/jira/development_panel', {
anchor: 'use-the-integration',
});
-export const OAUTH_SELF_MANAGED_DOC_LINK = helpPagePath('integration/jira/connect-app', {
- anchor: 'connect-the-gitlab-for-jira-cloud-app-for-self-managed-instances',
+export const OAUTH_SELF_MANAGED_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
+ anchor: 'set-up-oauth-authentication',
});
-export const FAILED_TO_UPDATE_DOC_LINK = helpPagePath('integration/jira/connect-app', {
- anchor: 'failed-to-update-the-gitlab-instance-for-self-managed-instances',
+export const FAILED_TO_UPDATE_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
+ anchor: 'failed-to-update-the-gitlab-instance',
});
export const GITLAB_COM_BASE_PATH = 'https://gitlab.com';
diff --git a/app/assets/javascripts/jira_import/components/jira_import_progress.vue b/app/assets/javascripts/jira_import/components/jira_import_progress.vue
index 78f10decd31..2a9ce9b15ef 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_progress.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_progress.vue
@@ -56,6 +56,7 @@ export default {
<template>
<gl-empty-state
:svg-path="illustration"
+ :svg-height="null"
:title="__('Import in progress')"
:primary-button-text="__('View issues')"
:primary-button-link="issuesLink"
diff --git a/app/assets/javascripts/jira_import/components/jira_import_setup.vue b/app/assets/javascripts/jira_import/components/jira_import_setup.vue
index 285c5c815ac..58154256357 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_setup.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_setup.vue
@@ -22,6 +22,7 @@ export default {
<template>
<gl-empty-state
:svg-path="illustration"
+ :svg-height="null"
title=""
:description="__('You will first need to set up Jira Integration to use this feature.')"
:primary-button-text="__('Set up Jira Integration')"
diff --git a/app/assets/javascripts/labels/index.js b/app/assets/javascripts/labels/index.js
index bb3975ce61d..79f125be5b7 100644
--- a/app/assets/javascripts/labels/index.js
+++ b/app/assets/javascripts/labels/index.js
@@ -120,10 +120,10 @@ export function initAdminLabels() {
const emptyState = document.querySelector('.js-admin-labels-empty-state');
function removeLabelSuccessCallback() {
- this.closest('li.label-list-item').classList.add('gl-display-none!');
+ this.closest('.js-label-list-item').classList.add('gl-display-none!');
const labelsCount = document.querySelectorAll(
- 'ul.manage-labels-list li.label-list-item:not(.gl-display-none\\!)',
+ 'ul.manage-labels-list .js-label-list-item:not(.gl-display-none\\!)',
).length;
// update labels count in UI
diff --git a/app/assets/javascripts/labels/label_manager.js b/app/assets/javascripts/labels/label_manager.js
index e3d56df53f8..e684e7f1649 100644
--- a/app/assets/javascripts/labels/label_manager.js
+++ b/app/assets/javascripts/labels/label_manager.js
@@ -68,7 +68,7 @@ export default class LabelManager {
const $detachedLabel = $label.detach();
this.toggleLabelPriorityBadge($detachedLabel, action);
- const $labelEls = $target.find('li.label-list-item');
+ const $labelEls = $target.find('.js-label-list-item');
/*
* If there is a label element in the target, we'd want to
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index 36f387205f8..4354785e585 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -170,7 +170,7 @@ export default class LazyLoader {
img.classList.remove('lazy');
img.classList.add('js-lazy-loaded');
// eslint-disable-next-line no-param-reassign
- img.dataset.qa_selector = 'js_lazy_loaded_content';
+ img.dataset.testid = 'js-lazy-loaded-content';
}
}
}
diff --git a/app/assets/javascripts/lib/utils/global_alerts.js b/app/assets/javascripts/lib/utils/global_alerts.js
new file mode 100644
index 00000000000..c1e4204189e
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/global_alerts.js
@@ -0,0 +1,37 @@
+export const GLOBAL_ALERTS_SESSION_STORAGE_KEY = 'vueGlobalAlerts';
+
+/**
+ * Get global alerts from session storage
+ */
+export const getGlobalAlerts = () => {
+ return JSON.parse(sessionStorage.getItem(GLOBAL_ALERTS_SESSION_STORAGE_KEY) || '[]');
+};
+
+/**
+ * Set alerts in session storage
+ * @param {{id: String, title?: String, message: String, variant: String, dismissible?: Boolean, persistOnPages?: String[]}[]} alerts
+ */
+export const setGlobalAlerts = (alerts) => {
+ sessionStorage.setItem(
+ GLOBAL_ALERTS_SESSION_STORAGE_KEY,
+ JSON.stringify([
+ ...alerts.map(({ dismissible = true, persistOnPages = [], ...alert }) => ({
+ dismissible,
+ persistOnPages,
+ ...alert,
+ })),
+ ]),
+ );
+};
+
+/**
+ * Remove global alert by id
+ * @param {String} id
+ */
+export const removeGlobalAlertById = (id) => {
+ const existingAlerts = getGlobalAlerts();
+ sessionStorage.setItem(
+ GLOBAL_ALERTS_SESSION_STORAGE_KEY,
+ JSON.stringify(existingAlerts.filter((alert) => alert.id !== id)),
+ );
+};
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index ea0520e3157..a579b010877 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,3 +1,5 @@
+import { getGlobalAlerts, setGlobalAlerts } from './global_alerts';
+
export const DASH_SCOPE = '-';
export const PATH_SEPARATOR = '/';
@@ -241,7 +243,11 @@ export function removeParams(params, url = window.location.href, skipEncoding =
return `${root}${writableQuery}${writableFragment}`;
}
-export const getLocationHash = (hash = window.location.hash) => hash.split('#')[1];
+/**
+ * Returns value after the '#' in the location hash
+ * @returns Current value of the hash, undefined if not set
+ */
+export const getLocationHash = () => window.location.hash?.split('#')[1];
/**
* Returns a boolean indicating whether the URL hash contains the given string value
@@ -717,6 +723,20 @@ export function visitUrl(destination, external = false) {
}
}
+/**
+ * Navigates to a URL and display alerts.
+ *
+ * If destination is a querystring, it will be automatically transformed into a fully qualified URL.
+ * If the URL is not a safe URL (see isSafeURL implementation), this function will log an exception into Sentry.
+ *
+ * @param {*} destination - url to navigate to. This can be a fully qualified URL or a querystring.
+ * @param {{id: String, title?: String, message: String, variant: String, dismissible?: Boolean, persistOnPages?: String[]}[]} alerts - Alerts to display
+ */
+export function visitUrlWithAlerts(destination, alerts) {
+ setGlobalAlerts([...getGlobalAlerts(), ...alerts]);
+ visitUrl(destination);
+}
+
export function refreshCurrentPage() {
visitUrl(window.location.href);
}
diff --git a/app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue b/app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue
index 920febb0e67..68bfb99a139 100644
--- a/app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue
+++ b/app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue
@@ -77,7 +77,7 @@ export default {
<template>
<gl-disclosure-dropdown-item
- data-qa-selector="delete_member_dropdown_item"
+ data-testid="delete-member-dropdown-item"
@action="showRemoveMemberModal(modalData)"
>
<template #list-item>
diff --git a/app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue b/app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue
index 25dc4831b11..a8c97060915 100644
--- a/app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue
+++ b/app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue
@@ -109,7 +109,6 @@ export default {
no-caret
placement="right"
data-testid="user-action-dropdown"
- data-qa-selector="user_action_dropdown"
>
<disable-two-factor-dropdown-item
v-if="permissions.canDisableTwoFactor"
diff --git a/app/assets/javascripts/members/components/app.vue b/app/assets/javascripts/members/components/app.vue
index a70ee8fc865..06499b6d2c6 100644
--- a/app/assets/javascripts/members/components/app.vue
+++ b/app/assets/javascripts/members/components/app.vue
@@ -12,10 +12,7 @@ export default {
components: { MembersTable, FilterSortContainer, GlAlert },
provide() {
return {
- // We can't use this.namespace due to bug in vue-apollo when
- // provide is called in beforeCreate
- // See https://github.com/vuejs/vue-apollo/pull/1153 for details
- namespace: this.$options.propsData.namespace,
+ namespace: this.namespace,
};
},
props: {
diff --git a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
index 0e5e394dd40..94773535e85 100644
--- a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
+++ b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
@@ -20,8 +20,7 @@ export default {
name: 'MembersFilteredSearchBar',
components: { FilteredSearchBar },
availableTokens: AVAILABLE_FILTERED_SEARCH_TOKENS,
- searchButtonAttributes: { 'data-qa-selector': 'search_button' },
- searchInputAttributes: { 'data-qa-selector': 'search_bar_input' },
+ searchButtonAttributes: { 'data-testid': 'search-button' },
inject: {
namespace: {},
sourceId: {},
diff --git a/app/assets/javascripts/members/components/members_tabs.vue b/app/assets/javascripts/members/components/members_tabs.vue
index 75241d1ff26..449ad20e7ab 100644
--- a/app/assets/javascripts/members/components/members_tabs.vue
+++ b/app/assets/javascripts/members/components/members_tabs.vue
@@ -22,7 +22,7 @@ export const TABS = [
{
namespace: MEMBER_TYPES.group,
title: __('Groups'),
- attrs: { 'data-qa-selector': 'groups_list_tab' },
+ attrs: { 'data-testid': 'groups-list-tab' },
queryParamValue: TAB_QUERY_PARAM_VALUES.group,
},
{
@@ -112,6 +112,7 @@ export default {
<template>
<gl-tabs
v-model="selectedTabIndex"
+ content-class="gl-py-0"
sync-active-tab-with-query-params
:query-param-name="$options.ACTIVE_TAB_QUERY_PARAM_NAME"
>
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 1bc67522e82..2095f24eb84 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -93,7 +93,7 @@ function mountPipelines() {
const { mrWidgetData } = gl;
const table = new Vue({
components: {
- CommitPipelinesTable: () => {
+ MergeRequestPipelinesTable: () => {
return gon.features.mrPipelinesGraphql
? import('~/ci/merge_requests/components/pipelines_table_wrapper.vue')
: import('~/commit/pipelines/legacy_pipelines_table_wrapper.vue');
@@ -109,10 +109,10 @@ function mountPipelines() {
manualActionsLimit: 50,
mergeRequestId: mrWidgetData ? mrWidgetData.iid : null,
sourceProjectFullPath: mrWidgetData?.source_project_full_path || '',
- withFailedJobsDetails: true,
+ useFailedJobsWidget: gon.features?.ciJobFailuresInMr || false,
},
render(createElement) {
- return createElement('commit-pipelines-table', {
+ return createElement('merge-request-pipelines-table', {
props: {
endpoint: pipelineTableViewEl.dataset.endpoint,
emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
@@ -347,11 +347,11 @@ export default class MergeRequestTabs {
}
// this.hideSidebar();
this.resetViewContainer();
- this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
+ this.mergeRequestPipelinesTable = destroyPipelines(this.mergeRequestPipelinesTable);
} else if (action === 'new') {
this.expandView();
this.resetViewContainer();
- this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
+ this.mergeRequestPipelinesTable = destroyPipelines(this.mergeRequestPipelinesTable);
} else if (this.isDiffAction(action)) {
if (!isInVueNoteablePage()) {
/*
@@ -366,7 +366,7 @@ export default class MergeRequestTabs {
}
// this.hideSidebar();
this.expandViewContainer();
- this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
+ this.mergeRequestPipelinesTable = destroyPipelines(this.mergeRequestPipelinesTable);
this.commitsTab.classList.remove('active');
} else if (action === 'pipelines') {
// this.hideSidebar();
@@ -384,7 +384,7 @@ export default class MergeRequestTabs {
// this.showSidebar();
this.resetViewContainer();
- this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
+ this.mergeRequestPipelinesTable = destroyPipelines(this.mergeRequestPipelinesTable);
}
renderGFM(document.querySelector('.detail-page-description'));
@@ -522,7 +522,7 @@ export default class MergeRequestTabs {
}
mountPipelinesView() {
- this.commitPipelinesTable = mountPipelines();
+ this.mergeRequestPipelinesTable = mountPipelines();
}
// load the diff tab content from the backend
diff --git a/app/assets/javascripts/merge_requests/components/compare_app.vue b/app/assets/javascripts/merge_requests/components/compare_app.vue
index c7c16e91e4c..538aa090aa8 100644
--- a/app/assets/javascripts/merge_requests/components/compare_app.vue
+++ b/app/assets/javascripts/merge_requests/components/compare_app.vue
@@ -32,6 +32,9 @@ export default {
toggleClass: {
default: () => ({}),
},
+ compareSide: {
+ default: null,
+ },
},
props: {
currentBranch: {
@@ -116,6 +119,7 @@ export default {
:input-name="inputs.branch.name"
:default="currentBranch"
:toggle-class="toggleClass.branch"
+ :data-qa-compare-side="compareSide"
data-testid="compare-dropdown"
@selected="selectBranch"
/>
diff --git a/app/assets/javascripts/merge_requests/components/compare_dropdown.vue b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
index 2855d704507..20989206a51 100644
--- a/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
+++ b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
@@ -137,7 +137,6 @@ export default {
'gl-align-items-flex-start! gl-justify-content-start! mr-compare-dropdown',
toggleClass,
]"
- data-testid="source-branch-dropdown"
@shown="fetchData"
@search="searchData"
@select="selectItem"
diff --git a/app/assets/javascripts/merge_requests/components/header_metadata.vue b/app/assets/javascripts/merge_requests/components/header_metadata.vue
deleted file mode 100644
index fce7ba385b4..00000000000
--- a/app/assets/javascripts/merge_requests/components/header_metadata.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapGetters } from 'vuex';
-import { __ } from '~/locale';
-import { TYPE_ISSUE, WORKSPACE_PROJECT } from '~/issues/constants';
-import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
-
-export default {
- TYPE_ISSUE,
- WORKSPACE_PROJECT,
- components: {
- GlIcon,
- ConfidentialityBadge,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- inject: ['hidden'],
- computed: {
- ...mapGetters(['getNoteableData']),
- isLocked() {
- return this.getNoteableData.discussion_locked;
- },
- isConfidential() {
- return this.getNoteableData.confidential;
- },
- warningIconsMeta() {
- return [
- {
- iconName: 'lock',
- visible: this.isLocked,
- dataTestId: 'locked',
- tooltip: __('This merge request is locked. Only project members can comment.'),
- },
- {
- iconName: 'spam',
- visible: this.hidden,
- dataTestId: 'hidden',
- tooltip: __('This merge request is hidden because its author has been banned'),
- },
- ];
- },
- },
-};
-</script>
-
-<template>
- <div class="gl-display-inline-block">
- <confidentiality-badge
- v-if="isConfidential"
- class="gl-mr-3"
- :issuable-type="$options.TYPE_ISSUE"
- :workspace-type="$options.WORKSPACE_PROJECT"
- />
- <template v-for="meta in warningIconsMeta">
- <div
- v-if="meta.visible"
- :key="meta.iconName"
- v-gl-tooltip.bottom
- :data-testid="meta.dataTestId"
- :title="meta.tooltip || null"
- class="issuable-warning-icon gl-mr-3 gl-mt-2 gl-display-flex gl-justify-content-center gl-align-items-center"
- >
- <gl-icon :name="meta.iconName" class="icon" />
- </div>
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/merge_requests/components/merge_request_header.vue b/app/assets/javascripts/merge_requests/components/merge_request_header.vue
new file mode 100644
index 00000000000..b2e7245bd88
--- /dev/null
+++ b/app/assets/javascripts/merge_requests/components/merge_request_header.vue
@@ -0,0 +1,113 @@
+<script>
+import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
+import { mapGetters } from 'vuex';
+import HiddenBadge from '~/issuable/components/hidden_badge.vue';
+import LockedBadge from '~/issuable/components/locked_badge.vue';
+import StatusBadge from '~/issuable/components/status_badge.vue';
+import { TYPE_ISSUE, TYPE_MERGE_REQUEST, WORKSPACE_PROJECT } from '~/issues/constants';
+import { fetchPolicies } from '~/lib/graphql';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
+
+export const badgeState = Vue.observable({
+ state: '',
+ updateStatus: null,
+});
+
+export default {
+ TYPE_ISSUE,
+ TYPE_MERGE_REQUEST,
+ WORKSPACE_PROJECT,
+ components: {
+ ConfidentialityBadge,
+ LockedBadge,
+ HiddenBadge,
+ StatusBadge,
+ },
+ inject: {
+ query: { default: null },
+ projectPath: { default: null },
+ hidden: { default: false },
+ iid: { default: null },
+ },
+ props: {
+ initialState: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ if (!this.iid) {
+ return {
+ state: this.initialState,
+ };
+ }
+
+ if (!badgeState.state && this.initialState) {
+ badgeState.state = this.initialState;
+ }
+
+ return badgeState;
+ },
+ computed: {
+ ...mapGetters(['getNoteableData']),
+ isLocked() {
+ return this.getNoteableData.discussion_locked;
+ },
+ isConfidential() {
+ return this.getNoteableData.confidential;
+ },
+ },
+ created() {
+ if (!badgeState.updateStatus) {
+ badgeState.updateStatus = this.fetchState;
+ }
+ },
+ beforeDestroy() {
+ if (badgeState.updateStatus && this.query) {
+ badgeState.updateStatus = null;
+ }
+ },
+ methods: {
+ async fetchState() {
+ const { data } = await this.$apollo.query({
+ query: this.query,
+ variables: {
+ projectPath: this.projectPath,
+ iid: this.iid,
+ },
+ fetchPolicy: fetchPolicies.NO_CACHE,
+ });
+
+ badgeState.state = data?.workspace?.issuable?.state;
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="gl-display-contents">
+ <status-badge
+ class="gl-align-self-center gl-mr-2"
+ :issuable-type="$options.TYPE_MERGE_REQUEST"
+ :state="state"
+ />
+ <confidentiality-badge
+ v-if="isConfidential"
+ class="gl-align-self-center gl-mr-2"
+ :issuable-type="$options.TYPE_ISSUE"
+ :workspace-type="$options.WORKSPACE_PROJECT"
+ />
+ <locked-badge
+ v-if="isLocked"
+ class="gl-align-self-center gl-mr-2"
+ :issuable-type="$options.TYPE_MERGE_REQUEST"
+ />
+ <hidden-badge
+ v-if="hidden"
+ class="gl-align-self-center gl-mr-2"
+ :issuable-type="$options.TYPE_MERGE_REQUEST"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/merge_requests/components/merge_request_status_badge.vue b/app/assets/javascripts/merge_requests/components/merge_request_status_badge.vue
deleted file mode 100644
index 3d5478757a8..00000000000
--- a/app/assets/javascripts/merge_requests/components/merge_request_status_badge.vue
+++ /dev/null
@@ -1,74 +0,0 @@
-<script>
-import Vue from 'vue';
-import { fetchPolicies } from '~/lib/graphql';
-import StatusBadge from '~/issuable/components/status_badge.vue';
-
-export const badgeState = Vue.observable({
- state: '',
- updateStatus: null,
-});
-
-export default {
- components: {
- StatusBadge,
- },
- inject: {
- query: { default: null },
- projectPath: { default: null },
- iid: { default: null },
- },
- props: {
- initialState: {
- type: String,
- required: false,
- default: null,
- },
- issuableType: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- if (!this.iid) {
- return {
- state: this.initialState,
- };
- }
-
- if (!badgeState.state && this.initialState) {
- badgeState.state = this.initialState;
- }
-
- return badgeState;
- },
- created() {
- if (!badgeState.updateStatus) {
- badgeState.updateStatus = this.fetchState;
- }
- },
- beforeDestroy() {
- if (badgeState.updateStatus && this.query) {
- badgeState.updateStatus = null;
- }
- },
- methods: {
- async fetchState() {
- const { data } = await this.$apollo.query({
- query: this.query,
- variables: {
- projectPath: this.projectPath,
- iid: this.iid,
- },
- fetchPolicy: fetchPolicies.NO_CACHE,
- });
-
- badgeState.state = data?.workspace?.issuable?.state;
- },
- },
-};
-</script>
-
-<template>
- <status-badge class="gl-align-self-center gl-mr-3" :issuable-type="issuableType" :state="state" />
-</template>
diff --git a/app/assets/javascripts/merge_requests/components/sticky_header.vue b/app/assets/javascripts/merge_requests/components/sticky_header.vue
index c1e88a901c4..e8bdb854334 100644
--- a/app/assets/javascripts/merge_requests/components/sticky_header.vue
+++ b/app/assets/javascripts/merge_requests/components/sticky_header.vue
@@ -11,8 +11,10 @@ import StatusBadge from '~/issuable/components/status_badge.vue';
import { TYPE_MERGE_REQUEST } from '~/issues/constants';
import DiscussionCounter from '~/notes/components/discussion_counter.vue';
import TodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
+import SubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import titleSubscription from '../queries/title.subscription.graphql';
+import { badgeState } from './merge_request_header.vue';
export default {
TYPE_MERGE_REQUEST,
@@ -46,6 +48,7 @@ export default {
DiscussionCounter,
StatusBadge,
TodoWidget,
+ SubscriptionsWidget,
ClipboardButton,
},
directives: {
@@ -71,6 +74,9 @@ export default {
activeTab: (state) => state.page.activeTab,
doneFetchingBatchDiscussions: (state) => state.notes.doneFetchingBatchDiscussions,
}),
+ badgeState() {
+ return badgeState;
+ },
issuableId() {
return convertToGraphQLId(TYPENAME_MERGE_REQUEST, this.getNoteableData.id);
},
@@ -80,6 +86,9 @@ export default {
isSignedIn() {
return isLoggedIn();
},
+ isNotificationsTodosButtons() {
+ return this.glFeatures.notificationsTodosButtons && this.glFeatures.movedMrSidebar;
+ },
},
watch: {
discussionTabCounter(val) {
@@ -120,7 +129,7 @@ export default {
<status-badge
class="gl-align-self-center gl-mr-3"
:issuable-type="$options.TYPE_MERGE_REQUEST"
- :state="getNoteableData.state"
+ :state="badgeState.state"
/>
<a
v-safe-html:[$options.safeHtmlConfig]="titleHtml"
@@ -189,13 +198,23 @@ export default {
</ul>
<div class="gl-display-none gl-lg-display-flex gl-align-items-center gl-ml-auto">
<discussion-counter blocks-merge hide-options />
- <todo-widget
+ <div
v-if="isSignedIn"
- :issuable-id="issuableId"
- :issuable-iid="issuableIid"
- :full-path="projectPath"
- issuable-type="merge_request"
- />
+ :class="{ 'gl-display-flex gl-gap-3': isNotificationsTodosButtons }"
+ >
+ <todo-widget
+ :issuable-id="issuableId"
+ :issuable-iid="issuableIid"
+ :full-path="projectPath"
+ issuable-type="merge_request"
+ />
+ <subscriptions-widget
+ v-if="isNotificationsTodosButtons"
+ :iid="issuableIid"
+ :full-path="projectPath"
+ issuable-type="merge_request"
+ />
+ </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/merge_requests/index.js b/app/assets/javascripts/merge_requests/index.js
deleted file mode 100644
index 29218eb53e0..00000000000
--- a/app/assets/javascripts/merge_requests/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import Vue from 'vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import HeaderMetadata from './components/header_metadata.vue';
-
-export function mountHeaderMetadata(store) {
- const el = document.querySelector('.js-header-metadata-root');
-
- if (!el) {
- return null;
- }
-
- return new Vue({
- el,
- name: 'HeaderMetadataRoot',
- store,
- provide: { hidden: parseBoolean(el.dataset.hidden) },
- render: (createElement) => createElement(HeaderMetadata),
- });
-}
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue
index 747e92b9e85..8c7460940a0 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue
@@ -6,18 +6,12 @@ export default {
type: String,
required: true,
},
- sectionLabel: {
- type: String,
- required: false,
- default: '',
- },
},
};
</script>
<template>
<tr>
- <td class="gl-text-secondary gl-font-weight-bold">{{ sectionLabel }}</td>
<td class="gl-font-weight-bold">{{ label }}</td>
<td>
<slot></slot>
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
index a68fb7d340a..43d28e3d699 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
@@ -1,7 +1,9 @@
<script>
-import { GlAvatarLabeled, GlLink } from '@gitlab/ui';
+import { GlAvatarLabeled, GlLink, GlTableLite } from '@gitlab/ui';
+import { isEmpty, maxBy, range } from 'lodash';
import ModelExperimentsHeader from '~/ml/experiment_tracking/components/model_experiments_header.vue';
import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue';
+import { __, sprintf } from '~/locale';
import DetailRow from './components/candidate_detail_row.vue';
import {
@@ -22,6 +24,11 @@ import {
JOB_LABEL,
CI_USER_LABEL,
CI_MR_LABEL,
+ PERFORMANCE_LABEL,
+ NO_PARAMETERS_MESSAGE,
+ NO_METRICS_MESSAGE,
+ NO_METADATA_MESSAGE,
+ NO_CI_MESSAGE,
} from './translations';
export default {
@@ -32,6 +39,7 @@ export default {
DetailRow,
GlAvatarLabeled,
GlLink,
+ GlTableLite,
},
props: {
candidate: {
@@ -54,6 +62,14 @@ export default {
JOB_LABEL,
CI_USER_LABEL,
CI_MR_LABEL,
+ PARAMETERS_LABEL,
+ METRICS_LABEL,
+ METADATA_LABEL,
+ PERFORMANCE_LABEL,
+ NO_PARAMETERS_MESSAGE,
+ NO_METRICS_MESSAGE,
+ NO_METADATA_MESSAGE,
+ NO_CI_MESSAGE,
},
computed: {
info() {
@@ -62,21 +78,38 @@ export default {
ciJob() {
return Object.freeze(this.info.ci_job);
},
- sections() {
- return [
- {
- sectionName: PARAMETERS_LABEL,
- sectionValues: this.candidate.params,
- },
- {
- sectionName: METRICS_LABEL,
- sectionValues: this.candidate.metrics,
- },
- {
- sectionName: METADATA_LABEL,
- sectionValues: this.candidate.metadata,
- },
- ];
+ hasMetadata() {
+ return !isEmpty(this.candidate.metadata);
+ },
+ hasParameters() {
+ return !isEmpty(this.candidate.params);
+ },
+ hasMetrics() {
+ return !isEmpty(this.candidate.metrics);
+ },
+ metricsTableFields() {
+ const maxStep = maxBy(this.candidate.metrics, 'step').step;
+ const rowClass = 'gl-p-3!';
+
+ const cssClasses = { thClass: rowClass, tdClass: rowClass };
+
+ const fields = range(maxStep + 1).map((step) => ({
+ key: step.toString(),
+ label: sprintf(__('Step %{step}'), { step }),
+ ...cssClasses,
+ }));
+
+ return [{ key: 'name', label: __('Metric'), ...cssClasses }, ...fields];
+ },
+ metricsTableItems() {
+ const items = {};
+ this.candidate.metrics.forEach((metric) => {
+ const metricRow = items[metric.name] || { name: metric.name };
+ metricRow[metric.step] = metric.value;
+ items[metric.name] = metricRow;
+ });
+
+ return Object.values(items);
},
},
};
@@ -93,33 +126,37 @@ export default {
/>
</model-experiments-header>
- <table class="candidate-details gl-w-full">
- <tbody>
- <tr class="divider"></tr>
-
- <detail-row :label="$options.i18n.ID_LABEL" :section-label="$options.i18n.INFO_LABEL">
- {{ info.iid }}
- </detail-row>
+ <section class="gl-mb-6">
+ <table class="candidate-details">
+ <tbody>
+ <detail-row :label="$options.i18n.ID_LABEL">
+ {{ info.iid }}
+ </detail-row>
- <detail-row :label="$options.i18n.MLFLOW_ID_LABEL">{{ info.eid }}</detail-row>
+ <detail-row :label="$options.i18n.MLFLOW_ID_LABEL">{{ info.eid }}</detail-row>
- <detail-row :label="$options.i18n.STATUS_LABEL">{{ info.status }}</detail-row>
+ <detail-row :label="$options.i18n.STATUS_LABEL">{{ info.status }}</detail-row>
- <detail-row :label="$options.i18n.EXPERIMENT_LABEL">
- <gl-link :href="info.path_to_experiment">
- {{ info.experiment_name }}
- </gl-link>
- </detail-row>
+ <detail-row :label="$options.i18n.EXPERIMENT_LABEL">
+ <gl-link :href="info.path_to_experiment">
+ {{ info.experiment_name }}
+ </gl-link>
+ </detail-row>
- <detail-row v-if="info.path_to_artifact" :label="$options.i18n.ARTIFACTS_LABEL">
- <gl-link :href="info.path_to_artifact">
- {{ $options.i18n.ARTIFACTS_LABEL }}
- </gl-link>
- </detail-row>
+ <detail-row v-if="info.path_to_artifact" :label="$options.i18n.ARTIFACTS_LABEL">
+ <gl-link :href="info.path_to_artifact">
+ {{ $options.i18n.ARTIFACTS_LABEL }}
+ </gl-link>
+ </detail-row>
+ </tbody>
+ </table>
+ </section>
- <template v-if="ciJob">
- <tr class="divider"></tr>
+ <section class="gl-mb-6">
+ <h4>{{ $options.i18n.CI_SECTION_LABEL }}</h4>
+ <table v-if="ciJob" class="candidate-details">
+ <tbody>
<detail-row
:label="$options.i18n.JOB_LABEL"
:section-label="$options.i18n.CI_SECTION_LABEL"
@@ -142,21 +179,53 @@ export default {
!{{ ciJob.merge_request.iid }} {{ ciJob.merge_request.title }}
</gl-link>
</detail-row>
- </template>
+ </tbody>
+ </table>
- <template v-for="{ sectionName, sectionValues } in sections">
- <tr v-if="sectionValues" :key="sectionName" class="divider"></tr>
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_CI_MESSAGE }}</div>
+ </section>
- <detail-row
- v-for="(item, index) in sectionValues"
- :key="item.name"
- :label="item.name"
- :section-label="index === 0 ? sectionName : ''"
- >
+ <section class="gl-mb-6">
+ <h4>{{ $options.i18n.PARAMETERS_LABEL }}</h4>
+
+ <table v-if="hasParameters" class="candidate-details">
+ <tbody>
+ <detail-row v-for="item in candidate.params" :key="item.name" :label="item.name">
{{ item.value }}
</detail-row>
- </template>
- </tbody>
- </table>
+ </tbody>
+ </table>
+
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_PARAMETERS_MESSAGE }}</div>
+ </section>
+
+ <section class="gl-mb-6">
+ <h4>{{ $options.i18n.METADATA_LABEL }}</h4>
+
+ <table v-if="hasMetadata" class="candidate-details">
+ <tbody>
+ <detail-row v-for="item in candidate.metadata" :key="item.name" :label="item.name">
+ {{ item.value }}
+ </detail-row>
+ </tbody>
+ </table>
+
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_METADATA_MESSAGE }}</div>
+ </section>
+
+ <section class="gl-mb-6">
+ <h4>{{ $options.i18n.PERFORMANCE_LABEL }}</h4>
+
+ <div v-if="hasMetrics" class="gl-overflow-x-auto">
+ <gl-table-lite
+ :items="metricsTableItems"
+ :fields="metricsTableFields"
+ class="gl-w-auto"
+ hover
+ />
+ </div>
+
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_METRICS_MESSAGE }}</div>
+ </section>
</div>
</template>
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js
index fa9518f3e27..98988e1db35 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js
@@ -9,13 +9,18 @@ export const EXPERIMENT_LABEL = s__('MlExperimentTracking|Experiment');
export const ARTIFACTS_LABEL = s__('MlExperimentTracking|Artifacts');
export const PARAMETERS_LABEL = s__('MlExperimentTracking|Parameters');
export const METRICS_LABEL = s__('MlExperimentTracking|Metrics');
+export const PERFORMANCE_LABEL = s__('MlExperimentTracking|Model performance');
export const METADATA_LABEL = s__('MlExperimentTracking|Metadata');
+export const NO_PARAMETERS_MESSAGE = s__('MlExperimentTracking|No logged parameters');
+export const NO_METRICS_MESSAGE = s__('MlExperimentTracking|No logged metrics');
+export const NO_METADATA_MESSAGE = s__('MlExperimentTracking|No logged metadata');
+export const NO_CI_MESSAGE = s__('MlExperimentTracking|Candidate not linked to a CI build');
export const DELETE_CANDIDATE_CONFIRMATION_MESSAGE = s__(
'MlExperimentTracking|Deleting this candidate will delete the associated parameters, metrics, and metadata.',
);
export const DELETE_CANDIDATE_PRIMARY_ACTION_LABEL = s__('MlExperimentTracking|Delete candidate');
export const DELETE_CANDIDATE_MODAL_TITLE = s__('MLExperimentTracking|Delete candidate?');
-export const CI_SECTION_LABEL = __('CI');
+export const CI_SECTION_LABEL = s__('MLExperimentTracking|CI Info');
export const JOB_LABEL = __('Job');
export const CI_USER_LABEL = s__('MlExperimentTracking|Triggered by');
export const CI_MR_LABEL = __('Merge request');
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue
index b543169d501..4710735f76e 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue
@@ -72,6 +72,7 @@ export default {
:primary-button-text="$options.i18n.CREATE_NEW_LABEL"
:primary-button-link="$options.constants.CREATE_EXPERIMENT_HELP_PATH"
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
:description="$options.i18n.EMPTY_STATE_DESCRIPTION_LABEL"
class="gl-py-8"
/>
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
index 25c06aa2f7f..28a27059b17 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
@@ -245,6 +245,7 @@ export default {
:primary-button-text="$options.i18n.CREATE_NEW_LABEL"
:primary-button-link="$options.constants.CREATE_CANDIDATE_HELP_PATH"
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
:description="$options.i18n.EMPTY_STATE_DESCRIPTION_LABEL"
class="gl-py-8"
/>
diff --git a/app/assets/javascripts/ml/model_registry/apps/index.js b/app/assets/javascripts/ml/model_registry/apps/index.js
new file mode 100644
index 00000000000..f9e5f82e708
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/apps/index.js
@@ -0,0 +1,3 @@
+import ShowMlModel from './show_ml_model.vue';
+
+export { ShowMlModel };
diff --git a/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue b/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
new file mode 100644
index 00000000000..d4f17c840d7
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
@@ -0,0 +1,16 @@
+<script>
+export default {
+ name: 'ShowMlModelApp',
+ components: {},
+ props: {
+ model: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>{{ model.name }}</div>
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue b/app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue
index 37e5877ec52..3770b4ec3ac 100644
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue
+++ b/app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue
@@ -1,17 +1,29 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { isEmpty } from 'lodash';
import * as translations from '~/ml/model_registry/routes/models/index/translations';
+import Pagination from '~/vue_shared/components/incubation/pagination.vue';
+import ModelRow from './model_row.vue';
export default {
- name: 'MlExperimentsIndexApp',
+ name: 'MlModelRegistryApp',
components: {
- GlLink,
+ Pagination,
+ ModelRow,
},
props: {
models: {
type: Array,
required: true,
},
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ hasModels() {
+ return !isEmpty(this.models);
+ },
},
i18n: translations,
};
@@ -27,8 +39,11 @@ export default {
</div>
</div>
- <div v-for="model in models" :key="model.name">
- <gl-link :href="model.path"> {{ model.name }} / {{ model.version }} </gl-link>
- </div>
+ <template v-if="hasModels">
+ <model-row v-for="model in models" :key="model.name" :model="model" />
+ <pagination v-bind="pageInfo" />
+ </template>
+
+ <p v-else class="gl-text-secondary">{{ $options.i18n.NO_MODELS_LABEL }}</p>
</div>
</template>
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue b/app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue
new file mode 100644
index 00000000000..4f91f0939a8
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { modelVersionCountMessage } from '../translations';
+
+export default {
+ name: 'MlModelRow',
+ components: {
+ GlLink,
+ },
+ props: {
+ model: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ hasVersions() {
+ return this.model.version != null;
+ },
+ },
+ modelVersionCountMessage,
+};
+</script>
+
+<template>
+ <div class="gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-py-3">
+ <gl-link :href="model.path" class="gl-text-body gl-font-weight-bold gl-line-height-24">
+ {{ model.name }}
+ </gl-link>
+
+ <div class="gl-text-secondary">
+ {{ $options.modelVersionCountMessage(model.version, model.versionCount) }}
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js b/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js
index f0f45f9424e..9210d816373 100644
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js
+++ b/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js
@@ -1,3 +1,16 @@
-import { s__ } from '~/locale';
+import { s__, n__, sprintf } from '~/locale';
-export const TITLE_LABEL = s__('MlExperimentTracking|Model registry');
+export const TITLE_LABEL = s__('MlModelRegistry|Model registry');
+export const NO_MODELS_LABEL = s__('MlModelRegistry|No models registered in this project');
+
+export const modelVersionCountMessage = (version, versionCount) => {
+ if (!versionCount) return s__('MlModelRegistry|No registered versions');
+
+ const message = n__(
+ 'MlModelRegistry|%{version} · No other versions',
+ 'MlModelRegistry|%{version} · %{versionCount} versions',
+ versionCount,
+ );
+
+ return sprintf(message, { version, versionCount });
+};
diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js
index 04167518d3f..265e2a2f880 100644
--- a/app/assets/javascripts/mr_notes/init_notes.js
+++ b/app/assets/javascripts/mr_notes/init_notes.js
@@ -44,6 +44,7 @@ export default () => {
reportAbusePath: notesDataset.reportAbusePath,
newCommentTemplatePath: notesDataset.newCommentTemplatePath,
mrFilter: true,
+ newCustomEmojiPath: notesDataset.newCustomEmojiPath,
},
data() {
const noteableData = JSON.parse(notesDataset.noteableData);
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 144cfa4295b..329d6cfec00 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -13,10 +13,9 @@ import {
slugifyWithUnderscore,
} from '~/lib/utils/text_utility';
import { sprintf } from '~/locale';
-import { badgeState } from '~/merge_requests/components/merge_request_status_badge.vue';
+import { badgeState } from '~/merge_requests/components/merge_request_header.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { trackSavedUsingEditor } from '~/vue_shared/components/markdown/tracking';
import * as constants from '../constants';
@@ -49,7 +48,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagsMixin(), issuableStateMixin],
+ mixins: [issuableStateMixin],
props: {
noteableType: {
type: String,
@@ -69,7 +68,7 @@ export default {
id: 'note-body',
name: 'note[note]',
class: 'js-note-text note-textarea js-gfm-input markdown-area',
- 'data-qa-selector': 'comment_field',
+ 'data-testid': 'comment-field',
},
};
},
@@ -361,7 +360,6 @@ export default {
>
<markdown-editor
ref="markdownEditor"
- :enable-content-editor="Boolean(glFeatures.contentEditorOnIssues)"
:value="note"
:render-markdown-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
diff --git a/app/assets/javascripts/notes/components/comment_type_dropdown.vue b/app/assets/javascripts/notes/components/comment_type_dropdown.vue
index 2e4f925194f..f85b0de0c4e 100644
--- a/app/assets/javascripts/notes/components/comment_type_dropdown.vue
+++ b/app/assets/javascripts/notes/components/comment_type_dropdown.vue
@@ -108,7 +108,7 @@ export default {
text: this.dropdownStartThreadButtonTitle,
description: this.startDiscussionDescription,
value: constants.DISCUSSION,
- qaSelector: 'discussion_menu_item',
+ testid: 'discussion-menu-item',
},
];
},
@@ -132,7 +132,6 @@ export default {
:data-track-label="trackingLabel"
data-track-action="click_button"
data-testid="comment-button"
- data-qa-selector="comment_button"
>
<gl-button variant="confirm" :disabled="disabled" @click="handleClick">
{{ commentButtonTitle }}
@@ -149,7 +148,7 @@ export default {
@select="setNoteType"
>
<template #list-item="{ item }">
- <div :data-qa-selector="item.qaSelector">
+ <div :data-testid="item.testid">
<strong>{{ item.text }}</strong>
<p class="gl-m-0">{{ item.description }}</p>
</div>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index f08c005259c..efb6fc67806 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSkeletonLoader } from '@gitlab/ui';
+import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapActions } from 'vuex';
import SafeHtml from '~/vue_shared/directives/safe_html';
@@ -19,6 +19,7 @@ export default {
GlSkeletonLoader,
DiffViewer,
ImageDiffOverlay,
+ GlButton,
},
directives: {
SafeHtml,
@@ -127,12 +128,12 @@ export default {
<td class="new_line diff-line-num"></td>
<td v-if="error" class="js-error-lazy-load-diff diff-loading-error-block">
{{ __('Unable to load the diff') }}
- <button
- class="gl-button btn-link btn-link-retry gl-p-0 js-toggle-lazy-diff-retry-button gl-reset-font-size!"
+ <gl-button
+ class="btn-link-retry gl-font-regular js-toggle-lazy-diff-retry-button"
@click="fetchDiff"
>
{{ __('Try again') }}
- </button>
+ </gl-button>
</td>
<td v-else class="line_content js-success-lazy-load">
<span></span>
diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue
index dcbf4a0e5d3..c68ffd73ecc 100644
--- a/app/assets/javascripts/notes/components/discussion_actions.vue
+++ b/app/assets/javascripts/notes/components/discussion_actions.vue
@@ -49,7 +49,7 @@ export default {
<template>
<div class="discussion-with-resolve-btn clearfix">
<reply-placeholder
- data-qa-selector="discussion_reply_tab"
+ data-testid="discussion-reply-tab"
:placeholder-text="__('Reply…')"
@focus="$emit('showReplyForm')"
/>
@@ -58,7 +58,6 @@ export default {
<div class="btn-group">
<resolve-discussion-button
v-if="discussion.resolvable"
- data-qa-selector="resolve_discussion_button"
data-testid="resolve-discussion-button"
:is-resolving="isResolving"
:button-title="resolveButtonTitle"
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index d8883f90eda..b392ad55fa2 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -71,6 +71,9 @@ export default {
return options;
},
+ isNotificationsTodosButtons() {
+ return this.glFeatures.notificationsTodosButtons && this.glFeatures.movedMrSidebar;
+ },
},
methods: {
...mapActions(['setExpandDiscussions']),
@@ -92,10 +95,12 @@ export default {
class="gl-display-flex discussions-counter"
>
<div
- class="gl-display-flex gl-align-items-center gl-pl-4 gl-rounded-base gl-mr-3 gl-min-h-7"
+ class="gl-display-flex gl-align-items-center gl-pl-4 gl-rounded-base gl-min-h-7"
:class="{
'gl-bg-orange-50': blocksMerge && !allResolved,
'gl-bg-gray-50': !blocksMerge || allResolved,
+ 'gl-mr-3': !isNotificationsTodosButtons,
+ 'gl-mr-5': isNotificationsTodosButtons,
}"
data-testid="discussions-counter-text"
>
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 90f7a6862f0..bf3a750cf40 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -175,7 +175,7 @@ export default {
<gl-disclosure-dropdown
id="discussion-preferences-dropdown"
class="full-width-mobile"
- data-qa-selector="discussion_preferences_dropdown"
+ data-testid="discussion-preferences-dropdown"
:toggle-text="__('Sort or filter')"
:disabled="isLoading"
placement="right"
@@ -213,7 +213,7 @@ export default {
:is-selected="filter.value === currentValue"
:class="{ 'is-active': filter.value === currentValue }"
:data-filter-type="filterType(filter.value)"
- data-qa-selector="filter_menu_item"
+ data-testid="filter-menu-item"
@action="selectFilter(filter.value)"
>
<template #list-item>
diff --git a/app/assets/javascripts/notes/components/discussion_filter_note.vue b/app/assets/javascripts/notes/components/discussion_filter_note.vue
index d02327a37a7..bbfde7f2e0c 100644
--- a/app/assets/javascripts/notes/components/discussion_filter_note.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter_note.vue
@@ -26,7 +26,7 @@ export default {
<template>
<li
class="timeline-entry note note-wrapper discussion-filter-note js-discussion-filter-note"
- data-qa-selector="discussion_filter_container"
+ data-testid="discussion-filter-container"
>
<div
class="gl-float-left gl--flex-center gl-rounded-full gl-mt-n1 gl-ml-2 gl-w-6 gl-h-6 gl-bg-gray-50 gl-text-gray-600"
diff --git a/app/assets/javascripts/notes/components/email_participants_warning.vue b/app/assets/javascripts/notes/components/email_participants_warning.vue
index cf9108992be..478c5847b41 100644
--- a/app/assets/javascripts/notes/components/email_participants_warning.vue
+++ b/app/assets/javascripts/notes/components/email_participants_warning.vue
@@ -1,11 +1,12 @@
<script>
-import { GlSprintf } from '@gitlab/ui';
+import { GlSprintf, GlButton } from '@gitlab/ui';
import { toNounSeriesText } from '~/lib/utils/grammar';
import { s__, sprintf } from '~/locale';
export default {
components: {
GlSprintf,
+ GlButton,
},
props: {
emails: {
@@ -58,9 +59,9 @@ export default {
<div class="issuable-note-warning">
<gl-sprintf :message="message">
<template #andMore>
- <button type="button" class="gl-button btn-link" @click="showMoreParticipants">
+ <gl-button variant="link" class="gl-vertical-align-baseline" @click="showMoreParticipants">
{{ moreLabel }}
- </button>
+ </gl-button>
</template>
<template #emails>
<span>{{ title }}</span>
diff --git a/app/assets/javascripts/notes/components/multiline_comment_form.vue b/app/assets/javascripts/notes/components/multiline_comment_form.vue
index 2c2264c36f3..78097ff1033 100644
--- a/app/assets/javascripts/notes/components/multiline_comment_form.vue
+++ b/app/assets/javascripts/notes/components/multiline_comment_form.vue
@@ -88,7 +88,7 @@ export default {
id="comment-line-start"
:value="commentLineStart"
:options="commentLineOptions"
- size="sm"
+ width="sm"
class="gl-w-auto gl-vertical-align-baseline"
@change="updateCommentLineStart"
/>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 7f23ee70086..5a1795d7479 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -337,7 +337,7 @@ export default {
icon="pencil"
category="tertiary"
class="note-action-button js-note-edit gl-display-none gl-sm-display-block"
- data-qa-selector="note_edit_button"
+ data-testid="note-edit-button"
@click="onEdit"
/>
<gl-button
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 363383fd7ad..f8a0db93e37 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -5,7 +5,6 @@ import { mapGetters, mapActions, mapState } from 'vuex';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import glFeaturesFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { trackSavedUsingEditor } from '~/vue_shared/components/markdown/tracking';
import eventHub from '../event_hub';
import issuableStateMixin from '../mixins/issuable_state';
@@ -24,7 +23,7 @@ export default {
GlLink,
GlFormCheckbox,
},
- mixins: [issuableStateMixin, resolvable, glFeaturesFlagMixin()],
+ mixins: [issuableStateMixin, resolvable],
props: {
noteBody: {
type: String,
@@ -117,7 +116,7 @@ export default {
'aria-label': __('Reply to comment'),
placeholder: this.$options.i18n.bodyPlaceholder,
class: 'note-textarea js-gfm-input js-note-text markdown-area js-vue-issue-note-form',
- 'data-qa-selector': 'reply_field',
+ 'data-testid': 'reply-field',
},
};
},
@@ -202,6 +201,9 @@ export default {
isDisabled() {
return !this.updatedNoteBody.length || this.isSubmitting;
},
+ isInternalNote() {
+ return this.discussionNote.internal || this.discussion.confidential;
+ },
discussionNote() {
const discussionNote = this.discussion.id
? this.getDiscussionLastNote(this.discussion)
@@ -221,9 +223,6 @@ export default {
placeholder: { link: ['startTag', 'endTag'] },
};
},
- enableContentEditor() {
- return Boolean(this.glFeatures.contentEditorOnIssues);
- },
codeSuggestionsConfig() {
return {
canSuggest: this.canSuggest,
@@ -355,13 +354,9 @@ export default {
</div>
<div class="flash-container timeline-content"></div>
<form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form">
- <comment-field-layout
- :noteable-data="getNoteableData"
- :is-internal-note="discussionNote.internal"
- >
+ <comment-field-layout :noteable-data="getNoteableData" :is-internal-note="isInternalNote">
<markdown-editor
ref="markdownEditor"
- :enable-content-editor="enableContentEditor"
:value="updatedNoteBody"
:render-markdown-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
@@ -406,7 +401,7 @@ export default {
category="primary"
variant="confirm"
class="gl-sm-mr-3 gl-mb-3"
- data-qa-selector="start_review_button"
+ data-testid="start-review-button"
@click="handleAddToReview"
>
<template v-if="hasDrafts">{{ __('Add to review') }}</template>
@@ -416,7 +411,7 @@ export default {
:disabled="isDisabled"
category="secondary"
variant="confirm"
- data-qa-selector="comment_now_button"
+ data-testid="comment-now-button"
class="gl-sm-mr-3 gl-mb-3 js-comment-button"
@click="handleUpdate()"
>
@@ -439,7 +434,7 @@ export default {
:disabled="isDisabled"
category="primary"
variant="confirm"
- data-qa-selector="reply_comment_button"
+ data-testid="reply-comment-button"
class="gl-sm-mr-3 gl-xs-mb-3 js-vue-issue-save js-comment-button"
@click="handleUpdate()"
>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index bdf9ea2057c..c3701c01ee2 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -188,7 +188,10 @@ export default {
v-text="authorName"
></span>
</a>
- <span v-if="!isSystemNote && !emailParticipant" class="text-nowrap author-username">
+ <span
+ v-if="!isSystemNote && !emailParticipant"
+ class="text-nowrap author-username gl-text-truncate"
+ >
<a
ref="authorUsernameLink"
class="author-username-link"
@@ -205,7 +208,7 @@ export default {
</template>
<span v-else>{{ __('A deleted user') }}</span>
<span class="note-headline-light note-headline-meta">
- <span class="system-note-message" data-qa-selector="system_note_content">
+ <span class="system-note-message" data-testid="system-note-content">
<slot></slot>
</span>
<template v-if="createdAt">
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 94d5dc25b9e..e0b1f7a8c6a 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -310,7 +310,7 @@ export default {
:data-discussion-resolvable="discussion.resolvable"
:data-discussion-resolved="discussion.resolved"
class="discussion js-discussion-container"
- data-qa-selector="discussion_content"
+ data-testid="discussion-content"
>
<diff-discussion-header v-if="shouldRenderDiffs" :discussion="discussion" />
<div v-if="!shouldHideDiscussionBody" class="discussion-body">
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 9a7cc1a4d37..809b1716b91 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -421,7 +421,7 @@ export default {
:data-award-url="note.toggle_award_path"
:data-note-id="note.id"
class="note note-wrapper note-comment"
- data-qa-selector="noteable_note_container"
+ data-testid="noteable-note-container"
>
<div
v-if="showMultiLineComment"
diff --git a/app/assets/javascripts/notes/components/notes_activity_header.vue b/app/assets/javascripts/notes/components/notes_activity_header.vue
index a91c825710d..ce642733396 100644
--- a/app/assets/javascripts/notes/components/notes_activity_header.vue
+++ b/app/assets/javascripts/notes/components/notes_activity_header.vue
@@ -38,7 +38,11 @@ export default {
},
computed: {
showAiActions() {
- return this.resourceGlobalId && this.glFeatures.summarizeComments;
+ return (
+ this.resourceGlobalId &&
+ this.glFeatures.openaiExperimentation &&
+ this.glFeatures.summarizeNotes
+ );
},
},
};
@@ -56,7 +60,7 @@ export default {
:loading="aiLoading"
/>
<timeline-toggle v-if="showTimelineViewToggle" />
- <mr-discussion-filter v-if="mrFilter && glFeatures.mrActivityFilters" />
+ <mr-discussion-filter v-if="mrFilter" />
<discussion-filter v-else :filters="notesFilters" :selected-value="notesFilterValue" />
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
index a012b4411bc..981b9324688 100644
--- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -84,12 +84,7 @@ export default {
:tooltip-text="author.name"
tooltip-placement="bottom"
/>
- <gl-button
- class="gl-mr-2"
- variant="link"
- data-qa-selector="expand_replies_button"
- @click="toggle"
- >
+ <gl-button class="gl-mr-2" variant="link" data-testid="expand-replies-button" @click="toggle">
{{ n__('%d reply', '%d replies', replies.length) }}
</gl-button>
<gl-sprintf :message="$options.i18n.lastReplyBy">
@@ -111,7 +106,7 @@ export default {
v-else
class="gl-text-body! gl-text-decoration-none!"
variant="link"
- data-qa-selector="collapse_replies_button"
+ data-testid="collapse-replies-button"
@click="toggle"
>
{{ $options.i18n.collapseReplies }}
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index 724b47bf44b..f9fbe6659ee 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -62,6 +62,7 @@ export default ({ editorAiActions = [] } = {}) => {
newCommentTemplatePath: notesDataset.newCommentTemplatePath,
resourceGlobalId: convertToGraphQLId(noteableData.noteableType, noteableData.id),
editorAiActions: editorAiActions.map((factory) => factory(noteableData)),
+ newCustomEmojiPath: notesDataset.newCustomEmojiPath,
},
data() {
return {
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 7eb01897296..4071218d100 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -95,10 +95,7 @@ export const fetchDiscussions = (
? { params: { notes_filter: filter, persist_filter: persistFilter } }
: null;
- if (
- window.gon?.features?.mrActivityFilters &&
- getters.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE
- ) {
+ if (getters.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE) {
config = { params: { notes_filter: 0, persist_filter: false } };
}
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index c43430639ad..62d991c2d9e 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -2,7 +2,7 @@ import { flattenDeep, clone } from 'lodash';
import { match } from '~/diffs/utils/diff_file';
import { isInMRPage } from '~/lib/utils/common_utils';
import { doesHashExistInUrl } from '~/lib/utils/url_utility';
-import { badgeState } from '~/merge_requests/components/merge_request_status_badge.vue';
+import { badgeState } from '~/merge_requests/components/merge_request_header.vue';
import * as constants from '../constants';
import { collapseSystemNotes } from './collapse_utils';
@@ -52,10 +52,7 @@ export const discussions = (state, getters, rootState) => {
let discussionsInState = clone(state.discussions);
// NOTE: not testing bc will be removed when backend is finished.
- if (
- state.noteableData.targetType === 'merge_request' &&
- window.gon?.features?.mrActivityFilters
- ) {
+ if (state.noteableData.targetType === 'merge_request') {
discussionsInState = discussionsInState.reduce((acc, discussion) => {
if (hideActivity(state.mergeRequestFilters, discussion)) {
return acc;
diff --git a/app/assets/javascripts/observability/client.js b/app/assets/javascripts/observability/client.js
index 718001e98fe..2e976cd6230 100644
--- a/app/assets/javascripts/observability/client.js
+++ b/app/assets/javascripts/observability/client.js
@@ -140,7 +140,7 @@ function filterObjToQueryParams(filterObj) {
let value = rawValue;
if (filterName === 'durationMs') {
// converting durationMs to duration_nano
- value *= 1000;
+ value *= 1000000;
}
if (paramName && value) {
@@ -166,28 +166,80 @@ function filterObjToQueryParams(filterObj) {
*
* @returns Array<Trace> : A list of traces
*/
-async function fetchTraces(tracingUrl, filters = {}) {
- const filterParams = filterObjToQueryParams(filters);
+async function fetchTraces(tracingUrl, { filters = {}, pageToken, pageSize } = {}) {
+ const params = filterObjToQueryParams(filters);
+ if (pageToken) {
+ params.append('page_token', pageToken);
+ }
+ if (pageSize) {
+ params.append('page_size', pageSize);
+ }
try {
const { data } = await axios.get(tracingUrl, {
withCredentials: true,
- params: filterParams,
+ params,
});
if (!Array.isArray(data.traces)) {
throw new Error('traces are missing/invalid in the response'); // eslint-disable-line @gitlab/require-i18n-strings
}
- return data.traces;
+ return data;
+ } catch (e) {
+ return reportErrorAndThrow(e);
+ }
+}
+
+async function fetchServices(servicesUrl) {
+ try {
+ const { data } = await axios.get(servicesUrl, {
+ withCredentials: true,
+ });
+
+ if (!Array.isArray(data.services)) {
+ throw new Error('failed to fetch services. invalid response'); // eslint-disable-line @gitlab/require-i18n-strings
+ }
+
+ return data.services;
+ } catch (e) {
+ return reportErrorAndThrow(e);
+ }
+}
+
+async function fetchOperations(operationsUrl, serviceName) {
+ try {
+ if (!serviceName) {
+ throw new Error('fetchOperations() - serviceName is required.');
+ }
+ if (!operationsUrl.includes('$SERVICE_NAME$')) {
+ throw new Error('fetchOperations() - operationsUrl must contain $SERVICE_NAME$');
+ }
+ const url = operationsUrl.replace('$SERVICE_NAME$', serviceName);
+ const { data } = await axios.get(url, {
+ withCredentials: true,
+ });
+
+ if (!Array.isArray(data.operations)) {
+ throw new Error('failed to fetch operations. invalid response'); // eslint-disable-line @gitlab/require-i18n-strings
+ }
+
+ return data.operations;
} catch (e) {
return reportErrorAndThrow(e);
}
}
-export function buildClient({ provisioningUrl, tracingUrl }) {
+export function buildClient({ provisioningUrl, tracingUrl, servicesUrl, operationsUrl } = {}) {
+ if (!provisioningUrl || !tracingUrl || !servicesUrl || !operationsUrl) {
+ throw new Error(
+ 'missing required params. provisioningUrl, tracingUrl, servicesUrl, operationsUrl are required',
+ );
+ }
return {
enableTraces: () => enableTraces(provisioningUrl),
isTracingEnabled: () => isTracingEnabled(provisioningUrl),
fetchTraces: (filters) => fetchTraces(tracingUrl, filters),
fetchTrace: (traceId) => fetchTrace(tracingUrl, traceId),
+ fetchServices: () => fetchServices(servicesUrl),
+ fetchOperations: (serviceName) => fetchOperations(operationsUrl, serviceName),
};
}
diff --git a/app/assets/javascripts/observability/components/observability_app.vue b/app/assets/javascripts/observability/components/observability_app.vue
deleted file mode 100644
index 36cbe715149..00000000000
--- a/app/assets/javascripts/observability/components/observability_app.vue
+++ /dev/null
@@ -1,87 +0,0 @@
-<script>
-import { darkModeEnabled } from '~/lib/utils/color_utils';
-import { setUrlParams } from '~/lib/utils/url_utility';
-
-import { MESSAGE_EVENT_TYPE, FULL_APP_DIMENSIONS } from '../constants';
-import ObservabilitySkeleton from './skeleton/index.vue';
-
-export default {
- components: {
- ObservabilitySkeleton,
- },
- props: {
- observabilityIframeSrc: {
- type: String,
- required: true,
- },
- inlineEmbed: {
- type: Boolean,
- required: false,
- default: false,
- },
- skeletonVariant: {
- type: String,
- required: false,
- default: 'dashboards',
- },
- height: {
- type: String,
- required: false,
- default: FULL_APP_DIMENSIONS.HEIGHT,
- },
- width: {
- type: String,
- required: false,
- default: FULL_APP_DIMENSIONS.WIDTH,
- },
- },
- computed: {
- iframeSrcWithParams() {
- return `${setUrlParams(
- { theme: darkModeEnabled() ? 'dark' : 'light', username: gon?.current_username },
- this.observabilityIframeSrc,
- )}${this.inlineEmbed ? '&kiosk=inline-embed' : ''}`;
- },
- },
- mounted() {
- window.addEventListener('message', this.messageHandler);
- },
- destroyed() {
- window.removeEventListener('message', this.messageHandler);
- },
- methods: {
- messageHandler(e) {
- const isExpectedOrigin = e.origin === new URL(this.observabilityIframeSrc)?.origin;
- if (!isExpectedOrigin) return;
-
- const {
- data: { type, payload },
- } = e;
- switch (type) {
- case MESSAGE_EVENT_TYPE.GOUI_LOADED:
- this.$refs.observabilitySkeleton.onContentLoaded();
- break;
- case MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE:
- this.$emit('route-update', payload);
- break;
- default:
- break;
- }
- },
- },
-};
-</script>
-
-<template>
- <observability-skeleton ref="observabilitySkeleton" :variant="skeletonVariant">
- <iframe
- id="observability-ui-iframe"
- data-testid="observability-ui-iframe"
- frameborder="0"
- :width="width"
- :height="height"
- :src="iframeSrcWithParams"
- sandbox="allow-same-origin allow-forms allow-scripts"
- ></iframe>
- </observability-skeleton>
-</template>
diff --git a/app/assets/javascripts/observability/components/observability_container.vue b/app/assets/javascripts/observability/components/observability_container.vue
index b7697cea299..1518c132560 100644
--- a/app/assets/javascripts/observability/components/observability_container.vue
+++ b/app/assets/javascripts/observability/components/observability_container.vue
@@ -13,11 +13,19 @@ export default {
type: String,
required: true,
},
+ provisioningUrl: {
+ type: String,
+ required: true,
+ },
tracingUrl: {
type: String,
required: true,
},
- provisioningUrl: {
+ servicesUrl: {
+ type: String,
+ required: true,
+ },
+ operationsUrl: {
type: String,
required: true,
},
@@ -58,6 +66,8 @@ export default {
this.observabilityClient = buildClient({
provisioningUrl: this.provisioningUrl,
tracingUrl: this.tracingUrl,
+ servicesUrl: this.servicesUrl,
+ operationsUrl: this.operationsUrl,
});
this.$refs.observabilitySkeleton?.onContentLoaded();
} else if (status === 'error') {
diff --git a/app/assets/javascripts/observability/components/skeleton/dashboards.vue b/app/assets/javascripts/observability/components/skeleton/dashboards.vue
deleted file mode 100644
index 887a0a9f094..00000000000
--- a/app/assets/javascripts/observability/components/skeleton/dashboards.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-<!-- eslint-disable vue/multi-word-component-names -->
-<script>
-import { GlSkeletonLoader } from '@gitlab/ui';
-
-export default {
- components: {
- GlSkeletonLoader,
- },
-};
-</script>
-<template>
- <gl-skeleton-loader :height="200">
- <!-- Top left -->
- <rect y="2" width="10" height="8" />
- <rect y="2" x="15" width="15" height="8" />
- <rect y="2" x="35" width="15" height="8" />
-
- <!-- Top right -->
- <rect y="2" x="354" width="10" height="8" />
- <rect y="2" x="366" width="10" height="8" />
- <rect y="2" x="378" width="10" height="8" />
- <rect y="2" x="390" width="10" height="8" />
-
- <!-- Middle header -->
- <rect y="15" width="400" height="30" rx="2" ry="2" />
-
- <!-- Dashboard container -->
- <rect y="50" width="200" height="100" rx="2" ry="2" />
- </gl-skeleton-loader>
-</template>
diff --git a/app/assets/javascripts/observability/components/skeleton/embed.vue b/app/assets/javascripts/observability/components/skeleton/embed.vue
deleted file mode 100644
index 965beb168bf..00000000000
--- a/app/assets/javascripts/observability/components/skeleton/embed.vue
+++ /dev/null
@@ -1,16 +0,0 @@
-<!-- eslint-disable vue/multi-word-component-names -->
-<script>
-import { GlSkeletonLoader } from '@gitlab/ui';
-
-export default {
- components: {
- GlSkeletonLoader,
- },
-};
-</script>
-<template>
- <gl-skeleton-loader>
- <rect y="5" width="400" height="30" rx="2" ry="2" />
- <rect y="50" width="400" height="80" rx="2" ry="2" />
- </gl-skeleton-loader>
-</template>
diff --git a/app/assets/javascripts/observability/components/skeleton/explore.vue b/app/assets/javascripts/observability/components/skeleton/explore.vue
deleted file mode 100644
index 3f748086eef..00000000000
--- a/app/assets/javascripts/observability/components/skeleton/explore.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<!-- eslint-disable vue/multi-word-component-names -->
-<script>
-import { GlSkeletonLoader } from '@gitlab/ui';
-
-export default {
- components: {
- GlSkeletonLoader,
- },
-};
-</script>
-<template>
- <gl-skeleton-loader :height="200">
- <!-- Top left -->
- <circle y="2" cx="6" cy="6" r="4" />
- <rect y="2" x="15" width="15" height="8" />
- <rect y="2" x="35" width="40" height="8" />
-
- <!-- Top right -->
-
- <rect y="2" x="263" width="13" height="8" />
- <rect y="2" x="278" width="8" height="8" />
- <rect y="2" x="288" width="50" height="8" />
- <rect y="2" x="340" width="18" height="8" />
- <rect y="2" x="360" width="30" height="8" />
-
- <rect y="15" width="400" height="30" rx="2" ry="2" />
- </gl-skeleton-loader>
-</template>
diff --git a/app/assets/javascripts/observability/components/skeleton/index.vue b/app/assets/javascripts/observability/components/skeleton/index.vue
index d3c6892df50..c3d0a7c90b1 100644
--- a/app/assets/javascripts/observability/components/skeleton/index.vue
+++ b/app/assets/javascripts/observability/components/skeleton/index.vue
@@ -3,34 +3,20 @@
import { GlSkeletonLoader, GlAlert, GlLoadingIcon } from '@gitlab/ui';
import {
- SKELETON_VARIANTS_BY_ROUTE,
SKELETON_STATE,
DEFAULT_TIMERS,
- OBSERVABILITY_ROUTES,
TIMEOUT_ERROR_LABEL,
TIMEOUT_ERROR_MESSAGE,
- SKELETON_VARIANT_EMBED,
SKELETON_SPINNER_VARIANT,
} from '../../constants';
-import DashboardsSkeleton from './dashboards.vue';
-import ExploreSkeleton from './explore.vue';
-import ManageSkeleton from './manage.vue';
-import EmbedSkeleton from './embed.vue';
export default {
components: {
GlSkeletonLoader,
- DashboardsSkeleton,
- ExploreSkeleton,
- ManageSkeleton,
- EmbedSkeleton,
GlAlert,
GlLoadingIcon,
},
- SKELETON_VARIANTS_BY_ROUTE,
SKELETON_STATE,
- OBSERVABILITY_ROUTES,
- SKELETON_VARIANT_EMBED,
i18n: {
TIMEOUT_ERROR_LABEL,
TIMEOUT_ERROR_MESSAGE,
@@ -62,9 +48,6 @@ export default {
spinnerVariant() {
return this.variant === SKELETON_SPINNER_VARIANT;
},
- embedVariant() {
- return this.variant === SKELETON_VARIANT_EMBED;
- },
},
mounted() {
this.setLoadingTimeout();
@@ -118,9 +101,6 @@ export default {
showError() {
this.state = SKELETON_STATE.ERROR;
},
- isVariantByRoute(route) {
- return this.variant === SKELETON_VARIANTS_BY_ROUTE[route];
- },
},
};
</script>
@@ -128,12 +108,7 @@ export default {
<div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch">
<transition name="fade">
<div v-if="skeletonVisible" class="gl-px-5 gl-my-5">
- <dashboards-skeleton v-if="isVariantByRoute($options.OBSERVABILITY_ROUTES.DASHBOARDS)" />
- <explore-skeleton v-else-if="isVariantByRoute($options.OBSERVABILITY_ROUTES.EXPLORE)" />
- <manage-skeleton v-else-if="isVariantByRoute($options.OBSERVABILITY_ROUTES.MANAGE)" />
- <embed-skeleton v-else-if="embedVariant" />
- <gl-loading-icon v-else-if="spinnerVariant" size="lg" />
-
+ <gl-loading-icon v-if="spinnerVariant" size="lg" />
<gl-skeleton-loader v-else>
<rect y="2" width="10" height="8" />
<rect y="2" x="15" width="15" height="8" />
diff --git a/app/assets/javascripts/observability/components/skeleton/manage.vue b/app/assets/javascripts/observability/components/skeleton/manage.vue
deleted file mode 100644
index cf8c900fe11..00000000000
--- a/app/assets/javascripts/observability/components/skeleton/manage.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<!-- eslint-disable vue/multi-word-component-names -->
-<script>
-import { GlSkeletonLoader } from '@gitlab/ui';
-
-export default {
- components: {
- GlSkeletonLoader,
- },
-};
-</script>
-<template>
- <gl-skeleton-loader :height="200">
- <!-- Top header-->
- <rect y="2" width="400" height="30" />
-
- <rect y="35" x="65" width="80" height="8" />
- <rect y="35" x="205" width="30" height="8" />
- <rect y="35" x="240" width="25" height="8" />
- <rect y="35" x="270" width="20" height="8" />
-
- <rect y="55" x="65" width="100" height="8" />
- <rect y="55" x="225" width="65" height="8" />
-
- <rect y="65" x="65" width="225" height="200" rx="2" ry="2" />
- </gl-skeleton-loader>
-</template>
diff --git a/app/assets/javascripts/observability/constants.js b/app/assets/javascripts/observability/constants.js
index b0a0941779d..83eaea185e5 100644
--- a/app/assets/javascripts/observability/constants.js
+++ b/app/assets/javascripts/observability/constants.js
@@ -1,23 +1,5 @@
import { __ } from '~/locale';
-export const MESSAGE_EVENT_TYPE = Object.freeze({
- GOUI_LOADED: 'GOUI_LOADED',
- GOUI_ROUTE_UPDATE: 'GOUI_ROUTE_UPDATE',
-});
-
-export const OBSERVABILITY_ROUTES = Object.freeze({
- DASHBOARDS: 'observability/dashboards',
- EXPLORE: 'observability/explore',
- MANAGE: 'observability/manage',
-});
-
-export const SKELETON_VARIANTS_BY_ROUTE = Object.freeze({
- [OBSERVABILITY_ROUTES.DASHBOARDS]: 'dashboards',
- [OBSERVABILITY_ROUTES.EXPLORE]: 'explore',
- [OBSERVABILITY_ROUTES.MANAGE]: 'manage',
-});
-
-export const SKELETON_VARIANT_EMBED = 'embed';
export const SKELETON_SPINNER_VARIANT = 'spinner';
export const SKELETON_STATE = Object.freeze({
@@ -33,13 +15,3 @@ export const DEFAULT_TIMERS = Object.freeze({
export const TIMEOUT_ERROR_LABEL = __('Unable to load the page');
export const TIMEOUT_ERROR_MESSAGE = __('Reload the page to try again.');
-
-export const INLINE_EMBED_DIMENSIONS = Object.freeze({
- HEIGHT: '366px',
- WIDTH: '768px',
-});
-
-export const FULL_APP_DIMENSIONS = Object.freeze({
- HEIGHT: '100%',
- WIDTH: '100%',
-});
diff --git a/app/assets/javascripts/observability/index.js b/app/assets/javascripts/observability/index.js
deleted file mode 100644
index 72ff1357551..00000000000
--- a/app/assets/javascripts/observability/index.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import Vue from 'vue';
-import VueRouter from 'vue-router';
-
-import ObservabilityApp from './components/observability_app.vue';
-import { SKELETON_VARIANTS_BY_ROUTE } from './constants';
-
-Vue.use(VueRouter);
-
-export default () => {
- const el = document.getElementById('js-observability-app');
-
- if (!el) return false;
-
- const router = new VueRouter({
- mode: 'history',
- });
-
- return new Vue({
- el,
- router,
- computed: {
- skeletonVariant() {
- const [, variant] =
- Object.entries(SKELETON_VARIANTS_BY_ROUTE).find(([path]) =>
- this.$route.path.endsWith(path),
- ) || [];
-
- return variant;
- },
- },
- methods: {
- routeUpdateHandler(payload) {
- const isNewObservabilityPath = this.$route?.query?.observability_path !== payload?.url;
-
- const shouldNotHandleMessage = !payload.url || !isNewObservabilityPath;
-
- if (shouldNotHandleMessage) {
- return;
- }
-
- // this will update the `observability_path` query param on each route change inside Observability UI
- this.$router.replace({
- name: this.$route?.pathname,
- query: { ...this.$route.query, observability_path: payload.url },
- });
- },
- },
- render(h) {
- return h(ObservabilityApp, {
- props: {
- observabilityIframeSrc: el.dataset.observabilityIframeSrc,
- skeletonVariant: this.skeletonVariant,
- },
- on: {
- 'route-update': (payload) => this.routeUpdateHandler(payload),
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/observability/mock_traces.json b/app/assets/javascripts/observability/mock_traces.json
deleted file mode 100644
index cd7dfb40af6..00000000000
--- a/app/assets/javascripts/observability/mock_traces.json
+++ /dev/null
@@ -1,107 +0,0 @@
-{
- "project_id": 123,
- "traces": [
- {
- "timestamp": "2023-08-07T15:03:32.199806Z",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "tracegentracegentracegenttracegentracegentracegent",
- "operation": "lets-golets-golets-goletslets-golets-golets-golets",
- "statusCode": "STATUS_CODE_UNSET",
- "duration_nano": 100120000,
- "spans": [
- {
- "timestamp": "2023-08-07T15:03:32.199806Z",
- "span_id": "A1FB81EB031B09E8",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "tracegentracegentracegentracegentracegentracegentracegentracegentracegentracegentracegentracegentracegen",
- "operation": "lets-golets-golets-golets-golets-golets-golets-golets-golets-golets-golets-golets-go",
- "duration_nano": 100120000,
- "parent_span_id": "",
- "statusCode": "STATUS_CODE_UNSET"
- },
- {
- "timestamp": "2023-08-07T15:03:32.199871Z",
- "span_id": "9C920500FE9C85E3",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "tracegen",
- "operation": "okey-dokey",
- "duration_nano": 100055000,
- "parent_span_id": "A1FB81EB031B09E8",
- "statusCode": "STATUS_CODE_UNSET"
- },
- {
- "timestamp": "2023-08-07T15:03:53.199871Z",
- "span_id": "FAKE",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "tracegen",
- "operation": "okey-dokey",
- "duration_nano": 50027500,
- "parent_span_id": "9C920500FE9C85E3",
- "statusCode": "STATUS_CODE_UNSET"
- },
- {
- "timestamp": "2023-08-07T15:03:53.199871Z",
- "span_id": "FAKE-2",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "fake-service-2",
- "operation": "okey-dokey",
- "duration_nano": 50027500,
- "parent_span_id": "9C920500FE9C85E3",
- "statusCode": "STATUS_CODE_UNSET"
- },
- {
- "timestamp": "2023-08-07T15:04:13.199871Z",
- "span_id": "FAKE-3",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "fake-service-3",
- "operation": "okey-dokey",
- "duration_nano": 30000000,
- "parent_span_id": "FAKE-2",
- "statusCode": "STATUS_CODE_UNSET"
- },
- {
- "timestamp": "2023-08-07T15:04:13.199871Z",
- "span_id": "FAKE-4",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "fake-service-4",
- "operation": "okey-dokey",
- "duration_nano": 25000000,
- "parent_span_id": "FAKE-3",
- "statusCode": "STATUS_CODE_UNSET"
- },
- {
- "timestamp": "2023-08-07T15:04:13.199871Z",
- "span_id": "FAKE-5",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "fake-service-5",
- "operation": "okey-dokey",
- "duration_nano": 10000000,
- "parent_span_id": "FAKE-4",
- "statusCode": "STATUS_CODE_UNSET"
- },
- {
- "timestamp": "2023-08-07T15:04:13.199871Z",
- "span_id": "FAKE-6",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "fake-service-6",
- "operation": "okey-dokey",
- "duration_nano": 10000000,
- "parent_span_id": "FAKE-5",
- "statusCode": "STATUS_CODE_UNSET"
- },
- {
- "timestamp": "2023-08-07T15:04:13.199871Z",
- "span_id": "FAKE-7",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "fake-service-7",
- "operation": "okey-dokey",
- "duration_nano": 5000000,
- "parent_span_id": "FAKE-6",
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 5
- }
- ],
- "totalTraces": 50
-}
diff --git a/app/assets/javascripts/organizations/index/components/app.vue b/app/assets/javascripts/organizations/index/components/app.vue
new file mode 100644
index 00000000000..c47f4ed52c5
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/components/app.vue
@@ -0,0 +1,61 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import { createAlert } from '~/alert';
+import organizationsQuery from '../graphql/organizations.query.graphql';
+import OrganizationsView from './organizations_view.vue';
+
+export default {
+ name: 'OrganizationsIndexApp',
+ i18n: {
+ organizations: __('Organizations'),
+ newOrganization: s__('Organization|New organization'),
+ errorMessage: s__(
+ 'Organization|An error occurred loading user organizations. Please refresh the page to try again.',
+ ),
+ },
+ components: {
+ GlButton,
+ OrganizationsView,
+ },
+ inject: ['newOrganizationUrl'],
+ data() {
+ return {
+ organizations: [],
+ };
+ },
+ apollo: {
+ organizations: {
+ query: organizationsQuery,
+ update(data) {
+ return data.currentUser.organizations.nodes;
+ },
+ error(error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ },
+ },
+ },
+ computed: {
+ showHeader() {
+ return this.loading || this.organizations.length;
+ },
+ loading() {
+ return this.$apollo.queries.organizations.loading;
+ },
+ },
+};
+</script>
+
+<template>
+ <section>
+ <div v-if="showHeader" class="gl-display-flex gl-align-items-center">
+ <h1 class="gl-my-4 gl-font-size-h-display">{{ $options.i18n.organizations }}</h1>
+ <div class="gl-ml-auto">
+ <gl-button :href="newOrganizationUrl" variant="confirm">{{
+ $options.i18n.newOrganization
+ }}</gl-button>
+ </div>
+ </div>
+ <organizations-view :organizations="organizations" :loading="loading" />
+ </section>
+</template>
diff --git a/app/assets/javascripts/organizations/index/components/organizations_list.vue b/app/assets/javascripts/organizations/index/components/organizations_list.vue
new file mode 100644
index 00000000000..539a4fcfe29
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/components/organizations_list.vue
@@ -0,0 +1,26 @@
+<script>
+import OrganizationsListItem from './organizations_list_item.vue';
+
+export default {
+ name: 'OrganizationsList',
+ components: {
+ OrganizationsListItem,
+ },
+ props: {
+ organizations: {
+ type: Array,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <ul class="gl-p-0 gl-list-style-none">
+ <organizations-list-item
+ v-for="organization in organizations"
+ :key="organization.id"
+ :organization="organization"
+ />
+ </ul>
+</template>
diff --git a/app/assets/javascripts/organizations/index/components/organizations_list_item.vue b/app/assets/javascripts/organizations/index/components/organizations_list_item.vue
new file mode 100644
index 00000000000..589835874ad
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/components/organizations_list_item.vue
@@ -0,0 +1,54 @@
+<script>
+import { GlAvatarLabeled, GlTruncateText } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+
+export default {
+ name: 'OrganizationsListItem',
+ components: {
+ GlAvatarLabeled,
+ GlTruncateText,
+ },
+ safeHtmlConfig: {
+ ADD_TAGS: ['gl-emoji'],
+ },
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ organization: {
+ type: Object,
+ required: true,
+ },
+ },
+ avatarSize: { default: 32, md: 48 },
+ getIdFromGraphQLId,
+};
+</script>
+
+<template>
+ <li class="organization-row gl-py-3 gl-border-b gl-display-flex gl-align-items-flex-start">
+ <gl-avatar-labeled
+ :size="$options.avatarSize"
+ :src="organization.avatarUrl"
+ :entity-id="$options.getIdFromGraphQLId(organization.id)"
+ :entity-name="organization.name"
+ :label="organization.name"
+ :label-link="organization.webUrl"
+ shape="rect"
+ >
+ <gl-truncate-text
+ v-if="organization.descriptionHtml"
+ :lines="2"
+ :mobile-lines="2"
+ class="gl-mt-2"
+ >
+ <div
+ v-safe-html:[$options.safeHtmlConfig]="organization.descriptionHtml"
+ data-testid="organization-description-html"
+ class="organization-description gl-text-secondary gl-font-sm"
+ ></div>
+ </gl-truncate-text>
+ </gl-avatar-labeled>
+ </li>
+</template>
diff --git a/app/assets/javascripts/organizations/index/components/organizations_view.vue b/app/assets/javascripts/organizations/index/components/organizations_view.vue
new file mode 100644
index 00000000000..9720646bca3
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/components/organizations_view.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import OrganizationsList from './organizations_list.vue';
+
+export default {
+ name: 'OrganizationsView',
+ i18n: {
+ emptyStateTitle: s__('Organization|Get started with organizations'),
+ emptyStateDescription: s__(
+ 'Organization|Create an organization to contain all of your groups and projects.',
+ ),
+ emptyStateButtonText: s__('Organization|New organization'),
+ },
+ components: {
+ GlLoadingIcon,
+ OrganizationsList,
+ GlEmptyState,
+ },
+ inject: ['newOrganizationUrl', 'organizationsEmptyStateSvgPath'],
+ props: {
+ organizations: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-loading-icon v-if="loading" class="gl-mt-5" size="md" />
+ <organizations-list
+ v-else-if="organizations.length"
+ :organizations="organizations"
+ class="gl-border-t"
+ />
+ <gl-empty-state
+ v-else
+ :svg-height="144"
+ :svg-path="organizationsEmptyStateSvgPath"
+ :title="$options.i18n.emptyStateTitle"
+ :description="$options.i18n.emptyStateDescription"
+ :primary-button-link="newOrganizationUrl"
+ :primary-button-text="$options.i18n.emptyStateButtonText"
+ />
+</template>
diff --git a/app/assets/javascripts/organizations/index/graphql/organizations.query.graphql b/app/assets/javascripts/organizations/index/graphql/organizations.query.graphql
new file mode 100644
index 00000000000..6090e2ec789
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/graphql/organizations.query.graphql
@@ -0,0 +1,14 @@
+query getCurrentUserOrganizations {
+ currentUser {
+ id
+ organizations @client {
+ nodes {
+ id
+ name
+ descriptionHtml
+ avatarUrl
+ webUrl
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/organizations/index/index.js b/app/assets/javascripts/organizations/index/index.js
new file mode 100644
index 00000000000..7cbb9c9165d
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/index.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import resolvers from '../shared/graphql/resolvers';
+import OrganizationsIndexApp from './components/app.vue';
+
+export const initOrganizationsIndex = () => {
+ const el = document.getElementById('js-organizations-index');
+
+ if (!el) return false;
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
+
+ const { newOrganizationUrl, organizationsEmptyStateSvgPath } = convertObjectPropsToCamelCase(
+ el.dataset,
+ );
+
+ return new Vue({
+ el,
+ name: 'OrganizationsIndexRoot',
+ apolloProvider,
+ provide: {
+ newOrganizationUrl,
+ organizationsEmptyStateSvgPath,
+ },
+ render(createElement) {
+ return createElement(OrganizationsIndexApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/organizations/mock_data.js b/app/assets/javascripts/organizations/mock_data.js
index 17ab7bd1d34..d281a0d8a1c 100644
--- a/app/assets/javascripts/organizations/mock_data.js
+++ b/app/assets/javascripts/organizations/mock_data.js
@@ -4,10 +4,34 @@
// https://gitlab.com/gitlab-org/gitlab/-/issues/420777
// https://gitlab.com/gitlab-org/gitlab/-/issues/421441
-export const organization = {
- id: 'gid://gitlab/Organization/1',
- __typename: 'Organization',
-};
+export const organizations = [
+ {
+ id: 'gid://gitlab/Organization/1',
+ name: 'My First Organization',
+ descriptionHtml:
+ '<p>This is where an organization can be explained in <strong>detail</strong></p>',
+ avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61',
+ webUrl: '/-/organizations/default',
+ __typename: 'Organization',
+ },
+ {
+ id: 'gid://gitlab/Organization/2',
+ name: 'Vegetation Co.',
+ descriptionHtml:
+ '<p> Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt<script>alert(1)</script></p>',
+ avatarUrl: null,
+ webUrl: '/-/organizations/default',
+ __typename: 'Organization',
+ },
+ {
+ id: 'gid://gitlab/Organization/3',
+ name: 'Dude where is my car?',
+ descriptionHtml: null,
+ avatarUrl: null,
+ webUrl: '/-/organizations/default',
+ __typename: 'Organization',
+ },
+];
export const organizationProjects = {
nodes: [
@@ -256,3 +280,11 @@ export const organizationGroups = {
},
],
};
+
+export const createOrganizationResponse = {
+ organization: {
+ name: 'Default',
+ path: '/-/organizations/default',
+ },
+ errors: [],
+};
diff --git a/app/assets/javascripts/organizations/new/components/app.vue b/app/assets/javascripts/organizations/new/components/app.vue
new file mode 100644
index 00000000000..8f71fdfe68b
--- /dev/null
+++ b/app/assets/javascripts/organizations/new/components/app.vue
@@ -0,0 +1,82 @@
+<script>
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
+import { createAlert } from '~/alert';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import createOrganizationMutation from '../graphql/mutations/create_organization.mutation.graphql';
+import NewEditForm from '../../shared/components/new_edit_form.vue';
+
+export default {
+ name: 'OrganizationNewApp',
+ components: { NewEditForm, GlSprintf, GlLink },
+ i18n: {
+ pageTitle: s__('Organization|New organization'),
+ pageDescription: s__(
+ 'Organization|%{linkStart}Organizations%{linkEnd} are a top-level container to hold your groups and projects.',
+ ),
+ errorMessage: s__('Organization|An error occurred creating an organization. Please try again.'),
+ successAlertTitle: s__('Organization|Organization successfully created.'),
+ successAlertMessage: s__('Organization|You can now start using your new organization.'),
+ },
+ data() {
+ return {
+ loading: false,
+ };
+ },
+ computed: {
+ organizationsHelpPagePath() {
+ return helpPagePath('user/organization/index');
+ },
+ },
+ methods: {
+ async onSubmit(formValues) {
+ this.loading = true;
+ try {
+ const {
+ data: {
+ createOrganization: { organization, errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: createOrganizationMutation,
+ variables: {
+ ...formValues,
+ },
+ });
+
+ if (errors.length) {
+ // TODO: handle errors when using real API after https://gitlab.com/gitlab-org/gitlab/-/issues/417891 is complete.
+ return;
+ }
+
+ visitUrlWithAlerts(organization.path, [
+ {
+ id: 'organization-successfully-created',
+ title: this.$options.i18n.successAlertTitle,
+ message: this.$options.i18n.successAlertMessage,
+ variant: 'success',
+ },
+ ]);
+ } catch (error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-py-6">
+ <h1 class="gl-mt-0 gl-font-size-h-display">{{ $options.i18n.pageTitle }}</h1>
+ <p>
+ <gl-sprintf :message="$options.i18n.pageDescription">
+ <template #link="{ content }"
+ ><gl-link :href="organizationsHelpPagePath">{{ content }}</gl-link></template
+ >
+ </gl-sprintf>
+ </p>
+ <new-edit-form :loading="loading" @submit="onSubmit" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql b/app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql
new file mode 100644
index 00000000000..766c7e96d14
--- /dev/null
+++ b/app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql
@@ -0,0 +1,9 @@
+mutation createOrganization($input: LocalCreateOrganizationInput!) {
+ createOrganization(input: $input) @client {
+ organization {
+ name
+ path
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/organizations/new/graphql/typedefs.graphql b/app/assets/javascripts/organizations/new/graphql/typedefs.graphql
new file mode 100644
index 00000000000..f708c4ad162
--- /dev/null
+++ b/app/assets/javascripts/organizations/new/graphql/typedefs.graphql
@@ -0,0 +1,5 @@
+# TODO: Use real input type when https://gitlab.com/gitlab-org/gitlab/-/issues/417891 is complete.
+input LocalCreateOrganizationInput {
+ name: String
+ path: String
+}
diff --git a/app/assets/javascripts/organizations/new/index.js b/app/assets/javascripts/organizations/new/index.js
new file mode 100644
index 00000000000..a65603227f6
--- /dev/null
+++ b/app/assets/javascripts/organizations/new/index.js
@@ -0,0 +1,35 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import createDefaultClient from '~/lib/graphql';
+import resolvers from '../shared/graphql/resolvers';
+import App from './components/app.vue';
+
+export const initOrganizationsNew = () => {
+ const el = document.getElementById('js-organizations-new');
+
+ if (!el) return false;
+
+ const {
+ dataset: { appData },
+ } = el;
+ const { organizationsPath, rootUrl } = convertObjectPropsToCamelCase(JSON.parse(appData));
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
+
+ return new Vue({
+ el,
+ name: 'OrganizationNewRoot',
+ apolloProvider,
+ provide: {
+ organizationsPath,
+ rootUrl,
+ },
+ render(createElement) {
+ return createElement(App);
+ },
+ });
+};
diff --git a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
new file mode 100644
index 00000000000..db33f240966
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
@@ -0,0 +1,125 @@
+<script>
+import {
+ GlForm,
+ GlFormFields,
+ GlButton,
+ GlFormInputGroup,
+ GlFormInput,
+ GlInputGroupText,
+ GlTruncate,
+} from '@gitlab/ui';
+import { formValidators } from '@gitlab/ui/dist/utils';
+import { s__, __ } from '~/locale';
+import { slugify } from '~/lib/utils/text_utility';
+import { joinPaths } from '~/lib/utils/url_utility';
+
+export default {
+ name: 'NewEditForm',
+ components: {
+ GlForm,
+ GlFormFields,
+ GlButton,
+ GlFormInputGroup,
+ GlFormInput,
+ GlInputGroupText,
+ GlTruncate,
+ },
+ i18n: {
+ createOrganization: s__('Organization|Create organization'),
+ cancel: __('Cancel'),
+ pathPlaceholder: s__('Organization|my-organization'),
+ },
+ formId: 'new-organization-form',
+ fields: {
+ name: {
+ label: s__('Organization|Organization name'),
+ validators: [formValidators.required(s__('Organization|Organization name is required.'))],
+ groupAttrs: {
+ description: s__(
+ 'Organization|Must start with a letter, digit, emoji, or underscore. Can also contain periods, dashes, spaces, and parentheses.',
+ ),
+ },
+ inputAttrs: {
+ class: 'gl-md-form-input-lg',
+ placeholder: s__('Organization|My organization'),
+ },
+ },
+ path: {
+ label: s__('Organization|Organization URL'),
+ validators: [formValidators.required(s__('Organization|Organization URL is required.'))],
+ },
+ },
+ inject: ['organizationsPath', 'rootUrl'],
+ props: {
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ formValues: {
+ name: '',
+ path: '',
+ },
+ hasPathBeenManuallySet: false,
+ };
+ },
+ computed: {
+ baseUrl() {
+ return joinPaths(this.rootUrl, this.organizationsPath, '/');
+ },
+ },
+ watch: {
+ 'formValues.name': function watchName(value) {
+ if (this.hasPathBeenManuallySet) {
+ return;
+ }
+
+ this.formValues.path = slugify(value);
+ },
+ },
+ methods: {
+ onPathInput(event, formFieldsInputEvent) {
+ formFieldsInputEvent(event);
+ this.hasPathBeenManuallySet = true;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form :id="$options.formId">
+ <gl-form-fields
+ v-model="formValues"
+ :form-id="$options.formId"
+ :fields="$options.fields"
+ @submit="$emit('submit', formValues)"
+ >
+ <template #input(path)="{ id, value, validation, input, blur }">
+ <gl-form-input-group>
+ <template #prepend>
+ <gl-input-group-text class="organization-root-path">
+ <gl-truncate :text="baseUrl" position="middle" />
+ </gl-input-group-text>
+ </template>
+ <gl-form-input
+ v-bind="validation"
+ :id="id"
+ :value="value"
+ :placeholder="$options.i18n.pathPlaceholder"
+ class="gl-h-auto! gl-md-form-input-lg"
+ @input="onPathInput($event, input)"
+ @blur="blur"
+ />
+ </gl-form-input-group>
+ </template>
+ </gl-form-fields>
+ <div class="gl-display-flex gl-gap-3">
+ <gl-button type="submit" variant="confirm" class="js-no-auto-disable" :loading="loading">{{
+ $options.i18n.createOrganization
+ }}</gl-button>
+ <gl-button :href="organizationsPath">{{ $options.i18n.cancel }}</gl-button>
+ </div>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/organizations/shared/graphql/resolvers.js b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
index c78266b0476..9f7e9b22e1d 100644
--- a/app/assets/javascripts/organizations/shared/graphql/resolvers.js
+++ b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
@@ -1,18 +1,44 @@
-import { organization, organizationProjects, organizationGroups } from '../../mock_data';
+import {
+ organizations,
+ organizationProjects,
+ organizationGroups,
+ createOrganizationResponse,
+} from '../../mock_data';
+
+const simulateLoading = () => {
+ return new Promise((resolve) => {
+ setTimeout(resolve, 1000);
+ });
+};
export default {
Query: {
organization: async () => {
// Simulate API loading
- await new Promise((resolve) => {
- setTimeout(resolve, 1000);
- });
+ await simulateLoading();
return {
- ...organization,
+ ...organizations[0],
projects: organizationProjects,
groups: organizationGroups,
};
},
},
+ UserCore: {
+ organizations: async () => {
+ await simulateLoading();
+
+ return {
+ nodes: organizations,
+ };
+ },
+ },
+ Mutation: {
+ createOrganization: async () => {
+ // Simulate API loading
+ await simulateLoading();
+
+ return createOrganizationResponse;
+ },
+ },
};
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
index b58e2249829..7c594a6c091 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
@@ -231,6 +231,7 @@ export default {
v-if="hasNoTags"
:title="emptyStateTitle"
:svg-path="config.noContainersImage"
+ :svg-height="null"
:description="emptyStateDescription"
class="gl-mx-auto gl-my-0"
/>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue
index a68c4de5aa6..93bdb942faa 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue
@@ -15,6 +15,7 @@ export default {
<gl-empty-state
:title="s__('ContainerRegistry|There are no container images available in this group')"
:svg-path="config.noContainersImage"
+ :svg-height="null"
>
<template #description>
<p>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue
index 5aa04419ca0..4ddcaa5c9a7 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue
@@ -41,6 +41,7 @@ export default {
<gl-empty-state
:title="s__('ContainerRegistry|There are no container images stored for this project')"
:svg-path="config.noContainersImage"
+ :svg-height="null"
>
<template #description>
<p>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
index c266dbf7e98..3eb1b2b4ba5 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
@@ -190,6 +190,7 @@ export default {
:title="$options.i18n.MISSING_OR_DELETED_IMAGE_TITLE"
:description="$options.i18n.MISSING_OR_DELETED_IMAGE_MESSAGE"
:svg-path="config.noContainersImage"
+ :svg-height="null"
class="gl-mx-auto gl-my-0"
/>
</div>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
index df87ee79111..a1c4d7ea1f2 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
@@ -251,6 +251,7 @@ export default {
v-if="showConnectionError"
:title="$options.i18n.CONNECTION_ERROR_TITLE"
:svg-path="config.containersErrorImage"
+ :svg-height="null"
>
<template #description>
<p>
@@ -325,6 +326,7 @@ export default {
<gl-empty-state
v-else
:svg-path="config.noContainersImage"
+ :svg-height="null"
data-testid="emptySearch"
:title="$options.i18n.EMPTY_RESULT_TITLE"
>
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_empty_state.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_empty_state.vue
index b0d03a7cebe..7a29cb2d5ab 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_empty_state.vue
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_empty_state.vue
@@ -35,7 +35,11 @@ export default {
</script>
<template>
- <gl-empty-state :svg-path="noManifestsIllustration" :title="$options.i18n.noManifestTitle">
+ <gl-empty-state
+ :svg-path="noManifestsIllustration"
+ :svg-height="null"
+ :title="$options.i18n.noManifestTitle"
+ >
<template #description>
<p class="gl-mb-5">
<gl-sprintf :message="$options.i18n.emptyText">
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list.vue
index b55204de875..65ca4de7055 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list.vue
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list.vue
@@ -73,6 +73,7 @@ export default {
v-if="hasNoTags"
:title="emptyStateTitle"
:svg-path="noContainersImage"
+ :svg-height="null"
:description="emptyStateDescription"
class="gl-mx-auto gl-my-0"
/>
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue
index b34d3a950c0..ea265430865 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue
@@ -62,6 +62,7 @@ export default {
v-else-if="hasNoTags"
:title="emptyStateTitle"
:svg-path="noContainersImage"
+ :svg-height="null"
:description="emptyStateDescription"
class="gl-mx-auto gl-my-0"
/>
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue
index 1d8cb0f1360..9daed3e1211 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue
@@ -164,6 +164,7 @@ export default {
v-if="showConnectionError"
:title="$options.i18n.connectionErrorTitle"
:svg-path="containersErrorImage"
+ :svg-height="null"
>
<template #description>
<p>
@@ -220,6 +221,7 @@ export default {
<gl-empty-state
v-else
:svg-path="noContainersImage"
+ :svg-height="null"
data-testid="emptySearch"
:title="emptyStateTexts.title"
>
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
index cb96f3d96cb..b49c448c478 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
@@ -146,6 +146,7 @@ export default {
:title="s__('PackageRegistry|Unable to load package')"
:description="s__('PackageRegistry|There was a problem fetching the details for this package.')"
:svg-path="svgPath"
+ :svg-height="null"
/>
<div v-else class="packages-app">
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
index d1982464eb9..265e3de0512 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
@@ -245,6 +245,7 @@ export default {
:title="s__('PackageRegistry|Unable to load package')"
:description="s__('PackageRegistry|There was a problem fetching the details for this package.')"
:svg-path="emptyListIllustration"
+ :svg-height="null"
/>
<div v-else-if="projectName" class="packages-app">
<package-title :package-entity="packageEntity">
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/exceptions_input.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/exceptions_input.vue
index 9ac1673dbf3..5a7feba35a4 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/exceptions_input.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/exceptions_input.vue
@@ -70,7 +70,7 @@ export default {
<gl-form-input
:id="id"
:disabled="duplicatesAllowed || loading"
- size="lg"
+ width="lg"
:value="duplicateExceptionRegex"
:state="isExceptionRegexValid"
@change="update(name, $event)"
diff --git a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
index cde46c3da50..cd6c9677b5f 100644
--- a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
+++ b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
@@ -268,7 +268,7 @@ export default {
name="application_setting[signup_enabled]"
:help-text="signupEnabledHelpText"
:label="$options.i18n.signupEnabledLabel"
- data-qa-selector="signup_enabled_checkbox"
+ data-testid="signup-enabled-checkbox"
/>
<signup-checkbox
@@ -277,7 +277,6 @@ export default {
name="application_setting[require_admin_approval_after_user_signup]"
:help-text="requireAdminApprovalHelpText"
:label="$options.i18n.requireAdminApprovalLabel"
- data-qa-selector="require_admin_approval_after_user_signup_checkbox"
data-testid="require-admin-approval-checkbox"
/>
@@ -452,7 +451,7 @@ export default {
</section>
<gl-button
- data-qa-selector="save_changes_button"
+ data-testid="save-changes-button"
variant="confirm"
@click.prevent="submitButtonHandler"
>
diff --git a/app/assets/javascripts/pages/groups/custom_emoji/index.js b/app/assets/javascripts/pages/groups/custom_emoji/index.js
new file mode 100644
index 00000000000..dd02a6f5348
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/custom_emoji/index.js
@@ -0,0 +1,3 @@
+import { initCustomEmojis } from '~/custom_emoji/custom_emoji_bundle';
+
+requestIdleCallback(initCustomEmojis);
diff --git a/app/assets/javascripts/pages/groups/observability/dashboards/index.js b/app/assets/javascripts/pages/groups/observability/dashboards/index.js
deleted file mode 100644
index c3b6ce6f99f..00000000000
--- a/app/assets/javascripts/pages/groups/observability/dashboards/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import ObservabilityApp from '~/observability';
-
-ObservabilityApp();
diff --git a/app/assets/javascripts/pages/groups/observability/datasources/index.js b/app/assets/javascripts/pages/groups/observability/datasources/index.js
deleted file mode 100644
index c3b6ce6f99f..00000000000
--- a/app/assets/javascripts/pages/groups/observability/datasources/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import ObservabilityApp from '~/observability';
-
-ObservabilityApp();
diff --git a/app/assets/javascripts/pages/groups/observability/explore/index.js b/app/assets/javascripts/pages/groups/observability/explore/index.js
deleted file mode 100644
index c3b6ce6f99f..00000000000
--- a/app/assets/javascripts/pages/groups/observability/explore/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import ObservabilityApp from '~/observability';
-
-ObservabilityApp();
diff --git a/app/assets/javascripts/pages/groups/observability/manage/index.js b/app/assets/javascripts/pages/groups/observability/manage/index.js
deleted file mode 100644
index c3b6ce6f99f..00000000000
--- a/app/assets/javascripts/pages/groups/observability/manage/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import ObservabilityApp from '~/observability';
-
-ObservabilityApp();
diff --git a/app/assets/javascripts/pages/groups/work_items/show/index.js b/app/assets/javascripts/pages/groups/work_items/show/index.js
new file mode 100644
index 00000000000..c091fbcc2b2
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/work_items/show/index.js
@@ -0,0 +1,4 @@
+import { WORKSPACE_GROUP } from '~/issues/constants';
+import { initWorkItemsRoot } from '~/work_items';
+
+initWorkItemsRoot(WORKSPACE_GROUP);
diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
index 1d0eaae4c57..459546a5562 100644
--- a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
+++ b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
@@ -168,13 +168,22 @@ export default {
}
},
- getFullDestinationUrl(params) {
+ destinationLinkHref(params) {
return joinPaths(gon.relative_url_root || '', '/', params.destination_full_path);
},
- getPresentationUrl(item) {
+ pathWithSuffix(path, item) {
const suffix = item.entity_type === WORKSPACE_GROUP ? '/' : '';
- return `${item.destination_full_path}${suffix}`;
+ return `${path}${suffix}`;
+ },
+
+ destinationLinkText(item) {
+ return this.pathWithSuffix(item.destination_full_path, item);
+ },
+
+ destinationText(item) {
+ const fullPath = joinPaths(item.destination_namespace, item.destination_slug);
+ return this.pathWithSuffix(fullPath, item);
},
getEntityTooltip(item) {
@@ -187,6 +196,11 @@ export default {
return '';
}
},
+
+ setPageSize(size) {
+ this.paginationConfig.perPage = size;
+ this.paginationConfig.page = 1;
+ },
},
gitlabLogo: window.gon.gitlab_logo,
@@ -218,19 +232,21 @@ export default {
class="gl-w-full"
>
<template #cell(destination_name)="{ item }">
- <template v-if="item.destination_full_path">
- <gl-icon
- v-gl-tooltip
- :name="item.entity_type"
- :title="getEntityTooltip(item)"
- :aria-label="getEntityTooltip(item)"
- class="gl-text-gray-500"
- />
- <gl-link :href="getFullDestinationUrl(item)" target="_blank">
- {{ getPresentationUrl(item) }}
- </gl-link>
- </template>
- <gl-loading-icon v-else inline />
+ <gl-icon
+ v-gl-tooltip
+ :name="item.entity_type"
+ :title="getEntityTooltip(item)"
+ :aria-label="getEntityTooltip(item)"
+ class="gl-text-gray-500"
+ />
+ <gl-link
+ v-if="item.destination_full_path"
+ :href="destinationLinkHref(item)"
+ target="_blank"
+ >
+ {{ destinationLinkText(item) }}
+ </gl-link>
+ <span v-else>{{ destinationText(item) }}</span>
</template>
<template #cell(created_at)="{ value }">
<time-ago :time="value" />
@@ -253,7 +269,7 @@ export default {
:page-info="pageInfo"
class="gl-m-0 gl-mt-3"
@set-page="paginationConfig.page = $event"
- @set-page-size="paginationConfig.perPage = $event"
+ @set-page-size="setPageSize"
/>
</template>
<local-storage-sync
diff --git a/app/assets/javascripts/pages/organizations/organizations/index/index.js b/app/assets/javascripts/pages/organizations/organizations/index/index.js
new file mode 100644
index 00000000000..c7e087b81c6
--- /dev/null
+++ b/app/assets/javascripts/pages/organizations/organizations/index/index.js
@@ -0,0 +1,3 @@
+import { initOrganizationsIndex } from '~/organizations/index';
+
+initOrganizationsIndex();
diff --git a/app/assets/javascripts/pages/organizations/organizations/new/index.js b/app/assets/javascripts/pages/organizations/organizations/new/index.js
new file mode 100644
index 00000000000..ab23fbf155d
--- /dev/null
+++ b/app/assets/javascripts/pages/organizations/organizations/new/index.js
@@ -0,0 +1,3 @@
+import { initOrganizationsNew } from '~/organizations/new';
+
+initOrganizationsNew();
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index a3d930433c3..07662e4411e 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -21,6 +21,7 @@ import RefSelector from '~/ref/components/ref_selector.vue';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { parseBoolean } from '~/lib/utils/common_utils';
import HighlightWorker from '~/vue_shared/components/source_viewer/workers/highlight_worker?worker';
+import initAmbiguousRefModal from '~/ref/init_ambiguous_ref_modal';
Vue.use(Vuex);
Vue.use(VueApollo);
@@ -62,6 +63,7 @@ const initRefSwitcher = () => {
};
initRefSwitcher();
+initAmbiguousRefModal();
if (viewBlobEl) {
const {
diff --git a/app/assets/javascripts/pages/projects/find_file/ref_switcher/index.js b/app/assets/javascripts/pages/projects/find_file/ref_switcher/index.js
index 9a3bb25de70..ffd4ef9efbb 100644
--- a/app/assets/javascripts/pages/projects/find_file/ref_switcher/index.js
+++ b/app/assets/javascripts/pages/projects/find_file/ref_switcher/index.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import { s__ } from '~/locale';
import Translate from '~/vue_shared/translate';
import RefSelector from '~/ref/components/ref_selector.vue';
-import { visitUrl } from '~/lib/utils/url_utility';
+import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { generateRefDestinationPath } from './ref_switcher_utils';
Vue.use(Translate);
@@ -13,7 +13,7 @@ export default () => {
const el = document.getElementById('js-blob-ref-switcher');
if (!el) return false;
- const { projectId, ref, namespace } = el.dataset;
+ const { projectId, ref, refType, namespace } = el.dataset;
return new Vue({
el,
@@ -21,7 +21,8 @@ export default () => {
return createElement(RefSelector, {
props: {
projectId,
- value: ref,
+ value: refType ? joinPaths('refs', refType, ref) : ref,
+ useSymbolicRefNames: Boolean(refType),
translations: {
dropdownHeader: REF_SWITCH_HEADER,
searchPlaceholder: REF_SWITCH_HEADER,
diff --git a/app/assets/javascripts/pages/projects/find_file/ref_switcher/ref_switcher_utils.js b/app/assets/javascripts/pages/projects/find_file/ref_switcher/ref_switcher_utils.js
index 5fecd024f1a..21a30f1c54b 100644
--- a/app/assets/javascripts/pages/projects/find_file/ref_switcher/ref_switcher_utils.js
+++ b/app/assets/javascripts/pages/projects/find_file/ref_switcher/ref_switcher_utils.js
@@ -10,19 +10,32 @@ export function generateRefDestinationPath(selectedRef, namespace) {
return window.location.href;
}
+ let refType = null;
const { pathname } = window.location;
const encodedHash = '%23';
const [projectRootPath] = pathname.split(namespace);
+ let actualRef = selectedRef;
+
+ const matches = selectedRef.match(/^refs\/(heads|tags)\/(.+)/);
+ if (matches) {
+ [, refType, actualRef] = matches;
+ }
const destinationPath = joinPaths(
projectRootPath,
namespace,
- encodeURI(selectedRef).replace(/#/g, encodedHash),
+ encodeURI(actualRef).replace(/#/g, encodedHash),
);
const newURL = new URL(window.location);
newURL.pathname = destinationPath;
+ if (refType) {
+ newURL.searchParams.set('ref_type', refType.toLowerCase());
+ } else {
+ newURL.searchParams.delete('ref_type');
+ }
+
return newURL.href;
}
diff --git a/app/assets/javascripts/pages/projects/find_file/show/index.js b/app/assets/javascripts/pages/projects/find_file/show/index.js
index e207df2434b..22c21430e8b 100644
--- a/app/assets/javascripts/pages/projects/find_file/show/index.js
+++ b/app/assets/javascripts/pages/projects/find_file/show/index.js
@@ -6,8 +6,9 @@ import InitBlobRefSwitcher from '../ref_switcher';
InitBlobRefSwitcher();
const findElement = document.querySelector('.js-file-finder');
const projectFindFile = new ProjectFindFile($('.file-finder-holder'), {
- url: findElement.dataset.fileFindUrl,
treeUrl: findElement.dataset.findTreeUrl,
blobUrlTemplate: findElement.dataset.blobUrlTemplate,
+ refType: findElement.dataset.refType,
});
+projectFindFile.load(findElement.dataset.fileFindUrl);
new ShortcutsFindFile(projectFindFile); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js
index 10c794c9ba2..c24a69bc26b 100644
--- a/app/assets/javascripts/pages/projects/graphs/charts/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js
@@ -1,6 +1,5 @@
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import Vue from 'vue';
-import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
import { __ } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
@@ -10,205 +9,203 @@ import SeriesDataMixin from './series_data_mixin';
const seriesDataToBarData = (raw) => Object.entries(raw).map(([name, data]) => ({ name, data }));
-waitForCSSLoaded(() => {
- const languagesContainer = document.getElementById('js-languages-chart');
- const codeCoverageContainer = document.getElementById('js-code-coverage-chart');
- const monthContainer = document.getElementById('js-month-chart');
- const weekdayContainer = document.getElementById('js-weekday-chart');
- const hourContainer = document.getElementById('js-hour-chart');
- const branchSelector = document.getElementById('js-project-graph-ref-switcher');
- const LANGUAGE_CHART_HEIGHT = 300;
- const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => {
- if (firstDayOfWeek === 0) {
- return weekDays;
- }
+const languagesContainer = document.getElementById('js-languages-chart');
+const codeCoverageContainer = document.getElementById('js-code-coverage-chart');
+const monthContainer = document.getElementById('js-month-chart');
+const weekdayContainer = document.getElementById('js-weekday-chart');
+const hourContainer = document.getElementById('js-hour-chart');
+const branchSelector = document.getElementById('js-project-graph-ref-switcher');
+const LANGUAGE_CHART_HEIGHT = 300;
+const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => {
+ if (firstDayOfWeek === 0) {
+ return weekDays;
+ }
- return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => {
- const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length];
+ return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => {
+ const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length];
- return {
- ...acc,
- [reorderedDayName]: weekDays[reorderedDayName],
- };
- }, {});
- };
+ return {
+ ...acc,
+ [reorderedDayName]: weekDays[reorderedDayName],
+ };
+ }, {});
+};
- // eslint-disable-next-line no-new
- new Vue({
- el: languagesContainer,
- components: {
- GlColumnChart,
- },
- data() {
- return {
- chartData: JSON.parse(languagesContainer.dataset.chartData),
- };
- },
- computed: {
- seriesData() {
- return [{ name: 'full', data: this.chartData.map((d) => [d.label, d.value]) }];
+// eslint-disable-next-line no-new
+new Vue({
+ el: languagesContainer,
+ components: {
+ GlColumnChart,
+ },
+ data() {
+ return {
+ chartData: JSON.parse(languagesContainer.dataset.chartData),
+ };
+ },
+ computed: {
+ seriesData() {
+ return [{ name: 'full', data: this.chartData.map((d) => [d.label, d.value]) }];
+ },
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ bars: this.seriesData,
+ xAxisTitle: __('Used programming language'),
+ yAxisTitle: __('Percentage'),
+ xAxisType: 'category',
},
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- bars: this.seriesData,
- xAxisTitle: __('Used programming language'),
- yAxisTitle: __('Percentage'),
- xAxisType: 'category',
- },
- attrs: {
- height: LANGUAGE_CHART_HEIGHT,
- responsive: true,
- },
- });
- },
- });
+ attrs: {
+ height: LANGUAGE_CHART_HEIGHT,
+ responsive: true,
+ },
+ });
+ },
+});
- const {
- graphEndpoint,
- graphEndDate,
- graphStartDate,
- graphRef,
- graphCsvPath,
- } = codeCoverageContainer.dataset;
- // eslint-disable-next-line no-new
- new Vue({
- el: codeCoverageContainer,
- render(h) {
- return h(CodeCoverage, {
- props: {
- graphEndpoint,
- graphEndDate,
- graphStartDate,
- graphRef,
- graphCsvPath,
- },
- });
- },
- });
+const {
+ graphEndpoint,
+ graphEndDate,
+ graphStartDate,
+ graphRef,
+ graphCsvPath,
+} = codeCoverageContainer.dataset;
+// eslint-disable-next-line no-new
+new Vue({
+ el: codeCoverageContainer,
+ render(h) {
+ return h(CodeCoverage, {
+ props: {
+ graphEndpoint,
+ graphEndDate,
+ graphStartDate,
+ graphRef,
+ graphCsvPath,
+ },
+ });
+ },
+});
- // eslint-disable-next-line no-new
- new Vue({
- el: monthContainer,
- components: {
- GlColumnChart,
- },
- mixins: [SeriesDataMixin],
- data() {
- return {
- chartData: JSON.parse(monthContainer.dataset.chartData),
- };
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- bars: seriesDataToBarData(this.seriesData),
- xAxisTitle: __('Day of month'),
- yAxisTitle: __('No. of commits'),
- xAxisType: 'category',
- },
- attrs: {
- responsive: true,
- },
- });
- },
- });
+// eslint-disable-next-line no-new
+new Vue({
+ el: monthContainer,
+ components: {
+ GlColumnChart,
+ },
+ mixins: [SeriesDataMixin],
+ data() {
+ return {
+ chartData: JSON.parse(monthContainer.dataset.chartData),
+ };
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ bars: seriesDataToBarData(this.seriesData),
+ xAxisTitle: __('Day of month'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
+ },
+ attrs: {
+ responsive: true,
+ },
+ });
+ },
+});
- // eslint-disable-next-line no-new
- new Vue({
- el: weekdayContainer,
- components: {
- GlColumnChart,
- },
- data() {
- return {
- chartData: JSON.parse(weekdayContainer.dataset.chartData),
- };
- },
- computed: {
- seriesData() {
- const weekDays = reorderWeekDays(this.chartData, gon.first_day_of_week);
- const data = Object.keys(weekDays).reduce((acc, key) => {
- acc.push([key, weekDays[key]]);
- return acc;
- }, []);
- return [{ name: 'full', data }];
+// eslint-disable-next-line no-new
+new Vue({
+ el: weekdayContainer,
+ components: {
+ GlColumnChart,
+ },
+ data() {
+ return {
+ chartData: JSON.parse(weekdayContainer.dataset.chartData),
+ };
+ },
+ computed: {
+ seriesData() {
+ const weekDays = reorderWeekDays(this.chartData, gon.first_day_of_week);
+ const data = Object.keys(weekDays).reduce((acc, key) => {
+ acc.push([key, weekDays[key]]);
+ return acc;
+ }, []);
+ return [{ name: 'full', data }];
+ },
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ bars: this.seriesData,
+ xAxisTitle: __('Weekday'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
},
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- bars: this.seriesData,
- xAxisTitle: __('Weekday'),
- yAxisTitle: __('No. of commits'),
- xAxisType: 'category',
- },
- attrs: {
- responsive: true,
- },
- });
- },
- });
+ attrs: {
+ responsive: true,
+ },
+ });
+ },
+});
- // eslint-disable-next-line no-new
- new Vue({
- el: hourContainer,
- components: {
- GlColumnChart,
- },
- mixins: [SeriesDataMixin],
- data() {
- return {
- chartData: JSON.parse(hourContainer.dataset.chartData),
- };
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- bars: seriesDataToBarData(this.seriesData),
- xAxisTitle: __('Hour (UTC)'),
- yAxisTitle: __('No. of commits'),
- xAxisType: 'category',
- },
- attrs: {
- responsive: true,
- },
- });
- },
- });
+// eslint-disable-next-line no-new
+new Vue({
+ el: hourContainer,
+ components: {
+ GlColumnChart,
+ },
+ mixins: [SeriesDataMixin],
+ data() {
+ return {
+ chartData: JSON.parse(hourContainer.dataset.chartData),
+ };
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ bars: seriesDataToBarData(this.seriesData),
+ xAxisTitle: __('Hour (UTC)'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
+ },
+ attrs: {
+ responsive: true,
+ },
+ });
+ },
+});
- const { projectId, projectBranch, graphPath } = branchSelector.dataset;
+const { projectId, projectBranch, graphPath } = branchSelector.dataset;
- const GRAPHS_PATH_REGEX = /^(.*?)\/-\/graphs/g;
- const graphsPathPrefix = graphPath.match(GRAPHS_PATH_REGEX)?.[0];
- if (!graphsPathPrefix) {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- throw new Error('Path is not correct');
- }
+const GRAPHS_PATH_REGEX = /^(.*?)\/-\/graphs/g;
+const graphsPathPrefix = graphPath.match(GRAPHS_PATH_REGEX)?.[0];
+if (!graphsPathPrefix) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Path is not correct');
+}
- // eslint-disable-next-line no-new
- new Vue({
- el: branchSelector,
- name: 'RefSelector',
- render(createComponent) {
- return createComponent(RefSelector, {
- props: {
- enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS],
- value: projectBranch,
- translations: {
- dropdownHeader: __('Switch branch/tag'),
- searchPlaceholder: __('Search branches and tags'),
- },
- projectId,
+// eslint-disable-next-line no-new
+new Vue({
+ el: branchSelector,
+ name: 'RefSelector',
+ render(createComponent) {
+ return createComponent(RefSelector, {
+ props: {
+ enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS],
+ value: projectBranch,
+ translations: {
+ dropdownHeader: __('Switch branch/tag'),
+ searchPlaceholder: __('Search branches and tags'),
},
- class: 'gl-w-20',
- on: {
- input(selected) {
- visitUrl(`${graphsPathPrefix}/${encodeURIComponent(selected)}/charts`);
- },
+ projectId,
+ },
+ class: 'gl-w-20',
+ on: {
+ input(selected) {
+ visitUrl(`${graphsPathPrefix}/${encodeURIComponent(selected)}/charts`);
},
- });
- },
- });
+ },
+ });
+ },
});
diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/index.js b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
index ead15143072..4118541d973 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/index.js
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
@@ -1,6 +1,4 @@
-import { initFilteredSearchServiceDesk } from '~/issues';
-import { mountServiceDeskListApp } from '~/issues/service_desk';
+import { initFilteredSearchServiceDesk, mountServiceDeskListApp } from '~/issues/service_desk';
initFilteredSearchServiceDesk();
-
mountServiceDeskListApp();
diff --git a/app/assets/javascripts/pages/projects/jobs/show/index.js b/app/assets/javascripts/pages/projects/jobs/show/index.js
index cd83f2b7b64..6618b68e9fe 100644
--- a/app/assets/javascripts/pages/projects/jobs/show/index.js
+++ b/app/assets/javascripts/pages/projects/jobs/show/index.js
@@ -1,3 +1,3 @@
-import initJobDetails from '~/ci/job_details';
+import { initJobDetails } from '~/ci/job_details';
initJobDetails();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
index d23a0615bb8..8cb1462c883 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
@@ -43,6 +43,7 @@ if (mrNewCompareNode) {
project: 'js-source-project',
branch: 'js-source-branch gl-font-monospace',
},
+ compareSide: 'source',
},
methods: {
async selectedBranch(branchName) {
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index 2cdbf0fb830..af1635221ab 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -4,8 +4,9 @@ import { s__ } from '~/locale';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import { initPipelineCountListener } from '~/commit/pipelines/utils';
import { initIssuableSidebar } from '~/issuable';
-import MergeRequestStatusBadge from '~/merge_requests/components/merge_request_status_badge.vue';
+import MergeRequestHeader from '~/merge_requests/components/merge_request_header.vue';
import createDefaultClient from '~/lib/graphql';
+import { parseBoolean } from '~/lib/utils/common_utils';
import initSourcegraph from '~/sourcegraph';
import ZenMode from '~/zen_mode';
import initAwardsApp from '~/emoji/awards_app';
@@ -14,7 +15,7 @@ import { initMrExperienceSurvey } from '~/surveys/merge_request_experience';
import toast from '~/vue_shared/plugins/global_toast';
import getStateQuery from './queries/get_state.query.graphql';
-export default function initMergeRequestShow() {
+export default function initMergeRequestShow(store) {
new ZenMode(); // eslint-disable-line no-new
initPipelineCountListener(document.querySelector('#commit-pipeline-table-view'));
new ShortcutsIssuable(true); // eslint-disable-line no-new
@@ -23,26 +24,27 @@ export default function initMergeRequestShow() {
initAwardsApp(document.getElementById('js-vue-awards-block'));
initMrExperienceSurvey();
- const el = document.querySelector('.js-mr-status-box');
- const { iid, issuableType, projectPath, state } = el.dataset;
+ const el = document.querySelector('.js-mr-header');
+ const { hidden, iid, projectPath, state } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
- name: 'IssuableStatusBoxRoot',
+ name: 'MergeRequestHeaderRoot',
+ store,
apolloProvider: new VueApollo({
defaultClient: createDefaultClient(),
}),
provide: {
query: getStateQuery,
+ hidden: parseBoolean(hidden),
iid,
projectPath,
},
render(createElement) {
- return createElement(MergeRequestStatusBadge, {
+ return createElement(MergeRequestHeader, {
props: {
initialState: state,
- issuableType,
},
});
},
diff --git a/app/assets/javascripts/pages/projects/merge_requests/page.js b/app/assets/javascripts/pages/projects/merge_requests/page.js
index f7b522f7c85..fb243d01dc6 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/page.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/page.js
@@ -1,7 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import initMrNotes from 'ee_else_ce/mr_notes';
-import { mountHeaderMetadata } from '~/merge_requests';
import StickyHeader from '~/merge_requests/components/sticky_header.vue';
import { start as startCodeReviewMessaging } from '~/code_review/signals';
import diffsEventHub from '~/diffs/event_hub';
@@ -17,14 +16,13 @@ Vue.use(VueApollo);
export function initMrPage() {
initMrNotes();
- initShow();
+ initShow(store);
initMrMoreDropdown();
startCodeReviewMessaging({ signalBus: diffsEventHub });
}
requestIdleCallback(() => {
initSidebarBundle(store);
- mountHeaderMetadata(store);
const el = document.getElementById('js-merge-sticky-header');
diff --git a/app/assets/javascripts/pages/projects/ml/models/show/index.js b/app/assets/javascripts/pages/projects/ml/models/show/index.js
new file mode 100644
index 00000000000..87ee5c851f6
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/ml/models/show/index.js
@@ -0,0 +1,4 @@
+import { initSimpleApp } from '~/helpers/init_simple_app_helper';
+import { ShowMlModel } from '~/ml/model_registry/apps';
+
+initSimpleApp('#js-mount-show-ml-model', ShowMlModel);
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index 642fd56eab1..9c4582ece21 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -1,12 +1,5 @@
<script>
-import {
- GlFormRadio,
- GlFormRadioGroup,
- GlIcon,
- GlLink,
- GlSprintf,
- GlTooltipDirective,
-} from '@gitlab/ui';
+import { GlFormRadio, GlFormRadioGroup, GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui';
import { getWeekdayNames } from '~/lib/utils/datetime_utility';
import { __, s__, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -23,7 +16,6 @@ export default {
GlFormRadioGroup,
GlIcon,
GlLink,
- GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -97,8 +89,7 @@ export default {
},
{
value: KEY_CUSTOM,
- text: s__('PipelineScheduleIntervalPattern|Custom (%{linkStart}Learn more%{linkEnd}.)'),
- link: this.cronSyntaxUrl,
+ text: s__('PipelineScheduleIntervalPattern|Custom'),
},
];
},
@@ -155,6 +146,10 @@ export default {
return value === KEY_CUSTOM && this.dailyLimit;
},
},
+ i18n: {
+ learnCronSyntax: s__('PipelineScheduleIntervalPattern|Set a custom interval with Cron syntax.'),
+ cronSyntaxLink: s__('PipelineScheduleIntervalPattern|What is Cron syntax?'),
+ },
};
</script>
@@ -167,19 +162,14 @@ export default {
:value="option.value"
:data-testid="option.value"
>
- <gl-sprintf v-if="option.link" :message="option.text">
- <template #link="{ content }">
- <gl-link :href="option.link" target="_blank" class="gl-font-sm">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
-
- <template v-else>{{ option.text }}</template>
+ {{ option.text }}
<gl-icon
v-if="showDailyLimitMessage(option)"
v-gl-tooltip.hover
name="question-o"
:title="scheduleDailyLimitMsg"
+ data-testid="daily-limit"
/>
</gl-form-radio>
</gl-form-radio-group>
@@ -193,5 +183,11 @@ export default {
required="true"
@input="onCustomInput"
/>
+ <p class="gl-mt-1 gl-mb-0 gl-text-secondary">
+ {{ $options.i18n.learnCronSyntax }}
+ <gl-link :href="cronSyntaxUrl" target="_blank">
+ {{ $options.i18n.cronSyntaxLink }}
+ </gl-link>
+ </p>
</div>
</template>
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index bee0731d711..98c58515d24 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -7,6 +7,7 @@ import initTerraformNotification from '~/projects/terraform_notification';
import { initUploadFileTrigger } from '~/projects/upload_file';
import initReadMore from '~/read_more';
import initForksButton from '~/forks/init_forks_button';
+import initAmbiguousRefModal from '~/ref/init_ambiguous_ref_modal';
// Project show page loads different overview content based on user preferences
if (document.getElementById('js-tree-list')) {
@@ -45,6 +46,7 @@ initTerraformNotification();
initReadMore();
initStarButton();
+initAmbiguousRefModal();
if (document.querySelector('.js-autodevops-banner')) {
import(/* webpackChunkName: 'userCallOut' */ '~/user_callout')
diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js
index 17c17014ece..d87f8898c63 100644
--- a/app/assets/javascripts/pages/projects/tree/show/index.js
+++ b/app/assets/javascripts/pages/projects/tree/show/index.js
@@ -2,7 +2,9 @@ import $ from 'jquery';
import initTree from 'ee_else_ce/repository';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import NewCommitForm from '~/new_commit_form';
+import initAmbiguousRefModal from '~/ref/init_ambiguous_ref_modal';
new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new
initTree();
+initAmbiguousRefModal();
new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/work_items/index.js b/app/assets/javascripts/pages/projects/work_items/index.js
index 11c257611f0..b44ca708b28 100644
--- a/app/assets/javascripts/pages/projects/work_items/index.js
+++ b/app/assets/javascripts/pages/projects/work_items/index.js
@@ -1,3 +1,3 @@
-import { initWorkItemsRoot } from '~/work_items/index';
+import { initWorkItemsRoot } from '~/work_items';
initWorkItemsRoot();
diff --git a/app/assets/javascripts/pages/registrations/new/index.js b/app/assets/javascripts/pages/registrations/new/index.js
index 84050c3cb0f..90a9c9e7279 100644
--- a/app/assets/javascripts/pages/registrations/new/index.js
+++ b/app/assets/javascripts/pages/registrations/new/index.js
@@ -1,5 +1,3 @@
-import { trackNewRegistrations } from '~/google_tag_manager';
-
import NoEmojiValidator from '~/emoji/no_emoji_validator';
import LengthValidator from '~/validators/length_validator';
import UsernameValidator from '~/pages/sessions/new/username_validator';
@@ -13,8 +11,6 @@ new LengthValidator(); // eslint-disable-line no-new
new NoEmojiValidator(); // eslint-disable-line no-new
new EmailFormatValidator(); // eslint-disable-line no-new
-trackNewRegistrations();
-
Tracking.enableFormTracking({
forms: { allow: ['new_user'] },
});
diff --git a/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue
index 3792dad376b..3c070d2708d 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue
@@ -77,7 +77,7 @@ export default {
v-gl-modal="$options.modal.modalId"
category="secondary"
variant="danger"
- data-qa-selector="delete_button"
+ data-qa-selector="delete-button"
>
{{ $options.i18n.deletePageText }}
</gl-button>
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
index 553cb1f0464..eaa99556994 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -317,7 +317,7 @@ export default {
name="wiki[title]"
type="text"
class="form-control"
- data-qa-selector="wiki_title_textbox"
+ data-testid="wiki-title-textbox"
:required="true"
:autofocus="!pageInfo.persisted"
:placeholder="$options.i18n.title.placeholder"
@@ -397,7 +397,7 @@ export default {
name="wiki[message]"
type="text"
class="form-control"
- data-qa-selector="wiki_message_textbox"
+ data-testid="wiki-message-textbox"
:placeholder="$options.i18n.commitMessage.label"
/>
</gl-form-group>
@@ -409,7 +409,6 @@ export default {
category="primary"
variant="confirm"
type="submit"
- data-qa-selector="wiki_submit_button"
data-testid="wiki-submit-button"
:disabled="disableSubmitButton"
>{{ submitButtonText }}</gl-button
diff --git a/app/assets/javascripts/pages/users/terms/index/index.js b/app/assets/javascripts/pages/users/terms/index/index.js
index 29ddde6da94..3619bcff65c 100644
--- a/app/assets/javascripts/pages/users/terms/index/index.js
+++ b/app/assets/javascripts/pages/users/terms/index/index.js
@@ -1,4 +1,3 @@
import { initTermsApp } from '~/terms';
-import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
-waitForCSSLoaded(initTermsApp);
+initTermsApp();
diff --git a/app/assets/javascripts/performance_bar/components/add_request.vue b/app/assets/javascripts/performance_bar/components/add_request.vue
index 6702c49030b..9a8ebedaf15 100644
--- a/app/assets/javascripts/performance_bar/components/add_request.vue
+++ b/app/assets/javascripts/performance_bar/components/add_request.vue
@@ -1,5 +1,5 @@
<script>
-import { GlForm, GlFormInput, GlButton } from '@gitlab/ui';
+import { GlForm, GlFormInput, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
@@ -12,6 +12,9 @@ export default {
GlButton,
GlFormInput,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
data() {
return {
inputEnabled: false,
@@ -37,7 +40,8 @@ export default {
<div id="peek-view-add-request" class="view gl-display-flex">
<gl-form class="gl-display-flex gl-align-items-center" @submit.prevent>
<gl-button
- class="gl-text-blue-300! gl-mr-2"
+ v-gl-tooltip.viewport
+ class="gl-mr-2"
category="tertiary"
variant="link"
icon="plus"
@@ -52,7 +56,7 @@ export default {
type="text"
:placeholder="$options.i18n.inputLabel"
:aria-label="$options.i18n.inputLabel"
- class="gl-ml-2"
+ class="gl-ml-2 gl-px-3! gl-py-2!"
@keyup.enter="addRequest"
@keyup.esc="clearForm"
/>
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index b53e2709f83..ab10283b3c4 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -1,5 +1,11 @@
<script>
-import { GlButton, GlModal, GlModalDirective, GlCollapsibleListbox } from '@gitlab/ui';
+import {
+ GlButton,
+ GlTooltipDirective,
+ GlModal,
+ GlModalDirective,
+ GlCollapsibleListbox,
+} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { sortOrders, sortOrderOptions } from '../constants';
@@ -13,6 +19,7 @@ export default {
GlCollapsibleListbox,
},
directives: {
+ GlTooltip: GlTooltipDirective,
'gl-modal': GlModalDirective,
},
props: {
@@ -133,14 +140,17 @@ export default {
<div
v-if="currentRequest.details && metricDetails"
:id="`peek-view-${metric}`"
- class="gl-display-flex gl-align-items-center view"
+ class="gl-display-flex gl-align-items-baseline view"
data-qa-selector="detailed_metric_content"
>
- <gl-button v-gl-modal="modalId" class="gl-mr-2" type="button" variant="link">
- <span
- class="gl-text-blue-200 gl-font-weight-bold"
- data-testid="performance-bar-details-label"
- >
+ <gl-button
+ v-gl-tooltip.viewport
+ v-gl-modal="modalId"
+ class="gl-mr-2"
+ :title="header"
+ variant="link"
+ >
+ <span class="gl-font-sm gl-font-weight-semibold" data-testid="performance-bar-details-label">
{{ metricDetailsLabel }}
</span>
</gl-button>
@@ -150,7 +160,7 @@ export default {
<div v-for="(value, name) in metricDetailsSummary" :key="name" class="gl-pr-8">
<div v-if="value" data-testid="performance-bar-summary-item">
<div>{{ name }}</div>
- <div class="gl-font-size-h1 gl-font-weight-bold">{{ value }}</div>
+ <div class="gl-font-size-h1 gl-font-weight-semibold">{{ value }}</div>
</div>
</div>
</div>
@@ -178,7 +188,7 @@ export default {
v-for="(key, keyIndex) in keys"
:key="key"
class="text-break-word"
- :class="{ 'mb-3 bold': keyIndex == 0 }"
+ :class="{ 'mb-3 gl-font-weight-semibold': keyIndex == 0 }"
>
{{ item[key] }}
<gl-button
@@ -214,7 +224,7 @@ export default {
<div></div>
</template>
</gl-modal>
- {{ actualTitle }}
+ <span class="gl-opacity-7">{{ actualTitle }}</span>
<request-warning :html-id="htmlId" :warnings="warnings" />
</div>
</template>
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index 128c744f282..720c1e0d7f2 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -1,7 +1,5 @@
<script>
-import { GlLink, GlPopover } from '@gitlab/ui';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import { glEmojiTag } from '~/emoji';
+import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
@@ -11,14 +9,13 @@ import RequestSelector from './request_selector.vue';
export default {
components: {
- GlPopover,
AddRequest,
DetailedMetric,
GlLink,
RequestSelector,
},
directives: {
- SafeHtml,
+ GlTooltip: GlTooltipDirective,
},
props: {
store: {
@@ -123,11 +120,8 @@ export default {
hasHost() {
return this.currentRequest && this.currentRequest.details && this.currentRequest.details.host;
},
- birdEmoji() {
- if (this.hasHost && this.currentRequest.details.host.canary) {
- return glEmojiTag('baby_chick');
- }
- return '';
+ isCanary() {
+ return Boolean(this.currentRequest.details.host.canary);
},
downloadPath() {
const data = JSON.stringify(this.requests);
@@ -165,7 +159,6 @@ export default {
this.currentRequest = this.requestId;
},
methods: {
- glEmojiTag,
changeCurrentRequest(newRequestId) {
this.currentRequest = newRequestId;
this.$emit('change-request', newRequestId);
@@ -180,96 +173,117 @@ export default {
return this.store.findRequest(requestId)?.method?.toUpperCase() === 'GET';
},
},
- safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
};
</script>
<template>
<div id="js-peek" :class="env">
<div
v-if="currentRequest"
- class="d-flex container-fluid container-limited justify-content-center gl-align-items-center"
+ class="gl-display-flex container-fluid gl-overflow-x-auto"
data-qa-selector="performance_bar"
>
- <div id="peek-view-host" class="view">
- <span
- v-if="hasHost"
- class="current-host"
- :class="{ canary: currentRequest.details.host.canary }"
+ <div class="gl-display-flex gl-flex-shrink-0 view-performance-container">
+ <div v-if="hasHost" id="peek-view-host" class="gl-display-flex gl-gap-2 view">
+ <span class="current-host" :class="{ canary: isCanary }">
+ <gl-emoji
+ v-if="isCanary"
+ id="canary-emoji"
+ v-gl-tooltip.viewport="'Canary'"
+ data-name="baby_chick"
+ />
+ <gl-emoji
+ id="host-emoji"
+ v-gl-tooltip.viewport="currentRequest.details.host.hostname"
+ data-name="computer"
+ />
+ </span>
+ </div>
+ <detailed-metric
+ v-for="metric in $options.detailedMetrics"
+ :key="metric.metric"
+ :current-request="currentRequest"
+ :metric="metric.metric"
+ :title="metric.title"
+ :header="metric.header"
+ :keys="metric.keys"
+ />
+ <div
+ v-if="currentRequest.details && currentRequest.details.tracing"
+ id="peek-view-trace"
+ class="view"
>
- <span id="canary-emoji" v-safe-html:[$options.safeHtmlConfig]="birdEmoji"></span>
- <gl-popover placement="bottom" target="canary-emoji" content="Canary" />
- <span
- id="host-emoji"
- v-safe-html:[$options.safeHtmlConfig]="glEmojiTag('computer')"
- ></span>
- <gl-popover
- placement="bottom"
- target="host-emoji"
- :content="currentRequest.details.host.hostname"
- />
- </span>
+ <gl-link
+ class="gl-text-decoration-underline"
+ :href="currentRequest.details.tracing.tracing_url"
+ >{{ s__('PerformanceBar|Trace') }}</gl-link
+ >
+ </div>
+ <div v-if="showFlamegraphButtons" id="peek-flamegraph" class="view">
+ <gl-link
+ v-gl-tooltip.viewport
+ class="gl-font-sm"
+ :href="flamegraphPath('wall', currentRequestId)"
+ :title="s__('PerformanceBar|Wall flamegraph')"
+ >{{ s__('PerformanceBar|Wall') }}</gl-link
+ >
+ /
+ <gl-link
+ v-gl-tooltip.viewport
+ class="gl-font-sm"
+ :href="flamegraphPath('cpu', currentRequestId)"
+ :title="s__('PerformanceBar|CPU flamegraph')"
+ >{{ s__('PerformanceBar|CPU') }}</gl-link
+ >
+ /
+ <gl-link
+ v-gl-tooltip.viewport
+ class="gl-font-sm"
+ :href="flamegraphPath('object', currentRequestId)"
+ :title="s__('PerformanceBar|Object flamegraph')"
+ >{{ s__('PerformanceBar|Object') }}</gl-link
+ >
+ <span class="gl-opacity-7">{{ s__('PerformanceBar|flamegraph') }}</span>
+ </div>
</div>
- <detailed-metric
- v-for="metric in $options.detailedMetrics"
- :key="metric.metric"
- :current-request="currentRequest"
- :metric="metric.metric"
- :title="metric.title"
- :header="metric.header"
- :keys="metric.keys"
- />
- <div
- v-if="currentRequest.details && currentRequest.details.tracing"
- id="peek-view-trace"
- class="view"
- >
- <gl-link class="gl-text-blue-200" :href="currentRequest.details.tracing.tracing_url">{{
- s__('PerformanceBar|Trace')
- }}</gl-link>
+ <div class="gl-display-flex gl-flex-shrink-0 gl-ml-auto">
+ <div class="gl-display-flex view-reports-container">
+ <gl-link
+ v-if="currentRequest.details"
+ id="peek-download"
+ v-gl-tooltip.viewport
+ class="view gl-font-sm"
+ is-unsafe-link
+ :download="downloadName"
+ :href="downloadPath"
+ :title="s__('PerformanceBar|Download report')"
+ >{{ s__('PerformanceBar|Download') }}</gl-link
+ >
+ <gl-link
+ v-if="showMemoryReportButton"
+ id="peek-memory-report"
+ v-gl-tooltip.viewport
+ class="view gl-font-sm"
+ :href="memoryReportPath"
+ :title="s__('PerformanceBar|Download memory report')"
+ >{{ s__('PerformanceBar|Memory report') }}</gl-link
+ >
+ <gl-link
+ v-if="statsUrl"
+ v-gl-tooltip.viewport
+ class="view gl-font-sm"
+ :href="statsUrl"
+ :title="s__('PerformanceBar|Show stats')"
+ >{{ s__('PerformanceBar|Stats') }}</gl-link
+ >
+ </div>
+ <request-selector
+ v-if="currentRequest"
+ :current-request="currentRequest"
+ :requests="requests"
+ @change-current-request="changeCurrentRequest"
+ />
+ <add-request v-on="$listeners" />
</div>
- <div v-if="currentRequest.details" id="peek-download" class="view">
- <gl-link
- class="gl-text-blue-200"
- is-unsafe-link
- :download="downloadName"
- :href="downloadPath"
- >{{ s__('PerformanceBar|Download') }}</gl-link
- >
- </div>
- <div v-if="showMemoryReportButton" id="peek-memory-report" class="view">
- <gl-link class="gl-text-blue-200" :href="memoryReportPath">{{
- s__('PerformanceBar|Memory report')
- }}</gl-link>
- </div>
- <div v-if="showFlamegraphButtons" id="peek-flamegraph" class="view">
- <span id="flamegraph-emoji" class="gl-text-white-200">
- <span v-safe-html:[$options.safeHtmlConfig]="glEmojiTag('fire')"></span>
- <span v-safe-html:[$options.safeHtmlConfig]="glEmojiTag('bar_chart')"></span>
- </span>
- <gl-popover placement="bottom" target="flamegraph-emoji" content="Flamegraph" />
- <gl-link class="gl-text-blue-200" :href="flamegraphPath('wall', currentRequestId)">{{
- s__('PerformanceBar|wall')
- }}</gl-link>
- /
- <gl-link class="gl-text-blue-200" :href="flamegraphPath('cpu', currentRequestId)">{{
- s__('PerformanceBar|cpu')
- }}</gl-link>
- /
- <gl-link class="gl-text-blue-200" :href="flamegraphPath('object', currentRequestId)">{{
- s__('PerformanceBar|object')
- }}</gl-link>
- </div>
- <gl-link v-if="statsUrl" class="gl-text-blue-200 view" :href="statsUrl">{{
- s__('PerformanceBar|Stats')
- }}</gl-link>
- <request-selector
- v-if="currentRequest"
- :current-request="currentRequest"
- :requests="requests"
- class="gl-ml-auto"
- @change-current-request="changeCurrentRequest"
- />
- <add-request v-on="$listeners" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue
index f2177e102ec..2914b9762ac 100644
--- a/app/assets/javascripts/performance_bar/components/request_selector.vue
+++ b/app/assets/javascripts/performance_bar/components/request_selector.vue
@@ -1,5 +1,10 @@
<script>
+import { GlFormSelect } from '@gitlab/ui';
+
export default {
+ components: {
+ GlFormSelect,
+ },
props: {
currentRequest: {
type: Object,
@@ -23,8 +28,8 @@ export default {
};
</script>
<template>
- <div id="peek-request-selector" data-qa-selector="request_dropdown" class="view">
- <select v-model="currentRequestId">
+ <div id="peek-request-selector" data-qa-selector="request_dropdown" class="view gl-mr-5">
+ <gl-form-select v-model="currentRequestId" class="gl-px-3! gl-py-2!">
<option
v-for="request in requests"
:key="request.id"
@@ -33,6 +38,6 @@ export default {
>
{{ request.displayName }}
</option>
- </select>
+ </gl-form-select>
</div>
</template>
diff --git a/app/assets/javascripts/performance_bar/components/request_warning.vue b/app/assets/javascripts/performance_bar/components/request_warning.vue
index 91e905d62e6..96c11ea9e4e 100644
--- a/app/assets/javascripts/performance_bar/components/request_warning.vue
+++ b/app/assets/javascripts/performance_bar/components/request_warning.vue
@@ -1,14 +1,9 @@
<script>
-import { GlPopover } from '@gitlab/ui';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import { glEmojiTag } from '~/emoji';
+import { GlTooltipDirective } from '@gitlab/ui';
export default {
- components: {
- GlPopover,
- },
directives: {
- SafeHtml,
+ GlTooltip: GlTooltipDirective,
},
props: {
htmlId: {
@@ -32,15 +27,17 @@ export default {
return this.warnings.join('\n');
},
},
- methods: {
- glEmojiTag,
- },
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
};
</script>
<template>
<span v-if="hasWarnings" class="gl-cursor-default">
- <span :id="htmlId" v-safe-html:[$options.safeHtmlConfig]="glEmojiTag('warning')"></span>
- <gl-popover placement="bottom" :target="htmlId" :content="warningMessage" />
+ <gl-emoji
+ v-if="hasWarnings"
+ :id="htmlId"
+ v-gl-tooltip.viewport="warningMessage"
+ data-name="warning"
+ class="gl-ml-2"
+ />
</span>
</template>
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
index ccecc914cf1..0feaf8db82b 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
@@ -1,6 +1,6 @@
<script>
-import { GlLoadingIcon, GlLink } from '@gitlab/ui';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { createAlert } from '~/alert';
import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
import getLatestPipelineStatusQuery from '../graphql/queries/get_latest_pipeline_status.query.graphql';
@@ -9,9 +9,8 @@ import { COMMIT_BOX_POLL_INTERVAL, PIPELINE_STATUS_FETCH_ERROR } from '../consta
export default {
PIPELINE_STATUS_FETCH_ERROR,
components: {
- CiIcon,
+ CiBadgeLink,
GlLoadingIcon,
- GlLink,
},
inject: {
fullPath: {
@@ -64,8 +63,12 @@ export default {
<template>
<div class="gl-display-inline-block gl-vertical-align-middle gl-mr-2">
<gl-loading-icon v-if="loading" />
- <gl-link v-else :href="pipelineStatus.detailsPath">
- <ci-icon :status="pipelineStatus" :size="24" />
- </gl-link>
+ <ci-badge-link
+ v-else
+ :status="pipelineStatus"
+ :details-path="pipelineStatus.detailsPath"
+ size="md"
+ :show-text="false"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/projects/components/shared/delete_button.vue b/app/assets/javascripts/projects/components/shared/delete_button.vue
index c749034d2a8..b290b2b085f 100644
--- a/app/assets/javascripts/projects/components/shared/delete_button.vue
+++ b/app/assets/javascripts/projects/components/shared/delete_button.vue
@@ -87,7 +87,7 @@ export default {
<gl-button
category="primary"
variant="danger"
- data-qa-selector="delete_button"
+ data-testid="delete-button"
@click="onButtonClick"
>{{ $options.i18n.deleteProject }}</gl-button
>
diff --git a/app/assets/javascripts/projects/project_find_file.js b/app/assets/javascripts/projects/project_find_file.js
index a8b884a68a0..711a8278e07 100644
--- a/app/assets/javascripts/projects/project_find_file.js
+++ b/app/assets/javascripts/projects/project_find_file.js
@@ -50,8 +50,6 @@ export default class ProjectFindFile {
this.initEvent();
// focus text input box
this.inputElement.focus();
- // load file list
- this.load(this.options.url);
}
initEvent() {
@@ -110,7 +108,14 @@ export default class ProjectFindFile {
if (searchText) {
matches = fuzzaldrinPlus.match(filePath, searchText);
}
- const blobItemUrl = joinPaths(this.options.blobUrlTemplate, escapeFileUrl(filePath));
+
+ let blobItemUrl = joinPaths(this.options.blobUrlTemplate, escapeFileUrl(filePath));
+
+ if (this.options.refType) {
+ const blobUrlObject = new URL(blobItemUrl, window.location.origin);
+ blobUrlObject.searchParams.append('ref_type', this.options.refType);
+ blobItemUrl = blobUrlObject.toString();
+ }
const html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl);
results.push(this.element.find('.tree-table > tbody').append(html));
}
diff --git a/app/assets/javascripts/projects/settings/api/access_dropdown_api.js b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
index b886bf43b57..df99aac6b9e 100644
--- a/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
+++ b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
@@ -1,9 +1,7 @@
-import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
-import { ACCESS_LEVEL_DEVELOPER_INTEGER } from '~/access_level/constants';
-const GROUPS_PATH = '/-/autocomplete/project_groups.json';
const USERS_PATH = '/-/autocomplete/users.json';
+const GROUPS_PATH = '/-/autocomplete/project_groups.json';
const DEPLOY_KEYS_PATH = '/-/autocomplete/deploy_keys_with_owners.json';
const buildUrl = (urlRoot, url) => {
@@ -28,14 +26,10 @@ export const getUsers = (query, states) => {
};
export const getGroups = () => {
- if (gon.current_project_id) {
- return Api.projectGroups(gon.current_project_id, {
- with_shared: true,
- shared_min_access_level: ACCESS_LEVEL_DEVELOPER_INTEGER,
- });
- }
- return axios.get(buildUrl(gon.relative_url_root || '', GROUPS_PATH)).then(({ data }) => {
- return data;
+ return axios.get(buildUrl(gon.relative_url_root || '', GROUPS_PATH), {
+ params: {
+ project_id: gon.current_project_id,
+ },
});
};
diff --git a/app/assets/javascripts/projects/settings/components/access_dropdown.vue b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
index ca24e948f69..2dd7633e2c8 100644
--- a/app/assets/javascripts/projects/settings/components/access_dropdown.vue
+++ b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
@@ -229,10 +229,10 @@ export default {
Promise.all([
getDeployKeys(this.query),
getUsers(this.query),
- this.groups.length ? Promise.resolve(this.groups) : getGroups(),
+ this.groups.length ? Promise.resolve({ data: this.groups }) : getGroups(),
])
.then(([deployKeysResponse, usersResponse, groupsResponse]) => {
- this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse);
+ this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data);
this.setSelected({ initial });
})
.catch(() =>
diff --git a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
index fd5fabd7c8a..a426d6d7bb8 100644
--- a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
+++ b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
@@ -51,7 +51,7 @@ export default {
:disabled="!hasSelectedNamespace"
:phrase="confirmationPhrase"
:button-text="confirmButtonText"
- button-qa-selector="transfer_project_button"
+ button-testid="transfer-project-button"
@confirm="$emit('confirm')"
/>
</div>
diff --git a/app/assets/javascripts/ref/components/ambiguous_ref_modal.vue b/app/assets/javascripts/ref/components/ambiguous_ref_modal.vue
new file mode 100644
index 00000000000..d17144669fe
--- /dev/null
+++ b/app/assets/javascripts/ref/components/ambiguous_ref_modal.vue
@@ -0,0 +1,80 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<script>
+import { GlModal, GlButton, GlSprintf } from '@gitlab/ui';
+import { sprintf, s__ } from '~/locale';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { REF_TYPE_PARAM_NAME, TAG_REF_TYPE, BRANCH_REF_TYPE } from '../constants';
+
+export default {
+ i18n: {
+ title: s__('AmbiguousRef|Which reference do you want to view?'),
+ description: sprintf(
+ s__('AmbiguousRef|There is a branch and a tag with the same name of %{ref}.'),
+ ),
+ secondaryDescription: s__('AmbiguousRef|Which reference would you like to view?'),
+ viewTagButton: s__('AmbiguousRef|View tag'),
+ viewBranchButton: s__('AmbiguousRef|View branch'),
+ },
+ tagRefType: TAG_REF_TYPE,
+ branchRefType: BRANCH_REF_TYPE,
+ components: {
+ GlModal,
+ GlButton,
+ GlSprintf,
+ },
+
+ props: {
+ refName: {
+ type: String,
+ required: true,
+ },
+ },
+ mounted() {
+ this.$refs.ambiguousRefModal.show();
+ },
+ methods: {
+ navigate(refType) {
+ const url = new URL(window.location.href);
+ url.searchParams.set(REF_TYPE_PARAM_NAME, refType);
+
+ visitUrl(url.toString());
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="ambiguousRefModal"
+ modal-id="ambiguous-ref"
+ :title="$options.i18n.title"
+ @primary="navigate"
+ >
+ <p class="gl-mb-0">
+ <gl-sprintf :message="$options.i18n.description">
+ <template #ref
+ ><code>{{ refName }}</code></template
+ >
+ </gl-sprintf>
+ </p>
+
+ <p>
+ {{ $options.i18n.secondaryDescription }}
+ </p>
+
+ <template #modal-footer>
+ <gl-button
+ category="secondary"
+ variant="confirm"
+ @click="() => navigate($options.tagRefType)"
+ >{{ $options.i18n.viewTagButton }}</gl-button
+ >
+ <gl-button
+ category="secondary"
+ variant="confirm"
+ @click="() => navigate($options.branchRefType)"
+ >{{ $options.i18n.viewBranchButton }}</gl-button
+ >
+ </template>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue
index e5f5800c99c..ed9fd521e67 100644
--- a/app/assets/javascripts/ref/components/ref_selector.vue
+++ b/app/assets/javascripts/ref/components/ref_selector.vue
@@ -11,6 +11,10 @@ import {
REF_TYPE_BRANCHES,
REF_TYPE_TAGS,
REF_TYPE_COMMITS,
+ TAG_REF_TYPE,
+ BRANCH_REF_TYPE,
+ TAG_REF_TYPE_ICON,
+ BRANCH_REF_TYPE_ICON,
} from '../constants';
import createStore from '../stores';
import { formatListBoxItems, formatErrors } from '../format_refs';
@@ -159,6 +163,17 @@ export default {
})
: this.i18n.noResults;
},
+ dropdownIcon() {
+ let icon;
+
+ if (this.selectedRef.includes(`refs/${TAG_REF_TYPE}`)) {
+ icon = TAG_REF_TYPE_ICON;
+ } else if (this.selectedRef.includes(`refs/${BRANCH_REF_TYPE}`)) {
+ icon = BRANCH_REF_TYPE_ICON;
+ }
+
+ return icon;
+ },
},
watch: {
// Keep the Vuex store synchronized if the parent
@@ -246,6 +261,7 @@ export default {
:search-placeholder="i18n.searchPlaceholder"
:toggle-class="extendedToggleButtonClass"
:toggle-text="buttonText"
+ :icon="dropdownIcon"
v-bind="$attrs"
v-on="$listeners"
@hidden="$emit('hide')"
diff --git a/app/assets/javascripts/ref/constants.js b/app/assets/javascripts/ref/constants.js
index 4b5b18cf6c1..5fd4660b8e3 100644
--- a/app/assets/javascripts/ref/constants.js
+++ b/app/assets/javascripts/ref/constants.js
@@ -7,6 +7,9 @@ export const REF_TYPE_COMMITS = 'REF_TYPE_COMMITS';
export const ALL_REF_TYPES = Object.freeze([REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS]);
export const BRANCH_REF_TYPE = 'heads';
export const TAG_REF_TYPE = 'tags';
+export const TAG_REF_TYPE_ICON = 'tag';
+export const BRANCH_REF_TYPE_ICON = 'branch';
+export const REF_TYPE_PARAM_NAME = 'ref_type';
export const X_TOTAL_HEADER = 'x-total';
diff --git a/app/assets/javascripts/ref/init_ambiguous_ref_modal.js b/app/assets/javascripts/ref/init_ambiguous_ref_modal.js
new file mode 100644
index 00000000000..00fb8f10401
--- /dev/null
+++ b/app/assets/javascripts/ref/init_ambiguous_ref_modal.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
+import AmbiguousRefModal from './components/ambiguous_ref_modal.vue';
+import { REF_TYPE_PARAM_NAME, TAG_REF_TYPE, BRANCH_REF_TYPE } from './constants';
+
+export default (el = document.querySelector('#js-ambiguous-ref-modal')) => {
+ const refType = getParameterByName(REF_TYPE_PARAM_NAME);
+ const isRefTypeSet = refType === TAG_REF_TYPE || refType === BRANCH_REF_TYPE; // if ref_type is already set in the URL, we don't want to display the modal
+ if (!el || isRefTypeSet || !parseBoolean(el.dataset.ambiguous)) return false;
+
+ const { ref } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(AmbiguousRefModal, { props: { refName: ref } });
+ },
+ });
+};
diff --git a/app/assets/javascripts/related_issues/components/add_issuable_form.vue b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
index d36b29f69a5..a0e876b4c19 100644
--- a/app/assets/javascripts/related_issues/components/add_issuable_form.vue
+++ b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
@@ -219,7 +219,7 @@ export default {
type="submit"
size="small"
class="gl-mr-2"
- data-testid="add_issue_button"
+ data-testid="add-issue-button"
>
{{ __('Add') }}
</gl-button>
diff --git a/app/assets/javascripts/related_issues/components/related_issuable_input.vue b/app/assets/javascripts/related_issues/components/related_issuable_input.vue
index f92c81a7eb2..4811dfef3d0 100644
--- a/app/assets/javascripts/related_issues/components/related_issuable_input.vue
+++ b/app/assets/javascripts/related_issues/components/related_issuable_input.vue
@@ -217,7 +217,7 @@ export default {
:aria-label="inputPlaceholder"
type="text"
class="gl-w-full gl-border-none gl-outline-0"
- data-testid="add_issue_field"
+ data-testid="add-issue-field"
autocomplete="off"
@input="onInput"
@focus="onFocus"
diff --git a/app/assets/javascripts/related_issues/components/related_issues_block.vue b/app/assets/javascripts/related_issues/components/related_issues_block.vue
index 1044d25c1a3..f1b6b335509 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_block.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_block.vue
@@ -278,20 +278,17 @@ export default {
@saveReorder="$emit('saveReorder', $event)"
/>
</template>
- <div v-if="!shouldShowTokenBody && !isFormVisible">
- <p class="gl-new-card-empty">
- {{ emptyStateMessage }}
- <gl-link
- v-if="hasHelpPath"
- :href="helpPath"
- target="_blank"
- data-testid="help-link"
- :aria-label="helpLinkText"
- >
- {{ __('Learn more.') }}
- </gl-link>
- </p>
- </div>
+ <p v-if="!shouldShowTokenBody && !isFormVisible" class="gl-new-card-empty">
+ {{ emptyStateMessage }}
+ <gl-link
+ v-if="hasHelpPath"
+ :href="helpPath"
+ data-testid="help-link"
+ :aria-label="helpLinkText"
+ >
+ {{ __('Learn more.') }}
+ </gl-link>
+ </p>
</div>
</gl-card>
</div>
diff --git a/app/assets/javascripts/related_issues/components/related_issues_list.vue b/app/assets/javascripts/related_issues/components/related_issues_list.vue
index 8d26917f749..0e47184e24e 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_list.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_list.vue
@@ -104,7 +104,7 @@ export default {
{{ heading }}
</h4>
<div class="related-issues-token-body" :class="{ 'sortable-container': canReorder }">
- <div v-if="isFetching" class="gl-mb-2" data-testid="related_issues_loading_placeholder">
+ <div v-if="isFetching" class="gl-mb-2" data-testid="related-issues-loading-placeholder">
<gl-loading-icon
ref="loadingIcon"
size="sm"
@@ -146,7 +146,7 @@ export default {
:locked-message="issue.lockedMessage"
:work-item-type="issue.type"
event-namespace="relatedIssue"
- data-testid="related_issuable_content"
+ data-testid="related-issuable-content"
class="gl-mx-n2"
@relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)"
/>
diff --git a/app/assets/javascripts/releases/components/releases_empty_state.vue b/app/assets/javascripts/releases/components/releases_empty_state.vue
index ae94bd6872e..2893a42c73b 100644
--- a/app/assets/javascripts/releases/components/releases_empty_state.vue
+++ b/app/assets/javascripts/releases/components/releases_empty_state.vue
@@ -21,10 +21,11 @@ export default {
</script>
<template>
<gl-empty-state
- class="gl-layout-w-limited"
+ class="gl-layout-w-limited gl-mx-auto"
:title="$options.i18n.emptyStateTitle"
:description="$options.i18n.emptyStateText"
:svg-path="illustrationPath"
+ :svg-height="null"
:primary-button-link="newReleasePath"
:primary-button-text="$options.i18n.newRelease"
:secondary-button-link="documentationPath"
diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue
index fe996a2a734..04f3d73235b 100644
--- a/app/assets/javascripts/releases/components/tag_field_new.vue
+++ b/app/assets/javascripts/releases/components/tag_field_new.vue
@@ -1,15 +1,18 @@
<script>
-import { GlDropdown, GlFormGroup, GlPopover } from '@gitlab/ui';
+import { GlButton, GlTruncate, GlIcon, GlFormGroup, GlPopover } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapActions, mapGetters } from 'vuex';
import { __, s__ } from '~/locale';
+import { ESC_KEY } from '~/lib/utils/keys';
import TagSearch from './tag_search.vue';
import TagCreate from './tag_create.vue';
export default {
components: {
- GlDropdown,
+ GlTruncate,
+ GlButton,
+ GlIcon,
GlFormGroup,
GlPopover,
TagSearch,
@@ -74,6 +77,13 @@ export default {
},
hidePopover() {
this.show = false;
+ // gl-button doesn't expose focus method, but we can find button element by id
+ document.getElementById(this.id)?.focus();
+ },
+ onPopoverKeyUp(e) {
+ if (e.code === ESC_KEY) {
+ this.hidePopover();
+ }
},
},
i18n: {
@@ -97,15 +107,12 @@ export default {
:invalid-feedback="tagFeedback"
optional
>
- <gl-dropdown
- :id="id"
- :variant="buttonVariant"
- :text="buttonText"
- :toggle-class="['gl-text-gray-900!']"
- category="secondary"
- class="gl-w-30"
- @show.prevent="showPopover"
- />
+ <gl-button :id="id" class="gl-w-30 gl-px-0!" @click="showPopover">
+ <span class="gl-w-28 gl-display-flex gl-justify-content-space-between">
+ <gl-truncate :text="buttonText" class="gl-max-w-26" />
+ <gl-icon class="gl-button-icon gl-new-dropdown-chevron" name="chevron-down" />
+ </span>
+ </gl-button>
<gl-popover
:show="show"
:target="id"
@@ -118,7 +125,7 @@ export default {
@close-button-clicked="hidePopover"
@hide.once="markInputAsDirty"
>
- <div class="gl-border-t-solid gl-border-t-1 gl-border-gray-200">
+ <div class="gl-border-t-solid gl-border-t-1 gl-border-gray-200" @keyup="onPopoverKeyUp">
<tag-create
v-if="isCreating"
v-model="newTagName"
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
index 2e3cf3bf9b8..8bdfb057adc 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
@@ -1,6 +1,8 @@
+import { omit } from 'lodash';
import { getTag } from '~/rest_api';
import { createAlert } from '~/alert';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import AccessorUtilities from '~/lib/utils/accessor';
import { s__ } from '~/locale';
import createReleaseMutation from '~/releases/graphql/mutations/create_release.mutation.graphql';
import deleteReleaseMutation from '~/releases/graphql/mutations/delete_release.mutation.graphql';
@@ -15,16 +17,26 @@ import * as types from './mutation_types';
class GraphQLError extends Error {}
-export const initializeRelease = ({ commit, dispatch, state }) => {
+const updateDraft = (action) => (store, ...args) => {
+ action(store, ...args);
+
+ if (!store.state.isExistingRelease) {
+ store.dispatch('saveDraftRelease');
+ store.dispatch('saveDraftCreateFrom');
+ }
+};
+
+export const initializeRelease = ({ dispatch, state }) => {
if (state.isExistingRelease) {
// When editing an existing release,
// fetch the release object from the API
return dispatch('fetchRelease');
}
- // When creating a new release, initialize the
- // store with an empty release object
- commit(types.INITIALIZE_EMPTY_RELEASE);
+ // When creating a new release, try to load the
+ // store with a draft release object, otherwise
+ // initialize an empty one
+ dispatch('loadDraftRelease');
return Promise.resolve();
};
@@ -51,50 +63,58 @@ export const fetchRelease = async ({ commit, state }) => {
}
};
-export const updateReleaseTagName = ({ commit }, tagName) =>
- commit(types.UPDATE_RELEASE_TAG_NAME, tagName);
+export const updateReleaseTagName = updateDraft(({ commit }, tagName) =>
+ commit(types.UPDATE_RELEASE_TAG_NAME, tagName),
+);
-export const updateReleaseTagMessage = ({ commit }, tagMessage) =>
- commit(types.UPDATE_RELEASE_TAG_MESSAGE, tagMessage);
+export const updateReleaseTagMessage = updateDraft(({ commit }, tagMessage) =>
+ commit(types.UPDATE_RELEASE_TAG_MESSAGE, tagMessage),
+);
-export const updateCreateFrom = ({ commit }, createFrom) =>
- commit(types.UPDATE_CREATE_FROM, createFrom);
+export const updateCreateFrom = updateDraft(({ commit }, createFrom) =>
+ commit(types.UPDATE_CREATE_FROM, createFrom),
+);
export const updateShowCreateFrom = ({ commit }, showCreateFrom) =>
commit(types.UPDATE_SHOW_CREATE_FROM, showCreateFrom);
-export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title);
+export const updateReleaseTitle = updateDraft(({ commit }, title) =>
+ commit(types.UPDATE_RELEASE_TITLE, title),
+);
-export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);
+export const updateReleaseNotes = updateDraft(({ commit }, notes) =>
+ commit(types.UPDATE_RELEASE_NOTES, notes),
+);
-export const updateReleaseMilestones = ({ commit }, milestones) =>
- commit(types.UPDATE_RELEASE_MILESTONES, milestones);
+export const updateReleaseMilestones = updateDraft(({ commit }, milestones) =>
+ commit(types.UPDATE_RELEASE_MILESTONES, milestones),
+);
-export const updateReleaseGroupMilestones = ({ commit }, groupMilestones) =>
- commit(types.UPDATE_RELEASE_GROUP_MILESTONES, groupMilestones);
+export const updateReleaseGroupMilestones = updateDraft(({ commit }, groupMilestones) =>
+ commit(types.UPDATE_RELEASE_GROUP_MILESTONES, groupMilestones),
+);
-export const addEmptyAssetLink = ({ commit }) => {
- commit(types.ADD_EMPTY_ASSET_LINK);
-};
+export const addEmptyAssetLink = updateDraft(({ commit }) => commit(types.ADD_EMPTY_ASSET_LINK));
-export const updateAssetLinkUrl = ({ commit }, { linkIdToUpdate, newUrl }) => {
- commit(types.UPDATE_ASSET_LINK_URL, { linkIdToUpdate, newUrl });
-};
+export const updateAssetLinkUrl = updateDraft(({ commit }, { linkIdToUpdate, newUrl }) =>
+ commit(types.UPDATE_ASSET_LINK_URL, { linkIdToUpdate, newUrl }),
+);
-export const updateAssetLinkName = ({ commit }, { linkIdToUpdate, newName }) => {
- commit(types.UPDATE_ASSET_LINK_NAME, { linkIdToUpdate, newName });
-};
+export const updateAssetLinkName = updateDraft(({ commit }, { linkIdToUpdate, newName }) =>
+ commit(types.UPDATE_ASSET_LINK_NAME, { linkIdToUpdate, newName }),
+);
-export const updateAssetLinkType = ({ commit }, { linkIdToUpdate, newType }) => {
- commit(types.UPDATE_ASSET_LINK_TYPE, { linkIdToUpdate, newType });
-};
+export const updateAssetLinkType = updateDraft(({ commit }, { linkIdToUpdate, newType }) =>
+ commit(types.UPDATE_ASSET_LINK_TYPE, { linkIdToUpdate, newType }),
+);
-export const removeAssetLink = ({ commit }, linkIdToRemove) => {
- commit(types.REMOVE_ASSET_LINK, linkIdToRemove);
-};
+export const removeAssetLink = updateDraft(({ commit }, linkIdToRemove) =>
+ commit(types.REMOVE_ASSET_LINK, linkIdToRemove),
+);
-export const receiveSaveReleaseSuccess = ({ commit }, urlToRedirectTo) => {
+export const receiveSaveReleaseSuccess = ({ commit, dispatch }, urlToRedirectTo) => {
commit(types.RECEIVE_SAVE_RELEASE_SUCCESS);
+ dispatch('clearDraftRelease');
redirectTo(urlToRedirectTo); // eslint-disable-line import/no-deprecated
};
@@ -245,9 +265,9 @@ export const updateIncludeTagNotes = ({ commit }, includeTagNotes) => {
commit(types.UPDATE_INCLUDE_TAG_NOTES, includeTagNotes);
};
-export const updateReleasedAt = ({ commit }, releasedAt) => {
- commit(types.UPDATE_RELEASED_AT, releasedAt);
-};
+export const updateReleasedAt = updateDraft(({ commit }, releasedAt) =>
+ commit(types.UPDATE_RELEASED_AT, releasedAt),
+);
export const deleteRelease = ({ commit, getters, dispatch, state }) => {
commit(types.REQUEST_SAVE_RELEASE);
@@ -274,3 +294,56 @@ export const setCreating = ({ commit }) => commit(types.SET_CREATING);
export const setExistingTag = ({ commit }) => commit(types.SET_EXISTING_TAG);
export const setNewTag = ({ commit }) => commit(types.SET_NEW_TAG);
+
+export const saveDraftRelease = ({ getters, state }) => {
+ try {
+ window.localStorage.setItem(
+ getters.localStorageKey,
+ JSON.stringify(getters.releasedAtChanged ? state.release : omit(state.release, 'releasedAt')),
+ );
+ } catch {
+ return Promise.resolve();
+ }
+ return Promise.resolve();
+};
+
+export const saveDraftCreateFrom = ({ getters, state }) => {
+ try {
+ window.localStorage.setItem(
+ getters.localStorageCreateFromKey,
+ JSON.stringify(state.createFrom),
+ );
+ } catch {
+ return Promise.resolve();
+ }
+ return Promise.resolve();
+};
+
+export const clearDraftRelease = ({ getters }) => {
+ if (AccessorUtilities.canUseLocalStorage()) {
+ window.localStorage.removeItem(getters.localStorageKey);
+ window.localStorage.removeItem(getters.localStorageCreateFromKey);
+ }
+};
+
+export const loadDraftRelease = ({ commit, getters, state }) => {
+ try {
+ const release = window.localStorage.getItem(getters.localStorageKey);
+ const createFrom = window.localStorage.getItem(getters.localStorageCreateFromKey);
+
+ if (release) {
+ const parsedRelease = JSON.parse(release);
+ commit(types.INITIALIZE_RELEASE, {
+ ...parsedRelease,
+ releasedAt: parsedRelease.releasedAt
+ ? new Date(parsedRelease.releasedAt)
+ : state.originalReleasedAt,
+ });
+ commit(types.UPDATE_CREATE_FROM, JSON.parse(createFrom));
+ } else {
+ commit(types.INITIALIZE_EMPTY_RELEASE);
+ }
+ } catch {
+ commit(types.INITIALIZE_EMPTY_RELEASE);
+ }
+};
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
index edf6c81c9e9..0b37c2b81d1 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
@@ -190,3 +190,7 @@ export const isCreating = ({ step }) => step === CREATE;
export const isExistingTag = ({ tagStep }) => tagStep === EXISTING_TAG;
export const isNewTag = ({ tagStep }) => tagStep === NEW_TAG;
+
+export const localStorageKey = ({ projectPath }) => `${projectPath}/release/new`;
+export const localStorageCreateFromKey = ({ projectPath }) =>
+ `${projectPath}/release/new/createFrom`;
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
index fc450970cde..8a0eeaa4338 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
@@ -1,4 +1,5 @@
export const INITIALIZE_EMPTY_RELEASE = 'INITIALIZE_EMPTY_RELEASE';
+export const INITIALIZE_RELEASE = 'INITIALIZE_RELEASE';
export const REQUEST_RELEASE = 'REQUEST_RELEASE';
export const RECEIVE_RELEASE_SUCCESS = 'RECEIVE_RELEASE_SUCCESS';
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
index 7ff18245a80..3a68cdbb89a 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
@@ -22,6 +22,9 @@ export default {
},
};
},
+ [types.INITIALIZE_RELEASE](state, release) {
+ state.release = release;
+ },
[types.REQUEST_RELEASE](state) {
state.isFetchingRelease = true;
diff --git a/app/assets/javascripts/repository/components/blob_controls.vue b/app/assets/javascripts/repository/components/blob_controls.vue
index 460db0fe2ae..4730c9575da 100644
--- a/app/assets/javascripts/repository/components/blob_controls.vue
+++ b/app/assets/javascripts/repository/components/blob_controls.vue
@@ -30,6 +30,7 @@ export default {
projectPath: this.projectPath,
filePath: this.filePath,
ref: this.ref,
+ refType: this.refType?.toUpperCase(),
};
},
skip() {
@@ -45,6 +46,11 @@ export default {
type: String,
required: true,
},
+ refType: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue
index b347f97a5ae..e3cd2d2e842 100644
--- a/app/assets/javascripts/repository/components/breadcrumbs.vue
+++ b/app/assets/javascripts/repository/components/breadcrumbs.vue
@@ -184,7 +184,7 @@ export default {
this.currentPath ? encodeURIComponent(this.currentPath) : '',
),
extraAttrs: {
- 'data-qa-selector': 'new_file_menu_item',
+ 'data-testid': 'new-file-menu-item',
},
},
{
@@ -284,7 +284,6 @@ export default {
:toggle-text="__('Add to tree')"
toggle-class="add-to-tree gl-ml-2"
data-testid="add-to-tree"
- data-qa-selector="add_to_tree_dropdown"
text-sr-only
icon="plus"
:items="dropdownItems"
diff --git a/app/assets/javascripts/repository/components/commit_info.vue b/app/assets/javascripts/repository/components/commit_info.vue
new file mode 100644
index 00000000000..b6e3cdbb7a3
--- /dev/null
+++ b/app/assets/javascripts/repository/components/commit_info.vue
@@ -0,0 +1,116 @@
+<script>
+import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import defaultAvatarUrl from 'images/no_avatar.png';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+import getRefMixin from '../mixins/get_ref';
+
+export default {
+ components: {
+ UserAvatarLink,
+ TimeagoTooltip,
+ GlButton,
+ GlLink,
+ UserAvatarImage,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ SafeHtml,
+ },
+ mixins: [getRefMixin],
+ props: {
+ commit: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return { showDescription: false };
+ },
+ computed: {
+ commitDescription() {
+ // Strip the newline at the beginning
+ return this.commit?.descriptionHtml?.replace(/^&#x000A;/, '');
+ },
+ },
+ methods: {
+ toggleShowDescription() {
+ this.showDescription = !this.showDescription;
+ },
+ },
+ defaultAvatarUrl,
+ safeHtmlConfig: {
+ ADD_TAGS: ['gl-emoji'],
+ },
+ i18n: {
+ toggleCommitDescription: __('Toggle commit description'),
+ authored: __('authored'),
+ },
+};
+</script>
+
+<template>
+ <div class="well-segment commit gl-min-h-8 gl-p-2 gl-w-full gl-display-flex">
+ <user-avatar-link
+ v-if="commit.author"
+ :link-href="commit.author.webPath"
+ :img-src="commit.author.avatarUrl"
+ :img-size="32"
+ class="gl-my-2 gl-mr-4"
+ />
+ <user-avatar-image
+ v-else
+ class="gl-my-2 gl-mr-4"
+ :img-src="commit.authorGravatar || $options.defaultAvatarUrl"
+ :size="32"
+ />
+ <div
+ class="commit-detail flex-list gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-grow-1 gl-min-w-0"
+ >
+ <div class="commit-content" data-qa-selector="commit_content">
+ <gl-link
+ v-safe-html:[$options.safeHtmlConfig]="commit.titleHtml"
+ :href="commit.webPath"
+ :class="{ 'gl-font-style-italic': !commit.message }"
+ class="commit-row-message item-title"
+ />
+ <gl-button
+ v-if="commit.descriptionHtml"
+ v-gl-tooltip
+ :class="{ open: showDescription }"
+ :title="$options.i18n.toggleCommitDescription"
+ :aria-label="$options.i18n.toggleCommitDescription"
+ :selected="showDescription"
+ class="text-expander gl-vertical-align-bottom!"
+ icon="ellipsis_h"
+ @click="toggleShowDescription"
+ />
+ <div class="committer">
+ <gl-link
+ v-if="commit.author"
+ :href="commit.author.webPath"
+ class="commit-author-link js-user-link"
+ >
+ {{ commit.author.name }}</gl-link
+ >
+ <template v-else>
+ {{ commit.authorName }}
+ </template>
+ {{ $options.i18n.authored }}
+ <timeago-tooltip :time="commit.authoredDate" tooltip-placement="bottom" />
+ </div>
+ <pre
+ v-if="commitDescription"
+ v-safe-html:[$options.safeHtmlConfig]="commitDescription"
+ :class="{ 'gl-display-block!': showDescription }"
+ class="commit-row-description gl-mb-3 gl-white-space-pre-line"
+ ></pre>
+ </div>
+ <div class="gl-flex-grow-1"></div>
+ <slot></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repository/components/fork_info.vue b/app/assets/javascripts/repository/components/fork_info.vue
index 42108e8dfba..c0adbc6f38c 100644
--- a/app/assets/javascripts/repository/components/fork_info.vue
+++ b/app/assets/javascripts/repository/components/fork_info.vue
@@ -291,7 +291,7 @@ export default {
>
<div v-if="sourceName">
{{ $options.i18n.forkedFrom }}
- <gl-link data-qa-selector="forked_from_link" :href="sourcePath">{{ sourceName }}</gl-link>
+ <gl-link data-testid="forked-from-link" :href="sourcePath">{{ sourceName }}</gl-link>
<gl-skeleton-loader v-if="isLoading" :lines="1" />
<div v-else class="gl-text-secondary" data-testid="divergence-message">
<gl-sprintf :message="forkDivergenceMessage">
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 12edeeb0d2f..05d4d9e1f81 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -1,32 +1,26 @@
<script>
-import { GlTooltipDirective, GlLink, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
-import defaultAvatarUrl from 'images/no_avatar.png';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import { sprintf, s__ } from '~/locale';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import SignatureBadge from '~/commit/components/signature_badge.vue';
import getRefMixin from '../mixins/get_ref';
import projectPathQuery from '../queries/project_path.query.graphql';
import eventHub from '../event_hub';
import { FORK_UPDATED_EVENT } from '../constants';
+import CommitInfo from './commit_info.vue';
export default {
components: {
- UserAvatarLink,
- TimeagoTooltip,
+ CommitInfo,
ClipboardButton,
- GlButton,
- GlButtonGroup,
- GlLink,
- GlLoadingIcon,
- UserAvatarImage,
SignatureBadge,
CiBadgeLink,
+ GlButtonGroup,
+ GlButton,
+ GlLoadingIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -80,13 +74,12 @@ export default {
return {
projectPath: '',
commit: null,
- showDescription: false,
};
},
computed: {
statusTitle() {
return sprintf(s__('PipelineStatusTooltip|Pipeline: %{ciStatus}'), {
- ciStatus: this.commit.pipeline.detailedStatus.text,
+ ciStatus: this.commit?.pipeline?.detailedStatus?.text,
});
},
isLoading() {
@@ -95,10 +88,6 @@ export default {
showCommitId() {
return this.commit?.sha?.substr(0, 8);
},
- commitDescription() {
- // Strip the newline at the beginning
- return this.commit?.descriptionHtml?.replace(/^&#x000A;/, '');
- },
},
watch: {
currentPath() {
@@ -112,112 +101,39 @@ export default {
eventHub.$off(FORK_UPDATED_EVENT, this.refetchLastCommit);
},
methods: {
- toggleShowDescription() {
- this.showDescription = !this.showDescription;
- },
refetchLastCommit() {
this.$apollo.queries.commit.refetch();
},
},
- defaultAvatarUrl,
- safeHtmlConfig: {
- ADD_TAGS: ['gl-emoji'],
- },
};
</script>
<template>
- <div class="well-segment commit gl-p-5 gl-w-full gl-display-flex">
- <gl-loading-icon v-if="isLoading" size="lg" color="dark" class="m-auto" />
- <template v-else-if="commit">
- <user-avatar-link
- v-if="commit.author"
- :link-href="commit.author.webPath"
- :img-src="commit.author.avatarUrl"
- :img-size="32"
- class="gl-my-2 gl-mr-4"
- />
- <user-avatar-image
- v-else
- class="gl-my-2 gl-mr-4"
- :img-src="commit.authorGravatar || $options.defaultAvatarUrl"
- :size="32"
- />
- <div
- class="commit-detail flex-list gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-grow-1 gl-min-w-0"
- >
- <div class="commit-content" data-qa-selector="commit_content">
- <gl-link
- v-safe-html:[$options.safeHtmlConfig]="commit.titleHtml"
- :href="commit.webPath"
- :class="{ 'font-italic': !commit.message }"
- class="commit-row-message item-title"
- />
- <gl-button
- v-if="commit.descriptionHtml"
- v-gl-tooltip
- :class="{ open: showDescription }"
- :title="__('Toggle commit description')"
- :aria-label="__('Toggle commit description')"
- :selected="showDescription"
- class="text-expander gl-vertical-align-bottom!"
- icon="ellipsis_h"
- @click="toggleShowDescription"
- />
- <div class="committer">
- <gl-link
- v-if="commit.author"
- :href="commit.author.webPath"
- class="commit-author-link js-user-link"
- >
- {{ commit.author.name }}</gl-link
- >
- <template v-else>
- {{ commit.authorName }}
- </template>
- {{ s__('LastCommit|authored') }}
- <timeago-tooltip :time="commit.authoredDate" tooltip-placement="bottom" />
- </div>
- <pre
- v-if="commitDescription"
- v-safe-html:[$options.safeHtmlConfig]="commitDescription"
- :class="{ 'd-block': showDescription }"
- class="commit-row-description gl-mb-3 gl-white-space-pre-line"
- ></pre>
- </div>
- <div class="gl-flex-grow-1"></div>
- <div
- class="commit-actions gl-display-flex gl-flex-align gl-align-items-center gl-flex-direction-row"
- >
- <signature-badge v-if="commit.signature" :signature="commit.signature" />
- <div v-if="commit.pipeline" class="ci-status-link">
- <ci-badge-link
- :status="commit.pipeline.detailedStatus"
- :details-path="commit.pipeline.detailedStatus.detailsPath"
- :aria-label="statusTitle"
- size="lg"
- :show-text="false"
- class="js-commit-pipeline"
- />
- </div>
- <gl-button-group class="gl-ml-4 js-commit-sha-group">
- <gl-button label class="gl-font-monospace" data-testid="last-commit-id-label">{{
- showCommitId
- }}</gl-button>
- <clipboard-button
- :text="commit.sha"
- :title="__('Copy commit SHA')"
- class="input-group-text"
- />
- </gl-button-group>
- </div>
+ <gl-loading-icon v-if="isLoading" size="lg" color="dark" class="m-auto" />
+ <commit-info v-else-if="commit" :commit="commit">
+ <div
+ class="commit-actions gl-display-flex gl-flex-align gl-align-items-center gl-flex-direction-row"
+ >
+ <signature-badge v-if="commit.signature" :signature="commit.signature" />
+ <div v-if="commit.pipeline" class="ci-status-link">
+ <ci-badge-link
+ :status="commit.pipeline.detailedStatus"
+ :details-path="commit.pipeline.detailedStatus.detailsPath"
+ :aria-label="statusTitle"
+ :show-text="false"
+ class="js-commit-pipeline"
+ />
</div>
- </template>
- </div>
+ <gl-button-group class="gl-ml-4 js-commit-sha-group">
+ <gl-button label class="gl-font-monospace" data-testid="last-commit-id-label">{{
+ showCommitId
+ }}</gl-button>
+ <clipboard-button
+ :text="commit.sha"
+ :title="__('Copy commit SHA')"
+ class="input-group-text"
+ />
+ </gl-button-group>
+ </div>
+ </commit-info>
</template>
-
-<style scoped>
-.commit {
- min-height: 4.75rem;
-}
-</style>
diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue
index bdcacd80b30..be446260f82 100644
--- a/app/assets/javascripts/repository/components/preview/index.vue
+++ b/app/assets/javascripts/repository/components/preview/index.vue
@@ -67,7 +67,7 @@ export default {
</gl-link>
</div>
</div>
- <div class="blob-viewer" data-qa-selector="blob_viewer_content" itemprop="about">
+ <div class="blob-viewer" data-testid="blob-viewer-content" itemprop="about">
<gl-loading-icon v-if="isLoading" size="lg" color="dark" class="my-4 mx-auto" />
<div
v-else-if="readme"
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index 557e9cd168f..3da7daa3eec 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -118,7 +118,7 @@ export default {
class="table tree-table"
:class="{ 'gl-table-layout-fixed': !showParentRow }"
aria-live="polite"
- data-qa-selector="file_tree_table"
+ data-testid="file-tree-table"
>
<table-header v-once />
<tbody>
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index a76d822317a..526757e6147 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -219,7 +219,7 @@ export default {
'is-submodule': isSubmodule,
}"
class="tree-item-link str-truncated"
- data-qa-selector="file_name_link"
+ data-testid="file-name-link"
>
<file-icon
:file-name="fullPath"
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 9753173ac30..afe3f7b1983 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -122,7 +122,7 @@ export default function setupVueRepositoryList() {
return h(LastCommit, {
props: {
currentPath: this.$route.params.path,
- refType: this.$route.query.ref_type,
+ refType: this.$route.meta.refType || this.$route.query.ref_type,
},
});
},
@@ -137,6 +137,7 @@ export default function setupVueRepositoryList() {
return h(BlobControls, {
props: {
projectPath,
+ refType: this.$route.meta.refType || this.$route.query.ref_type,
},
});
},
@@ -231,19 +232,21 @@ export default function setupVueRepositoryList() {
const treeHistoryLinkEl = document.getElementById('js-tree-history-link');
const { historyLink } = treeHistoryLinkEl.dataset;
-
// eslint-disable-next-line no-new
new Vue({
el: treeHistoryLinkEl,
router,
render(h) {
+ const url = new URL(window.location.href);
+ url.pathname = `${historyLink}/${
+ this.$route.params.path ? escapeFileUrl(this.$route.params.path) : ''
+ }`;
+ url.searchParams.set('ref_type', this.$route.meta.refType || this.$route.query.ref_type);
return h(
GlButton,
{
attrs: {
- href: `${historyLink}/${
- this.$route.params.path ? escapeFileUrl(this.$route.params.path) : ''
- }`,
+ href: url.href,
// Ideally passing this class to `props` should work
// But it doesn't work here. :(
class: 'btn btn-default btn-md gl-button',
@@ -256,7 +259,7 @@ export default function setupVueRepositoryList() {
initWebIdeLink({ el: document.getElementById('js-tree-web-ide-link'), router });
- const directoryDownloadLinks = document.getElementById('js-directory-downloads');
+ const directoryDownloadLinks = document.querySelector('.js-directory-downloads');
if (directoryDownloadLinks) {
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/repository/queries/blob_controls.query.graphql b/app/assets/javascripts/repository/queries/blob_controls.query.graphql
index fc1cf5f254b..0c284dcc8e6 100644
--- a/app/assets/javascripts/repository/queries/blob_controls.query.graphql
+++ b/app/assets/javascripts/repository/queries/blob_controls.query.graphql
@@ -1,8 +1,8 @@
-query getBlobControls($projectPath: ID!, $filePath: String!, $ref: String!) {
+query getBlobControls($projectPath: ID!, $filePath: String!, $ref: String!, $refType: RefType) {
project(fullPath: $projectPath) {
id
repository {
- blobs(paths: [$filePath], ref: $ref) {
+ blobs(paths: [$filePath], ref: $ref, refType: $refType) {
nodes {
id
findFilePath
diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js
index 5f73912ed2b..31bafab742d 100644
--- a/app/assets/javascripts/repository/router.js
+++ b/app/assets/javascripts/repository/router.js
@@ -63,6 +63,9 @@ export default function createRouter(base, baseRef) {
props: {
refType: 'HEADS',
},
+ meta: {
+ refType: 'HEADS',
+ },
},
],
});
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index 532a66affd8..2ff138cabe5 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -15,6 +15,7 @@ import {
SCOPE_PROJECTS,
SCOPE_NOTES,
SCOPE_COMMITS,
+ SCOPE_MILESTONES,
SEARCH_TYPE_ADVANCED,
} from '../constants';
import IssuesFilters from './issues_filters.vue';
@@ -23,6 +24,7 @@ import BlobsFilters from './blobs_filters.vue';
import ProjectsFilters from './projects_filters.vue';
import NotesFilters from './notes_filters.vue';
import CommitsFilters from './commits_filters.vue';
+import MilestonesFilters from './milestones_filters.vue';
export default {
name: 'GlobalSearchSidebar',
@@ -38,6 +40,7 @@ export default {
DomElementListener,
SmallScreenDrawerNavigation,
CommitsFilters,
+ MilestonesFilters,
},
mixins: [glFeatureFlagsMixin()],
computed: {
@@ -57,18 +60,20 @@ export default {
return this.currentScope === SCOPE_PROJECTS;
},
showNotesFilters() {
- return (
- this.currentScope === SCOPE_NOTES &&
- this.searchType === SEARCH_TYPE_ADVANCED &&
- this.glFeatures.searchNotesHideArchivedProjects
- );
+ // for now, the feature flag is placed here. Since we have only one filter in notes scope
+ return this.currentScope === SCOPE_NOTES && this.glFeatures.searchNotesHideArchivedProjects;
},
showCommitsFilters() {
// for now, the feature flag is placed here. Since we have only one filter in commits scope
return (
- this.currentScope === SCOPE_COMMITS &&
- this.searchType === SEARCH_TYPE_ADVANCED &&
- this.glFeatures.searchCommitsHideArchivedProjects
+ this.currentScope === SCOPE_COMMITS && this.glFeatures.searchCommitsHideArchivedProjects
+ );
+ },
+ showMilestonesFilters() {
+ // for now, the feature flag is placed here. Since we have only one filter in milestones scope
+ return (
+ this.currentScope === SCOPE_MILESTONES &&
+ this.glFeatures.searchMilestonesHideArchivedProjects
);
},
showScopeNavigation() {
@@ -97,6 +102,7 @@ export default {
<projects-filters v-if="showProjectsFilters" />
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
+ <milestones-filters v-if="showMilestonesFilters" />
</sidebar-portal>
</section>
@@ -112,6 +118,7 @@ export default {
<projects-filters v-if="showProjectsFilters" />
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
+ <milestones-filters v-if="showMilestonesFilters" />
</div>
<small-screen-drawer-navigation class="gl-lg-display-none">
<scope-legacy-navigation />
@@ -121,6 +128,7 @@ export default {
<projects-filters v-if="showProjectsFilters" />
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
+ <milestones-filters v-if="showMilestonesFilters" />
</small-screen-drawer-navigation>
</section>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
index 5cddf5e744f..ed90e2aaded 100644
--- a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
+++ b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
@@ -5,14 +5,7 @@ const checkboxLabel = s__('GlobalSearch|Include archived');
export const TRACKING_NAMESPACE = 'search:archived:select';
export const TRACKING_LABEL_CHECKBOX = 'checkbox';
-const scopes = {
- PROJECTS: 'projects',
- ISSUES: 'issues',
- MERGE_REQUESTS: 'merge_requests',
- NOTES: 'notes',
- BLOBS: 'blobs',
- COMMITS: 'commits',
-};
+const scopes = ['projects', 'issues', 'merge_requests', 'notes', 'blobs', 'commits', 'milestones'];
const filterParam = 'include_archived';
diff --git a/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue b/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue
index c31c46f2e6a..b0e84beabc4 100644
--- a/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue
@@ -1,7 +1,8 @@
<script>
-import { GlFormCheckboxGroup, GlFormCheckbox } from '@gitlab/ui';
+import { GlFormCheckboxGroup, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapActions } from 'vuex';
+import { s__ } from '~/locale';
import Tracking from '~/tracking';
import { parseBoolean } from '~/lib/utils/common_utils';
@@ -13,6 +14,12 @@ export default {
GlFormCheckboxGroup,
GlFormCheckbox,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ i18n: {
+ tooltip: s__('GlobalSearch|Include search results from archived projects'),
+ },
computed: {
...mapState(['urlQuery', 'useSidebarNavigation']),
selectedFilter: {
@@ -20,9 +27,9 @@ export default {
return [parseBoolean(this.urlQuery?.include_archived)];
},
set(value) {
- const newValue = value?.pop() ?? false;
- this.setQuery({ key: archivedFilterData.filterParam, value: newValue?.toString() });
- this.trackSelectCheckbox(newValue);
+ const includeArchived = [...value].pop() ?? false;
+ this.setQuery({ key: archivedFilterData.filterParam, value: includeArchived?.toString() });
+ this.trackSelectCheckbox(includeArchived);
},
},
},
@@ -49,7 +56,7 @@ export default {
:class="$options.LABEL_DEFAULT_CLASSES"
:value="true"
>
- <span data-testid="label">
+ <span v-gl-tooltip="$options.i18n.tooltip" data-testid="label">
{{ $options.archivedFilterData.checkboxLabel }}
</span>
</gl-form-checkbox>
diff --git a/app/assets/javascripts/search/sidebar/components/issues_filters.vue b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
index dbd52978163..4a2d3df6921 100644
--- a/app/assets/javascripts/search/sidebar/components/issues_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
@@ -42,9 +42,8 @@ export default {
},
showArchivedFilter() {
return (
- Object.values(archivedFilterData.scopes).includes(this.currentScope) &&
- this.glFeatures.searchIssuesHideArchivedProjects &&
- this.searchType === SEARCH_TYPE_ADVANCED
+ archivedFilterData.scopes.includes(this.currentScope) &&
+ this.glFeatures.searchIssuesHideArchivedProjects
);
},
showDivider() {
diff --git a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
index a6af789baad..ebd0406bcec 100644
--- a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
@@ -225,7 +225,7 @@ export default {
v-if="isFocused"
v-outside="closeDropdown"
data-testid="header-search-dropdown-menu"
- class="header-search-dropdown-menu gl-overflow-y-auto gl-absolute gl-w-full gl-bg-white gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-200 gl-shadow-x0-y2-b4-s0 gl-mt-3 gl-z-index-1"
+ class="header-search-dropdown-menu gl-overflow-y-auto gl-absolute gl-w-full gl-bg-white gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-200 gl-shadow-x0-y2-b4-s0 gl-mt-3 gl-z-index-2"
:class="{
'gl-max-w-none!': useSidebarNavigation,
'gl-min-w-full!': useSidebarNavigation,
diff --git a/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue b/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
index 2845eb2049b..6e476ef7935 100644
--- a/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
@@ -2,7 +2,7 @@
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapState } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { HR_DEFAULT_CLASSES, SEARCH_TYPE_ADVANCED } from '../constants';
+import { HR_DEFAULT_CLASSES } from '../constants';
import { statusFilterData } from './status_filter/data';
import StatusFilter from './status_filter/index.vue';
import FiltersTemplate from './filters_template.vue';
@@ -22,9 +22,8 @@ export default {
...mapState(['useSidebarNavigation', 'searchType']),
showArchivedFilter() {
return (
- Object.values(archivedFilterData.scopes).includes(this.currentScope) &&
- this.glFeatures.searchMergeRequestsHideArchivedProjects &&
- this.searchType === SEARCH_TYPE_ADVANCED
+ archivedFilterData.scopes.includes(this.currentScope) &&
+ this.glFeatures.searchMergeRequestsHideArchivedProjects
);
},
showStatusFilter() {
diff --git a/app/assets/javascripts/search/sidebar/components/milestones_filters.vue b/app/assets/javascripts/search/sidebar/components/milestones_filters.vue
new file mode 100644
index 00000000000..098e2980c3f
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/milestones_filters.vue
@@ -0,0 +1,18 @@
+<script>
+import ArchivedFilter from './archived_filter/index.vue';
+import FiltersTemplate from './filters_template.vue';
+
+export default {
+ name: 'MilestonesFilters',
+ components: {
+ ArchivedFilter,
+ FiltersTemplate,
+ },
+};
+</script>
+
+<template>
+ <filters-template>
+ <archived-filter class="gl-mb-5" />
+ </filters-template>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js
index 19df875c292..b5446ecbb42 100644
--- a/app/assets/javascripts/search/sidebar/constants/index.js
+++ b/app/assets/javascripts/search/sidebar/constants/index.js
@@ -4,6 +4,7 @@ export const SCOPE_BLOB = 'blobs';
export const SCOPE_PROJECTS = 'projects';
export const SCOPE_NOTES = 'notes';
export const SCOPE_COMMITS = 'commits';
+export const SCOPE_MILESTONES = 'milestones';
export const LABEL_DEFAULT_CLASSES = [
'gl-display-flex',
'gl-flex-direction-row',
diff --git a/app/assets/javascripts/search/store/constants.js b/app/assets/javascripts/search/store/constants.js
index f3b4a09b45b..ad47cd975f8 100644
--- a/app/assets/javascripts/search/store/constants.js
+++ b/app/assets/javascripts/search/store/constants.js
@@ -35,3 +35,7 @@ export const ICON_MAP = {
wiki_blobs: 'book',
snippet_titles: 'snippet',
};
+
+export const ZOEKT_SEARCH_TYPE = 'zoekt';
+export const ADVANCED_SEARCH_TYPE = 'advanced';
+export const BASIC_SEARCH_TYPE = 'basic';
diff --git a/app/assets/javascripts/search/topbar/components/app.vue b/app/assets/javascripts/search/topbar/components/app.vue
index ee66bdb2632..49e66492519 100644
--- a/app/assets/javascripts/search/topbar/components/app.vue
+++ b/app/assets/javascripts/search/topbar/components/app.vue
@@ -5,7 +5,8 @@ import { mapState, mapActions } from 'vuex';
import { s__ } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils';
import MarkdownDrawer from '~/vue_shared/components/markdown_drawer/markdown_drawer.vue';
-import { SYNTAX_OPTIONS_DOCUMENT } from '../constants';
+import { ZOEKT_SEARCH_TYPE, ADVANCED_SEARCH_TYPE } from '~/search/store/constants';
+import { SYNTAX_OPTIONS_ADVANCED_DOCUMENT, SYNTAX_OPTIONS_ZOEKT_DOCUMENT } from '../constants';
import GroupFilter from './group_filter.vue';
import ProjectFilter from './project_filter.vue';
@@ -42,11 +43,6 @@ export default {
required: false,
default: () => ({}),
},
- elasticsearchEnabled: {
- type: Boolean,
- required: false,
- default: false,
- },
defaultBranchName: {
type: String,
required: false,
@@ -54,7 +50,7 @@ export default {
},
},
computed: {
- ...mapState(['query']),
+ ...mapState(['query', 'searchType']),
search: {
get() {
return this.query ? this.query.search : '';
@@ -67,7 +63,15 @@ export default {
return !parseBoolean(this.query.snippets);
},
showSyntaxOptions() {
- return this.elasticsearchEnabled && this.isDefaultBranch;
+ return (
+ (this.searchType === ZOEKT_SEARCH_TYPE || this.searchType === ADVANCED_SEARCH_TYPE) &&
+ this.isDefaultBranch
+ );
+ },
+ documentBasedOnSearchType() {
+ return this.searchType === ZOEKT_SEARCH_TYPE
+ ? SYNTAX_OPTIONS_ZOEKT_DOCUMENT
+ : SYNTAX_OPTIONS_ADVANCED_DOCUMENT;
},
isDefaultBranch() {
return !this.query.repository_ref || this.query.repository_ref === this.defaultBranchName;
@@ -82,7 +86,6 @@ export default {
this.$refs.markdownDrawer.toggleDrawer();
},
},
- SYNTAX_OPTIONS_DOCUMENT,
};
</script>
@@ -104,10 +107,7 @@ export default {
@click="onToggleDrawer"
>{{ $options.i18n.syntaxOptionsLabel }}
</gl-button>
- <markdown-drawer
- ref="markdownDrawer"
- :document-path="$options.SYNTAX_OPTIONS_DOCUMENT"
- />
+ <markdown-drawer ref="markdownDrawer" :document-path="documentBasedOnSearchType" />
</template>
</div>
<gl-search-box-by-click
diff --git a/app/assets/javascripts/search/topbar/constants.js b/app/assets/javascripts/search/topbar/constants.js
index 5b1c5819f2b..1ad40fbe3db 100644
--- a/app/assets/javascripts/search/topbar/constants.js
+++ b/app/assets/javascripts/search/topbar/constants.js
@@ -20,4 +20,5 @@ export const PROJECT_DATA = {
fullName: 'name_with_namespace',
};
-export const SYNTAX_OPTIONS_DOCUMENT = 'drawers/drawers/advanced_search_syntax.md';
+export const SYNTAX_OPTIONS_ADVANCED_DOCUMENT = 'drawers/drawers/advanced_search_syntax.md';
+export const SYNTAX_OPTIONS_ZOEKT_DOCUMENT = 'drawers/drawers/exact_code_search_syntax.md';
diff --git a/app/assets/javascripts/search/topbar/index.js b/app/assets/javascripts/search/topbar/index.js
index d6e16085c28..aad7445ebdc 100644
--- a/app/assets/javascripts/search/topbar/index.js
+++ b/app/assets/javascripts/search/topbar/index.js
@@ -11,18 +11,10 @@ export const initTopbar = (store) => {
return false;
}
- const {
- groupInitialJson,
- projectInitialJson,
- elasticsearchEnabled,
- defaultBranchName,
- } = el.dataset;
+ const { groupInitialJson, projectInitialJson, defaultBranchName } = el.dataset;
const groupInitialJsonParsed = JSON.parse(groupInitialJson);
const projectInitialJsonParsed = JSON.parse(projectInitialJson);
- const elasticsearchEnabledParsed = elasticsearchEnabled
- ? JSON.parse(elasticsearchEnabled)
- : false;
return new Vue({
el,
@@ -32,7 +24,6 @@ export const initTopbar = (store) => {
props: {
groupInitialJson: groupInitialJsonParsed,
projectInitialJson: projectInitialJsonParsed,
- elasticsearchEnabled: elasticsearchEnabledParsed,
defaultBranchName,
},
});
diff --git a/app/assets/javascripts/security_configuration/components/feature_card.vue b/app/assets/javascripts/security_configuration/components/feature_card.vue
index 7f0a049a6ad..395bdad5dcc 100644
--- a/app/assets/javascripts/security_configuration/components/feature_card.vue
+++ b/app/assets/javascripts/security_configuration/components/feature_card.vue
@@ -95,6 +95,9 @@ export default {
showSecondaryConfigurationHelpPath() {
return Boolean(this.available && this.feature.secondary?.configurationHelpPath);
},
+ hyphenatedFeature() {
+ return this.feature.type.replace(/_/g, '-');
+ },
},
methods: {
onError(message) {
@@ -167,7 +170,7 @@ export default {
:href="feature.configurationPath"
variant="confirm"
:category="configurationButton.category"
- :data-testid="`${feature.type}_enable_button`"
+ :data-testid="`${hyphenatedFeature}-enable-button`"
class="gl-mt-5"
>
{{ configurationButton.text }}
@@ -179,7 +182,7 @@ export default {
variant="confirm"
:category="manageViaMrButtonCategory"
class="gl-mt-5"
- :data-testid="`${feature.type}_mr_button`"
+ :data-testid="`${hyphenatedFeature}-mr-button`"
@error="onError"
/>
diff --git a/app/assets/javascripts/sentry/init_sentry.js b/app/assets/javascripts/sentry/init_sentry.js
index dbd12dc36ce..6f32c8c4165 100644
--- a/app/assets/javascripts/sentry/init_sentry.js
+++ b/app/assets/javascripts/sentry/init_sentry.js
@@ -4,11 +4,10 @@ import {
defaultStackParser,
makeFetchTransport,
defaultIntegrations,
+ BrowserTracing,
// exports
captureException,
- captureMessage,
- withScope,
SDK_VERSION,
} from 'sentrybrowser';
@@ -19,6 +18,8 @@ const initSentry = () => {
const hub = getCurrentHub();
+ const page = document?.body?.dataset?.page;
+
const client = new BrowserClient({
// Sentry.init(...) options
dsn: gon.sentry_dsn,
@@ -37,7 +38,19 @@ const initSentry = () => {
// https://github.com/getsentry/sentry-javascript/blob/7.66.0/MIGRATION.md#explicit-client-options
transport: makeFetchTransport,
stackParser: defaultStackParser,
- integrations: defaultIntegrations,
+ integrations: [
+ ...defaultIntegrations,
+ new BrowserTracing({
+ beforeNavigate(context) {
+ return {
+ ...context,
+ // `page` acts as transaction name for performance tracing.
+ // If missing, use default Sentry behavior: window.location.pathname
+ name: page || window?.location?.pathname,
+ };
+ },
+ }),
+ ],
});
hub.bindClient(client);
@@ -45,7 +58,7 @@ const initSentry = () => {
hub.setTags({
revision: gon.revision,
feature_category: gon.feature_category,
- page: document?.body?.dataset?.page,
+ page,
});
if (gon.current_user_id) {
@@ -68,8 +81,6 @@ const initSentry = () => {
// eslint-disable-next-line no-underscore-dangle
window._Sentry = {
captureException,
- captureMessage,
- withScope,
SDK_VERSION, // used to verify compatibility with the Sentry instance
};
};
diff --git a/app/assets/javascripts/sentry/sentry_browser_wrapper.js b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
index fbfd5d4f458..03cf53fabef 100644
--- a/app/assets/javascripts/sentry/sentry_browser_wrapper.js
+++ b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
@@ -13,19 +13,3 @@ export const captureException = (...args) => {
Sentry?.captureException(...args);
};
-
-/** @type {import('@sentry/core').captureMessage} */
-export const captureMessage = (...args) => {
- // eslint-disable-next-line no-underscore-dangle
- const Sentry = window._Sentry;
-
- Sentry?.captureMessage(...args);
-};
-
-/** @type {import('@sentry/core').withScope} */
-export const withScope = (...args) => {
- // eslint-disable-next-line no-underscore-dangle
- const Sentry = window._Sentry;
-
- Sentry?.withScope(...args);
-};
diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js
index da948cc85b6..1eee7a932a4 100644
--- a/app/assets/javascripts/settings_panels.js
+++ b/app/assets/javascripts/settings_panels.js
@@ -52,7 +52,7 @@ export function initTrackProductAnalyticsExpanded() {
const $analyticsSection = $('#js-product-analytics-settings');
$analyticsSection.on('click.toggleSection', '.js-settings-toggle', () => {
if (isExpanded($analyticsSection)) {
- InternalEvents.track_event('user_viewed_cluster_configuration');
+ InternalEvents.trackEvent('user_viewed_cluster_configuration');
}
});
}
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue
index d65c950b33a..81fc2267622 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue
@@ -50,7 +50,7 @@ export default {
:width="imgSize"
:class="`s${imgSize}`"
class="avatar avatar-inline m-0"
- data-qa-selector="avatar_image"
+ data-testid="avatar-image"
/>
<gl-icon v-if="hasMergeIcon" name="warning-solid" aria-hidden="true" class="merge-icon" />
</span>
diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
index ef7f12f273f..a4090800ae6 100644
--- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -92,7 +92,6 @@ export default {
<div
class="gl-ml-3 gl-line-height-normal gl-display-grid gl-align-items-center"
data-testid="username"
- data-qa-selector="username"
>
<user-name-with-status :name="user.name" :availability="userAvailability(user)" />
</div>
@@ -104,7 +103,6 @@ export default {
category="tertiary"
size="small"
data-testid="user-list-more-button"
- data-qa-selector="more_assignees_link"
@click="toggleShowLess"
>
<template v-if="showLess">
diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
index 7a1853b1b46..90c3fb0039d 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
@@ -1,7 +1,7 @@
<script>
import { GlSprintf, GlButton } from '@gitlab/ui';
import { createAlert } from '~/alert';
-import { TYPE_ISSUE, TYPE_TEST_CASE, IssuableTypeText } from '~/issues/constants';
+import { TYPE_ISSUE, TYPE_TEST_CASE, issuableTypeText } from '~/issues/constants';
import { __, sprintf } from '~/locale';
import { confidentialityQueries } from '../../queries/constants';
@@ -80,7 +80,7 @@ export default {
: __('at least the Reporter role');
},
issuableTypeText() {
- return IssuableTypeText[this.issuableType];
+ return issuableTypeText[this.issuableType];
},
commentText() {
return this.isTestCase ? '' : __(' and leave a comment on');
diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
index 295d37671cc..ecccb0abfd1 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
@@ -135,6 +135,7 @@ export default {
:tracking="$options.tracking"
:loading="isLoading"
class="block confidentiality"
+ data-testid="sidebar-confidentiality"
>
<template #collapsed>
<div>
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue
index 3e4297887f0..a1b7e65474a 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue
@@ -183,7 +183,7 @@ export default {
ref="searchInput"
v-model="searchKey"
:disabled="labelsFetchInProgress"
- data-qa-selector="dropdown_input_field"
+ data-testid="dropdown-input-field"
/>
</div>
<div ref="labelsListContainer" class="dropdown-content" data-testid="dropdown-content">
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
index 154a8e866d0..377200ab804 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
@@ -71,8 +71,7 @@ export default {
size="small"
class="dropdown-header-button gl-p-0!"
icon="close"
- data-testid="close-button"
- data-qa-selector="close_labels_dropdown_button"
+ data-testid="close-labels-dropdown-button"
@click="$emit('closeDropdown')"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue
index 57e3ee4aaa5..f2ce02526e7 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue
@@ -108,7 +108,7 @@ export default {
v-for="label in sortedSelectedLabels"
:key="label.id"
class="hide-collapsed"
- data-qa-selector="selected_label_content"
+ data-testid="selected-label-content"
:data-qa-label-name="label.title"
:title="label.title"
:description="label.description"
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
index f9a9cc316c1..ac52e4dbf3f 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
@@ -362,7 +362,6 @@ export default {
'is-embedded': isDropdownVariantEmbedded(variant),
}"
data-testid="sidebar-labels"
- data-qa-selector="labels_block"
>
<template v-if="isDropdownVariantSidebar(variant)">
<sidebar-editable-item
diff --git a/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue b/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue
index 24afb25e403..f2097ce589e 100644
--- a/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue
+++ b/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue
@@ -110,7 +110,7 @@ export default {
:issuable-attribute="$options.issuableAttribute"
:issuable-type="issuableType"
:workspace-type="workspaceType"
- data-qa-selector="issuable_milestone_dropdown"
+ data-testid="issuable-milestone-dropdown"
@change="handleChange"
>
<template #footer>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
index 55bb214aa65..92461183711 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
@@ -43,7 +43,7 @@ export default {
data-track-action="click_edit_button"
data-track-label="right_sidebar"
data-track-property="reviewer"
- data-qa-selector="reviewers_edit_button"
+ data-testid="reviewers-edit-button"
>
{{ __('Edit') }}
</a>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
index a3282932f84..ee9edd6a022 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
@@ -1,5 +1,6 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
+import { GlButton } from '@gitlab/ui';
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { TYPE_ISSUE } from '~/issues/constants';
@@ -11,6 +12,7 @@ export default {
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Reviewers',
components: {
+ GlButton,
CollapsedReviewerList,
UncollapsedReviewerList,
},
@@ -64,15 +66,16 @@ export default {
{{ __('None') }}
<template v-if="editable">
-
- <button
- type="button"
- class="gl-button btn-link gl-reset-color!"
+ <gl-button
+ category="tertiary"
+ variant="link"
+ class="gl-ml-2"
data-testid="assign-yourself"
data-qa-selector="assign_yourself_button"
@click="assignSelf"
>
- {{ __('assign yourself') }}
- </button>
+ <span class="gl-text-gray-500 gl-hover-text-blue-800">{{ __('assign yourself') }}</span>
+ </gl-button>
</template>
</span>
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index 7fde43a360d..28b88a59405 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -308,7 +308,7 @@ export default {
v-gl-tooltip="tooltipText"
class="gl-reset-color gl-hover-text-blue-800"
:href="attributeUrl"
- :data-qa-selector="`${formatIssuableAttribute.snake}_link`"
+ :data-testid="`${formatIssuableAttribute.kebab}-link`"
>
{{ attributeTitle }}
<span v-if="isAttributeOverdue(currentAttribute)">{{ $options.i18n.expired }}</span>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
index 568962cddc7..866db2a43b8 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
@@ -1,7 +1,7 @@
<script>
import {
+ GlButton,
GlDisclosureDropdownItem,
- GlDropdownForm,
GlIcon,
GlLoadingIcon,
GlToggle,
@@ -30,8 +30,8 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
+ GlButton,
GlDisclosureDropdownItem,
- GlDropdownForm,
GlIcon,
GlLoadingIcon,
GlToggle,
@@ -130,6 +130,12 @@ export default {
canSubscribe() {
return this.emailsDisabled || !this.isLoggedIn;
},
+ isNotificationsTodosButtons() {
+ return this.glFeatures.notificationsTodosButtons && this.glFeatures.movedMrSidebar;
+ },
+ isMergeRequest() {
+ return this.issuableType === 'merge_request';
+ },
},
methods: {
setSubscribed(subscribed) {
@@ -194,20 +200,8 @@ export default {
</script>
<template>
- <gl-dropdown-form v-if="isMovedMrSidebar && isIssuable" class="gl-dropdown-item">
- <div class="gl-px-5 gl-pb-2 gl-pt-1">
- <gl-toggle
- :value="subscribed"
- :label="$options.i18n.notifications"
- class="merge-request-notification-toggle"
- label-position="left"
- data-testid="notification-toggle"
- @change="toggleSubscribed"
- />
- </div>
- </gl-dropdown-form>
<gl-disclosure-dropdown-item
- v-else-if="isMovedMrSidebar"
+ v-if="isMovedMrSidebar && !isNotificationsTodosButtons"
data-testid="notification-toggle"
@action="toggleSubscribed"
>
@@ -220,6 +214,32 @@ export default {
/>
</template>
</gl-disclosure-dropdown-item>
+ <div v-else-if="isNotificationsTodosButtons" :class="{ 'inline-block': !isMergeRequest }">
+ <gl-button
+ ref="tooltip"
+ v-gl-tooltip.hover.top
+ category="secondary"
+ data-testid="subscribe-button"
+ class="hide-collapsed"
+ :title="notificationTooltip"
+ :class="{ 'gl-ml-2': isIssuable, 'btn-icon': isNotificationsTodosButtons }"
+ @click="toggleSubscribed"
+ >
+ <gl-icon :name="notificationIcon" :size="16" :class="{ 'gl-fill-blue-500': subscribed }" />
+ </gl-button>
+ <gl-button
+ v-if="!isMergeRequest"
+ ref="tooltip"
+ v-gl-tooltip.left.viewport
+ category="secondary"
+ data-testid="subscribe-button"
+ :title="notificationTooltip"
+ class="sidebar-collapsed-icon sidebar-collapsed-container gl-rounded-0! gl-shadow-none!"
+ @click="toggleSubscribed"
+ >
+ <gl-icon :name="notificationIcon" :size="16" :class="{ 'gl-fill-blue-500': subscribed }" />
+ </gl-button>
+ </div>
<sidebar-editable-item
v-else
ref="editable"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue b/app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue
index 9b582ba41ed..f11c7e6ac4d 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue
@@ -206,7 +206,7 @@ export default {
:value="spentAt"
show-clear-button
autocomplete="off"
- size="small"
+ width="small"
@input="updateSpentAtDate"
@clear="updateSpentAtDate(null)"
/>
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
index 1099dcb832f..f2257adb79c 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
@@ -114,6 +114,9 @@ export default {
tootltipTitle() {
return todoLabel(this.hasTodo);
},
+ isNotificationsTodosButtons() {
+ return this.glFeatures.notificationsTodosButtons && this.glFeatures.movedMrSidebar;
+ },
},
methods: {
toggleTodo() {
@@ -183,8 +186,26 @@ export default {
</script>
<template>
- <div data-testid="sidebar-todo">
+ <div data-testid="sidebar-todo" :class="{ 'inline-block': !isMergeRequest }">
+ <todo-button
+ v-if="isNotificationsTodosButtons"
+ v-gl-tooltip.hover.top
+ :title="tootltipTitle"
+ :issuable-type="issuableType"
+ :issuable-id="issuableId"
+ :is-todo="hasTodo"
+ :disabled="isLoading"
+ class="hide-collapsed btn-icon"
+ @click.stop.prevent="toggleTodo"
+ >
+ <gl-icon
+ v-if="isNotificationsTodosButtons"
+ :class="{ 'todo-undone gl-fill-blue-500': hasTodo }"
+ :name="collapsedButtonIcon"
+ />
+ </todo-button>
<todo-button
+ v-else
:issuable-type="issuableType"
:issuable-id="issuableId"
:is-todo="hasTodo"
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo_button.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo_button.vue
index b49b8fc389b..2aa79b45093 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo_button.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo_button.vue
@@ -39,6 +39,6 @@ export default {
<template>
<gl-button v-bind="$attrs" :aria-label="buttonLabel" @click="onToggle($event)">
- {{ buttonLabel }}
+ <slot>{{ buttonLabel }}</slot>
</gl-button>
</template>
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 1f3119e14db..4b6dbdcc2c9 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -15,7 +15,6 @@ import { __ } from '~/locale';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import Translate from '~/vue_shared/translate';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
-import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
import CollapsedAssigneeList from './components/assignees/collapsed_assignee_list.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import SidebarAssigneesWidget from './components/assignees/sidebar_assignees_widget.vue';
@@ -800,21 +799,6 @@ export function mountAssigneesDropdown() {
});
}
-function mountNewIssuePopover() {
- const el = document.querySelector('.js-sidebar-header-popover');
-
- if (!el) {
- return null;
- }
-
- return new Vue({
- el,
- name: 'NewHeaderActionsPopover',
- render: (createElement) =>
- createElement(NewHeaderActionsPopover, { props: { issueType: TYPE_MERGE_REQUEST } }),
- });
-}
-
const isAssigneesWidgetShown =
(isInIssuePage() || isInDesignPage() || isInMRPage()) && gon.features.issueAssigneesWidget;
@@ -840,7 +824,6 @@ export function mountSidebar(mediator, store) {
mountSidebarSeverityWidget();
mountSidebarEscalationStatus();
mountMoveIssueButton();
- mountNewIssuePopover();
}
export { getSidebarOptions };
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index 9e80210de51..aa3f33989c8 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -232,8 +232,7 @@ export default {
<gl-form-input
id="snippet-title"
v-model="snippet.title"
- data-testid="snippet-title-input"
- data-qa-selector="snippet_title_field"
+ data-testid="snippet-title-input-field"
:autofocus="true"
/>
</gl-form-group>
@@ -261,7 +260,7 @@ export default {
category="primary"
type="submit"
variant="confirm"
- data-qa-selector="submit_button"
+ data-testid="submit-button"
:disabled="isUpdating"
>{{ saveButtonLabel }}</gl-button
>
diff --git a/app/assets/javascripts/snippets/components/embed_dropdown.vue b/app/assets/javascripts/snippets/components/embed_dropdown.vue
index 17312c2373b..1510cc01810 100644
--- a/app/assets/javascripts/snippets/components/embed_dropdown.vue
+++ b/app/assets/javascripts/snippets/components/embed_dropdown.vue
@@ -53,7 +53,7 @@ export default {
:aria-label="$options.MSG_COPY"
:data-clipboard-text="value"
icon="copy-to-clipboard"
- data-qa-selector="copy_button"
+ data-testid="copy-button"
:data-qa-action="name"
/>
</template>
diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue
index 549b1bdd209..7a60fc6d26c 100644
--- a/app/assets/javascripts/snippets/components/show.vue
+++ b/app/assets/javascripts/snippets/components/show.vue
@@ -68,14 +68,14 @@ export default {
<embed-dropdown
v-if="embeddable"
:url="snippet.webUrl"
- data-qa-selector="snippet_embed_dropdown"
+ data-testid="snippet-embed-dropdown"
/>
<clone-dropdown-button
v-if="canBeCloned"
class="gl-ml-3"
:ssh-link="snippet.sshUrlToRepo"
:http-link="snippet.httpUrlToRepo"
- data-qa-selector="clone_button"
+ data-testid="clone-button"
/>
</div>
<gl-alert v-if="hasUnretrievableBlobs" variant="danger" class="gl-mb-3" :dismissible="false">
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
index 59f7c8d8d97..ca1d9f858a5 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
@@ -157,10 +157,9 @@ export default {
</gl-form-group>
<gl-button
:disabled="!canAdd"
- data-testid="add_button"
+ data-testid="add-button"
class="gl-my-3"
variant="dashed"
- data-qa-selector="add_file_button"
@click="addBlob"
>{{ addLabel }}</gl-button
>
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
index 021bd23781e..9b0a1db23f2 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
@@ -69,11 +69,11 @@ export default {
};
</script>
<template>
- <div class="file-holder snippet" data-qa-selector="file_holder_container">
+ <div class="file-holder snippet" data-testid="file-holder-container">
<blob-header-edit
:id="inputId"
:value="blob.path"
- data-qa-selector="file_name_field"
+ data-testid="file-name-field"
:can-delete="canDelete"
:show-delete="showDelete"
@input="notifyAboutUpdates({ path: $event })"
diff --git a/app/assets/javascripts/snippets/components/snippet_description_edit.vue b/app/assets/javascripts/snippets/components/snippet_description_edit.vue
index 3ce7ea231ff..93d52890675 100644
--- a/app/assets/javascripts/snippets/components/snippet_description_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_description_edit.vue
@@ -36,7 +36,7 @@ export default {
<gl-form-input
class="form-control"
:placeholder="s__('Snippets|Describe what your snippet does or how to use it…')"
- data-qa-selector="description_placeholder"
+ data-testid="description-placeholder"
/>
</div>
<markdown-field
@@ -54,7 +54,7 @@ export default {
:value="value"
class="note-textarea js-gfm-input js-autosize markdown-area"
dir="auto"
- data-qa-selector="snippet_description_field"
+ data-testid="snippet-description-field"
data-supports-quick-actions="false"
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')"
diff --git a/app/assets/javascripts/snippets/components/snippet_description_view.vue b/app/assets/javascripts/snippets/components/snippet_description_view.vue
index ab2ff6e0ef8..9eae096d6f2 100644
--- a/app/assets/javascripts/snippets/components/snippet_description_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_description_view.vue
@@ -20,7 +20,7 @@ export default {
};
</script>
<template>
- <markdown-field-view class="snippet-description" data-qa-selector="snippet_description_content">
+ <markdown-field-view class="snippet-description" data-testid="snippet-description-content">
<div
v-safe-html:[$options.safeHtmlConfig]="description"
class="md js-snippet-description"
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index 881e06113d9..56ea931fc8c 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -216,7 +216,7 @@ export default {
<div class="detail-page-header-body">
<div
class="snippet-box has-tooltip d-flex align-items-center gl-mr-2 mb-1"
- data-qa-selector="snippet_container"
+ data-testid="snippet-container"
:title="snippetVisibilityLevelDescription"
data-container="body"
>
@@ -267,7 +267,7 @@ export default {
:category="action.category"
:class="action.cssClass"
:href="action.href"
- data-qa-selector="snippet_action_button"
+ data-testid="snippet-action-button"
:data-qa-action="action.text"
@click="action.click ? action.click() : undefined"
>{{ action.text }}</gl-button
@@ -321,8 +321,7 @@ export default {
variant="danger"
category="primary"
:disabled="isLoading"
- data-qa-selector="delete_snippet_button"
- data-testid="delete-snippet"
+ data-testid="delete-snippet-button"
@click="deleteSnippet"
>
<gl-loading-icon v-if="isLoading" size="sm" inline />
diff --git a/app/assets/javascripts/snippets/components/snippet_title.vue b/app/assets/javascripts/snippets/components/snippet_title.vue
index 2cf7a1e267b..0e4dbf55963 100644
--- a/app/assets/javascripts/snippets/components/snippet_title.vue
+++ b/app/assets/javascripts/snippets/components/snippet_title.vue
@@ -20,7 +20,7 @@ export default {
</script>
<template>
<div class="snippet-header limited-header-width">
- <h2 class="snippet-title gl-mt-0 mb-3" data-qa-selector="snippet_title_content">
+ <h2 class="snippet-title gl-mt-0 mb-3" data-testid="snippet-title-content">
{{ snippet.title }}
</h2>
diff --git a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
index 24dd978585c..37d10cffc78 100644
--- a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
@@ -57,7 +57,7 @@ export default {
<gl-icon :size="16" :name="option.icon" />
<span
class="font-weight-bold ml-1 js-visibility-option"
- data-qa-selector="visibility_content"
+ data-testid="visibility-content"
:data-qa-visibility="option.label"
>{{ option.label }}</span
>
diff --git a/app/assets/javascripts/super_sidebar/components/brand_logo.vue b/app/assets/javascripts/super_sidebar/components/brand_logo.vue
index 02cf36fb053..c280c03591b 100644
--- a/app/assets/javascripts/super_sidebar/components/brand_logo.vue
+++ b/app/assets/javascripts/super_sidebar/components/brand_logo.vue
@@ -26,7 +26,7 @@ export default {
<template>
<a
- v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.homepage"
+ v-gl-tooltip:super-sidebar.bottom="$options.i18n.homepage"
class="brand-logo"
:href="rootPath"
data-track-action="click_link"
@@ -46,7 +46,7 @@ export default {
<span
v-else
v-safe-html="$options.logo"
- aria-hidden
+ aria-hidden="true"
data-testid="brand-header-default-logo"
></span>
</a>
diff --git a/app/assets/javascripts/super_sidebar/components/counter.vue b/app/assets/javascripts/super_sidebar/components/counter.vue
index c0e1959fba4..49efc5ab5b9 100644
--- a/app/assets/javascripts/super_sidebar/components/counter.vue
+++ b/app/assets/javascripts/super_sidebar/components/counter.vue
@@ -15,7 +15,7 @@ export default {
href: {
type: String,
required: false,
- default: '',
+ default: null,
},
icon: {
type: String,
diff --git a/app/assets/javascripts/super_sidebar/components/create_menu.vue b/app/assets/javascripts/super_sidebar/components/create_menu.vue
index d1e96479631..279e689bd8d 100644
--- a/app/assets/javascripts/super_sidebar/components/create_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/create_menu.vue
@@ -14,7 +14,7 @@ import {
import { DROPDOWN_Y_OFFSET, IMPERSONATING_OFFSET } from '../constants';
// Left offset required for the dropdown to be aligned with the super sidebar
-const DROPDOWN_X_OFFSET_BASE = -179;
+const DROPDOWN_X_OFFSET_BASE = -177;
const DROPDOWN_X_OFFSET_IMPERSONATING = DROPDOWN_X_OFFSET_BASE + IMPERSONATING_OFFSET;
export default {
@@ -62,7 +62,7 @@ export default {
<template>
<gl-disclosure-dropdown
- v-gl-tooltip:super-sidebar.hover.bottom="dropdownOpen ? '' : $options.i18n.createNew"
+ v-gl-tooltip:super-sidebar.bottom="dropdownOpen ? '' : $options.i18n.createNew"
category="tertiary"
icon="plus"
no-caret
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
index 4cfc329f8b8..61fa360c41f 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
@@ -17,6 +17,7 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { sprintf } from '~/locale';
import { ARROW_DOWN_KEY, ARROW_UP_KEY, END_KEY, HOME_KEY, ESC_KEY } from '~/lib/utils/keys';
import {
+ COMMAND_PALETTE,
MIN_SEARCH_TERM,
SEARCH_DESCRIBED_BY_WITH_RESULTS,
SEARCH_DESCRIBED_BY_DEFAULT,
@@ -50,6 +51,7 @@ export default {
name: 'GlobalSearchModal',
SEARCH_MODAL_ID,
i18n: {
+ COMMAND_PALETTE,
SEARCH_DESCRIBED_BY_WITH_RESULTS,
SEARCH_DESCRIBED_BY_DEFAULT,
SEARCH_DESCRIBED_BY_UPDATED,
@@ -279,6 +281,7 @@ export default {
hide-footer
hide-header-close
scrollable
+ :title="$options.i18n.COMMAND_PALETTE"
body-class="gl-p-0!"
modal-class="global-search-modal"
:centered="false"
diff --git a/app/assets/javascripts/super_sidebar/components/help_center.vue b/app/assets/javascripts/super_sidebar/components/help_center.vue
index 8ce82116194..069987d4006 100644
--- a/app/assets/javascripts/super_sidebar/components/help_center.vue
+++ b/app/assets/javascripts/super_sidebar/components/help_center.vue
@@ -38,7 +38,7 @@ export default {
shortcuts: __('Keyboard shortcuts'),
version: __('Your GitLab version'),
whatsnew: __("What's new"),
- chat: s__('TanukiBot|Ask GitLab Duo'),
+ chat: s__('TanukiBot|GitLab Duo Chat'),
},
props: {
sidebarData: {
diff --git a/app/assets/javascripts/super_sidebar/components/menu_section.vue b/app/assets/javascripts/super_sidebar/components/menu_section.vue
index 6b5002e1aa8..91b781b8235 100644
--- a/app/assets/javascripts/super_sidebar/components/menu_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/menu_section.vue
@@ -152,20 +152,20 @@ export default {
<gl-collapse
:id="itemId"
v-model="isExpanded"
- :aria-label="item.title"
class="gl-list-style-none gl-p-0 gl-m-0 gl-transition-duration-medium gl-transition-timing-function-ease"
data-qa-selector="menu_section"
:data-qa-section-name="item.title"
- tag="ul"
>
<slot>
- <nav-item
- v-for="subItem of item.items"
- :key="`${item.title}-${subItem.title}`"
- :item="subItem"
- @pin-add="(itemId) => $emit('pin-add', itemId)"
- @pin-remove="(itemId) => $emit('pin-remove', itemId)"
- />
+ <ul :aria-label="item.title" class="gl-list-style-none gl-p-0 gl-m-0">
+ <nav-item
+ v-for="subItem of item.items"
+ :key="`${item.title}-${subItem.title}`"
+ :item="subItem"
+ @pin-add="(itemId) => $emit('pin-add', itemId)"
+ @pin-remove="(itemId) => $emit('pin-remove', itemId)"
+ />
+ </ul>
</slot>
</gl-collapse>
</component>
diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue
index 5e0f8fffb0e..5416f86abeb 100644
--- a/app/assets/javascripts/super_sidebar/components/nav_item.vue
+++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue
@@ -7,6 +7,7 @@ import {
TRACKING_UNKNOWN_ID,
TRACKING_UNKNOWN_PANEL,
} from '~/super_sidebar/constants';
+import eventHub from '../event_hub';
import NavItemLink from './nav_item_link.vue';
import NavItemRouterLink from './nav_item_router_link.vue';
@@ -69,16 +70,14 @@ export default {
return {
isMouseIn: false,
canClickPinButton: false,
+ pillCount: this.item.pill_count,
};
},
computed: {
- pillData() {
- return this.item.pill_count;
- },
hasPill() {
return (
- Number.isFinite(this.pillData) ||
- (typeof this.pillData === 'string' && this.pillData !== '')
+ Number.isFinite(this.pillCount) ||
+ (typeof this.pillCount === 'string' && this.pillCount !== '')
);
},
isPinnable() {
@@ -145,6 +144,9 @@ export default {
hasAvatar() {
return Boolean(this.item.entity_id);
},
+ hasEndSpace() {
+ return this.hasPill || this.isPinnable || this.isFlyout;
+ },
avatarShape() {
return this.item.avatar_shape || 'rect';
},
@@ -179,11 +181,21 @@ export default {
if (this.item.is_active) {
this.$el.scrollIntoView(false);
}
+
+ eventHub.$on('updatePillValue', this.updatePillValue);
+ },
+ destroyed() {
+ eventHub.$off('updatePillValue', this.updatePillValue);
},
methods: {
togglePointerEvents() {
this.canClickPinButton = this.isMouseIn;
},
+ updatePillValue({ value, itemId }) {
+ if (this.item.id === itemId) {
+ this.pillCount = value;
+ }
+ },
},
};
</script>
@@ -236,7 +248,7 @@ export default {
</div>
</div>
<slot name="actions"></slot>
- <span v-if="hasPill || isPinnable" class="gl-text-right gl-relative gl-min-w-8">
+ <span v-if="hasEndSpace" class="gl-text-right gl-relative gl-min-w-6">
<gl-badge
v-if="hasPill"
size="sm"
@@ -246,7 +258,7 @@ export default {
'hide-on-focus-or-hover--target transition-opacity-on-hover--target': isPinnable,
}"
>
- {{ pillData }}
+ {{ pillCount }}
</gl-badge>
</span>
</component>
diff --git a/app/assets/javascripts/super_sidebar/components/pinned_section.vue b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
index 5da45b52bf4..ea3e9e9df1f 100644
--- a/app/assets/javascripts/super_sidebar/components/pinned_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
@@ -102,7 +102,7 @@ export default {
<draggable
v-if="items.length > 0"
v-model="draggableItems"
- class="gl-p-0 gl-m-0"
+ class="gl-p-0 gl-m-0 gl-list-style-none"
data-testid="pinned-nav-items"
handle=".js-draggable-icon"
tag="ul"
diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
index 02488e99c0e..772072c0996 100644
--- a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
@@ -158,7 +158,11 @@ export default {
<template>
<div class="gl-p-2 gl-relative">
- <ul v-if="hasStaticItems" class="gl-p-0 gl-m-0" data-testid="static-items-section">
+ <ul
+ v-if="hasStaticItems"
+ class="gl-list-style-none gl-p-0 gl-m-0"
+ data-testid="static-items-section"
+ >
<nav-item v-for="item in staticItems" :key="item.id" :item="item" is-static />
</ul>
<pinned-section
@@ -174,7 +178,11 @@ export default {
class="gl-my-2 gl-mx-4"
data-testid="main-menu-separator"
/>
- <ul class="gl-p-0 gl-list-style-none" data-testid="non-static-items-section">
+ <ul
+ aria-labelledby="super-sidebar-context-header"
+ class="gl-p-0 gl-list-style-none"
+ data-testid="non-static-items-section"
+ >
<template v-for="item in nonStaticItems">
<menu-section
v-if="isSection(item)"
@@ -182,6 +190,7 @@ export default {
:item="item"
:separated="item.separated"
:has-flyout="showFlyoutMenus"
+ tag="li"
@pin-add="createPin"
@pin-remove="destroyPin"
/>
@@ -189,7 +198,6 @@ export default {
v-else
:key="item.id"
:item="item"
- tag="li"
@pin-add="createPin"
@pin-remove="destroyPin"
/>
diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
index fe3e4a8199e..5f7cfce93b1 100644
--- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
@@ -36,7 +36,7 @@ export default {
mixins: [Tracking.mixin()],
i18n: {
skipToMainContent: __('Skip to main content'),
- primary: s__('Navigation|Primary'),
+ primaryNavigation: s__('Navigation|Primary navigation'),
},
inject: ['showTrialStatusWidget'],
props: {
@@ -130,7 +130,9 @@ export default {
<div>
<div class="super-sidebar-overlay" @click="collapseSidebar"></div>
<gl-button
+ v-if="sidebarData.is_logged_in"
class="super-sidebar-skip-to gl-sr-only-focusable gl-fixed gl-left-0 gl-m-3"
+ data-testid="super-sidebar-skip-to"
href="#content-body"
variant="confirm"
>
@@ -138,7 +140,7 @@ export default {
</gl-button>
<nav
id="super-sidebar"
- :aria-label="$options.i18n.primary"
+ aria-labelledby="super-sidebar-heading"
class="super-sidebar"
:class="peekClasses"
data-testid="super-sidebar"
@@ -147,6 +149,9 @@ export default {
@mouseenter="isMouseover = true"
@mouseleave="isMouseover = false"
>
+ <h2 id="super-sidebar-heading" class="gl-sr-only">
+ {{ $options.i18n.primaryNavigation }}
+ </h2>
<user-bar :has-collapse-button="!showOverlay" :sidebar-data="sidebarData" />
<div v-if="showTrialStatusWidget" class="gl-px-2 gl-py-2">
<trial-status-widget
@@ -158,12 +163,12 @@ export default {
class="contextual-nav gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-overflow-hidden"
>
<div class="gl-flex-grow-1 gl-overflow-auto" data-testid="nav-container">
- <h2
- class="gl-px-5 gl-pt-3 gl-pb-2 gl-m-0 gl-reset-line-height gl-font-sm super-sidebar-context-header"
+ <div
+ id="super-sidebar-context-header"
+ class="gl-px-5 gl-pt-3 gl-pb-2 gl-m-0 gl-reset-line-height gl-font-weight-bold gl-font-sm super-sidebar-context-header"
>
{{ sidebarData.current_context_header }}
- </h2>
-
+ </div>
<sidebar-menu
v-if="menuItems.length"
:items="menuItems"
diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue
index 30ee18cc369..71c1460423e 100644
--- a/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue
@@ -14,66 +14,82 @@ export default {
},
mixins: [Tracking.mixin()],
props: {
- tooltipContainer: {
+ type: {
type: String,
required: false,
- default: null,
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'right',
+ default: 'expand',
},
},
i18n: {
- collapseSidebar: __('Hide sidebar'),
- expandSidebar: __('Keep sidebar visible'),
primaryNavigationSidebar: __('Primary navigation sidebar'),
},
+ tooltipCollapse: {
+ placement: 'bottom',
+ container: 'super-sidebar',
+ title: __('Hide sidebar'),
+ },
+ tooltipExpand: {
+ placement: 'right',
+ title: __('Keep sidebar visible'),
+ },
data() {
return sidebarState;
},
computed: {
- canOpen() {
- return this.isCollapsed || this.isPeek || this.isHoverPeek;
+ isTypeCollapse() {
+ return this.type === 'collapse';
},
- tooltipTitle() {
- return this.canOpen ? this.$options.i18n.expandSidebar : this.$options.i18n.collapseSidebar;
+ isTypeExpand() {
+ return this.type === 'expand';
},
tooltip() {
- return {
- placement: this.tooltipPlacement,
- container: this.tooltipContainer,
- title: this.tooltipTitle,
- };
+ return this.isTypeExpand ? this.$options.tooltipExpand : this.$options.tooltipCollapse;
},
ariaExpanded() {
- return String(!this.canOpen);
+ return String(this.isTypeCollapse);
},
},
+ mounted() {
+ this.$root.$on('bv::tooltip::show', this.onTooltipShow);
+ },
+ beforeUnmount() {
+ this.$root.$off('bv::tooltip::show', this.onTooltipShow);
+ },
methods: {
toggle() {
- this.track(this.canOpen ? 'nav_show' : 'nav_hide', {
+ this.track(this.isTypeExpand ? 'nav_show' : 'nav_hide', {
label: 'nav_toggle',
property: 'nav_sidebar',
});
- toggleSuperSidebarCollapsed(!this.canOpen, true);
+ toggleSuperSidebarCollapsed(!this.isTypeExpand, true);
this.focusOtherToggle();
},
focusOtherToggle() {
this.$nextTick(() => {
- const classSelector = this.canOpen ? JS_TOGGLE_EXPAND_CLASS : JS_TOGGLE_COLLAPSE_CLASS;
+ const classSelector = this.isTypeExpand ? JS_TOGGLE_COLLAPSE_CLASS : JS_TOGGLE_EXPAND_CLASS;
const otherToggle = document.querySelector(`.${classSelector}`);
otherToggle?.focus();
});
},
+ onTooltipShow(bvEvent) {
+ if (
+ bvEvent.target !== this.$el ||
+ (this.isTypeCollapse && !this.isCollapsed) ||
+ (this.isTypeExpand && this.isCollapsed) ||
+ this.isPeek ||
+ this.isHoverPeek
+ )
+ return;
+
+ bvEvent.preventDefault();
+ },
},
};
</script>
<template>
<gl-button
- v-gl-tooltip.hover="tooltip"
+ v-gl-tooltip="tooltip"
aria-controls="super-sidebar"
:aria-expanded="ariaExpanded"
:aria-label="$options.i18n.primaryNavigationSidebar"
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
index 49aee4f3470..88ea4d828b7 100644
--- a/app/assets/javascripts/super_sidebar/components/user_bar.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -126,9 +126,8 @@ export default {
<super-sidebar-toggle
v-if="hasCollapseButton"
:class="$options.JS_TOGGLE_COLLAPSE_CLASS"
- tooltip-placement="bottom"
- tooltip-container="super-sidebar"
data-testid="super-sidebar-collapse-button"
+ type="collapse"
/>
<create-menu
v-if="sidebarData.is_logged_in && sidebarData.create_new_menu_groups.length > 0"
@@ -154,7 +153,7 @@ export default {
class="gl-display-flex gl-justify-content-space-between gl-gap-2"
>
<counter
- v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.issues"
+ v-gl-tooltip:super-sidebar.bottom="$options.i18n.issues"
class="gl-flex-basis-third dashboard-shortcuts-issues"
icon="issues"
:count="userCounts.assigned_issues"
@@ -172,7 +171,7 @@ export default {
@hidden="mrMenuShown = false"
>
<counter
- v-gl-tooltip:super-sidebar.hover.bottom="mrMenuShown ? '' : $options.i18n.mergeRequests"
+ v-gl-tooltip:super-sidebar.bottom="mrMenuShown ? '' : $options.i18n.mergeRequests"
class="gl-w-full"
icon="merge-request-open"
:count="mergeRequestTotalCount"
@@ -184,7 +183,7 @@ export default {
/>
</merge-request-menu>
<counter
- v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.todoList"
+ v-gl-tooltip:super-sidebar.bottom="$options.i18n.todoList"
class="gl-flex-basis-third shortcuts-todos js-todos-count"
icon="todo-done"
:count="userCounts.todos"
@@ -198,7 +197,7 @@ export default {
</div>
<button
id="super-sidebar-search"
- v-gl-tooltip.bottom.hover.html="searchTooltip"
+ v-gl-tooltip.bottom.html="searchTooltip"
v-gl-modal="$options.SEARCH_MODAL_ID"
class="counter gl-display-block gl-py-3 gl-bg-gray-10 gl-rounded-base gl-text-gray-900 gl-border-none gl-inset-border-1-gray-a-08 gl-line-height-1 gl-focus--focus gl-w-full"
data-testid="super-sidebar-search-button"
diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue
index ed6c41e85c6..891e883b6c0 100644
--- a/app/assets/javascripts/super_sidebar/components/user_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue
@@ -12,7 +12,7 @@ import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import Tracking from '~/tracking';
import PersistentUserCallout from '~/persistent_user_callout';
import { USER_MENU_TRACKING_DEFAULTS, DROPDOWN_Y_OFFSET, IMPERSONATING_OFFSET } from '../constants';
-import UserNameGroup from './user_name_group.vue';
+import UserMenuProfileItem from './user_menu_profile_item.vue';
// Left offset required for the dropdown to be aligned with the super sidebar
const DROPDOWN_X_OFFSET_BASE = -211;
@@ -40,7 +40,7 @@ export default {
GlDisclosureDropdownItem,
GlButton,
NewNavToggle,
- UserNameGroup,
+ UserMenuProfileItem,
},
directives: {
SafeHtml,
@@ -247,7 +247,10 @@ export default {
</gl-button>
</template>
- <user-name-group :user="data" />
+ <gl-disclosure-dropdown-group>
+ <user-menu-profile-item :user="data" />
+ </gl-disclosure-dropdown-group>
+
<gl-disclosure-dropdown-group bordered>
<gl-disclosure-dropdown-item
v-if="data.status.can_update"
diff --git a/app/assets/javascripts/super_sidebar/components/user_menu_profile_item.vue b/app/assets/javascripts/super_sidebar/components/user_menu_profile_item.vue
new file mode 100644
index 00000000000..95255ce3d8e
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/user_menu_profile_item.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlBadge, GlDisclosureDropdownItem, GlTooltip } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { s__ } from '~/locale';
+import { USER_MENU_TRACKING_DEFAULTS } from '../constants';
+
+export default {
+ i18n: {
+ user: {
+ busy: s__('UserProfile|Busy'),
+ },
+ },
+ components: {
+ GlBadge,
+ GlDisclosureDropdownItem,
+ GlTooltip,
+ },
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ user: {
+ required: true,
+ type: Object,
+ },
+ },
+ computed: {
+ menuItem() {
+ const item = {
+ text: this.user.name,
+ };
+ if (this.user.has_link_to_profile) {
+ item.href = this.user.link_to_profile;
+
+ item.extraAttrs = {
+ ...USER_MENU_TRACKING_DEFAULTS,
+ 'data-track-label': 'user_profile',
+ 'data-testid': 'user-profile-link',
+ };
+ }
+
+ return item;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown-item :item="menuItem">
+ <template #list-item>
+ <span class="gl-display-flex gl-flex-direction-column">
+ <span>
+ <span class="gl-font-weight-bold">
+ {{ user.name }}
+ </span>
+ <gl-badge v-if="user.status.busy" size="sm" variant="warning">
+ {{ $options.i18n.user.busy }}
+ </gl-badge>
+ </span>
+
+ <span class="gl-text-gray-400 gl-word-break-all">@{{ user.username }}</span>
+
+ <span
+ v-if="user.status.customized"
+ ref="statusTooltipTarget"
+ data-testid="user-menu-status"
+ class="gl-display-flex gl-align-items-baseline gl-mt-2 gl-font-sm"
+ >
+ <gl-emoji :data-name="user.status.emoji" class="gl-mr-1" />
+ <span v-safe-html="user.status.message_html" class="gl-text-truncate"></span>
+ <gl-tooltip
+ v-if="user.status.message_html"
+ :target="() => $refs.statusTooltipTarget"
+ boundary="viewport"
+ placement="bottom"
+ >
+ <span v-safe-html="user.status.message_html"></span>
+ </gl-tooltip>
+ </span>
+ </span>
+ </template>
+ </gl-disclosure-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/user_name_group.vue b/app/assets/javascripts/super_sidebar/components/user_name_group.vue
deleted file mode 100644
index 3c8059387fa..00000000000
--- a/app/assets/javascripts/super_sidebar/components/user_name_group.vue
+++ /dev/null
@@ -1,91 +0,0 @@
-<script>
-import {
- GlBadge,
- GlDisclosureDropdownGroup,
- GlDisclosureDropdownItem,
- GlTooltip,
-} from '@gitlab/ui';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import { s__ } from '~/locale';
-import { USER_MENU_TRACKING_DEFAULTS } from '../constants';
-
-export default {
- i18n: {
- user: {
- busy: s__('UserProfile|Busy'),
- },
- },
- components: {
- GlBadge,
- GlDisclosureDropdownGroup,
- GlDisclosureDropdownItem,
- GlTooltip,
- },
- directives: {
- SafeHtml,
- },
- props: {
- user: {
- required: true,
- type: Object,
- },
- },
- computed: {
- menuItem() {
- const item = {
- text: this.user.name,
- };
- if (this.user.has_link_to_profile) {
- item.href = this.user.link_to_profile;
-
- item.extraAttrs = {
- ...USER_MENU_TRACKING_DEFAULTS,
- 'data-track-label': 'user_profile',
- 'data-testid': 'user_profile_link',
- };
- }
-
- return item;
- },
- },
-};
-</script>
-
-<template>
- <gl-disclosure-dropdown-group>
- <gl-disclosure-dropdown-item :item="menuItem">
- <template #list-item>
- <span class="gl-display-flex gl-flex-direction-column">
- <span>
- <span class="gl-font-weight-bold">
- {{ user.name }}
- </span>
- <gl-badge v-if="user.status.busy" size="sm" variant="warning">
- {{ $options.i18n.user.busy }}
- </gl-badge>
- </span>
-
- <span class="gl-text-gray-400">@{{ user.username }}</span>
-
- <span
- v-if="user.status.customized"
- ref="statusTooltipTarget"
- data-testid="user-menu-status"
- class="gl-display-flex gl-align-items-baseline gl-mt-2 gl-font-sm"
- >
- <gl-emoji :data-name="user.status.emoji" class="gl-mr-1" />
- <span v-safe-html="user.status.message_html" class="gl-text-truncate"></span>
- <gl-tooltip
- v-if="user.status.message_html"
- :target="() => $refs.statusTooltipTarget"
- boundary="viewport"
- placement="bottom"
- >
- <span v-safe-html="user.status.message_html"></span>
- </gl-tooltip>
- </span>
- </span>
- </template>
- </gl-disclosure-dropdown-item>
- </gl-disclosure-dropdown-group>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/constants.js b/app/assets/javascripts/super_sidebar/constants.js
index 77bd8b4a734..e96dca3f365 100644
--- a/app/assets/javascripts/super_sidebar/constants.js
+++ b/app/assets/javascripts/super_sidebar/constants.js
@@ -59,4 +59,4 @@ export const DROPDOWN_Y_OFFSET = 4;
export const NAV_ITEM_LINK_ACTIVE_CLASS = 'gl-bg-t-gray-a-08';
-export const IMPERSONATING_OFFSET = 32;
+export const IMPERSONATING_OFFSET = 34;
diff --git a/app/assets/javascripts/super_sidebar/event_hub.js b/app/assets/javascripts/super_sidebar/event_hub.js
new file mode 100644
index 00000000000..e31806ad199
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/event_hub.js
@@ -0,0 +1,3 @@
+import createEventHub from '~/helpers/event_hub_factory';
+
+export default createEventHub();
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
index de16161efb5..f9e488ea5ee 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
@@ -33,6 +33,8 @@ const getTrialStatusWidgetData = (sidebarData) => {
companyName,
glmContent,
createHandRaiseLeadPath,
+ trackAction,
+ trackLabel,
} = convertObjectPropsToCamelCase(sidebarData.trial_status_popover_data_attrs);
return {
@@ -47,6 +49,8 @@ const getTrialStatusWidgetData = (sidebarData) => {
daysRemaining,
targetId,
createHandRaiseLeadPath,
+ trackAction,
+ trackLabel,
trialEndDate: new Date(trialEndDate),
user: { namespaceId, userName, firstName, lastName, companyName, glmContent },
};
diff --git a/app/assets/javascripts/super_sidebar/utils.js b/app/assets/javascripts/super_sidebar/utils.js
index 97830a32d78..d2fb72adb85 100644
--- a/app/assets/javascripts/super_sidebar/utils.js
+++ b/app/assets/javascripts/super_sidebar/utils.js
@@ -59,19 +59,17 @@ const updateItemAccess = (
const neverAccessed = !lastAccessedOn;
const shouldUpdate = neverAccessed || Math.abs(now - lastAccessedOn) / FIFTEEN_MINUTES_IN_MS > 1;
- if (shouldUpdate && gon.features?.serverSideFrecentNamespaces) {
- try {
- axios({
- url: trackVisitsPath,
- method: 'POST',
- data: {
- type: namespace,
- id: contextItem.id,
- },
- });
- } catch (e) {
+ if (shouldUpdate) {
+ axios({
+ url: trackVisitsPath,
+ method: 'POST',
+ data: {
+ type: namespace,
+ id: contextItem.id,
+ },
+ }).catch((e) => {
Sentry.captureException(e);
- }
+ });
}
return {
diff --git a/app/assets/javascripts/terms/components/app.vue b/app/assets/javascripts/terms/components/app.vue
index 0ae97a47170..29099bcc366 100644
--- a/app/assets/javascripts/terms/components/app.vue
+++ b/app/assets/javascripts/terms/components/app.vue
@@ -5,7 +5,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import csrf from '~/lib/utils/csrf';
-import { trackTrialAcceptTerms } from '~/google_tag_manager';
+import { trackTrialAcceptTerms } from 'ee_else_ce/google_tag_manager';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
export default {
diff --git a/app/assets/javascripts/terraform/components/empty_state.vue b/app/assets/javascripts/terraform/components/empty_state.vue
index 551b5498571..5a71f0d66de 100644
--- a/app/assets/javascripts/terraform/components/empty_state.vue
+++ b/app/assets/javascripts/terraform/components/empty_state.vue
@@ -32,13 +32,14 @@ export default {
</script>
<template>
- <gl-empty-state :svg-path="image" :title="$options.i18n.title">
+ <gl-empty-state :svg-path="image" :svg-height="null" :title="$options.i18n.title">
<template #actions>
- <gl-button variant="confirm" :href="$options.docsUrl">
+ <gl-button variant="confirm" :href="$options.docsUrl" class="gl-mx-2 gl-mb-3">
{{ $options.i18n.buttonDoc }}</gl-button
>
<gl-button
v-gl-modal-directive="$options.COMMAND_MODAL_ID"
+ class="gl-mx-2 gl-mb-3"
data-testid="terraform-state-copy-init-command"
icon="copy-to-clipboard"
>{{ $options.i18n.buttonCopy }}</gl-button
diff --git a/app/assets/javascripts/token_access/components/inbound_token_access.vue b/app/assets/javascripts/token_access/components/inbound_token_access.vue
index 234ac0505b2..7e55f56279e 100644
--- a/app/assets/javascripts/token_access/components/inbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/inbound_token_access.vue
@@ -30,7 +30,7 @@ export default {
'CICD|Allow CI job tokens from the following projects to access this project',
),
settingDisabledMessage: s__(
- 'CICD|Enable feature to allow job token access by the following projects.',
+ 'CICD|Enable feature to limit job token access, so only the projects in this list can access this project with a CI/CD job token.',
),
addProject: __('Add project'),
cancel: __('Cancel'),
diff --git a/app/assets/javascripts/tracking/constants.js b/app/assets/javascripts/tracking/constants.js
index 88b7f6d3532..46278152879 100644
--- a/app/assets/javascripts/tracking/constants.js
+++ b/app/assets/javascripts/tracking/constants.js
@@ -36,4 +36,3 @@ export const SERVICE_PING_SECURITY_CONFIGURATION_THREAT_MANAGEMENT_VISIT =
'users_visiting_security_configuration_threat_management';
export const SERVICE_PING_PIPELINE_SECURITY_VISIT = 'users_visiting_pipeline_security';
-export const USER_CONTEXT_SCHEMA = 'iglu:com.gitlab/user_context/jsonschema/1-0-0';
diff --git a/app/assets/javascripts/tracking/internal_events.js b/app/assets/javascripts/tracking/internal_events.js
index 9bd0200cad1..d5bc428934c 100644
--- a/app/assets/javascripts/tracking/internal_events.js
+++ b/app/assets/javascripts/tracking/internal_events.js
@@ -1,12 +1,10 @@
import API from '~/api';
-import getStandardContext from './get_standard_context';
import Tracking from './tracking';
import {
GITLAB_INTERNAL_EVENT_CATEGORY,
LOAD_INTERNAL_EVENTS_SELECTOR,
SERVICE_PING_SCHEMA,
- USER_CONTEXT_SCHEMA,
} from './constants';
import { Tracker } from './tracker';
import { InternalEventHandler, createInternalEventPayload } from './utils';
@@ -17,7 +15,7 @@ const InternalEvents = {
* @param {string} event
* @param {object} data
*/
- track_event(event, data = {}) {
+ trackEvent(event, data = {}) {
const { context, ...rest } = data;
const defaultContext = {
@@ -34,6 +32,7 @@ const InternalEvents = {
context: mergedContext,
...rest,
});
+ this.trackBrowserSDK(event);
},
/**
* Returns an implementation of this class in the form of
@@ -42,8 +41,8 @@ const InternalEvents = {
mixin() {
return {
methods: {
- track_event(event, data = {}) {
- InternalEvents.track_event(event, data);
+ trackEvent(event, data = {}) {
+ InternalEvents.trackEvent(event, data);
},
},
};
@@ -62,7 +61,10 @@ const InternalEvents = {
// eslint-disable-next-line no-param-reassign
parent.internalEventsTrackingBound = true;
- const handler = { name: 'click', func: (e) => InternalEventHandler(e, this.track_event) };
+ const handler = {
+ name: 'click',
+ func: (e) => InternalEventHandler(e, this.trackEvent.bind(this)),
+ };
parent.addEventListener(handler.name, handler.func);
return handler;
},
@@ -81,7 +83,7 @@ const InternalEvents = {
loadEvents.forEach((element) => {
const action = createInternalEventPayload(element);
if (action) {
- this.track_event(action);
+ this.trackEvent(action);
}
});
@@ -91,21 +93,24 @@ const InternalEvents = {
* Initialize browser sdk for product analytics
*/
initBrowserSDK() {
- const standardContext = getStandardContext();
-
if (window.glClient) {
window.glClient.setDocumentTitle('GitLab');
window.glClient.page({
title: 'GitLab',
- context: [
- {
- schema: USER_CONTEXT_SCHEMA,
- data: standardContext?.data || {},
- },
- ],
});
}
},
+ /**
+ * track events for Product Analytics
+ * @param {string} event
+ */
+ trackBrowserSDK(event) {
+ if (!Tracker.enabled()) {
+ return;
+ }
+
+ window.glClient?.track(event);
+ },
};
export default InternalEvents;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
index c49c1316b1b..e16ccdd35b9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
@@ -83,12 +83,6 @@ export default {
return btn.tooltipText;
},
- actionButtonQaSelector(btn) {
- if (btn.dataQaSelector) {
- return btn.dataQaSelector;
- }
- return 'mr_widget_extension_actions_button';
- },
},
};
</script>
@@ -105,7 +99,6 @@ export default {
:target="btn.target"
:class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]"
:data-clipboard-text="btn.dataClipboardText"
- :data-qa-selector="actionButtonQaSelector(btn)"
:data-method="btn.dataMethod"
:icon="btn.icon"
:data-testid="btn.testId || 'extension-actions-button'"
@@ -157,9 +150,8 @@ export default {
:title="setTooltip(btn)"
:href="btn.href"
:target="btn.target"
- :class="[{ 'gl-mr-1': index !== tertiaryButtons.length - 1 }, btn.class]"
+ :class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]"
:data-clipboard-text="btn.dataClipboardText"
- :data-qa-selector="actionButtonQaSelector(btn)"
:data-method="btn.dataMethod"
:icon="btn.icon"
:data-testid="btn.testId || 'extension-actions-button'"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
index 4ed470440cc..974b53caa15 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
@@ -24,10 +24,6 @@ export default {
GlSprintf,
},
mixins: [approvalsMixin, glFeatureFlagsMixin()],
- provide: {
- expandDetailsTooltip: __('Expand eligible approvers'),
- collapseDetailsTooltip: __('Collapse eligible approvers'),
- },
props: {
mr: {
type: Object,
@@ -248,6 +244,8 @@ export default {
is-collapsible
collapse-on-desktop
:collapsed="collapsed"
+ :expand-details-tooltip="__('Expand eligible approvers')"
+ :collapse-details-tooltip="__('Collapse eligible approvers')"
@toggle="() => $emit('toggle')"
>
<template v-if="$apollo.queries.approvals.loading">{{ $options.FETCH_LOADING }}</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue
new file mode 100644
index 00000000000..303952c787e
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue
@@ -0,0 +1,77 @@
+<script>
+import { __ } from '~/locale';
+import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
+import conflictsStateQuery from '../../queries/states/conflicts.query.graphql';
+import ActionButtons from '../action_buttons.vue';
+import MergeChecksMessage from './message.vue';
+
+export default {
+ name: 'MergeChecksConflicts',
+ components: {
+ MergeChecksMessage,
+ ActionButtons,
+ },
+ mixins: [mergeRequestQueryVariablesMixin],
+ apollo: {
+ state: {
+ query: conflictsStateQuery,
+ variables() {
+ return this.mergeRequestQueryVariables;
+ },
+ update: (data) => data?.project?.mergeRequest,
+ },
+ },
+ props: {
+ check: {
+ type: Object,
+ required: true,
+ },
+ mr: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ state: {},
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.state.loading;
+ },
+ userPermissions() {
+ return this.state.userPermissions;
+ },
+ showResolveButton() {
+ return (
+ this.mr.conflictResolutionPath &&
+ this.userPermissions.pushToSourceBranch &&
+ !this.state.sourceBranchProtected
+ );
+ },
+ tertiaryActionsButtons() {
+ if (this.state.shouldBeRebased) return [];
+
+ return [
+ {
+ text: __('Resolve locally'),
+ class: 'js-check-out-modal-trigger',
+ },
+ this.showResolveButton && {
+ text: __('Resolve conflicts'),
+ category: 'default',
+ href: this.mr.conflictResolutionPath,
+ },
+ ].filter((b) => b);
+ },
+ },
+};
+</script>
+
+<template>
+ <merge-checks-message :check="check">
+ <action-buttons v-if="!isLoading" :tertiary-buttons="tertiaryActionsButtons" />
+ </merge-checks-message>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
new file mode 100644
index 00000000000..d0d749aa441
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
@@ -0,0 +1,44 @@
+<script>
+import StatusIcon from '../widget/status_icon.vue';
+
+const ICON_NAMES = {
+ failed: 'failed',
+ allowed_to_fail: 'neutral',
+ passed: 'success',
+};
+
+export default {
+ name: 'MergeChecksMessage',
+ components: {
+ StatusIcon,
+ },
+ props: {
+ check: {
+ type: Object,
+ required: true,
+ },
+ mr: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ computed: {
+ iconName() {
+ return ICON_NAMES[this.check.result];
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-py-3 gl-pl-7">
+ <div class="gl-display-flex">
+ <status-icon :icon-name="iconName" :level="2" />
+ <div class="gl-w-full gl-min-w-0">
+ <div class="gl-display-flex">{{ check.failureReason }}</div>
+ </div>
+ <slot></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
index 8290e7e9232..1829b674455 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
@@ -37,7 +37,7 @@ export default {
</script>
<template>
- <div class="deploy-heading gl-px-5">
+ <div class="deploy-heading gl-pl-5 gl-pr-4">
<div class="ci-widget media">
<div class="media-body">
<div class="deploy-body">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
new file mode 100644
index 00000000000..1c57226f887
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
@@ -0,0 +1,91 @@
+import createMockApollo from 'helpers/mock_apollo_helper';
+import mergeChecksQuery from '../queries/merge_checks.query.graphql';
+import conflictsStateQuery from '../queries/states/conflicts.query.graphql';
+import MergeChecks from './merge_checks.vue';
+
+const stylesheetsRequireCtx = require.context(
+ '../../../stylesheets',
+ true,
+ /(page_bundles\/merge_requests)\.scss$/,
+);
+
+stylesheetsRequireCtx('./page_bundles/merge_requests.scss');
+
+const defaultRender = (apolloProvider) => ({
+ components: { MergeChecks },
+ apolloProvider,
+ data() {
+ return { mr: { conflictResolutionPath: 'https://gitlab.com' } };
+ },
+ template: '<merge-checks :mr="mr" />',
+});
+
+const Template = ({ canMerge, failed, pushToSourceBranch }) => {
+ const requestHandlers = [
+ [
+ mergeChecksQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: 1,
+ mergeRequest: {
+ id: 1,
+ userPermissions: { canMerge },
+ mergeChecks: [
+ {
+ failureReason: 'Unresolved discussions',
+ identifier: 'unresolved_discussions',
+ result: failed ? 'failed' : 'passed',
+ },
+ {
+ failureReason: 'Resolve conflicts',
+ identifier: 'conflicts',
+ result: failed ? 'failed' : 'passed',
+ },
+ ],
+ },
+ },
+ },
+ }),
+ ],
+ [
+ conflictsStateQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: 1,
+ mergeRequest: {
+ id: 1,
+ shouldBeRebased: false,
+ sourceBranchProtected: false,
+ userPermissions: { pushToSourceBranch },
+ },
+ },
+ },
+ }),
+ ],
+ ];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ return defaultRender(apolloProvider);
+};
+
+const LoadingTemplate = () => {
+ const requestHandlers = [[mergeChecksQuery, () => new Promise(() => {})]];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ return defaultRender(apolloProvider);
+};
+
+export const Default = Template.bind({});
+Default.args = { canMerge: true, failed: true, pushToSourceBranch: true };
+
+export const Loading = LoadingTemplate.bind({});
+Loading.args = {};
+
+export default {
+ title: 'vue_merge_request_widget/merge_checks',
+ component: MergeChecks,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
new file mode 100644
index 00000000000..fa84c0a4a6f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
@@ -0,0 +1,129 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { n__, __, sprintf } from '~/locale';
+import mergeRequestQueryVariablesMixin from '../mixins/merge_request_query_variables';
+import mergeChecksQuery from '../queries/merge_checks.query.graphql';
+import StateContainer from './state_container.vue';
+import BoldText from './bold_text.vue';
+
+const COMPONENTS = {
+ conflicts: () => import('./checks/conflicts.vue'),
+ default: () => import('./checks/message.vue'),
+};
+
+export default {
+ apollo: {
+ state: {
+ query: mergeChecksQuery,
+ skip() {
+ return !this.mr;
+ },
+ variables() {
+ return this.mergeRequestQueryVariables;
+ },
+ update: (data) => data?.project?.mergeRequest,
+ },
+ },
+ components: {
+ GlSkeletonLoader,
+ StateContainer,
+ BoldText,
+ },
+ mixins: [mergeRequestQueryVariablesMixin],
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ collapsed: true,
+ state: {},
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.state.loading;
+ },
+ statusIcon() {
+ return this.failedChecks.length ? 'failed' : 'success';
+ },
+ summaryText() {
+ if (!this.failedChecks.length) {
+ return this.state?.userPermissions?.canMerge
+ ? __('%{boldStart}Ready to merge!%{boldEnd}')
+ : __(
+ '%{boldStart}Ready to merge by members who can write to the target branch.%{boldEnd}',
+ );
+ }
+
+ return sprintf(
+ n__(
+ '%{boldStart}Merge blocked:%{boldEnd} %{count} check failed',
+ '%{boldStart}Merge blocked:%{boldEnd} %{count} checks failed',
+ this.failedChecks.length,
+ ),
+ { count: this.failedChecks.length },
+ );
+ },
+ checks() {
+ return this.state.mergeChecks || [];
+ },
+ failedChecks() {
+ return this.checks.filter((c) => c.result === 'failed');
+ },
+ },
+ methods: {
+ toggleCollapsed() {
+ this.collapsed = !this.collapsed;
+ },
+ checkComponent(check) {
+ return COMPONENTS[check.identifier] || COMPONENTS.default;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <state-container
+ :is-loading="isLoading"
+ :status="statusIcon"
+ is-collapsible
+ collapse-on-desktop
+ :collapsed="collapsed"
+ :expand-details-tooltip="__('Expand merge checks')"
+ :collapse-details-tooltip="__('Collapse merge checks')"
+ @toggle="toggleCollapsed"
+ >
+ <template v-if="isLoading" #loading>
+ <gl-skeleton-loader :width="334" :height="24">
+ <rect x="0" y="0" width="24" height="24" rx="4" />
+ <rect x="32" y="2" width="302" height="20" rx="4" />
+ </gl-skeleton-loader>
+ </template>
+ <template v-else>
+ <bold-text :message="summaryText" />
+ </template>
+ </state-container>
+ <div
+ v-if="!collapsed"
+ class="gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-relative gl-bg-gray-10"
+ data-testid="merge-checks-full"
+ >
+ <div class="gl-px-5">
+ <component
+ :is="checkComponent(check)"
+ v-for="(check, index) in checks"
+ :key="index"
+ :class="{
+ 'gl-border-b-solid gl-border-b-1 gl-border-gray-100': index !== checks.length - 1,
+ }"
+ :check="check"
+ :mr="mr"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index bfcd4610379..2e104f2b93b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -10,7 +10,7 @@ import {
} from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, n__ } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
import PipelineArtifacts from '~/ci/pipelines_page/components/pipelines_artifacts.vue';
import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
@@ -21,7 +21,7 @@ import { MT_MERGE_STRATEGY } from '../constants';
export default {
name: 'MRWidgetPipeline',
components: {
- CiIcon,
+ CiBadgeLink,
GlLink,
GlLoadingIcon,
GlIcon,
@@ -194,24 +194,23 @@ export default {
</p>
</template>
<template v-else-if="hasPipeline">
- <a :href="status.details_path" class="gl-align-self-start gl-mt-2 gl-mr-3">
- <ci-icon :status="status" :size="24" class="gl-display-flex" />
- </a>
+ <ci-badge-link
+ :status="status"
+ :href="status.details_path"
+ size="md"
+ :show-text="false"
+ class="gl-align-self-start gl-mt-2 gl-mr-3"
+ />
<div class="ci-widget-container d-flex">
<div class="ci-widget-content">
<div class="media-body">
<div
data-testid="pipeline-info-container"
- data-qa-selector="merge_request_pipeline_info_content"
class="gl-display-flex gl-flex-wrap gl-align-items-center gl-justify-content-space-between"
>
<p class="mr-pipeline-title gl-m-0! gl-mr-3! gl-font-weight-bold gl-text-gray-900">
{{ pipeline.details.event_type_name }}
- <gl-link
- :href="pipeline.path"
- class="pipeline-id"
- data-testid="pipeline-id"
- data-qa-selector="pipeline_link"
+ <gl-link :href="pipeline.path" class="pipeline-id" data-testid="pipeline-id"
>#{{ pipeline.id }}</gl-link
>
{{ pipeline.details.status.label }}
@@ -240,7 +239,7 @@ export default {
{{ s__('Pipeline|for') }}
<gl-link
:href="pipeline.commit.commit_path"
- class="commit-sha gl-font-weight-normal"
+ class="commit-sha-container"
data-testid="commit-link"
>{{ pipeline.commit.short_id }}</gl-link
>
@@ -251,7 +250,7 @@ export default {
v-safe-html="sourceBranchLink"
:title="sourceBranch"
truncate-target="child"
- class="label-branch label-truncate gl-font-weight-normal"
+ class="label-branch label-truncate ref-container"
/>
</template>
<template v-if="finishedAt">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
index dd899701de0..2a18af90495 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
@@ -1,5 +1,6 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
import { STATUS_CLOSED, STATUS_MERGED } from '~/issues/constants';
import StatusIcon from './mr_widget_status_icon.vue';
import Actions from './action_buttons.vue';
@@ -13,14 +14,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: {
- expandDetailsTooltip: {
- default: '',
- },
- collapseDetailsTooltip: {
- default: '',
- },
- },
props: {
isCollapsible: {
type: Boolean,
@@ -57,6 +50,16 @@ export default {
required: false,
default: () => [],
},
+ expandDetailsTooltip: {
+ required: false,
+ type: String,
+ default: __('Expand merge details'),
+ },
+ collapseDetailsTooltip: {
+ required: false,
+ type: String,
+ default: __('Collapse merge details'),
+ },
},
computed: {
wrapperClasses() {
@@ -120,6 +123,7 @@ export default {
<gl-button
v-gl-tooltip
:title="collapsed ? expandDetailsTooltip : collapseDetailsTooltip"
+ :aria-label="collapsed ? expandDetailsTooltip : collapseDetailsTooltip"
:icon="collapsed ? 'chevron-lg-down' : 'chevron-lg-up'"
category="tertiary"
size="small"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
index 6299f0fcbb8..ec72b74daa2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
@@ -75,7 +75,6 @@ export default {
actions.push({
text: this.cancelButtonText,
loading: this.isCancellingAutoMerge,
- dataQaSelector: 'cancel_auto_merge_button',
class: 'js-cancel-auto-merge',
testId: 'cancelAutomaticMergeButton',
onClick: () => this.cancelAutomaticMerge(),
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
index 742f5d4de14..122abc7d034 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
@@ -51,7 +51,6 @@ export default {
text: s__('mrWidget|Refresh now'),
onClick: () => this.refresh(),
testId: 'merge-request-failed-refresh-button',
- dataQaSelector: 'merge_request_error_content',
},
];
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index 4d906f29cb0..4454718a647 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -67,7 +67,7 @@ export default {
actions.push({
text: this.revertLabel,
tooltipText: this.revertTitle,
- dataQaSelector: 'revert_button',
+ testId: 'revert-button',
onClick: () => this.openRevertModal(),
});
} else if (this.mr.revertInForkPath) {
@@ -75,7 +75,7 @@ export default {
text: this.revertLabel,
tooltipText: this.revertTitle,
href: this.mr.revertInForkPath,
- dataQaSelector: 'revert_button',
+ testId: 'revert-button',
dataMethod: 'post',
});
}
@@ -84,7 +84,7 @@ export default {
actions.push({
text: this.cherryPickLabel,
tooltipText: this.cherryPickTitle,
- dataQaSelector: 'cherry_pick_button',
+ testId: 'cherry-pick-button',
onClick: () => this.openCherryPickModal(),
});
} else if (this.mr.cherryPickInForkPath) {
@@ -92,7 +92,7 @@ export default {
text: this.cherryPickLabel,
tooltipText: this.cherryPickTitle,
href: this.mr.cherryPickInForkPath,
- dataQaSelector: 'cherry_pick_button',
+ testId: 'cherry-pick-button',
dataMethod: 'post',
});
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index 415f58ea8e6..a4afdee4d49 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -230,7 +230,6 @@ export default {
v-if="!rebasingError"
class="gl-w-100 gl-md-w-auto gl-flex-grow-1 gl-ml-0! gl-text-body! gl-md-mr-3"
data-testid="rebase-message"
- data-qa-selector="no_fast_forward_message_content"
>
<bold-text :message="$options.i18n.rebaseError" />
</span>
@@ -247,7 +246,6 @@ export default {
:loading="isMakingRequest"
variant="confirm"
size="small"
- data-qa-selector="mr_rebase_button"
data-testid="standard-rebase-button"
class="gl-align-self-start"
@click="tryRebase"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 0ce8389579d..ac434c5be4e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -634,7 +634,6 @@ export default {
variant="confirm"
:disabled="isMergeButtonDisabled"
:loading="isMakingRequest"
- data-qa-selector="merge_button"
@click="handleMergeButtonClick(isAutoMergeAvailable)"
>{{ mergeButtonText }}</gl-button
>
@@ -644,7 +643,6 @@ export default {
:disabled="isMergeButtonDisabled"
variant="confirm"
data-testid="merge-immediately-dropdown"
- data-qa-selector="merge_moment_dropdown"
toggle-class="btn-icon js-merge-moment"
>
<template #button-content>
@@ -655,7 +653,6 @@ export default {
icon-name="warning"
button-class="accept-merge-request"
data-testid="merge-immediately-button"
- data-qa-selector="merge_immediately_menu_item"
@click="handleMergeImmediatelyButtonClick"
>
{{ __('Merge immediately') }}
@@ -692,7 +689,7 @@ export default {
<div
v-else
class="gl-w-full gl-order-n1 mr-widget-merge-details"
- data-qa-selector="merged_status_content"
+ data-testid="merged-status-content"
>
<p v-if="showMergeDetailsHeader" class="gl-mb-2 gl-text-gray-900">
{{ __('Merge details') }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
index 9da754d01fc..00383418f2d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
@@ -32,7 +32,7 @@ export default {
>
<span
class="gl-md-mr-3 gl-flex-grow-1 gl-ml-0! gl-text-body!"
- data-qa-selector="head_mismatch_content"
+ data-testid="head-mismatch-content"
>
<bold-text :message="$options.i18n.I18N_SHA_MISMATCH.warningMessage" />
</span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
index 97ef96fe382..f1bd5bb25bb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
@@ -46,7 +46,7 @@ export default {
:disabled="isDisabled"
name="squash"
class="js-squash-checkbox gl-mr-2"
- data-qa-selector="squash_checkbox"
+ data-testid="squash-checkbox"
:title="tooltipTitle"
@change="(checked) => $emit('input', checked)"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue
index 9dd4e76befe..5b7657f15d9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue
@@ -1,12 +1,18 @@
<script>
-import { GlButton, GlDropdown, GlDropdownItem, GlTooltipDirective } from '@gitlab/ui';
-import { sprintf, __ } from '~/locale';
+import {
+ GlButton,
+ GlDisclosureDropdown,
+ GlIcon,
+ GlLoadingIcon,
+ GlTooltipDirective,
+} from '@gitlab/ui';
export default {
components: {
GlButton,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
+ GlIcon,
+ GlLoadingIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -29,10 +35,22 @@ export default {
};
},
computed: {
- dropdownLabel() {
- if (!this.widget) return undefined;
-
- return sprintf(__('%{widget} options'), { widget: this.widget });
+ dropdownItems() {
+ return this.tertiaryButtons.map((button) => {
+ return {
+ text: button.text,
+ href: button.href,
+ action: () => this.onClickAction(button),
+ icon: button.icon || button.iconName,
+ loading: button.loading,
+ extraAttrs: {
+ dataClipboardText: button.dataClipboardText,
+ dataMethod: button.dataMethod,
+ target: button.target,
+ disabled: button.disabled,
+ },
+ };
+ });
},
},
methods: {
@@ -62,44 +80,31 @@ export default {
return btn.tooltipText;
},
- actionButtonQaSelector(btn) {
- if (btn.dataQaSelector) {
- return btn.dataQaSelector;
- }
- return 'mr_widget_extension_actions_button';
- },
},
};
</script>
<template>
<div class="gl-display-flex gl-align-items-flex-start">
- <gl-dropdown
- v-gl-tooltip
- :title="__('Options')"
- :text="dropdownLabel"
+ <gl-disclosure-dropdown
+ :items="dropdownItems"
icon="ellipsis_v"
no-caret
category="tertiary"
- right
- lazy
+ placement="right"
text-sr-only
size="small"
toggle-class="gl-p-2!"
class="gl-display-block gl-md-display-none!"
>
- <gl-dropdown-item
- v-for="(btn, index) in tertiaryButtons"
- :key="index"
- :href="btn.href"
- :target="btn.target"
- :data-clipboard-text="btn.dataClipboardText"
- :data-method="btn.dataMethod"
- @click="onClickAction(btn)"
- >
- {{ btn.text }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <template #list-item="{ item }">
+ <span class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
+ {{ item.text }}
+ <gl-loading-icon v-if="item.loading" size="sm" />
+ <gl-icon v-else-if="item.icon" :name="item.icon" />
+ </span>
+ </template>
+ </gl-disclosure-dropdown>
<gl-button
v-for="(btn, index) in tertiaryButtons"
:id="btn.id"
@@ -110,9 +115,8 @@ export default {
:target="btn.target"
:class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]"
:data-clipboard-text="btn.dataClipboardText"
- :data-qa-selector="actionButtonQaSelector(btn)"
:data-method="btn.dataMethod"
- :icon="btn.icon"
+ :icon="btn.icon || btn.iconName"
:data-testid="btn.testId || 'extension-actions-button'"
:variant="btn.variant || 'confirm'"
:loading="btn.loading"
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index e8b97098a2b..5e9b72e13cf 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -29,6 +29,8 @@ export default () => {
gl.mrWidgetData.gitlabLogo = gon.gitlab_logo;
gl.mrWidgetData.defaultAvatarUrl = gon.default_avatar_url;
+ const dismissalDescriptions = JSON.parse(gl.mrWidgetData.dismissal_descriptions || '{}');
+
// This is a false violation of @gitlab/no-runtime-template-compiler, since it
// creates a new Vue instance by spreading a _valid_ Vue component definition
// into the Vue constructor.
@@ -43,6 +45,8 @@ export default () => {
canCreatePipelineInTargetProject: parseBoolean(
gl.mrWidgetData.can_create_pipeline_in_target_project,
),
+ commitPathTemplate: gl.mrWidgetData.commit_path_template,
+ dismissalDescriptions,
},
...MrWidgetOptions,
apolloProvider,
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 175a0b0563f..02d73cf9cbd 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -153,10 +153,6 @@ export default {
},
},
mixins: [mergeRequestQueryVariablesMixin],
- provide: {
- expandDetailsTooltip: __('Expand merge details'),
- collapseDetailsTooltip: __('Collapse merge details'),
- },
props: {
mrData: {
type: Object,
@@ -576,7 +572,7 @@ export default {
</mr-widget-alert-message>
</div>
- <div class="mr-widget-section" data-qa-selector="mr_widget_content">
+ <div class="mr-widget-section" data-testid="mr-widget-content">
<component :is="componentName" :mr="mr" :service="service" />
<ready-to-merge
v-if="mr.commitsCount"
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql
new file mode 100644
index 00000000000..6b602a0095c
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql
@@ -0,0 +1,12 @@
+query mergeChecks($projectPath: ID!, $iid: String!) {
+ project(fullPath: $projectPath) {
+ id
+ mergeRequest(iid: $iid) {
+ id
+ userPermissions {
+ canMerge
+ }
+ mergeChecks @client
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql
index faf21b28f86..a4c42070530 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql
@@ -5,6 +5,9 @@ query workInProgress($projectPath: ID!, $iid: String!) {
id
shouldBeRebased
sourceBranchProtected
+ userPermissions {
+ pushToSourceBranch
+ }
}
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index bb74f82145f..a1b86c86979 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -2,7 +2,7 @@ import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_ke
import { STATUS_CLOSED, STATUS_MERGED, STATUS_OPEN } from '~/issues/constants';
import { formatDate, getTimeago, timeagoLanguageCode } from '~/lib/utils/datetime_utility';
import { machine } from '~/lib/utils/finite_state_machine';
-import { badgeState } from '~/merge_requests/components/merge_request_status_badge.vue';
+import { badgeState } from '~/merge_requests/components/merge_request_header.vue';
import {
MTWPS_MERGE_STRATEGY,
MT_MERGE_STRATEGY,
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
index 8d2ef20b381..3855e4fc078 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import updateAlertStatusMutation from '~/graphql_shared/mutations/alert_status_update.mutation.graphql';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
@@ -11,10 +11,10 @@ export default {
'AlertManagement|There was an error while updating the status of the alert.',
),
UPDATE_ALERT_STATUS_INSTRUCTION: s__('AlertManagement|Please try again.'),
+ ASSIGN_STATUS_HEADER: s__('AlertManagement|Assign status'),
},
components: {
- GlDropdown,
- GlDropdownItem,
+ GlCollapsibleListbox,
},
inject: {
trackAlertStatusUpdateOptions: {
@@ -44,10 +44,20 @@ export default {
default: () => PAGE_CONFIG.OPERATIONS.STATUSES,
},
},
+ data() {
+ return {
+ alertStatus: this.alert.status,
+ };
+ },
computed: {
dropdownClass() {
- // eslint-disable-next-line no-nested-ternary
- return this.isSidebar ? (this.isDropdownShowing ? 'show' : 'gl-display-none') : '';
+ return this.isSidebar && !this.isDropdownShowing ? 'gl-display-none' : '';
+ },
+ items() {
+ return Object.entries(this.statuses).map(([value, text]) => ({ value, text }));
+ },
+ headerText() {
+ return this.isSidebar ? this.$options.i18n.ASSIGN_STATUS_HEADER : '';
},
},
methods: {
@@ -97,30 +107,15 @@ export default {
<template>
<div class="dropdown dropdown-menu-selectable" :class="dropdownClass">
- <gl-dropdown
+ <gl-collapsible-listbox
ref="dropdown"
- right
- :text="statuses[alert.status]"
- class="w-100"
- toggle-class="dropdown-menu-toggle"
- @keydown.esc.native="$emit('hide-dropdown')"
- @hide="$emit('hide-dropdown')"
- >
- <p v-if="isSidebar" class="gl-dropdown-header-top" data-testid="dropdown-header">
- {{ s__('AlertManagement|Assign status') }}
- </p>
- <div class="dropdown-content dropdown-body">
- <gl-dropdown-item
- v-for="(label, field) in statuses"
- :key="field"
- data-testid="statusDropdownItem"
- :active="field === alert.status"
- :active-class="'is-active'"
- @click="updateAlertStatus(field)"
- >
- {{ label }}
- </gl-dropdown-item>
- </div>
- </gl-dropdown>
+ v-model="alertStatus"
+ placement="right"
+ :header-text="headerText"
+ :items="items"
+ block
+ @hidden="$emit('hide-dropdown')"
+ @select="updateAlertStatus"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
index c512585b980..7b099516c5b 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
@@ -58,9 +58,10 @@ export default {
},
toggleFormDropdown() {
this.isDropdownShowing = !this.isDropdownShowing;
- const { dropdown } = this.$refs.status.$refs.dropdown.$refs;
+ const { dropdown } = this.$refs.status.$refs;
+
if (dropdown && this.isDropdownShowing) {
- dropdown.show();
+ dropdown.open();
}
},
handleUpdating(isMutationInProgress) {
diff --git a/app/assets/javascripts/vue_shared/components/badges/beta_badge.vue b/app/assets/javascripts/vue_shared/components/badges/beta_badge.vue
index e8d33b5538e..9cac176a06f 100644
--- a/app/assets/javascripts/vue_shared/components/badges/beta_badge.vue
+++ b/app/assets/javascripts/vue_shared/components/badges/beta_badge.vue
@@ -1,10 +1,10 @@
<script>
-import { GlBadge, GlPopover } from '@gitlab/ui';
import { s__ } from '~/locale';
+import HoverBadge from './hover_badge.vue';
export default {
name: 'BetaBadge',
- components: { GlBadge, GlPopover },
+ components: { HoverBadge },
i18n: {
badgeLabel: s__('BetaBadge|Beta'),
popoverTitle: s__("BetaBadge|What's Beta?"),
@@ -41,27 +41,16 @@ export default {
</script>
<template>
- <div>
- <gl-badge ref="badge" href="#" :size="size" variant="neutral" class="gl-cursor-pointer">{{
- $options.i18n.badgeLabel
- }}</gl-badge>
- <gl-popover
- triggers="hover focus click"
- :show-close-button="true"
- :target="target"
- :title="$options.i18n.popoverTitle"
- data-testid="beta-badge"
- >
- <p>{{ $options.i18n.descriptionParagraph }}</p>
+ <hover-badge :label="$options.i18n.badgeLabel" :size="size" :title="$options.i18n.popoverTitle">
+ <p>{{ $options.i18n.descriptionParagraph }}</p>
- <p class="gl-mb-0">{{ $options.i18n.listIntroduction }}</p>
+ <p class="gl-mb-0">{{ $options.i18n.listIntroduction }}</p>
- <ul class="gl-pl-4">
- <li>{{ $options.i18n.listItemStability }}</li>
- <li>{{ $options.i18n.listItemDataLoss }}</li>
- <li>{{ $options.i18n.listItemReasonableEffort }}</li>
- <li>{{ $options.i18n.listItemNearCompletion }}</li>
- </ul>
- </gl-popover>
- </div>
+ <ul class="gl-pl-4">
+ <li>{{ $options.i18n.listItemStability }}</li>
+ <li>{{ $options.i18n.listItemDataLoss }}</li>
+ <li>{{ $options.i18n.listItemReasonableEffort }}</li>
+ <li>{{ $options.i18n.listItemNearCompletion }}</li>
+ </ul>
+ </hover-badge>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/badges/experiment_badge.stories.js b/app/assets/javascripts/vue_shared/components/badges/experiment_badge.stories.js
new file mode 100644
index 00000000000..8e964c9bdf8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/badges/experiment_badge.stories.js
@@ -0,0 +1,24 @@
+import ExperimentBadge from './experiment_badge.vue';
+
+export default {
+ component: ExperimentBadge,
+ title: 'vue_shared/experiment-badge',
+};
+
+const template = `
+ <div style="height:600px;" class="gl-display-flex gl-justify-content-center gl-align-items-center">
+ <experiment-badge :size="size" />
+ </div>
+ `;
+
+const Template = (args, { argTypes }) => ({
+ components: { ExperimentBadge },
+ data() {
+ return { value: args.value };
+ },
+ props: Object.keys(argTypes),
+ template,
+});
+
+export const Default = Template.bind({});
+Default.args = {};
diff --git a/app/assets/javascripts/vue_shared/components/badges/experiment_badge.vue b/app/assets/javascripts/vue_shared/components/badges/experiment_badge.vue
new file mode 100644
index 00000000000..26bae71ddb8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/badges/experiment_badge.vue
@@ -0,0 +1,43 @@
+<script>
+import { s__ } from '~/locale';
+import HoverBadge from './hover_badge.vue';
+
+export default {
+ name: 'ExperimentBadge',
+ components: { HoverBadge },
+ i18n: {
+ badgeLabel: s__('ExperimentBadge|Experiment'),
+ popoverTitle: s__("ExperimentBadge|What's an Experiment?"),
+ descriptionParagraph: s__(
+ "ExperimentBadge|An Experiment is a feature that's in the process of being developed. It's not production-ready. We encourage users to try Experimental features and provide feedback.",
+ ),
+ listIntroduction: s__('ExperimentBadge|An Experiment:'),
+ listItemStability: s__('ExperimentBadge|May be unstable.'),
+ listItemDataLoss: s__('ExperimentBadge|Can cause data loss.'),
+ listItemNoSupport: s__('ExperimentBadge|Has no support and might not be documented.'),
+ listItemCanBeRemoved: s__('ExperimentBadge|Can be removed at any time.'),
+ },
+ props: {
+ size: {
+ type: String,
+ required: false,
+ default: 'md',
+ },
+ },
+};
+</script>
+
+<template>
+ <hover-badge :label="$options.i18n.badgeLabel" :size="size" :title="$options.i18n.popoverTitle">
+ <p>{{ $options.i18n.descriptionParagraph }}</p>
+
+ <p class="gl-mb-0">{{ $options.i18n.listIntroduction }}</p>
+
+ <ul class="gl-pl-4">
+ <li>{{ $options.i18n.listItemStability }}</li>
+ <li>{{ $options.i18n.listItemDataLoss }}</li>
+ <li>{{ $options.i18n.listItemNoSupport }}</li>
+ <li>{{ $options.i18n.listItemCanBeRemoved }}</li>
+ </ul>
+ </hover-badge>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/badges/hover_badge.vue b/app/assets/javascripts/vue_shared/components/badges/hover_badge.vue
new file mode 100644
index 00000000000..351c7bd9da0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/badges/hover_badge.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlBadge, GlPopover } from '@gitlab/ui';
+
+export default {
+ name: 'HoverBadge',
+ components: { GlBadge, GlPopover },
+ props: {
+ label: {
+ type: String,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ size: {
+ type: String,
+ required: false,
+ default: 'md',
+ },
+ },
+ methods: {
+ target() {
+ /**
+ * BVPopover retrieves the target during the `beforeDestroy` hook to deregister attached
+ * events. Since during `beforeDestroy` refs are `undefined`, it throws a warning in the
+ * console because we're trying to access the `$el` property of `undefined`. Optional
+ * chaining is not working in templates, which is why the method is used.
+ *
+ * See more on https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49628#note_464803276
+ */
+ return this.$refs.badge?.$el;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-badge ref="badge" href="#" :size="size" variant="neutral" class="gl-cursor-pointer">{{
+ label
+ }}</gl-badge>
+ <gl-popover
+ triggers="hover focus click"
+ :show-close-button="true"
+ :target="target"
+ :title="title"
+ >
+ <slot></slot>
+ </gl-popover>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
index 27bdcc69120..b52752d7e2f 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
@@ -41,7 +41,6 @@ export default {
mounted() {
this.renderRemainingMarkup();
handleBlobRichViewer(this.$refs.content, this.type);
- handleLocationHash();
},
methods: {
optimizeMarkupRendering() {
@@ -76,8 +75,7 @@ export default {
* */
if (!this.isMarkup || !this.remainingContent.length) {
- this.$emit(CONTENT_LOADED_EVENT);
- this.isLoading = false;
+ this.onContentLoaded();
return;
}
@@ -89,11 +87,15 @@ export default {
setTimeout(() => {
fileContent.append(...content);
if (nextChunkEnd < this.remainingContent.length) return;
- this.$emit(CONTENT_LOADED_EVENT);
- this.isLoading = false;
+ this.onContentLoaded();
}, i);
}
},
+ onContentLoaded() {
+ this.$emit(CONTENT_LOADED_EVENT);
+ handleLocationHash();
+ this.isLoading = false;
+ },
},
safeHtmlConfig: {
ADD_TAGS: ['gl-emoji', 'copy-code'],
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index 1f45b4c5c9d..abbeac0e098 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -57,16 +57,29 @@ export default {
return badgeSizeOptions[value] !== undefined;
},
},
+ showTooltip: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ useLink: {
+ type: Boolean,
+ default: true,
+ required: false,
+ },
},
computed: {
- isSmallBadgeSize() {
- return this.size === badgeSizeOptions.sm;
+ isNotLargeBadgeSize() {
+ return this.size !== badgeSizeOptions.lg;
},
title() {
- return !this.showText ? this.status?.text : '';
+ return this.showTooltip && !this.showText ? this.status?.text : '';
},
detailsPath() {
// For now, this can either come from graphQL with camelCase or REST API in snake_case
+ if (!this.useLink) {
+ return null;
+ }
return this.status.detailsPath || this.status.details_path;
},
badgeStyles() {
@@ -121,7 +134,7 @@ export default {
<template>
<gl-badge
v-gl-tooltip
- :class="{ 'gl-pl-2': isSmallBadgeSize }"
+ :class="{ 'gl-px-2': !showText && isNotLargeBadgeSize }"
:title="title"
:href="detailsPath"
:size="size"
diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown.vue
index fa7c5bc1978..066b761ac9b 100644
--- a/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown.vue
@@ -47,13 +47,13 @@ export default {
v-if="sshLink"
:label="$options.labels.ssh"
:link="sshLink"
- qa-selector="copy_ssh_url_button"
+ test-id="copy-ssh-url-button"
/>
<clone-dropdown-item
v-if="httpLink"
:label="httpLabel"
:link="httpLink"
- qa-selector="copy_http_url_button"
+ test-id="copy-http-url-button"
/>
</gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue
index 0e322ebc686..6980e19733a 100644
--- a/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue
+++ b/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue
@@ -27,7 +27,7 @@ export default {
type: String,
required: true,
},
- qaSelector: {
+ testId: {
type: String,
required: true,
},
@@ -45,7 +45,7 @@ export default {
:title="$options.copyURLTooltip"
:aria-label="$options.copyURLTooltip"
:data-clipboard-text="link"
- :data-qa-selector="qaSelector"
+ :data-testid="testId"
icon="copy-to-clipboard"
class="gl-display-inline-flex"
/>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
index b34a6b11092..1f5896204ee 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
@@ -144,7 +144,7 @@ export default {
</slot>
</template>
<slot name="default">
- <gl-dropdown-form class="gl-relative gl-min-h-7" data-qa-selector="labels_dropdown_content">
+ <gl-dropdown-form class="gl-relative gl-min-h-7" data-testid="labels-dropdown-content">
<gl-loading-icon
v-if="isLoading"
size="lg"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
index 5a7382bcd7c..23de8dd5596 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
@@ -262,6 +262,7 @@ export default {
{{ __('No matches found') }}
</gl-dropdown-text>
<gl-dropdown-text v-else-if="hasFetched">{{ __('No suggestions found') }}</gl-dropdown-text>
+ <slot name="footer"></slot>
</template>
</gl-filtered-search-token>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
index c294c23abfc..4601287b417 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
@@ -4,6 +4,8 @@ import { compact } from 'lodash';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
+import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
+import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql';
import { OPTIONS_NONE_ANY } from '../constants';
import BaseToken from './base_token.vue';
@@ -41,6 +43,12 @@ export default {
preloadedUsers() {
return this.config.preloadedUsers || [];
},
+ namespace() {
+ return this.config.isProject ? WORKSPACE_PROJECT : WORKSPACE_GROUP;
+ },
+ fetchUsersQuery() {
+ return this.config.fetchUsers ? this.config.fetchUsers : this.fetchUsersBySearchTerm;
+ },
},
methods: {
getActiveUser(users, data) {
@@ -49,11 +57,19 @@ export default {
getAvatarUrl(user) {
return user.avatarUrl || user.avatar_url;
},
+ fetchUsersBySearchTerm(search) {
+ return this.$apollo
+ .query({
+ query: usersAutocompleteQuery,
+ variables: { fullPath: this.config.fullPath, search, isProject: this.config.isProject },
+ })
+ .then(({ data }) => data[this.namespace]?.autocompleteUsers);
+ },
fetchUsers(searchTerm) {
this.loading = true;
const fetchPromise = this.config.fetchPath
? this.config.fetchUsers(this.config.fetchPath, searchTerm)
- : this.config.fetchUsers(searchTerm);
+ : this.fetchUsersQuery(searchTerm);
fetchPromise
.then((res) => {
diff --git a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
index ebc6b2cd740..d97f1ae6135 100644
--- a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
+++ b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
@@ -156,7 +156,7 @@ export default {
<gl-form-input
ref="input"
:readonly="readonly"
- :size="size"
+ :width="size"
class="gl-font-monospace! gl-cursor-default!"
v-bind="formInputGroupProps"
:value="value"
@@ -183,7 +183,7 @@ export default {
v-if="showCopyButton"
:text="value"
:title="copyButtonTitle"
- data-qa-selector="clipboard_button"
+ data-testid="clipboard-button"
@click="handleCopyButtonClick"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/incubation/pagination.vue b/app/assets/javascripts/vue_shared/components/incubation/pagination.vue
index b5afe92316a..6b70e9f3ed9 100644
--- a/app/assets/javascripts/vue_shared/components/incubation/pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/incubation/pagination.vue
@@ -57,6 +57,7 @@ export default {
:next-text="$options.i18n.nextPageButtonLabel"
:prev-button-link="previousPageLink"
:next-button-link="nextPageLink"
+ class="gl-mt-4"
/>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
index 05ce007e615..4ebd8861a67 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
@@ -60,7 +60,7 @@ export default {
<template>
<gl-disclosure-dropdown
- data-qa-selector="apply_suggestion_dropdown"
+ data-testid="apply-suggestion-dropdown"
fluid-width
placement="right"
size="small"
@@ -81,7 +81,7 @@ export default {
class="apply-suggestions-input-min-width"
:placeholder="defaultCommitMessage"
submit-on-enter
- data-qa-selector="commit_message_field"
+ data-testid="commit-message-field"
@submit="onApply"
/>
@@ -93,7 +93,7 @@ export default {
class="gl-w-auto! gl-mt-3 gl-align-self-end"
category="primary"
variant="confirm"
- data-qa-selector="commit_with_custom_message_button"
+ data-testid="commit-with-custom-message-button"
@click="onApply"
>
{{ __('Apply') }}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
index f7f5ccdbf31..d99b90fa561 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
@@ -64,8 +64,8 @@ export default {
const savedReply = this.savedReplies.find((r) => r.id === id);
if (savedReply) {
this.$emit('select', savedReply.content);
- this.track_event(TRACKING_SAVED_REPLIES_USE);
- this.track_event(
+ this.trackEvent(TRACKING_SAVED_REPLIES_USE);
+ this.trackEvent(
isInMr ? TRACKING_SAVED_REPLIES_USE_IN_MR : TRACKING_SAVED_REPLIES_USE_IN_OTHER,
);
}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue
index 2426a917a53..1327436a9b4 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue
@@ -1,16 +1,10 @@
<script>
-import { GlButton, GlPopover, GlLink } from '@gitlab/ui';
-import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
-import RICH_TEXT_EDITOR_ILLUSTRATION from '../../../../images/callouts/rich_text_editor_illustration.svg?url';
-import { counter } from './utils';
export default {
components: {
GlButton,
- GlLink,
- GlPopover,
- UserCalloutDismisser,
},
props: {
value: {
@@ -18,15 +12,7 @@ export default {
required: true,
},
},
- data() {
- return {
- counter: counter(),
- };
- },
computed: {
- showPromoPopover() {
- return this.markdownEditorSelected && this.counter === 0;
- },
markdownEditorSelected() {
return this.value === 'markdown';
},
@@ -36,84 +22,19 @@ export default {
: __('Switch to plain text editing');
},
},
- methods: {
- switchEditorType(insertTemplate = false) {
- this.$emit('switch', insertTemplate);
- },
- },
richTextEditorButtonId: 'switch-to-rich-text-editor',
- RICH_TEXT_EDITOR_ILLUSTRATION,
};
</script>
<template>
<div class="content-editor-switcher gl-display-inline-flex gl-align-items-center">
- <user-callout-dismisser feature-name="rich_text_editor">
- <template #default="{ dismiss, shouldShowCallout }">
- <div>
- <gl-popover
- :target="$options.richTextEditorButtonId"
- :show="Boolean(showPromoPopover && shouldShowCallout)"
- show-close-button
- :css-classes="['rich-text-promo-popover gl-p-2']"
- triggers="manual"
- data-testid="rich-text-promo-popover"
- @close-button-clicked="dismiss"
- >
- <img
- :src="$options.RICH_TEXT_EDITOR_ILLUSTRATION"
- :alt="''"
- class="rich-text-promo-popover-illustration"
- width="280"
- height="130"
- />
- <h5 class="gl-mt-3 gl-mb-3">{{ __('Writing just got easier') }}</h5>
- <p class="gl-m-0">
- {{
- __(
- 'Use the new rich text editor to see your text and tables fully formatted as you type. No need to remember any formatting syntax, or switch between preview and editing modes!',
- )
- }}
- </p>
- <gl-link
- class="gl-button btn btn-confirm block gl-mb-2 gl-mt-4"
- variant="confirm"
- category="primary"
- target="_blank"
- block
- @click="
- switchEditorType(showPromoPopover);
- dismiss();
- "
- >
- {{ __('Try the rich text editor now') }}
- </gl-link>
- </gl-popover>
- <gl-button
- :id="$options.richTextEditorButtonId"
- class="btn btn-default btn-sm gl-button btn-default-tertiary gl-font-sm! gl-text-secondary! gl-px-4!"
- data-qa-selector="editing_mode_switcher"
- @click="
- switchEditorType();
- dismiss();
- "
- >{{ text }}</gl-button
- >
- </div>
- </template>
- </user-callout-dismisser>
+ <gl-button
+ :id="$options.richTextEditorButtonId"
+ size="small"
+ category="tertiary"
+ class="gl-font-sm! gl-text-secondary! gl-px-4!"
+ data-testid="editing-mode-switcher"
+ @click="$emit('switch')"
+ >{{ text }}</gl-button
+ >
</div>
</template>
-<style>
-.rich-text-promo-popover {
- box-shadow: 0 0 18px -1.9px rgba(119, 89, 194, 0.16), 0 0 12.9px -1.7px rgba(119, 89, 194, 0.16),
- 0 0 9.2px -1.4px rgba(119, 89, 194, 0.16), 0 0 6.4px -1.1px rgba(119, 89, 194, 0.16),
- 0 0 4.5px -0.8px rgba(119, 89, 194, 0.16), 0 0 3px -0.6px rgba(119, 89, 194, 0.16),
- 0 0 1.8px -0.3px rgba(119, 89, 194, 0.16), 0 0 0.6px rgba(119, 89, 194, 0.16);
- z-index: 999;
-}
-
-.rich-text-promo-popover-illustration {
- width: calc(100% + 32px);
- margin: -32px -16px 0;
-}
-</style>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index a26f8f71601..24211833026 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -12,6 +12,8 @@ import { __, sprintf } from '~/locale';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
+import { MARKDOWN_EDITOR_READY_EVENT } from '~/vue_shared/constants';
+import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub';
import MarkdownHeader from './header.vue';
import MarkdownToolbar from './toolbar.vue';
@@ -259,7 +261,8 @@ export default {
},
mounted() {
// GLForm class handles all the toolbar buttons
- return new GLForm(
+ // eslint-disable-next-line no-new
+ new GLForm(
$(this.$refs['gl-form']),
{
emojis: this.enableAutocomplete,
@@ -276,6 +279,8 @@ export default {
true,
this.autocompleteDataSources,
);
+
+ markdownEditorEventHub.$emit(MARKDOWN_EDITOR_READY_EVENT);
},
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('glForm');
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 286a1b87ad0..741bdfd211b 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -263,7 +263,7 @@ export default {
<gl-button
v-if="enablePreview"
data-testid="preview-toggle"
- value="preview"
+ :value="previewMarkdown ? 'preview' : 'edit'"
:label="$options.i18n.previewTabTitle"
class="js-md-preview-button gl-flex-direction-row-reverse gl-align-items-center gl-font-weight-normal! gl-mr-2"
size="small"
@@ -281,7 +281,7 @@ export default {
:tag-content="lineContent"
tracking-property="codeSuggestion"
icon="doc-code"
- data-qa-selector="suggestion_button"
+ data-testid="suggestion-button"
class="js-suggestion-btn"
@click="handleSuggestDismissed"
/>
@@ -305,7 +305,7 @@ export default {
variant="confirm"
category="primary"
size="small"
- data-qa-selector="dismiss_suggestion_popover_button"
+ data-testid="dismiss-suggestion-popover-button"
@click="handleSuggestDismissed"
>
{{ __('Got it') }}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
index fc7e0a7c732..4a3c3cf0053 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -248,6 +248,13 @@ export default {
});
}
},
+ onKeydown(event) {
+ const isModifierKey = event.ctrlKey || event.metaKey;
+ if (isModifierKey && event.key === 'k') {
+ event.preventDefault();
+ }
+ this.$emit('keydown', event);
+ },
},
EDITING_MODE_KEY,
};
@@ -292,7 +299,7 @@ export default {
class="note-textarea js-gfm-input markdown-area"
dir="auto"
:data-supports-quick-actions="supportsQuickActions"
- :data-qa-selector="formFieldProps['data-qa-selector'] || 'markdown_editor_form_field'"
+ :data-testid="formFieldProps['data-testid'] || 'markdown-editor-form-field'"
:disabled="disabled"
@input="updateMarkdownFromMarkdownField"
@keydown="$emit('keydown', $event)"
@@ -317,13 +324,13 @@ export default {
:code-suggestions-config="codeSuggestionsConfig"
@initialized="setEditorAsAutofocused"
@change="updateMarkdownFromContentEditor"
- @keydown="$emit('keydown', $event)"
+ @keydown="onKeydown"
@enableMarkdownEditor="onEditingModeChange('markdownField')"
/>
<input
v-bind="formFieldProps"
:value="markdown"
- data-qa-selector="markdown_editor_form_field"
+ data-testid="markdown-editor-form-field"
type="hidden"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
index 6c2f084591e..f7fb1339bbc 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
+++ b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
@@ -105,7 +105,6 @@ export function mountMarkdownEditor(options = {}) {
return h(MarkdownEditor, {
props: {
setFacade,
- enableContentEditor: Boolean(gon.features?.contentEditorOnIssues),
value: formFieldValue,
renderMarkdownPath,
markdownDocsPath,
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index 8a0ca8ebac1..a822e2a6151 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -144,13 +144,13 @@ export default {
<gl-icon name="question-o" css-classes="link-highlight" />
</a>
</div>
- <gl-badge v-if="isApplied" variant="success" data-qa-selector="applied_badge">
+ <gl-badge v-if="isApplied" variant="success" data-testid="applied-badge">
{{ __('Applied') }}
</gl-badge>
<div
v-else-if="isApplying"
class="gl-display-flex gl-align-items-center text-secondary"
- data-qa-selector="applying_badge"
+ data-testid="applying-badge"
>
<gl-loading-icon size="sm" class="gl-align-items-center gl-justify-content-center gl-mr-3" />
<span>{{ applyingSuggestionsMessage }}</span>
@@ -169,7 +169,7 @@ export default {
<div v-else-if="!isDisableButton && suggestionsCount > 1">
<gl-button
class="btn-inverted js-add-to-batch-btn btn-grouped"
- data-qa-selector="add_suggestion_batch_button"
+ data-testid="add-suggestion-batch-button"
:disabled="isDisableButton"
size="small"
@click="addSuggestionToBatch"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index a4516fae73d..c0c8c4735e7 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -2,8 +2,6 @@
<script>
import { GlButton, GlLoadingIcon, GlSprintf, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { updateText } from '~/lib/utils/text_markdown';
-import { __, sprintf } from '~/locale';
-import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility';
import EditorModeSwitcher from './editor_mode_switcher.vue';
export default {
@@ -56,23 +54,6 @@ export default {
});
}
},
- handleEditorModeChanged(isFirstSwitch) {
- if (isFirstSwitch) {
- this.insertIntoTextarea(
- __(`### Rich text editor`),
- '',
- sprintf(
- __(
- 'Try out **styling** _your_ content right here or read the [direction](%{directionUrl}).',
- ),
- {
- directionUrl: `${PROMO_URL}/direction/plan/knowledge/content_editor/`,
- },
- ),
- );
- }
- this.$emit('enableContentEditor');
- },
},
};
</script>
@@ -91,7 +72,7 @@ export default {
v-if="showEditorModeSwitcher"
size="small"
value="markdown"
- @switch="handleEditorModeChanged"
+ @switch="$emit('enableContentEditor')"
/>
<div class="gl-display-flex">
<div v-if="canAttachFile" class="uploading-container gl-font-sm gl-line-height-32 gl-mr-3">
@@ -152,6 +133,7 @@ export default {
category="tertiary"
size="small"
:title="__('Markdown is supported')"
+ :aria-label="__('Markdown is supported')"
class="gl-px-3!"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/utils.js b/app/assets/javascripts/vue_shared/components/markdown/utils.js
deleted file mode 100644
index 0227d5a0fbc..00000000000
--- a/app/assets/javascripts/vue_shared/components/markdown/utils.js
+++ /dev/null
@@ -1,7 +0,0 @@
-let i = 0;
-
-export const counter = () => {
- const n = i;
- i += 1;
- return n;
-};
diff --git a/app/assets/javascripts/vue_shared/components/mr_more_dropdown.vue b/app/assets/javascripts/vue_shared/components/mr_more_dropdown.vue
index 7871721f38b..5c6766bbe45 100644
--- a/app/assets/javascripts/vue_shared/components/mr_more_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/mr_more_dropdown.vue
@@ -19,7 +19,6 @@ import MergeRequest from '~/merge_request';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
-import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
import { TYPE_MERGE_REQUEST } from '~/issues/constants';
Vue.use(VueApollo);
@@ -50,7 +49,6 @@ export default {
GlDisclosureDropdownGroup,
SidebarSubscriptionsWidget,
AbuseCategorySelector,
- NewHeaderActionsPopover,
SummaryNotesToggle: () =>
import('ee_component/merge_requests/components/summary_notes_toggle.vue'),
},
@@ -143,6 +141,9 @@ export default {
isMovedMrSidebar() {
return this.glFeatures.movedMrSidebar;
},
+ isNotificationsTodosButtons() {
+ return this.glFeatures.notificationsTodosButtons && this.glFeatures.movedMrSidebar;
+ },
draftLabel() {
return this.draft ? this.$options.i18n.markAsReady : this.$options.i18n.markAsDraft;
},
@@ -250,7 +251,9 @@ export default {
/>
</div>
</template>
- <gl-disclosure-dropdown-group v-if="isLoggedIn && isMovedMrSidebar">
+ <gl-disclosure-dropdown-group
+ v-if="isLoggedIn && isMovedMrSidebar && !isNotificationsTodosButtons"
+ >
<sidebar-subscriptions-widget
:iid="String(mr.iid)"
:full-path="fullPath"
@@ -261,7 +264,10 @@ export default {
<gl-disclosure-dropdown-group
bordered
- :class="{ 'gl-mt-0! gl-pt-0! gl-border-t-0!': !(isLoggedIn && isMovedMrSidebar) }"
+ :class="{
+ 'gl-mt-0! gl-pt-0! gl-border-t-0!':
+ !(isLoggedIn && isMovedMrSidebar) || isNotificationsTodosButtons,
+ }"
>
<gl-disclosure-dropdown-item
v-if="canUpdateMergeRequest"
@@ -358,8 +364,6 @@ export default {
</gl-disclosure-dropdown-group>
</gl-disclosure-dropdown>
- <new-header-actions-popover v-if="isMovedMrSidebar" :issue-type="issuableType" />
-
<abuse-category-selector
v-if="!isCurrentUser && isReportAbuseDrawerOpen"
:reported-user-id="reportedUserId"
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index fac32bfdb24..cb9b85b9ef3 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -12,7 +12,7 @@ export default {
</script>
<template>
- <timeline-entry-item class="note note-wrapper" data-qa-selector="skeleton_note_placeholder">
+ <timeline-entry-item class="note note-wrapper">
<div
class="gl-float-left gl--flex-center gl-rounded-full gl-mt-n1 gl-ml-2 gl-w-6 gl-h-6 gl-bg-gray-50 gl-text-gray-600"
></div>
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue
index 36e608a068b..f59664e8d1d 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlCollapsibleListbox, GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import { REGISTRATION_TOKEN_PLACEHOLDER } from '../constants';
@@ -8,8 +8,7 @@ import getRunnerSetupInstructionsQuery from '../graphql/get_runner_setup.query.g
export default {
components: {
GlButton,
- GlDropdown,
- GlDropdownItem,
+ GlCollapsibleListbox,
GlLoadingIcon,
ModalCopyButton,
},
@@ -27,7 +26,7 @@ export default {
},
data() {
return {
- selectedArchitecture: this.platform?.architectures[0] || null,
+ selectedArchName: this.platform?.architectures[0]?.name || null,
instructions: null,
};
},
@@ -55,6 +54,9 @@ export default {
architectures() {
return this.platform?.architectures || [];
},
+ selectedArchitecture() {
+ return this.architectures.find(({ name }) => name === this.selectedArchName) || null;
+ },
binaryUrl() {
return this.selectedArchitecture?.downloadLocation;
},
@@ -69,20 +71,22 @@ export default {
}
return registerInstructions;
},
+ listboxItems() {
+ return this.architectures.map(({ name }) => {
+ return { text: name, value: name };
+ });
+ },
},
watch: {
platform() {
// reset selection if architecture is not in this list
- const arch = this.architectures.find(({ name }) => name === this.selectedArchitecture.name);
+ const arch = this.architectures.find(({ name }) => name === this.selectedArchName);
if (!arch) {
- this.selectArchitecture(this.architectures[0]);
+ this.selectedArchName = this.architectures[0]?.name || null;
}
},
},
methods: {
- selectArchitecture(architecture) {
- this.selectedArchitecture = architecture;
- },
onClose() {
this.$emit('close');
},
@@ -104,18 +108,7 @@ export default {
<gl-loading-icon v-if="$apollo.loading" size="sm" inline />
</h5>
- <gl-dropdown class="gl-mb-3" :text="selectedArchitecture.name">
- <gl-dropdown-item
- v-for="architecture in architectures"
- :key="architecture.name"
- is-check-item
- :is-checked="selectedArchitecture.name === architecture.name"
- data-testid="architecture-dropdown-item"
- @click="selectArchitecture(architecture)"
- >
- {{ architecture.name }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <gl-collapsible-listbox v-model="selectedArchName" class="gl-mb-3" :items="listboxItems" />
<div class="gl-sm-display-flex gl-align-items-center gl-mb-3">
<h5>{{ $options.i18n.downloadInstallBinary }}</h5>
<gl-button
diff --git a/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue b/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue
index f50706b6de8..e0e8200580a 100644
--- a/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue
+++ b/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue
@@ -1,6 +1,21 @@
<script>
import { GlButtonGroup, GlButton } from '@gitlab/ui';
+const validateOptionsProp = (options) => {
+ const requiredOptionPropType = {
+ value: ['string', 'number', 'boolean'],
+ disabled: ['boolean', 'undefined'],
+ };
+ const optionProps = Object.keys(requiredOptionPropType);
+
+ return options.every((option) => {
+ if (!option) {
+ return false;
+ }
+ return optionProps.every((name) => requiredOptionPropType[name].includes(typeof option[name]));
+ });
+};
+
// TODO: We're planning to move this component to GitLab UI
// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1787
export default {
@@ -12,6 +27,7 @@ export default {
options: {
type: Array,
required: true,
+ validator: validateOptionsProp,
},
value: {
type: [String, Number, Boolean],
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue
new file mode 100644
index 00000000000..9bce9402afa
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue
@@ -0,0 +1,51 @@
+<script>
+import { GlTooltipDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import CommitInfo from '~/repository/components/commit_info.vue';
+import { calculateBlameOffset, toggleBlameClasses } from '../utils';
+
+export default {
+ name: 'BlameInfo',
+ components: {
+ CommitInfo,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ SafeHtml,
+ },
+ props: {
+ blameData: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ blameInfo() {
+ return this.blameData.map((blame, index) => ({
+ ...blame,
+ blameOffset: calculateBlameOffset(blame.lineno, index),
+ }));
+ },
+ },
+ mounted() {
+ toggleBlameClasses(this.blameData, true);
+ },
+ destroyed() {
+ toggleBlameClasses(this.blameData, false);
+ },
+};
+</script>
+<template>
+ <div class="blame gl-bg-gray-10">
+ <div class="blame-commit gl-border-none!">
+ <commit-info
+ v-for="(blame, index) in blameInfo"
+ :key="index"
+ :class="{ 'gl-border-t': index !== 0 }"
+ class="gl-display-flex gl-absolute gl-px-3"
+ :style="{ top: blame.blameOffset }"
+ :commit="blame.commit"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
index 797a38d8171..4d5d877d43b 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
@@ -258,7 +258,7 @@ export default {
:class="$options.userColorScheme"
data-type="simple"
:data-path="blob.path"
- data-qa-selector="blob_viewer_file_content"
+ data-testid="blob-viewer-file-content"
>
<codeowners-validation
v-if="isCodeownersFile"
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
new file mode 100644
index 00000000000..af01653fc0d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
@@ -0,0 +1,37 @@
+const BLAME_INFO_CLASSLIST = ['gl-border-t', 'gl-border-gray-500', 'gl-pt-3!'];
+const PADDING_BOTTOM_LARGE = 'gl-pb-6!';
+const PADDING_BOTTOM_SMALL = 'gl-pb-3!';
+
+const findLineNumberElement = (lineNumber) => document.getElementById(`L${lineNumber}`);
+
+const findLineContentElement = (lineNumber) => document.getElementById(`LC${lineNumber}`);
+
+export const calculateBlameOffset = (lineNumber) => {
+ if (lineNumber === 1) return '0px';
+ const lineContentOffset = findLineContentElement(lineNumber)?.offsetTop;
+ return `${lineContentOffset}px`;
+};
+
+export const toggleBlameClasses = (blameData, isVisible) => {
+ /**
+ * Adds/removes classes to line number/content elements to match the line with the blame info
+ * */
+ const method = isVisible ? 'add' : 'remove';
+ blameData.forEach(({ lineno, span }) => {
+ const lineNumberEl = findLineNumberElement(lineno)?.parentElement;
+ const lineContentEl = findLineContentElement(lineno);
+ const lineNumberSpanEl = findLineNumberElement(lineno + span - 1)?.parentElement;
+ const lineContentSpanEl = findLineContentElement(lineno + span - 1);
+
+ lineNumberEl?.classList[method](...BLAME_INFO_CLASSLIST);
+ lineContentEl?.classList[method](...BLAME_INFO_CLASSLIST);
+
+ if (span === 1) {
+ lineNumberSpanEl?.classList[method](PADDING_BOTTOM_LARGE);
+ lineContentSpanEl?.classList[method](PADDING_BOTTOM_LARGE);
+ } else {
+ lineNumberSpanEl?.classList[method](PADDING_BOTTOM_SMALL);
+ lineContentSpanEl?.classList[method](PADDING_BOTTOM_SMALL);
+ }
+ });
+};
diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
index 7c9a1bcd8cc..058a00e169a 100644
--- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
+++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlTruncate, GlTooltipDirective } from '@gitlab/ui';
import { DATE_TIME_FORMATS, DEFAULT_DATE_TIME_FORMAT } from '~/lib/utils/datetime_utility';
import timeagoMixin from '../mixins/timeago';
@@ -12,6 +12,9 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ components: {
+ GlTruncate,
+ },
mixins: [timeagoMixin],
props: {
time: {
@@ -34,11 +37,19 @@ export default {
default: DEFAULT_DATE_TIME_FORMAT,
validator: (timeFormat) => DATE_TIME_FORMATS.includes(timeFormat),
},
+ enableTruncation: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
timeAgo() {
return this.timeFormatted(this.time, this.dateTimeFormat);
},
+ tooltipText() {
+ return this.enableTruncation ? undefined : this.tooltipTitle(this.time);
+ },
},
};
</script>
@@ -46,8 +57,11 @@ export default {
<time
v-gl-tooltip.viewport="{ placement: tooltipPlacement }"
:class="cssClass"
- :title="tooltipTitle(time)"
+ :title="tooltipText"
:datetime="time"
- ><slot :time-ago="timeAgo">{{ timeAgo }}</slot></time
+ ><slot :time-ago="timeAgo"
+ ><template v-if="enableTruncation"><gl-truncate :text="timeAgo" with-tooltip /></template
+ ><template v-else>{{ timeAgo }}</template></slot
+ ></time
>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/toggle_labels.vue b/app/assets/javascripts/vue_shared/components/toggle_labels.vue
new file mode 100644
index 00000000000..05c837e32f0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/toggle_labels.vue
@@ -0,0 +1,62 @@
+<script>
+import { GlToggle } from '@gitlab/ui';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import isShowingLabelsQuery from '~/graphql_shared/client/is_showing_labels.query.graphql';
+import setIsShowingLabelsMutation from '~/graphql_shared/client/set_is_showing_labels.mutation.graphql';
+
+export default {
+ components: {
+ GlToggle,
+ LocalStorageSync,
+ },
+ data() {
+ return {
+ isShowingLabels: null,
+ };
+ },
+ apollo: {
+ isShowingLabels: {
+ query: isShowingLabelsQuery,
+ update: (data) => data.isShowingLabels,
+ },
+ },
+ computed: {
+ trackProperty() {
+ return this.isShowingLabels ? 'on' : 'off';
+ },
+ },
+ methods: {
+ setShowLabels(val) {
+ this.$apollo.mutate({
+ mutation: setIsShowingLabelsMutation,
+ variables: {
+ isShowingLabels: val,
+ },
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="board-labels-toggle-wrapper gl-display-flex gl-align-items-center gl-ml-3 gl-h-7">
+ <local-storage-sync
+ :value="isShowingLabels"
+ storage-key="gl-show-board-labels"
+ @input="setShowLabels"
+ />
+ <gl-toggle
+ :value="isShowingLabels"
+ :label="__('Show labels')"
+ :data-track-property="trackProperty"
+ data-track-action="toggle"
+ data-track-label="show_labels"
+ label-position="left"
+ aria-describedby="board-labels-toggle-text"
+ data-testid="show-labels-toggle"
+ data-qa-selector="show_labels_toggle"
+ class="gl-flex-direction-row"
+ @change="setShowLabels"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue b/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue
index 9665e188469..46496d2e483 100644
--- a/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue
+++ b/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue
@@ -2,12 +2,7 @@
export default {
provide() {
return {
- // We can't use this.vuexModule due to bug in vue-apollo when
- // provide is called in beforeCreate
- // See https://github.com/vuejs/vue-apollo/pull/1153 for details
-
- // @vue-compat does not care to normalize propsData fields
- vuexModule: this.$options.propsData.vuexModule ?? this.$options.propsData['vuex-module'],
+ vuexModule: this.vuexModule,
};
},
props: {
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index beb8321a271..9fb0add5522 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -355,7 +355,7 @@ export default {
<span data-testid="action-primary-text" class="gl-font-weight-bold gl-mb-2">{{
action.text
}}</span>
- <span data-testid="action-secondary-text" class="gl-text-gray-700">
+ <span data-testid="action-secondary-text" class="gl-font-sm gl-text-secondary">
{{ action.secondaryText }}
</span>
</div>
diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js
index 9c001fa2e9a..81e75c4e1d5 100644
--- a/app/assets/javascripts/vue_shared/constants.js
+++ b/app/assets/javascripts/vue_shared/constants.js
@@ -97,4 +97,7 @@ export const confidentialityInfoText = (workspaceType, issuableType) =>
export const EDITING_MODE_KEY = 'gl-markdown-editor-mode';
export const EDITING_MODE_MARKDOWN_FIELD = 'markdownField';
export const EDITING_MODE_CONTENT_EDITOR = 'contentEditor';
+
export const CLEAR_AUTOSAVE_ENTRY_EVENT = 'markdown_clear_autosave_entry';
+export const CONTENT_EDITOR_READY_EVENT = 'content_editor_ready';
+export const MARKDOWN_EDITOR_READY_EVENT = 'markdown_editor_ready';
diff --git a/app/assets/javascripts/vue_shared/global_search/constants.js b/app/assets/javascripts/vue_shared/global_search/constants.js
index 43110c0c9af..14ea0389bad 100644
--- a/app/assets/javascripts/vue_shared/global_search/constants.js
+++ b/app/assets/javascripts/vue_shared/global_search/constants.js
@@ -8,6 +8,7 @@ export const ALL_GITLAB = __('All GitLab');
export const SEARCH_GITLAB = s__('GlobalSearch|Search GitLab');
export const PLACES = s__('GlobalSearch|Places');
+export const COMMAND_PALETTE = s__('GlobalSearch|Command palette');
export const SEARCH_DESCRIBED_BY_DEFAULT = s__(
'GlobalSearch|%{count} default results provided. Use the up and down arrow keys to navigate search results list.',
);
diff --git a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue
index 033bb8c3885..679332163b5 100644
--- a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue
@@ -22,6 +22,10 @@ export default {
type: String,
required: true,
},
+ issuableType: {
+ type: String,
+ required: true,
+ },
},
};
</script>
@@ -34,6 +38,7 @@ export default {
:description-help-path="descriptionHelpPath"
:labels-fetch-path="labelsFetchPath"
:labels-manage-path="labelsManagePath"
+ :issuable-type="issuableType"
>
<template #actions="issuableMeta">
<slot name="actions" v-bind="issuableMeta"></slot>
diff --git a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
index 1cfa3f6d3d7..64f0ec3fbc7 100644
--- a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
@@ -1,15 +1,17 @@
<script>
-import { GlForm, GlFormInput, GlFormGroup } from '@gitlab/ui';
+import { GlForm, GlFormInput, GlFormCheckbox, GlFormGroup } from '@gitlab/ui';
import LabelsSelect from '~/sidebar/components/labels/labels_select_vue/labels_select_root.vue';
import { VARIANT_EMBEDDED } from '~/sidebar/components/labels/labels_select_widget/constants';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
+import { issuableTypeText } from '~/issues/constants';
export default {
VARIANT_EMBEDDED,
components: {
GlForm,
GlFormInput,
+ GlFormCheckbox,
GlFormGroup,
MarkdownEditor,
LabelsSelect,
@@ -31,6 +33,10 @@ export default {
type: String,
required: true,
},
+ issuableType: {
+ type: String,
+ required: true,
+ },
},
descriptionFormFieldProps: {
ariaLabel: __('Description'),
@@ -44,10 +50,20 @@ export default {
return {
issuableTitle: '',
issuableDescription: '',
+ issuableConfidential: false,
selectedLabels: [],
};
},
- computed: {},
+ computed: {
+ confidentialityText() {
+ return sprintf(
+ __(
+ 'This %{issuableType} is confidential and should only be visible to team members with at least Reporter access.',
+ ),
+ { issuableType: issuableTypeText[this.issuableType] },
+ );
+ },
+ },
methods: {
handleUpdateSelectedLabels(labels) {
if (labels.length) {
@@ -85,6 +101,15 @@ export default {
/>
</div>
</div>
+ <div data-testid="issuable-confidential" class="form-group row">
+ <div class="col-12">
+ <gl-form-group :label="__('Confidentiality')" label-for="issuable-confidential">
+ <gl-form-checkbox id="issuable-confidential" v-model="issuableConfidential">
+ {{ confidentialityText }}
+ </gl-form-checkbox>
+ </gl-form-group>
+ </div>
+ </div>
<div data-testid="issuable-labels" class="form-group row">
<label for="issuable-labels" class="col-12">{{ __('Labels') }}</label>
<div class="col-12">
@@ -111,6 +136,7 @@ export default {
name="actions"
:issuable-title="issuableTitle"
:issuable-description="issuableDescription"
+ :issuable-confidential="issuableConfidential"
:selected-labels="selectedLabels"
></slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
index 690d9523a63..bb36df0a778 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
@@ -8,7 +8,6 @@ import { isExternal, setUrlFragment } from '~/lib/utils/url_utility';
import { __, n__, sprintf } from '~/locale';
import IssuableAssignees from '~/issuable/components/issue_assignees.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-import SafeHtml from '~/vue_shared/directives/safe_html';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import { STATE_CLOSED } from '~/work_items/constants';
import { isAssigneesWidget, isLabelsWidget } from '~/work_items/utils';
@@ -25,7 +24,6 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml,
},
mixins: [timeagoMixin],
props: {
@@ -91,9 +89,6 @@ export default {
authorId() {
return getIdFromGraphQLId(this.author.id);
},
- isIssueTrackerExternal() {
- return Boolean(this.issuable.externalTracker);
- },
isIssuableUrlExternal() {
return isExternal(this.webUrl ?? '');
},
@@ -266,36 +261,20 @@ export default {
v-if="issuable.hidden"
v-gl-tooltip
name="spam"
- :title="__('This issue is hidden because its author has been banned')"
+ :title="__('This issue is hidden because its author has been banned.')"
:aria-label="__('Hidden')"
/>
- <template v-if="isIssueTrackerExternal">
- <gl-link
- class="issue-title-text"
- dir="auto"
- :href="webUrl"
- data-qa-selector="issuable_title_link"
- data-testid="issuable-title-link"
- v-bind="issuableTitleProps"
- @click="handleIssuableItemClick"
- >
- {{ issuable.title }}
- <gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2" />
- </gl-link>
- </template>
- <template v-else>
- <gl-link
- v-safe-html="issuable.titleHtml || issuable.title"
- class="issue-title-text"
- dir="auto"
- :href="webUrl"
- data-qa-selector="issuable_title_link"
- data-testid="issuable-title-link"
- v-bind="issuableTitleProps"
- @click="handleIssuableItemClick"
- />
+ <gl-link
+ class="issue-title-text"
+ dir="auto"
+ :href="webUrl"
+ data-testid="issuable-title-link"
+ v-bind="issuableTitleProps"
+ @click="handleIssuableItemClick"
+ >
+ {{ issuable.title }}
<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2" />
- </template>
+ </gl-link>
<span
v-if="taskStatus"
class="task-status gl-display-none gl-sm-display-inline-block! gl-ml-2 gl-font-sm"
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
index c4b92454ac0..a9b5e3a66a8 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
@@ -1,6 +1,8 @@
<script>
import { GlIcon, GlBadge, GlButton, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import HiddenBadge from '~/issuable/components/hidden_badge.vue';
+import LockedBadge from '~/issuable/components/locked_badge.vue';
import { issuableStatusText, STATUS_OPEN, STATUS_REOPENED } from '~/issues/constants';
import { isExternal } from '~/lib/utils/url_utility';
import { __, n__, sprintf } from '~/locale';
@@ -16,6 +18,8 @@ export default {
GlButton,
GlLink,
GlSprintf,
+ HiddenBadge,
+ LockedBadge,
TimeAgoTooltip,
WorkItemTypeIcon,
},
@@ -101,16 +105,6 @@ export default {
? 'success'
: 'info';
},
- blockedTooltip() {
- return sprintf(__('This %{issuable} is locked. Only project members can comment.'), {
- issuable: this.issuableType,
- });
- },
- hiddenTooltip() {
- return sprintf(__('This %{issuable} is hidden because its author has been banned'), {
- issuable: this.issuableType,
- });
- },
shouldShowWorkItemTypeIcon() {
return this.showWorkItemTypeIcon && this.issuableType;
},
@@ -174,22 +168,8 @@ export default {
:issuable-type="issuableType"
:workspace-type="workspaceType"
/>
- <span v-if="blocked" class="issuable-warning-icon">
- <gl-icon
- v-gl-tooltip.bottom
- name="lock"
- :title="blockedTooltip"
- :aria-label="__('Blocked')"
- />
- </span>
- <span v-if="isHidden" class="issuable-warning-icon">
- <gl-icon
- v-gl-tooltip.bottom
- name="spam"
- :title="hiddenTooltip"
- :aria-label="__('Hidden')"
- />
- </span>
+ <locked-badge v-if="blocked" :issuable-type="issuableType" />
+ <hidden-badge v-if="isHidden" :issuable-type="issuableType" />
<work-item-type-icon
v-if="shouldShowWorkItemTypeIcon"
show-text
diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
index f54c4c52743..3412848a9b7 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
@@ -138,7 +138,10 @@ export default {
</div>
<template v-if="activePanel">
- <div class="gl-display-flex gl-align-items-center gl-py-5">
+ <div
+ data-testid="active-panel-template"
+ class="gl-display-flex gl-align-items-center gl-py-5"
+ >
<div class="col-auto">
<img aria-hidden :src="activePanel.imageSrc" />
</div>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
index 57faed61280..c867e53dc30 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
@@ -5,6 +5,7 @@ import { ASC } from '~/notes/constants';
import { __ } from '~/locale';
import { clearDraft } from '~/lib/utils/autosave';
import createNoteMutation from '../../graphql/notes/create_work_item_note.mutation.graphql';
+import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import { TRACKING_CATEGORY_SHOW, i18n } from '../../constants';
import WorkItemNoteSignedOut from './work_item_note_signed_out.vue';
@@ -21,8 +22,12 @@ export default {
WorkItemCommentForm,
},
mixins: [Tracking.mixin()],
- inject: ['fullPath'],
+ inject: ['isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: true,
@@ -90,7 +95,9 @@ export default {
},
apollo: {
workItem: {
- query: workItemByIidQuery,
+ query() {
+ return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
@@ -109,6 +116,9 @@ export default {
},
},
computed: {
+ isLoading() {
+ return this.$apollo.queries.workItem.loading;
+ },
signedIn() {
return Boolean(window.gon.current_user_id);
},
@@ -248,7 +258,7 @@ export default {
<li :class="timelineEntryClass">
<work-item-note-signed-out v-if="!signedIn" />
<work-item-comment-locked
- v-else-if="!canCreateNote"
+ v-else-if="!isLoading && !canCreateNote"
:work-item-type="workItemType"
:is-project-archived="isProjectArchived"
/>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
index a79169bde1e..c7d8a50f402 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
@@ -35,7 +35,6 @@ export default {
GlTooltip: GlTooltipDirective,
},
mixins: [Tracking.mixin()],
- inject: ['fullPath'],
props: {
workItemId: {
type: String,
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
index fd8842aa01a..fed21a1c277 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
@@ -18,8 +18,11 @@ export default {
DiscussionNotesRepliesWrapper,
WorkItemNoteReplying,
},
- inject: ['fullPath'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: true,
@@ -154,6 +157,7 @@ export default {
:is-first-note="true"
:note="note"
:discussion-id="discussionId"
+ :full-path="fullPath"
:has-replies="hasReplies"
:work-item-type="workItemType"
:is-modal="isModal"
@@ -180,6 +184,7 @@ export default {
:is-first-note="true"
:note="note"
:discussion-id="discussionId"
+ :full-path="fullPath"
:has-replies="hasReplies"
:work-item-type="workItemType"
:is-modal="isModal"
@@ -207,6 +212,7 @@ export default {
<work-item-note
:key="threadKey(reply)"
:discussion-id="discussionId"
+ :full-path="fullPath"
:note="reply"
:work-item-type="workItemType"
:is-modal="isModal"
@@ -231,6 +237,7 @@ export default {
v-if="shouldShowReplyForm"
:notes-form="false"
:autofocus="autofocus"
+ :full-path="fullPath"
:work-item-id="workItemId"
:work-item-iid="workItemIid"
:discussion-id="discussionId"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
index b5e3ea68725..f4c654f054c 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
@@ -3,7 +3,6 @@ import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
-import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import Tracking from '~/tracking';
import { updateDraft, clearDraft } from '~/lib/utils/autosave';
import { renderMarkdown } from '~/notes/utils';
@@ -11,15 +10,17 @@ import { getLocationHash } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import EditedAt from '~/issues/show/components/edited.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
import NoteHeader from '~/notes/components/note_header.vue';
-import NoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
-import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import { i18n, TRACKING_CATEGORY_SHOW } from '../../constants';
+import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
+import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import updateWorkItemNoteMutation from '../../graphql/notes/update_work_item_note.mutation.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import { isAssigneesWidget } from '../../utils';
import WorkItemCommentForm from './work_item_comment_form.vue';
+import NoteActions from './work_item_note_actions.vue';
import WorkItemNoteAwardsList from './work_item_note_awards_list.vue';
+import NoteBody from './work_item_note_body.vue';
export default {
name: 'WorkItemNoteThread',
@@ -35,8 +36,12 @@ export default {
EditedAt,
},
mixins: [Tracking.mixin()],
- inject: ['fullPath'],
+ inject: ['isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: true,
@@ -169,7 +174,9 @@ export default {
},
apollo: {
workItem: {
- query: workItemByIidQuery,
+ query() {
+ return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
@@ -335,6 +342,7 @@ export default {
</note-header>
<div class="gl-display-inline-flex">
<note-actions
+ :full-path="fullPath"
:show-award-emoji="hasAwardEmojiPermission"
:work-item-iid="workItemIid"
:note="note"
@@ -372,7 +380,12 @@ export default {
/>
</div>
<div class="note-awards" :class="isFirstNote ? '' : 'gl-pl-7'">
- <work-item-note-awards-list :note="note" :work-item-iid="workItemIid" :is-modal="isModal" />
+ <work-item-note-awards-list
+ :full-path="fullPath"
+ :note="note"
+ :work-item-iid="workItemIid"
+ :is-modal="isModal"
+ />
</div>
</div>
</timeline-entry-item>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
index e5da3d346ae..2cdf8b5ea9d 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
@@ -33,8 +33,11 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: ['fullPath'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemIid: {
type: String,
required: true,
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
index 3c30c204ab6..17d22e66530 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
@@ -8,8 +8,11 @@ export default {
components: {
AwardsList,
},
- inject: ['fullPath'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemIid: {
type: String,
required: true,
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
index f50cfac90f7..49813edf6fc 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
@@ -43,9 +43,14 @@ export default {
type: Boolean,
required: true,
},
- childPath: {
- type: String,
- required: true,
+ /*
+ This flag is added to manage between two different work items; Task and Objective/Key result.
+ Status icon is shown on the task while the actual task icon is shown on any Objective/Key result.
+ */
+ showTaskIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
},
},
computed: {
@@ -69,7 +74,7 @@ export default {
return this.childItem.state === STATE_OPEN;
},
iconName() {
- if (this.childItemType === TASK_TYPE_NAME) {
+ if (this.childItemType === TASK_TYPE_NAME && !this.showTaskIcon) {
return this.isChildItemOpen ? 'issue-open-m' : 'issue-close';
}
return WORK_ITEM_NAME_TO_ICON_MAP[this.childItemType];
@@ -78,7 +83,7 @@ export default {
return this.childItem.workItemType.name;
},
iconClass() {
- if (this.childItemType === TASK_TYPE_NAME) {
+ if (this.childItemType === TASK_TYPE_NAME && !this.showTaskIcon) {
return this.isChildItemOpen ? 'gl-text-green-500' : 'gl-text-blue-500';
}
return '';
@@ -148,9 +153,8 @@ export default {
/>
</span>
<gl-link
- :href="childPath"
- class="gl-text-truncate gl-font-weight-semibold"
- data-testid="item-title"
+ :href="childItem.webUrl"
+ class="gl-overflow-break-word gl-font-weight-semibold"
@click="$emit('click', $event)"
@mouseover="$emit('mouseover')"
@mouseout="$emit('mouseout')"
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue
index 38d8d239a7e..c0e87f0bb6e 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue
@@ -69,6 +69,7 @@ export default {
badge-tooltip-prop="name"
:badge-sr-only-text="assigneesCollapsedTooltip"
:class="assigneesContainerClass"
+ class="gl-white-space-nowrap"
>
<template #avatar="{ avatar }">
<gl-avatar-link v-gl-tooltip target="blank" :href="avatar.webUrl" :title="avatar.name">
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
index 7b38e838033..3595ab631df 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
@@ -7,7 +7,6 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
import {
WORK_ITEMS_TYPE_MAP,
- WORK_ITEM_TYPE_ENUM_TASK,
I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER,
sprintfWorkItem,
} from '../../constants';
@@ -29,7 +28,7 @@ export default {
childrenType: {
type: String,
required: false,
- default: WORK_ITEM_TYPE_ENUM_TASK,
+ default: '',
},
childrenIds: {
type: Array,
@@ -53,7 +52,7 @@ export default {
return {
fullPath: this.fullPath,
searchTerm: this.search?.title || this.search,
- types: [this.childrenType],
+ types: this.childrenType ? [this.childrenType] : [],
in: this.search ? 'TITLE' : undefined,
};
},
@@ -106,6 +105,7 @@ export default {
},
handleFocus() {
this.searchStarted = true;
+ this.$emit('searching', true);
},
handleMouseOver() {
this.timeout = setTimeout(() => {
@@ -115,11 +115,22 @@ export default {
handleMouseOut() {
clearTimeout(this.timeout);
},
+ handleBlur() {
+ this.$emit('searching', false);
+ },
+ focusInputText() {
+ this.$nextTick(() => {
+ if (this.areWorkItemsToAddValid) {
+ this.$refs.tokenSelector.$el.querySelector('input[type="text"]').focus();
+ }
+ });
+ },
},
};
</script>
<template>
<gl-token-selector
+ ref="tokenSelector"
v-model="workItemsToAdd"
:dropdown-items="availableWorkItems"
:loading="isLoading"
@@ -131,13 +142,14 @@ export default {
@focus="handleFocus"
@mouseover.native="handleMouseOver"
@mouseout.native="handleMouseOut"
+ @token-add="focusInputText"
+ @token-remove="focusInputText"
+ @blur="handleBlur"
>
- <template #token-content="{ token }">
- {{ token.title }}
- </template>
+ <template #token-content="{ token }"> {{ token.iid }} {{ token.title }} </template>
<template #dropdown-item-content="{ dropdownItem }">
<div class="gl-display-flex">
- <div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(dropdownItem.id) }}</div>
+ <div class="gl-text-secondary gl-font-sm gl-mr-4">{{ dropdownItem.iid }}</div>
<div class="gl-text-truncate">{{ dropdownItem.title }}</div>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue
index 18aa4d55086..02d2ea24ca0 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -7,7 +7,6 @@ import {
GlModalDirective,
GlToggle,
} from '@gitlab/ui';
-import { produce } from 'immer';
import * as Sentry from '@sentry/browser';
@@ -15,7 +14,6 @@ import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
import toast from '~/vue_shared/plugins/global_toast';
import { isLoggedIn } from '~/lib/utils/common_utils';
-import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
sprintfWorkItem,
@@ -28,7 +26,6 @@ import {
TEST_ID_PROMOTE_ACTION,
TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
TEST_ID_COPY_REFERENCE_ACTION,
- WIDGET_TYPE_NOTIFICATIONS,
I18N_WORK_ITEM_ERROR_CONVERTING,
WORK_ITEM_TYPE_VALUE_KEY_RESULT,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
@@ -70,8 +67,12 @@ export default {
copyCreateNoteEmailTestId: TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
deleteActionTestId: TEST_ID_DELETE_ACTION,
promoteActionTestId: TEST_ID_PROMOTE_ACTION,
- inject: ['fullPath'],
+ inject: ['isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: false,
@@ -127,10 +128,6 @@ export default {
required: false,
default: false,
},
- workItemIid: {
- type: String,
- required: true,
- },
},
apollo: {
workItemTypes: {
@@ -199,80 +196,31 @@ export default {
}
},
toggleNotifications(subscribed) {
- const inputVariables = {
- projectPath: this.fullPath,
- iid: this.workItemIid,
- subscribedState: subscribed,
- };
this.$apollo
.mutate({
mutation: updateWorkItemNotificationsMutation,
variables: {
- input: inputVariables,
- },
- optimisticResponse: {
- updateWorkItemNotificationsSubscription: {
- issue: {
- id: this.workItemId,
- subscribed,
- },
- errors: [],
- },
- },
- update: (
- cache,
- {
- data: {
- updateWorkItemNotificationsSubscription: { issue = {} },
- },
+ input: {
+ id: this.workItemId,
+ subscribed,
},
- ) => {
- // As the mutation and the query both are different,
- // overwrite the subscribed value in the cache
- this.updateWorkItemNotificationsWidgetCache({
- cache,
- issue,
- });
},
})
- .then(
- ({
- data: {
- updateWorkItemNotificationsSubscription: { errors },
- },
- }) => {
- if (errors?.length) {
- throw new Error(errors[0]);
- }
- toast(
- subscribed ? this.$options.i18n.notificationOn : this.$options.i18n.notificationOff,
- );
- },
- )
+ .then(({ data }) => {
+ const { errors } = data.workItemSubscribe;
+ if (errors?.length) {
+ throw new Error(errors[0]);
+ }
+
+ toast(
+ subscribed ? this.$options.i18n.notificationOn : this.$options.i18n.notificationOff,
+ );
+ })
.catch((error) => {
this.$emit('error', error.message);
Sentry.captureException(error);
});
},
- updateWorkItemNotificationsWidgetCache({ cache, issue }) {
- const query = {
- query: workItemByIidQuery,
- variables: { fullPath: this.fullPath, iid: this.workItemIid },
- };
- // Read the work item object
- const sourceData = cache.readQuery(query);
-
- const newData = produce(sourceData, (draftState) => {
- const { widgets } = draftState.workspace.workItems.nodes[0];
-
- const widgetNotifications = widgets.find(({ type }) => type === WIDGET_TYPE_NOTIFICATIONS);
- // overwrite the subscribed value
- widgetNotifications.subscribed = issue.subscribed;
- });
-
- // write to the cache
- cache.writeQuery({ ...query, data: newData });
- },
throwConvertError() {
this.$emit('error', this.i18n.convertError);
},
@@ -337,7 +285,6 @@ export default {
:data-testid="$options.notificationsToggleTestId"
class="work-item-notification-toggle"
label-position="left"
- label-id="notifications-toggle"
@change="toggleNotifications($event)"
/>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees.vue
index f9527884adc..a9aafbb3d84 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -13,7 +13,8 @@ import {
import { debounce, uniqueId } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
-import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
+import groupUsersSearchQuery from '~/graphql_shared/queries/group_users_search.query.graphql';
+import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import { n__, s__ } from '~/locale';
import Tracking from '~/tracking';
@@ -54,8 +55,12 @@ export default {
GlIntersectionObserver,
},
mixins: [Tracking.mixin()],
- inject: ['fullPath'],
+ inject: ['isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: true,
@@ -99,7 +104,7 @@ export default {
apollo: {
users: {
query() {
- return userSearchQuery;
+ return this.isGroup ? groupUsersSearchQuery : usersSearchQuery;
},
variables() {
return {
diff --git a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
index 139f0f7919c..fd01d855782 100644
--- a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
@@ -4,17 +4,21 @@ import {
sprintfWorkItem,
WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_HEALTH_STATUS,
+ WIDGET_TYPE_HIERARCHY,
WIDGET_TYPE_ITERATION,
WIDGET_TYPE_LABELS,
WIDGET_TYPE_MILESTONE,
WIDGET_TYPE_PROGRESS,
WIDGET_TYPE_START_AND_DUE_DATE,
WIDGET_TYPE_WEIGHT,
+ WORK_ITEM_TYPE_VALUE_KEY_RESULT,
+ WORK_ITEM_TYPE_VALUE_OBJECTIVE,
} from '../constants';
import WorkItemDueDate from './work_item_due_date.vue';
import WorkItemAssignees from './work_item_assignees.vue';
import WorkItemLabels from './work_item_labels.vue';
import WorkItemMilestone from './work_item_milestone.vue';
+import WorkItemParent from './work_item_parent.vue';
export default {
components: {
@@ -22,6 +26,7 @@ export default {
WorkItemMilestone,
WorkItemAssignees,
WorkItemDueDate,
+ WorkItemParent,
WorkItemWeight: () => import('ee_component/work_items/components/work_item_weight.vue'),
WorkItemProgress: () => import('ee_component/work_items/components/work_item_progress.vue'),
WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
@@ -29,8 +34,11 @@ export default {
import('ee_component/work_items/components/work_item_health_status.vue'),
},
mixins: [glFeatureFlagMixin()],
- inject: ['fullPath'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItem: {
type: Object,
required: true,
@@ -81,9 +89,21 @@ export default {
workItemHealthStatus() {
return this.isWidgetPresent(WIDGET_TYPE_HEALTH_STATUS);
},
+ workItemHierarchy() {
+ return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY);
+ },
workItemMilestone() {
return this.isWidgetPresent(WIDGET_TYPE_MILESTONE);
},
+ showWorkItemParent() {
+ return (
+ this.workItemType === WORK_ITEM_TYPE_VALUE_OBJECTIVE ||
+ this.workItemType === WORK_ITEM_TYPE_VALUE_KEY_RESULT
+ );
+ },
+ workItemParent() {
+ return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY)?.parent;
+ },
},
methods: {
isWidgetPresent(type) {
@@ -98,6 +118,7 @@ export default {
<work-item-assignees
v-if="workItemAssignees"
:can-update="canUpdate"
+ :full-path="fullPath"
:work-item-id="workItem.id"
:assignees="workItemAssignees.assignees.nodes"
:allows-multiple-assignees="workItemAssignees.allowsMultipleAssignees"
@@ -108,6 +129,7 @@ export default {
<work-item-labels
v-if="workItemLabels"
:can-update="canUpdate"
+ :full-path="fullPath"
:work-item-id="workItem.id"
:work-item-iid="workItem.iid"
@error="$emit('error', $event)"
@@ -123,6 +145,7 @@ export default {
/>
<work-item-milestone
v-if="workItemMilestone"
+ :full-path="fullPath"
:work-item-id="workItem.id"
:work-item-milestone="workItemMilestone.milestone"
:work-item-type="workItemType"
@@ -151,6 +174,7 @@ export default {
<work-item-iteration
v-if="workItemIteration"
class="gl-mb-5"
+ :full-path="fullPath"
:iteration="workItemIteration.iteration"
:can-update="canUpdate"
:work-item-id="workItem.id"
@@ -168,5 +192,14 @@ export default {
:work-item-type="workItemType"
@error="$emit('error', $event)"
/>
+ <work-item-parent
+ v-if="showWorkItemParent"
+ class="gl-mb-5"
+ :can-update="canUpdate"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ :parent="workItemParent"
+ @error="$emit('error', $event)"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_created_updated.vue b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
index 14e55134048..460b5d35187 100644
--- a/app/assets/javascripts/work_items/components/work_item_created_updated.vue
+++ b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
@@ -3,10 +3,11 @@ import { GlAvatarLink, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { WORKSPACE_PROJECT } from '~/issues/constants';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import WorkItemStateBadge from '~/work_items/components/work_item_state_badge.vue';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
-import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
+import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
+import WorkItemStateBadge from './work_item_state_badge.vue';
+import WorkItemTypeIcon from './work_item_type_icon.vue';
export default {
components: {
@@ -18,8 +19,12 @@ export default {
ConfidentialityBadge,
GlLoadingIcon,
},
- inject: ['fullPath'],
+ inject: ['isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemIid: {
type: String,
required: false,
@@ -59,7 +64,9 @@ export default {
},
apollo: {
workItem: {
- query: workItemByIidQuery,
+ query() {
+ return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index 58bf524f450..b7f3ac93cdb 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -10,6 +10,7 @@ import Tracking from '~/tracking';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import { autocompleteDataSources, markdownPreviewPath } from '../utils';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
+import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_DESCRIPTION } from '../constants';
import WorkItemDescriptionRendered from './work_item_description_rendered.vue';
@@ -25,8 +26,12 @@ export default {
WorkItemDescriptionRendered,
},
mixins: [Tracking.mixin()],
- inject: ['fullPath'],
+ inject: ['isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: true,
@@ -55,7 +60,9 @@ export default {
},
apollo: {
workItem: {
- query: workItemByIidQuery,
+ query() {
+ return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index edecd7addcc..53929775684 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -16,7 +16,6 @@ import { getParameterByName, updateHistory, setUrlParams } from '~/lib/utils/url
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isLoggedIn } from '~/lib/utils/common_utils';
-import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import { WORKSPACE_PROJECT } from '~/issues/constants';
@@ -37,6 +36,7 @@ import {
import workItemUpdatedSubscription from '../graphql/work_item_updated.subscription.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
+import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import { findHierarchyWidgetChildren } from '../utils';
@@ -52,6 +52,7 @@ import WorkItemDetailModal from './work_item_detail_modal.vue';
import WorkItemAwardEmoji from './work_item_award_emoji.vue';
import WorkItemStateToggleButton from './work_item_state_toggle_button.vue';
import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue';
+import WorkItemTypeIcon from './work_item_type_icon.vue';
export default {
i18n,
@@ -84,7 +85,7 @@ export default {
WorkItemRelationships,
},
mixins: [glFeatureFlagMixin()],
- inject: ['fullPath', 'reportAbusePath'],
+ inject: ['fullPath', 'isGroup', 'reportAbusePath'],
props: {
isModal: {
type: Boolean,
@@ -118,7 +119,9 @@ export default {
},
apollo: {
workItem: {
- query: workItemByIidQuery,
+ query() {
+ return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
@@ -189,8 +192,8 @@ export default {
canAssignUnassignUser() {
return this.workItemAssignees && this.canSetWorkItemMetadata;
},
- fullPath() {
- return this.workItem?.project.fullPath;
+ projectFullPath() {
+ return this.workItem?.project?.fullPath;
},
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
@@ -460,11 +463,12 @@ export default {
v-if="showWorkItemCurrentUserTodos"
:work-item-id="workItem.id"
:work-item-iid="workItemIid"
- :work-item-fullpath="workItem.project.fullPath"
+ :work-item-fullpath="projectFullPath"
:current-user-todos="currentUserTodos"
@error="updateError = $event"
/>
<work-item-actions
+ :full-path="fullPath"
:work-item-id="workItem.id"
:subscribed-to-notifications="workItemNotificationsSubscribed"
:work-item-type="workItemType"
@@ -476,7 +480,6 @@ export default {
:work-item-reference="workItem.reference"
:work-item-create-note-email="workItem.createNoteEmail"
:is-modal="isModal"
- :work-item-iid="workItemIid"
@deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
@toggleWorkItemConfidentiality="toggleConfidentiality"
@error="updateError = $event"
@@ -503,6 +506,7 @@ export default {
@error="updateError = $event"
/>
<work-item-created-updated
+ :full-path="fullPath"
:work-item-iid="workItemIid"
:update-in-progress="updateInProgress"
/>
@@ -535,11 +539,12 @@ export default {
v-if="showWorkItemCurrentUserTodos"
:work-item-id="workItem.id"
:work-item-iid="workItemIid"
- :work-item-fullpath="workItem.project.fullPath"
+ :work-item-fullpath="projectFullPath"
:current-user-todos="currentUserTodos"
@error="updateError = $event"
/>
<work-item-actions
+ :full-path="fullPath"
:work-item-id="workItem.id"
:subscribed-to-notifications="workItemNotificationsSubscribed"
:work-item-type="workItemType"
@@ -551,7 +556,6 @@ export default {
:work-item-reference="workItem.reference"
:work-item-create-note-email="workItem.createNoteEmail"
:is-modal="isModal"
- :work-item-iid="workItemIid"
@deleteWorkItem="
$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })
"
@@ -571,12 +575,14 @@ export default {
<work-item-attributes-wrapper
:class="{ 'gl-md-display-none!': workItemsMvc2Enabled }"
class="gl-border-b"
+ :full-path="fullPath"
:work-item="workItem"
:work-item-parent-id="workItemParentId"
@error="updateError = $event"
/>
<work-item-description
v-if="hasDescriptionWidget"
+ :full-path="fullPath"
:work-item-id="workItem.id"
:work-item-iid="workItem.iid"
class="gl-pt-5"
@@ -585,7 +591,7 @@ export default {
<work-item-award-emoji
v-if="workItemAwardEmoji"
:work-item-id="workItem.id"
- :work-item-fullpath="workItem.project.fullPath"
+ :work-item-fullpath="projectFullPath"
:award-emoji="workItemAwardEmoji.awardEmoji"
:work-item-iid="workItemIid"
@error="updateError = $event"
@@ -593,6 +599,7 @@ export default {
/>
<work-item-tree
v-if="workItemType === $options.WORK_ITEM_TYPE_VALUE_OBJECTIVE"
+ :full-path="fullPath"
:work-item-type="workItemType"
:parent-work-item-type="workItem.workItemType.name"
:work-item-id="workItem.id"
@@ -605,12 +612,15 @@ export default {
/>
<work-item-relationships
v-if="showWorkItemLinkedItems"
+ :work-item-id="workItem.id"
:work-item-iid="workItemIid"
- :work-item-full-path="workItem.project.fullPath"
+ :work-item-full-path="projectFullPath"
+ :work-item-type="workItem.workItemType.name"
@showModal="openInModal"
/>
<work-item-notes
v-if="workItemNotes"
+ :full-path="fullPath"
:work-item-id="workItem.id"
:work-item-iid="workItem.iid"
:work-item-type="workItemType"
@@ -629,6 +639,7 @@ export default {
:title="$options.i18n.fetchErrorTitle"
:description="error"
:svg-path="noAccessSvgPath"
+ :svg-height="null"
/>
</section>
<aside
@@ -638,6 +649,7 @@ export default {
:class="{ 'is-modal': isModal }"
>
<work-item-attributes-wrapper
+ :full-path="fullPath"
:work-item="workItem"
:work-item-parent-id="workItemParentId"
@error="updateError = $event"
diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue
index 1405a12a101..3cdbf816421 100644
--- a/app/assets/javascripts/work_items/components/work_item_labels.vue
+++ b/app/assets/javascripts/work_items/components/work_item_labels.vue
@@ -8,6 +8,7 @@ import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_it
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
+import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import { i18n, I18N_WORK_ITEM_ERROR_FETCHING_LABELS, TRACKING_CATEGORY_SHOW } from '../constants';
import { isLabelsWidget } from '../utils';
@@ -37,8 +38,12 @@ export default {
LabelItem,
},
mixins: [Tracking.mixin()],
- inject: ['fullPath'],
+ inject: ['isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: true,
@@ -65,7 +70,9 @@ export default {
},
apollo: {
workItem: {
- query: workItemByIidQuery,
+ query() {
+ return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
index 9d9414b5399..f4de7c1dddc 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
@@ -13,6 +13,7 @@ import { findHierarchyWidgets } from '../../utils';
import { addHierarchyChild, removeHierarchyChild } from '../../graphql/cache_utils';
import reorderWorkItem from '../../graphql/reorder_work_item.mutation.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
+import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import WorkItemLinkChild from './work_item_link_child.vue';
@@ -20,8 +21,12 @@ export default {
components: {
WorkItemLinkChild,
},
- inject: ['fullPath'],
+ inject: ['isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemType: {
type: String,
required: false,
@@ -83,7 +88,14 @@ export default {
const { data } = await this.$apollo.mutate({
mutation: updateWorkItemMutation,
variables: { input: { id: child.id, hierarchyWidget: { parentId: null } } },
- update: (cache) => removeHierarchyChild(cache, this.fullPath, this.workItemIid, child),
+ update: (cache) =>
+ removeHierarchyChild({
+ cache,
+ fullPath: this.fullPath,
+ iid: this.workItemIid,
+ isGroup: this.isGroup,
+ workItem: child,
+ }),
});
if (data.workItemUpdate.errors.length) {
@@ -109,7 +121,14 @@ export default {
const { data } = await this.$apollo.mutate({
mutation: updateWorkItemMutation,
variables: { input: { id: child.id, hierarchyWidget: { parentId: this.workItemId } } },
- update: (cache) => addHierarchyChild(cache, this.fullPath, this.workItemIid, child),
+ update: (cache) =>
+ addHierarchyChild({
+ cache,
+ fullPath: this.fullPath,
+ iid: this.workItemIid,
+ isGroup: this.isGroup,
+ workItem: child,
+ }),
});
if (data.workItemUpdate.errors.length) {
@@ -124,7 +143,7 @@ export default {
},
addWorkItemQuery({ iid }) {
this.$apollo.addSmartQuery('prefetchedWorkItem', {
- query: workItemByIidQuery,
+ query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: {
fullPath: this.fullPath,
iid,
@@ -206,7 +225,7 @@ export default {
update: (store) => {
store.updateQuery(
{
- query: workItemByIidQuery,
+ query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: { fullPath: this.fullPath, iid: this.workItemIid },
},
(sourceData) =>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
index 679287338c8..847a3585ac4 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
@@ -13,7 +13,6 @@ import {
WIDGET_TYPE_HIERARCHY,
WORK_ITEM_NAME_TO_ICON_MAP,
} from '../../constants';
-import { workItemPath } from '../../utils';
import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql';
import WorkItemLinkChildContents from '../shared/work_item_link_child_contents.vue';
import WorkItemTreeChildren from './work_item_tree_children.vue';
@@ -27,7 +26,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: ['fullPath'],
props: {
canUpdate: {
type: Boolean,
@@ -90,9 +88,6 @@ export default {
stateTimestampTypeText() {
return this.isItemOpen ? __('Created') : __('Closed');
},
- childPath() {
- return workItemPath(this.fullPath, this.childItem.iid);
- },
chevronType() {
return this.isExpanded ? 'chevron-down' : 'chevron-right';
},
@@ -236,7 +231,6 @@ export default {
:can-update="canUpdate"
:parent-work-item-id="issuableGid"
:work-item-type="workItemType"
- :child-path="childPath"
@click="$emit('click', $event)"
@removeChild="$emit('removeChild', childItem)"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
index eb836007e75..7fa6ac2c57f 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
@@ -18,6 +18,7 @@ import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_sel
import { FORM_TYPES, WIDGET_ICONS, WORK_ITEM_STATUS_TEXT } from '../../constants';
import { findHierarchyWidgetChildren } from '../../utils';
import { removeHierarchyChild } from '../../graphql/cache_utils';
+import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import WidgetWrapper from '../widget_wrapper.vue';
import WorkItemDetailModal from '../work_item_detail_modal.vue';
@@ -39,7 +40,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: ['fullPath', 'reportAbusePath'],
+ inject: ['fullPath', 'isGroup', 'reportAbusePath'],
props: {
issuableId: {
type: Number,
@@ -52,7 +53,9 @@ export default {
},
apollo: {
workItem: {
- query: workItemByIidQuery,
+ query() {
+ return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
@@ -171,7 +174,13 @@ export default {
},
handleWorkItemDeleted(child) {
const { defaultClient: cache } = this.$apollo.provider.clients;
- removeHierarchyChild(cache, this.fullPath, this.iid, child);
+ removeHierarchyChild({
+ cache,
+ fullPath: this.fullPath,
+ iid: this.iid,
+ isGroup: this.isGroup,
+ workItem: child,
+ });
this.$toast.show(s__('WorkItem|Task deleted'));
},
updateWorkItemIdUrlQuery({ iid } = {}) {
@@ -256,6 +265,7 @@ export default {
v-if="isShownAddForm"
ref="wiLinksForm"
data-testid="add-links-form"
+ :full-path="fullPath"
:issuable-gid="issuableGid"
:work-item-iid="iid"
:children-ids="childrenIds"
@@ -269,6 +279,7 @@ export default {
<work-item-children-wrapper
:children="children"
:can-update="canUpdate"
+ :full-path="fullPath"
:work-item-id="issuableGid"
:work-item-iid="iid"
@error="error = $event"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
index 55440e1603c..f24b56cac36 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
@@ -37,8 +37,12 @@ export default {
GlTooltip,
WorkItemTokenInput,
},
- inject: ['fullPath', 'hasIterationsFeature'],
+ inject: ['hasIterationsFeature', 'isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
issuableGid: {
type: String,
required: false,
@@ -225,7 +229,6 @@ export default {
this.error = null;
},
addChild() {
- this.searchStarted = false;
this.$apollo
.mutate({
mutation: updateWorkItemMutation,
@@ -261,7 +264,13 @@ export default {
input: this.workItemInput,
},
update: (cache, { data }) =>
- addHierarchyChild(cache, this.fullPath, this.workItemIid, data.workItemCreate.workItem),
+ addHierarchyChild({
+ cache,
+ fullPath: this.fullPath,
+ iid: this.workItemIid,
+ isGroup: this.isGroup,
+ workItem: data.workItemCreate.workItem,
+ }),
})
.then(({ data }) => {
if (data.workItemCreate?.errors?.length) {
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
index bc3f5201fb8..b61b3b2e0d3 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
@@ -22,8 +22,11 @@ export default {
WorkItemLinksForm,
WorkItemChildrenWrapper,
},
- inject: ['fullPath'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemType: {
type: String,
required: true,
@@ -139,6 +142,7 @@ export default {
v-if="isShownAddForm"
ref="wiLinksForm"
data-testid="add-tree-form"
+ :full-path="fullPath"
:issuable-gid="workItemId"
:work-item-iid="workItemIid"
:form-type="formType"
@@ -152,6 +156,7 @@ export default {
<work-item-children-wrapper
:children="children"
:can-update="canUpdate"
+ :full-path="fullPath"
:work-item-id="workItemId"
:work-item-iid="workItemIid"
:work-item-type="workItemType"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
index 2cabf489bc6..401223c3593 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
@@ -3,7 +3,6 @@ export default {
components: {
WorkItemLinkChild: () => import('./work_item_link_child.vue'),
},
- inject: ['fullPath'],
props: {
workItemType: {
type: String,
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue
index 6cc61ed4756..a2cbb7f7598 100644
--- a/app/assets/javascripts/work_items/components/work_item_milestone.vue
+++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue
@@ -46,8 +46,11 @@ export default {
GlDropdownText,
},
mixins: [Tracking.mixin()],
- inject: ['fullPath'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: true,
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
index 256f8ed53d1..fe8aea99f53 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -46,8 +46,11 @@ export default {
WorkItemNotesActivityHeader,
WorkItemHistoryOnlyFilterNote,
},
- inject: ['fullPath'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: true,
@@ -364,6 +367,7 @@ export default {
<work-item-discussion
:key="getDiscussionKey(discussion)"
:discussion="discussion.notes.nodes"
+ :full-path="fullPath"
:work-item-id="workItemId"
:work-item-iid="workItemIid"
:work-item-type="workItemType"
diff --git a/app/assets/javascripts/work_items/components/work_item_parent.vue b/app/assets/javascripts/work_items/components/work_item_parent.vue
new file mode 100644
index 00000000000..e16299f482f
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_parent.vue
@@ -0,0 +1,249 @@
+<script>
+import { GlFormGroup, GlCollapsibleListbox } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { debounce } from 'lodash';
+
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { s__ } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+
+import projectWorkItemsQuery from '../graphql/project_work_items.query.graphql';
+import {
+ I18N_WORK_ITEM_ERROR_UPDATING,
+ sprintfWorkItem,
+ WORK_ITEM_TYPE_ENUM_OBJECTIVE,
+} from '../constants';
+
+export default {
+ i18n: {
+ assignParentLabel: s__('WorkItem|Assign parent'),
+ parentLabel: s__('WorkItem|Parent'),
+ none: s__('WorkItem|None'),
+ noMatchingResults: s__('WorkItem|No matching results'),
+ unAssign: s__('WorkItem|Unassign'),
+ workItemsFetchError: s__(
+ 'WorkItem|Something went wrong while fetching items. Please try again.',
+ ),
+ },
+ components: {
+ GlFormGroup,
+ GlCollapsibleListbox,
+ },
+ mixins: [glFeatureFlagMixin()],
+ inject: ['fullPath'],
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ parent: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
+ workItemType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ search: '',
+ updateInProgress: false,
+ searchStarted: false,
+ availableWorkItems: [],
+ localSelectedItem: this.parent?.id,
+ isNotFocused: true,
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.availableWorkItems.loading;
+ },
+ listboxText() {
+ return (
+ this.workItems.filter((item) => this.localSelectedItem === item.value)?.[0]?.text ||
+ this.parent?.title ||
+ this.$options.i18n.none
+ );
+ },
+ workItemsMvc2Enabled() {
+ return this.glFeatures.workItemsMvc2;
+ },
+ workItems() {
+ return this.availableWorkItems.map(({ id, title }) => ({ text: title, value: id }));
+ },
+ listboxCategory() {
+ return this.searchStarted ? 'secondary' : 'tertiary';
+ },
+ listboxClasses() {
+ return {
+ 'is-not-focused': this.isNotFocused && !this.searchStarted,
+ };
+ },
+ },
+ watch: {
+ parent: {
+ handler(newVal) {
+ this.localSelectedItem = newVal?.id;
+ },
+ },
+ },
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ apollo: {
+ availableWorkItems: {
+ query: projectWorkItemsQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ searchTerm: this.search,
+ types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
+ in: this.search ? 'TITLE' : undefined,
+ };
+ },
+ skip() {
+ return !this.searchStarted;
+ },
+ update(data) {
+ return data.workspace.workItems.nodes.filter((wi) => this.workItemId !== wi.id) || [];
+ },
+ error() {
+ this.$emit('error', this.$options.i18n.workItemsFetchError);
+ },
+ },
+ },
+ methods: {
+ setSearchKey(value) {
+ this.search = value;
+ },
+ async updateParent() {
+ if (this.parent?.id === this.localSelectedItem) {
+ return;
+ }
+ this.updateInProgress = true;
+ try {
+ const {
+ data: {
+ workItemUpdate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ hierarchyWidget: {
+ parentId:
+ this.localSelectedItem === 'no-work-item-id' ? null : this.localSelectedItem,
+ },
+ },
+ },
+ });
+
+ if (errors.length) {
+ this.$emit('error', errors.join('\n'));
+ this.localSelectedItem = this.parent?.id || 'no-work-item-id';
+ }
+ } catch (error) {
+ this.$emit('error', sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType));
+ Sentry.captureException(error);
+ } finally {
+ this.updateInProgress = false;
+ }
+ },
+ handleItemClick(item) {
+ this.localSelectedItem = item;
+ this.searchStarted = false;
+ this.search = '';
+ this.updateParent();
+ },
+ unAssignParent() {
+ this.localSelectedItem = 'no-work-item-id';
+ this.updateParent();
+ },
+ onListboxShown() {
+ this.searchStarted = true;
+ this.isNotFocused = false;
+ },
+ onListboxHide() {
+ this.searchStarted = false;
+ this.search = '';
+ this.isNotFocused = true;
+ },
+ setListboxFocused() {
+ // This is to match the caret behaviour of parent listbox
+ // to the other dropdown fields of work items
+ if (document.activeElement.parentElement.id !== 'work-item-parent-listbox-value') {
+ this.isNotFocused = true;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group
+ class="work-item-dropdown gl-flex-nowrap"
+ data-testid="work-item-parent-form"
+ :label="$options.i18n.parentLabel"
+ label-for="work-item-parent-listbox-value"
+ label-class="gl-pb-0! gl-mt-3 gl-overflow-wrap-break work-item-field-label"
+ label-cols="3"
+ label-cols-lg="2"
+ >
+ <span
+ v-if="!canUpdate"
+ class="gl-text-secondary gl-ml-4 gl-mt-3 gl-display-inline-block gl-line-height-normal work-item-field-value"
+ data-testid="disabled-text"
+ >
+ {{ listboxText }}
+ </span>
+ <div
+ v-else
+ :class="{ 'gl-max-w-max-content': !workItemsMvc2Enabled }"
+ @mouseover="isNotFocused = false"
+ @mouseleave="setListboxFocused"
+ @focusout="isNotFocused = true"
+ @focusin="isNotFocused = false"
+ >
+ <gl-collapsible-listbox
+ id="work-item-parent-listbox-value"
+ class="gl-max-w-max-content"
+ data-testid="work-item-parent-listbox"
+ block
+ searchable
+ :no-caret="isNotFocused && !searchStarted"
+ is-check-centered
+ :category="listboxCategory"
+ :searching="isLoading"
+ :header-text="$options.i18n.assignParentLabel"
+ :no-results-text="$options.i18n.noMatchingResults"
+ :loading="updateInProgress"
+ :items="workItems"
+ :toggle-text="listboxText"
+ :toggle-class="listboxClasses"
+ :selected="localSelectedItem"
+ :reset-button-label="$options.i18n.unAssign"
+ @reset="unAssignParent"
+ @search="debouncedSearchKeyUpdate"
+ @select="handleItemClick"
+ @shown="onListboxShown"
+ @hidden="onListboxHide"
+ >
+ <template #list-item="{ item }">
+ <div @click="handleItemClick(item.value, $event)">
+ {{ item.text }}
+ </div>
+ </template>
+ </gl-collapsible-listbox>
+ </div>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
new file mode 100644
index 00000000000..d242db95896
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
@@ -0,0 +1,249 @@
+<script>
+import { produce } from 'immer';
+import { GlFormGroup, GlForm, GlFormRadioGroup, GlButton, GlAlert } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import WorkItemTokenInput from '../shared/work_item_token_input.vue';
+import addLinkedItemsMutation from '../../graphql/add_linked_items.mutation.graphql';
+import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
+import {
+ LINK_ITEM_FORM_HEADER_LABEL,
+ WIDGET_TYPE_LINKED_ITEMS,
+ LINKED_ITEM_TYPE_VALUE,
+ MAX_WORK_ITEMS,
+ I18N_MAX_WORK_ITEMS_ERROR_MESSAGE,
+ I18N_MAX_WORK_ITEMS_NOTE_LABEL,
+} from '../../constants';
+
+export default {
+ components: {
+ GlForm,
+ GlButton,
+ GlFormGroup,
+ GlFormRadioGroup,
+ GlAlert,
+ WorkItemTokenInput,
+ },
+ props: {
+ workItemId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ workItemIid: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ workItemFullPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ workItemType: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ childrenIds: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ linkedItemType: LINKED_ITEM_TYPE_VALUE.RELATED,
+ linkedItemTypes: [
+ {
+ text: this.$options.i18n.relatedToLabel,
+ value: LINKED_ITEM_TYPE_VALUE.RELATED,
+ },
+ {
+ text: this.$options.i18n.blockingLabel,
+ value: LINKED_ITEM_TYPE_VALUE.BLOCKS,
+ },
+ {
+ text: this.$options.i18n.blockedByLabel,
+ value: LINKED_ITEM_TYPE_VALUE.BLOCKED_BY,
+ },
+ ],
+ workItemsToAdd: [],
+ error: null,
+ showWorkItemsToAddInvalidMessage: false,
+ isSubmitting: false,
+ searchInProgress: false,
+ maxWorkItems: MAX_WORK_ITEMS,
+ };
+ },
+ computed: {
+ linkItemFormHeaderLabel() {
+ return LINK_ITEM_FORM_HEADER_LABEL[this.workItemType];
+ },
+ workItemsToAddInvalidMessage() {
+ return this.$options.i18n.addChildErrorMessage;
+ },
+ isSubmitButtonDisabled() {
+ return this.workItemsToAdd.length <= 0 || !this.areWorkItemsToAddValid;
+ },
+ areWorkItemsToAddValid() {
+ return this.workItemsToAdd.length <= this.maxWorkItems;
+ },
+ errorMessage() {
+ return !this.areWorkItemsToAddValid ? this.$options.i18n.maxItemsErrorMessage : '';
+ },
+ },
+ methods: {
+ async linkWorkItem() {
+ try {
+ if (this.searchInProgress) {
+ return;
+ }
+ this.isSubmitting = true;
+ const {
+ data: {
+ workItemAddLinkedItems: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: addLinkedItemsMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ linkType: this.linkedItemType,
+ workItemsIds: this.workItemsToAdd.map((wi) => wi.id),
+ },
+ },
+ update: (
+ cache,
+ {
+ data: {
+ workItemAddLinkedItems: { workItem },
+ },
+ },
+ ) => {
+ const queryArgs = {
+ query: workItemByIidQuery,
+ variables: { fullPath: this.workItemFullPath, iid: this.workItemIid },
+ };
+ const sourceData = cache.readQuery(queryArgs);
+
+ if (!sourceData) {
+ return;
+ }
+
+ cache.writeQuery({
+ ...queryArgs,
+ data: produce(sourceData, (draftState) => {
+ const linkedItemsWidget = draftState.workspace.workItems.nodes[0].widgets?.find(
+ (widget) => widget.type === WIDGET_TYPE_LINKED_ITEMS,
+ );
+
+ linkedItemsWidget.linkedItems = workItem.widgets?.find(
+ (widget) => widget.type === WIDGET_TYPE_LINKED_ITEMS,
+ ).linkedItems;
+ }),
+ });
+ },
+ });
+
+ if (errors.length > 0) {
+ [this.error] = errors;
+ return;
+ }
+
+ this.workItemsToAdd = [];
+ this.unsetError();
+ this.showWorkItemsToAddInvalidMessage = false;
+ this.linkedItemType = LINKED_ITEM_TYPE_VALUE.RELATED;
+ this.$emit('submitted');
+ } catch (e) {
+ this.error = this.$options.i18n.addLinkedItemErrorMessage;
+ } finally {
+ this.isSubmitting = false;
+ }
+ },
+ unsetError() {
+ this.error = null;
+ },
+ },
+ i18n: {
+ addButtonLabel: __('Add'),
+ relatedToLabel: s__('WorkItem|relates to'),
+ blockingLabel: s__('WorkItem|blocks'),
+ blockedByLabel: s__('WorkItem|is blocked by'),
+ linkItemInputLabel: s__('WorkItem|the following item(s)'),
+ addLinkedItemErrorMessage: s__(
+ 'WorkItem|Something went wrong when trying to link a item. Please try again.',
+ ),
+ maxItemsNoteLabel: I18N_MAX_WORK_ITEMS_NOTE_LABEL,
+ maxItemsErrorMessage: I18N_MAX_WORK_ITEMS_ERROR_MESSAGE,
+ },
+};
+</script>
+
+<template>
+ <gl-form
+ class="gl-new-card-add-form"
+ data-testid="link-work-item-form"
+ @submit.stop.prevent="linkWorkItem"
+ >
+ <gl-alert v-if="error" variant="danger" class="gl-mb-3" @dismiss="unsetError">
+ {{ error }}
+ </gl-alert>
+ <gl-form-group
+ :label="linkItemFormHeaderLabel"
+ label-for="linked-item-type-radio"
+ label-class="label-bold"
+ class="gl-mb-3"
+ >
+ <gl-form-radio-group
+ id="linked-item-type-radio"
+ v-model="linkedItemType"
+ :options="linkedItemTypes"
+ :checked="linkedItemType"
+ />
+ </gl-form-group>
+ <p class="gl-font-weight-bold gl-mb-2">
+ {{ $options.i18n.linkItemInputLabel }}
+ </p>
+ <div class="gl-mb-5">
+ <work-item-token-input
+ v-model="workItemsToAdd"
+ class="gl-mb-2"
+ :parent-work-item-id="workItemId"
+ :children-ids="childrenIds"
+ :are-work-items-to-add-valid="areWorkItemsToAddValid"
+ :full-path="workItemFullPath"
+ :max-selection-limit="maxWorkItems"
+ @searching="searchInProgress = $event"
+ />
+ <div v-if="errorMessage" class="gl-mb-2 gl-text-red-500">
+ {{ $options.i18n.maxItemsErrorMessage }}
+ </div>
+ <div v-if="!errorMessage" data-testid="max-work-item-note" class="gl-text-gray-500">
+ {{ $options.i18n.maxItemsNoteLabel }}
+ </div>
+ <div
+ v-if="showWorkItemsToAddInvalidMessage"
+ class="gl-text-red-500"
+ data-testid="work-items-invalid"
+ >
+ {{ workItemsToAddInvalidMessage }}
+ </div>
+ </div>
+ <gl-button
+ data-testid="link-work-item-button"
+ category="primary"
+ variant="confirm"
+ size="small"
+ type="submit"
+ :disabled="isSubmitButtonDisabled"
+ :loading="isSubmitting"
+ class="gl-mr-2"
+ >
+ {{ $options.i18n.addButtonLabel }}
+ </gl-button>
+ <gl-button category="secondary" size="small" @click="$emit('cancel')">
+ {{ s__('WorkItem|Cancel') }}
+ </gl-button>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
index cbe830f9565..002c1786044 100644
--- a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
@@ -1,6 +1,5 @@
<script>
import WorkItemLinkChildContents from '../shared/work_item_link_child_contents.vue';
-import { workItemPath } from '../../utils';
export default {
components: {
@@ -20,20 +19,11 @@ export default {
type: Boolean,
required: true,
},
- workItemFullPath: {
- type: String,
- required: true,
- },
- },
- methods: {
- linkedItemPath(fullPath, id) {
- return workItemPath(fullPath, id);
- },
},
};
</script>
<template>
- <div>
+ <div data-testid="work-item-linked-items-list">
<h4
v-if="heading"
data-testid="work-items-list-heading"
@@ -51,8 +41,9 @@ export default {
<work-item-link-child-contents
:child-item="linkedItem.workItem"
:can-update="canUpdate"
- :child-path="linkedItemPath(workItemFullPath, linkedItem.workItem.iid)"
+ :show-task-icon="true"
@click="$emit('showModal', { event: $event, child: linkedItem.workItem })"
+ @removeChild="$emit('removeLinkedItem', linkedItem.workItem)"
/>
</li>
</ul>
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
index 4f6879e9605..20427fe96c4 100644
--- a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
@@ -1,23 +1,37 @@
<script>
-import { GlLoadingIcon, GlIcon, GlButton } from '@gitlab/ui';
+import { produce } from 'immer';
+import { GlLoadingIcon, GlIcon, GlButton, GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
+import removeLinkedItemsMutation from '../../graphql/remove_linked_items.mutation.graphql';
import { WIDGET_TYPE_LINKED_ITEMS, LINKED_CATEGORIES_MAP } from '../../constants';
import WidgetWrapper from '../widget_wrapper.vue';
import WorkItemRelationshipList from './work_item_relationship_list.vue';
+import WorkItemAddRelationshipForm from './work_item_add_relationship_form.vue';
export default {
+ helpPath: helpPagePath('/user/okrs.md#linked-items-in-okrs'),
components: {
GlLoadingIcon,
GlIcon,
GlButton,
+ GlLink,
WidgetWrapper,
WorkItemRelationshipList,
+ WorkItemAddRelationshipForm,
},
+ inject: ['isGroup'],
props: {
+ workItemId: {
+ type: String,
+ required: false,
+ default: null,
+ },
workItemIid: {
type: String,
required: true,
@@ -26,10 +40,17 @@ export default {
type: String,
required: true,
},
+ workItemType: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
apollo: {
workItem: {
- query: workItemByIidQuery,
+ query() {
+ return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
+ },
variables() {
return {
fullPath: this.workItemFullPath,
@@ -74,13 +95,13 @@ export default {
linksRelatesTo: [],
linksIsBlockedBy: [],
linksBlocks: [],
+ isShownLinkItemForm: false,
widgetName: 'linkeditems',
};
},
computed: {
- canUpdate() {
- // This will be false untill we implement remove item mutation
- return false;
+ canAdminWorkItemLink() {
+ return this.workItem?.userPermissions?.adminWorkItemLink;
},
isLoading() {
return this.$apollo.queries.workItem.loading;
@@ -91,18 +112,88 @@ export default {
linkedWorkItems() {
return this.linkedWorkItemsWidget?.linkedItems?.nodes || [];
},
+ childrenIds() {
+ return this.linkedWorkItems.map((item) => item.workItem.id);
+ },
linkedWorkItemsCount() {
return this.linkedWorkItems.length;
},
isEmptyRelatedWorkItems() {
- return !this.error && this.linkedWorkItems.length === 0;
+ return !this.isShownLinkItemForm && !this.error && this.linkedWorkItems.length === 0;
+ },
+ },
+ methods: {
+ showLinkItemForm() {
+ this.isShownLinkItemForm = true;
+ },
+ hideLinkItemForm() {
+ this.isShownLinkItemForm = false;
+ },
+ async removeLinkedItem(linkedItem) {
+ try {
+ const {
+ data: {
+ workItemRemoveLinkedItems: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: removeLinkedItemsMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ workItemsIds: [linkedItem.id],
+ },
+ },
+ update: (cache, { data: { workItemRemoveLinkedItems } }) => {
+ const errorMessages = workItemRemoveLinkedItems?.errors;
+ if (errorMessages && errorMessages.length > 0) {
+ [this.error] = errorMessages;
+ return;
+ }
+ const queryArgs = {
+ query: workItemByIidQuery,
+ variables: { fullPath: this.workItemFullPath, iid: this.workItemIid },
+ };
+ const sourceData = cache.readQuery(queryArgs);
+
+ if (!sourceData) {
+ return;
+ }
+
+ cache.writeQuery({
+ ...queryArgs,
+ data: produce(sourceData, (draftState) => {
+ const linkedItems =
+ draftState.workspace.workItems.nodes[0].widgets?.find(
+ (widget) => widget.type === WIDGET_TYPE_LINKED_ITEMS,
+ )?.linkedItems?.nodes || [];
+ const index = linkedItems.findIndex((item) => {
+ return item.workItem.id === linkedItem.id;
+ });
+ linkedItems.splice(index, 1);
+ }),
+ });
+ },
+ });
+
+ if (errors.length > 0) {
+ [this.error] = errors;
+ return;
+ }
+
+ this.$toast.show(s__('WorkItem|Linked item removed'));
+ } catch {
+ this.error = this.$options.i18n.removeLinkedItemErrorMessage;
+ }
},
},
i18n: {
title: s__('WorkItem|Linked Items'),
- fetchError: s__('WorkItem|Something went wrong when fetching tasks. Please refresh this page.'),
+ fetchError: s__('WorkItem|Something went wrong when fetching items. Please refresh this page.'),
emptyStateMessage: s__(
- "WorkItem|Link work items together to show that they're related or that one is blocking others.",
+ "WorkItem|Link items together to show that they're related or that one is blocking others.",
+ ),
+ removeLinkedItemErrorMessage: s__(
+ 'WorkItem|Something went wrong when removing item. Please refresh this page.',
),
addChildButtonLabel: s__('WorkItem|Add'),
relatedToTitle: s__('WorkItem|Related to'),
@@ -131,17 +222,36 @@ export default {
</div>
</template>
<template #header-right>
- <gl-button size="small" class="gl-ml-3">
+ <gl-button
+ v-if="canAdminWorkItemLink"
+ data-testid="link-item-add-button"
+ size="small"
+ class="gl-ml-3"
+ @click="showLinkItemForm"
+ >
<slot name="add-button-text">{{ $options.i18n.addLinkedWorkItemButtonLabel }}</slot>
</gl-button>
</template>
<template #body>
<div class="gl-new-card-content">
+ <work-item-add-relationship-form
+ v-if="isShownLinkItemForm"
+ :work-item-id="workItemId"
+ :work-item-iid="workItemIid"
+ :work-item-full-path="workItemFullPath"
+ :children-ids="childrenIds"
+ :work-item-type="workItemType"
+ @submitted="hideLinkItemForm"
+ @cancel="hideLinkItemForm"
+ />
<gl-loading-icon v-if="isLoading" color="dark" class="gl-my-2" />
<template v-else>
- <div v-if="isEmptyRelatedWorkItems" data-testid="links-empty">
+ <div v-if="!isShownLinkItemForm && isEmptyRelatedWorkItems" data-testid="links-empty">
<p class="gl-new-card-empty">
{{ $options.i18n.emptyStateMessage }}
+ <gl-link :href="$options.helpPath" data-testid="help-link">
+ {{ __('Learn more.') }}
+ </gl-link>
</p>
</div>
<template v-else>
@@ -153,9 +263,9 @@ export default {
}"
:linked-items="linksBlocks"
:heading="$options.i18n.blockingTitle"
- :work-item-full-path="workItemFullPath"
- :can-update="canUpdate"
+ :can-update="canAdminWorkItemLink"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ @removeLinkedItem="removeLinkedItem"
/>
<work-item-relationship-list
v-if="linksIsBlockedBy.length"
@@ -165,17 +275,17 @@ export default {
}"
:linked-items="linksIsBlockedBy"
:heading="$options.i18n.blockedByTitle"
- :work-item-full-path="workItemFullPath"
- :can-update="canUpdate"
+ :can-update="canAdminWorkItemLink"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ @removeLinkedItem="removeLinkedItem"
/>
<work-item-relationship-list
v-if="linksRelatesTo.length"
:linked-items="linksRelatesTo"
:heading="$options.i18n.relatedToTitle"
- :work-item-full-path="workItemFullPath"
- :can-update="canUpdate"
+ :can-update="canAdminWorkItemLink"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ @removeLinkedItem="removeLinkedItem"
/>
</template>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_todos.vue b/app/assets/javascripts/work_items/components/work_item_todos.vue
index b21abf21be5..e6d7f2067ba 100644
--- a/app/assets/javascripts/work_items/components/work_item_todos.vue
+++ b/app/assets/javascripts/work_items/components/work_item_todos.vue
@@ -4,9 +4,10 @@ import { produce } from 'immer';
import { s__ } from '~/locale';
import { updateGlobalTodoCount } from '~/sidebar/utils';
-import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
-import createWorkItemTodosMutation from '~/work_items/graphql/create_work_item_todos.mutation.graphql';
-import markDoneWorkItemTodosMutation from '~/work_items/graphql/mark_done_work_item_todos.mutation.graphql';
+import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
+import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
+import createWorkItemTodosMutation from '../graphql/create_work_item_todos.mutation.graphql';
+import markDoneWorkItemTodosMutation from '../graphql/mark_done_work_item_todos.mutation.graphql';
import {
TODO_ADD_ICON,
@@ -28,6 +29,7 @@ export default {
GlIcon,
GlButton,
},
+ inject: ['isGroup'],
props: {
workItemId: {
type: String,
@@ -148,7 +150,7 @@ export default {
},
updateWorkItemCurrentTodosWidgetCache({ cache, todos }) {
const query = {
- query: workItemByIidQuery,
+ query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: { fullPath: this.workItemFullpath, iid: this.workItemIid },
};
diff --git a/app/assets/javascripts/work_items/components/work_item_type_icon.vue b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
index 5426f3965b3..76a73093206 100644
--- a/app/assets/javascripts/work_items/components/work_item_type_icon.vue
+++ b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
@@ -36,6 +36,11 @@ export default {
return this.workItemType.toUpperCase().split(' ').join('_');
},
iconName() {
+ // TODO Delete this conditional once we have an `issue-type-epic` icon
+ if (this.workItemIconName === 'issue-type-epic') {
+ return 'epic';
+ }
+
return (
this.workItemIconName ||
WORK_ITEMS_TYPE_MAP[this.workItemTypeUppercase]?.icon ||
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 2b118247426..a64172acff4 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -112,8 +112,19 @@ export const I18N_WORK_ITEM_COPY_CREATE_NOTE_EMAIL = s__(
'WorkItem|Copy %{workItemType} email address',
);
+export const MAX_WORK_ITEMS = 10;
+
+export const I18N_MAX_WORK_ITEMS_ERROR_MESSAGE = sprintf(
+ s__('WorkItem|Only %{MAX_WORK_ITEMS} items can be added at a time.'),
+ { MAX_WORK_ITEMS },
+);
+export const I18N_MAX_WORK_ITEMS_NOTE_LABEL = sprintf(
+ s__('WorkItem|Add a maximum of %{MAX_WORK_ITEMS} items at a time.'),
+ { MAX_WORK_ITEMS },
+);
+
export const sprintfWorkItem = (msg, workItemTypeArg, parentWorkItemType = '') => {
- const workItemType = workItemTypeArg || s__('WorkItem|Work item');
+ const workItemType = workItemTypeArg || s__('WorkItem|item');
return capitalizeFirstCharacter(
sprintf(msg, {
workItemType: workItemType.toLocaleLowerCase(),
@@ -186,8 +197,11 @@ export const WORK_ITEM_NAME_TO_ICON_MAP = {
Issue: 'issue-type-issue',
Task: 'issue-type-task',
Objective: 'issue-type-objective',
+ Incident: 'issue-type-incident',
// eslint-disable-next-line @gitlab/require-i18n-strings
'Key Result': 'issue-type-keyresult',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ 'Test Case': 'issue-type-test-case',
};
export const FORM_TYPES = {
@@ -262,3 +276,15 @@ export const LINKED_CATEGORIES_MAP = {
IS_BLOCKED_BY: 'is_blocked_by',
BLOCKS: 'blocks',
};
+
+export const LINKED_ITEM_TYPE_VALUE = {
+ RELATED: 'RELATED',
+ BLOCKED_BY: 'BLOCKED_BY',
+ BLOCKS: 'BLOCKS',
+};
+
+export const LINK_ITEM_FORM_HEADER_LABEL = {
+ [WORK_ITEM_TYPE_VALUE_OBJECTIVE]: s__('WorkItem|The current objective'),
+ [WORK_ITEM_TYPE_VALUE_KEY_RESULT]: s__('WorkItem|The current key result'),
+ [WORK_ITEM_TYPE_VALUE_TASK]: s__('WorkItem|The current task'),
+};
diff --git a/app/assets/javascripts/work_items/graphql/add_linked_items.mutation.graphql b/app/assets/javascripts/work_items/graphql/add_linked_items.mutation.graphql
new file mode 100644
index 00000000000..ba12c7f9b51
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/add_linked_items.mutation.graphql
@@ -0,0 +1,10 @@
+#import "./work_item.fragment.graphql"
+
+mutation addLinkedItems($input: WorkItemAddLinkedItemsInput!) {
+ workItemAddLinkedItems(input: $input) {
+ workItem {
+ ...WorkItem
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/cache_utils.js b/app/assets/javascripts/work_items/graphql/cache_utils.js
index 14eedf5cdd8..aeeffea24e7 100644
--- a/app/assets/javascripts/work_items/graphql/cache_utils.js
+++ b/app/assets/javascripts/work_items/graphql/cache_utils.js
@@ -1,5 +1,6 @@
import { produce } from 'immer';
import { WIDGET_TYPE_NOTES } from '~/work_items/constants';
+import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import { findHierarchyWidgetChildren } from '~/work_items/utils';
@@ -127,8 +128,11 @@ export const updateCacheAfterRemovingAwardEmojiFromNote = (currentNotes, note) =
});
};
-export const addHierarchyChild = (cache, fullPath, iid, workItem) => {
- const queryArgs = { query: workItemByIidQuery, variables: { fullPath, iid } };
+export const addHierarchyChild = ({ cache, fullPath, iid, isGroup, workItem }) => {
+ const queryArgs = {
+ query: isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
+ variables: { fullPath, iid },
+ };
const sourceData = cache.readQuery(queryArgs);
if (!sourceData) {
@@ -143,8 +147,11 @@ export const addHierarchyChild = (cache, fullPath, iid, workItem) => {
});
};
-export const removeHierarchyChild = (cache, fullPath, iid, workItem) => {
- const queryArgs = { query: workItemByIidQuery, variables: { fullPath, iid } };
+export const removeHierarchyChild = ({ cache, fullPath, iid, isGroup, workItem }) => {
+ const queryArgs = {
+ query: isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
+ variables: { fullPath, iid },
+ };
const sourceData = cache.readQuery(queryArgs);
if (!sourceData) {
diff --git a/app/assets/javascripts/work_items/graphql/group_work_item_by_iid.query.graphql b/app/assets/javascripts/work_items/graphql/group_work_item_by_iid.query.graphql
new file mode 100644
index 00000000000..f23bafa20c3
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/group_work_item_by_iid.query.graphql
@@ -0,0 +1,12 @@
+#import "./work_item.fragment.graphql"
+
+query groupWorkItemByIid($fullPath: ID!, $iid: String) {
+ workspace: group(fullPath: $fullPath) @persist {
+ id
+ workItems(iid: $iid) {
+ nodes {
+ ...WorkItem
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
index 7d63af448d4..2be436aa8c2 100644
--- a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
@@ -9,6 +9,7 @@ query projectWorkItems(
workItems(search: $searchTerm, types: $types, in: $in) {
nodes {
id
+ iid
title
state
confidential
diff --git a/app/assets/javascripts/work_items/graphql/remove_linked_items.mutation.graphql b/app/assets/javascripts/work_items/graphql/remove_linked_items.mutation.graphql
new file mode 100644
index 00000000000..f83f5474606
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/remove_linked_items.mutation.graphql
@@ -0,0 +1,6 @@
+mutation removeLinkedItems($input: WorkItemRemoveLinkedItemsInput!) {
+ workItemRemoveLinkedItems(input: $input) {
+ errors
+ message
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/update_work_item_notifications.mutation.graphql b/app/assets/javascripts/work_items/graphql/update_work_item_notifications.mutation.graphql
index f28317b79b5..9d71d452430 100644
--- a/app/assets/javascripts/work_items/graphql/update_work_item_notifications.mutation.graphql
+++ b/app/assets/javascripts/work_items/graphql/update_work_item_notifications.mutation.graphql
@@ -1,9 +1,14 @@
-mutation updateWorkItemNotificationsWidget($input: IssueSetSubscriptionInput!) {
- updateWorkItemNotificationsSubscription: issueSetSubscription(input: $input) {
- issue {
+mutation workItemSubscribe($input: WorkItemSubscribeInput!) {
+ workItemSubscribe(input: $input) {
+ errors
+ workItem {
id
- subscribed
+ widgets {
+ ... on WorkItemWidgetNotifications {
+ type
+ subscribed
+ }
+ }
}
- errors
}
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
index 1ae5617f04d..fac99310890 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -33,6 +33,7 @@ fragment WorkItem on WorkItem {
adminParentLink
setWorkItemMetadata
createNote
+ adminWorkItemLink
}
widgets {
...WorkItemWidgets
diff --git a/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
index f303a797e9c..d15e3086560 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
@@ -52,4 +52,12 @@ fragment WorkItemMetadataWidgets on WorkItemWidget {
... on WorkItemWidgetAwardEmoji {
type
}
+
+ ... on WorkItemWidgetLinkedItems {
+ type
+ }
+
+ ... on WorkItemWidgetHierarchy {
+ type
+ }
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql
index b4fb83b24c2..5c797367903 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql
@@ -37,6 +37,7 @@ query workItemTreeQuery($id: WorkItemID!) {
state
createdAt
closedAt
+ webUrl
widgets {
... on WorkItemWidgetHierarchy {
type
diff --git a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
index ffc9fe2f7f7..b357e765d16 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
@@ -66,6 +66,7 @@ fragment WorkItemWidgets on WorkItemWidget {
state
createdAt
closedAt
+ webUrl
widgets {
... on WorkItemWidgetHierarchy {
type
@@ -120,6 +121,7 @@ fragment WorkItemWidgets on WorkItemWidget {
state
createdAt
closedAt
+ webUrl
widgets {
...WorkItemMetadataWidgets
}
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index 70bda7d3783..0b7f9290d6e 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -1,17 +1,25 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { WORKSPACE_GROUP } from '~/issues/constants';
import { parseBoolean } from '~/lib/utils/common_utils';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import App from './components/app.vue';
+import WorkItemRoot from './pages/work_item_root.vue';
import { createRouter } from './router';
Vue.use(VueApollo);
-export const initWorkItemsRoot = () => {
+export const initWorkItemsRoot = (workspace) => {
const el = document.querySelector('#js-work-items');
+
+ if (!el) {
+ return undefined;
+ }
+
const {
fullPath,
hasIssueWeightsFeature,
+ iid,
issuesListPath,
registerPath,
signInPath,
@@ -22,6 +30,8 @@ export const initWorkItemsRoot = () => {
reportAbusePath,
} = el.dataset;
+ const Component = workspace === WORKSPACE_GROUP ? WorkItemRoot : App;
+
return new Vue({
el,
name: 'WorkItemsRoot',
@@ -29,6 +39,7 @@ export const initWorkItemsRoot = () => {
apolloProvider,
provide: {
fullPath,
+ isGroup: workspace === WORKSPACE_GROUP,
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasOkrsFeature: parseBoolean(hasOkrsFeature),
issuesListPath,
@@ -40,7 +51,11 @@ export const initWorkItemsRoot = () => {
reportAbusePath,
},
render(createElement) {
- return createElement(App);
+ return createElement(Component, {
+ props: {
+ iid: workspace === WORKSPACE_GROUP ? iid : undefined,
+ },
+ });
},
});
};
diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue
index b5705b21b5a..31e790254d9 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -10,6 +10,7 @@ import {
} from '../constants';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
+import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import ItemTitle from '../components/item_title.vue';
@@ -22,7 +23,7 @@ export default {
ItemTitle,
GlFormSelect,
},
- inject: ['fullPath'],
+ inject: ['fullPath', 'isGroup'],
props: {
initialTitle: {
type: String,
@@ -94,7 +95,7 @@ export default {
const { workItem } = workItemCreate;
store.writeQuery({
- query: workItemByIidQuery,
+ query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: {
fullPath: this.fullPath,
iid: workItem.iid,
diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js
index 1443e4b509d..ac5d8b32fad 100644
--- a/app/assets/javascripts/work_items/utils.js
+++ b/app/assets/javascripts/work_items/utils.js
@@ -1,4 +1,3 @@
-import { joinPaths } from '~/lib/utils/url_utility';
import {
WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_HEALTH_STATUS,
@@ -43,7 +42,3 @@ export const markdownPreviewPath = (fullPath, iid) =>
`${
gon.relative_url_root || ''
}/${fullPath}/preview_markdown?target_type=WorkItem&target_id=${iid}`;
-
-export const workItemPath = (fullPath, workItemIid) => {
- return joinPaths(gon?.relative_url_root || '/', fullPath, '-', 'work_items', workItemIid);
-};
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index 47701d0490a..be9a06d7bb5 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -4,11 +4,9 @@
@import './pages/groups';
@import './pages/hierarchy';
@import './pages/issues';
-@import './pages/labels';
@import './pages/note_form';
@import './pages/notes';
@import './pages/pipelines';
@import './pages/profile';
-@import './pages/projects';
@import './pages/registry';
@import './pages/settings';
diff --git a/app/assets/stylesheets/components/detail_page.scss b/app/assets/stylesheets/components/detail_page.scss
index 74f61faa9ae..de8142924f9 100644
--- a/app/assets/stylesheets/components/detail_page.scss
+++ b/app/assets/stylesheets/components/detail_page.scss
@@ -74,7 +74,3 @@
color: $gl-text-color;
}
}
-
-.new-header-popover {
- z-index: 999;
-}
diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss
index 04a7590d531..4d53ae9ed4b 100644
--- a/app/assets/stylesheets/components/related_items_list.scss
+++ b/app/assets/stylesheets/components/related_items_list.scss
@@ -5,7 +5,6 @@ $item-remove-button-space: 42px;
.related-items-list {
padding: $gl-padding-4;
- padding-right: $gl-padding-6;
border-bottom-left-radius: $gl-border-size-3;
border-bottom-right-radius: $gl-border-size-3;
diff --git a/app/assets/stylesheets/fonts.scss b/app/assets/stylesheets/fonts.scss
index 0d87d49ac18..6886e751b72 100644
--- a/app/assets/stylesheets/fonts.scss
+++ b/app/assets/stylesheets/fonts.scss
@@ -7,7 +7,7 @@ Usage:
@font-face {
font-family: 'GitLab Sans';
font-weight: 100 900;
- font-display: optional;
+ font-display: swap;
font-style: normal;
/* stylelint-disable-next-line property-no-unknown */
font-named-instance: 'Regular';
@@ -17,7 +17,7 @@ Usage:
@font-face {
font-family: 'GitLab Sans';
font-weight: 100 900;
- font-display: optional;
+ font-display: swap;
font-style: italic;
/* stylelint-disable-next-line property-no-unknown */
font-named-instance: 'Regular';
@@ -33,7 +33,7 @@ Usage:
@font-face {
font-family: 'GitLab Mono';
font-weight: 100 900;
- font-display: optional;
+ font-display: swap;
font-style: normal;
src: font-url('gitlab-mono/GitLabMono.woff2') format('woff2');
}
@@ -41,7 +41,7 @@ Usage:
@font-face {
font-family: 'GitLab Mono';
font-weight: 100 900;
- font-display: optional;
+ font-display: swap;
font-style: italic;
src: font-url('gitlab-mono/GitLabMono-Italic.woff2') format('woff2');
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index cbdc55d66c1..cae2ea1716c 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -192,3 +192,7 @@
padding: inherit;
}
}
+
+.gl-empty-state {
+ margin-top: $gl-spacing-scale-7;
+}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 514247d2913..21c252038af 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -43,7 +43,8 @@
.right-sidebar-collapsed {
--application-bar-right: #{$right-sidebar-collapsed-width};
- &.is-merge-request {
+ &.is-merge-request,
+ &.build-sidebar {
--application-bar-right: 0px;
}
}
@@ -51,6 +52,10 @@
.right-sidebar-expanded {
--application-bar-right: #{$right-sidebar-width};
}
+
+ .build-sidebar {
+ --application-bar-right: 0px;
+ }
}
@include media-breakpoint-up(md) {
@@ -567,3 +572,43 @@ See https://gitlab.com/gitlab-org/gitlab/issues/36857 for more details.
}
}
}
+
+// --- moved from labels.scss when moving to page_bundles ---
+// Fix scoped label padding in cases where old markdown uses the old label structure */
+.gl-label-text + .gl-label-text {
+ @include gl-pl-2;
+ @include gl-pr-3;
+}
+
+// used in the Markdown rendering of labels
+.scoped-label-tooltip-title {
+ color: var(--indigo-300, $indigo-300);
+}
+
+.gl-label-scoped {
+ box-shadow: 0 0 0 2px currentColor inset;
+
+ &.gl-label-sm {
+ box-shadow: 0 0 0 1px inset;
+ }
+}
+
+.ref-container,
+.commit-sha-container {
+ font-family: $gl-monospace-font;
+ font-variant-ligatures: none;
+ font-size: $gl-font-size-sm;
+ padding-left: $gl-spacing-scale-2;
+ padding-right: $gl-spacing-scale-2;
+ border-radius: $gl-border-radius-base;
+}
+
+.ref-container {
+ color: var(--blue-500, $blue-500) !important;
+ background-color: var(--blue-50, $blue-50);
+}
+
+.commit-sha-container {
+ color: var(--gray-700, $gray-700) !important;
+ background-color: var(--gray-50, $gray-50);
+}
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index 9b22e4cebb2..d3986f31d52 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -63,6 +63,7 @@ gl-emoji {
border-bottom-color: $blue-500;
}
-.emoji-picker .gl-dropdown-inner > :last-child {
+.emoji-picker .gl-dropdown-contents > :last-child {
padding-bottom: 0;
+ overflow-y: hidden;
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index a32663b17d3..df107798a87 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -254,7 +254,7 @@
@mixin build-log-bar($height) {
height: $height;
min-height: $height;
- background: var(--gray-50, $gray-50);
+ background: var(--white, $white);
border: 1px solid var(--border-color, $border-color);
color: var(--gl-text-color, $gl-text-color);
padding: $grid-size;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index f2afa94e000..0619d5f166e 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -55,6 +55,10 @@
padding-right: 0;
z-index: $zindex-dropdown-menu;
+ .inline-block {
+ @include gl-display-inline-block;
+ }
+
&.right-sidebar-merge-requests {
width: $right-sidebar-width;
@@ -73,13 +77,21 @@
}
}
- &:not(.is-merge-request) {
+ &:not(.is-merge-request):not(.build-sidebar) {
@include media-breakpoint-up(md) {
.content-wrapper {
padding-right: $right-sidebar-width;
}
}
}
+
+ &.build-sidebar {
+ @include media-breakpoint-up(lg) {
+ .content-wrapper {
+ padding-right: $right-sidebar-width;
+ }
+ }
+ }
}
.right-sidebar {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index e83f6af603a..a4bb39e0764 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -481,7 +481,7 @@ $count-arrow-border: #dce0e5;
$general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232);
-$performance-bar-height: 35px;
+$performance-bar-height: 2.5rem;
$system-header-height: 16px;
$system-footer-height: $system-header-height;
$mr-sticky-header-height: 72px;
@@ -834,7 +834,7 @@ Performance Bar
*/
$perf-bar-production: $gray-950;
$perf-bar-staging: $indigo-950;
-$perf-bar-development: $red-950;
+$perf-bar-development: $red-900;
$perf-bar-bucket-bg: $black;
$perf-bar-bucket-box-shadow-from: rgba($white, 0.2);
$perf-bar-bucket-box-shadow-to: rgba($black, 0.25);
diff --git a/app/assets/stylesheets/page_bundles/build.scss b/app/assets/stylesheets/page_bundles/build.scss
index 09c4d184f3f..16fc0e7ebae 100644
--- a/app/assets/stylesheets/page_bundles/build.scss
+++ b/app/assets/stylesheets/page_bundles/build.scss
@@ -17,7 +17,7 @@
@include build-log-top-bar(50px);
z-index: 2;
border-radius: $border-radius-default $border-radius-default 0 0;
- box-shadow: 0 -2px 0 0 var(--white);
+ box-shadow: 0 -4px 0 0 var(--white);
&.has-archived-block {
top: calc(#{$calc-application-header-height} + 28px);
@@ -89,13 +89,20 @@
}
.right-sidebar.build-sidebar {
+ padding: 0;
+
+ @include media-breakpoint-up(lg) {
+ @include gl-border-l-0;
+ }
+
&.right-sidebar-collapsed {
display: none;
}
.sidebar-container {
- padding-right: 100px;
- height: 100%;
+ @include gl-sticky;
+ top: #{$top-bar-height - 1px};
+ max-height: calc(100vh - #{$top-bar-height - 1px} - var(--performance-bar-height));
overflow-y: scroll;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
@@ -155,10 +162,6 @@
}
.build-sidebar-item {
- display: grid;
- grid-template-columns: 1fr 2fr;
- grid-gap: $gl-padding-8;
-
&:last-of-type {
@include gl-mb-0;
}
diff --git a/app/assets/stylesheets/page_bundles/escalation_policies.scss b/app/assets/stylesheets/page_bundles/escalation_policies.scss
index 84c62ba93dd..49423fccea4 100644
--- a/app/assets/stylesheets/page_bundles/escalation_policies.scss
+++ b/app/assets/stylesheets/page_bundles/escalation_policies.scss
@@ -42,3 +42,9 @@ $stroke-size: 1px;
@include gl-w-full;
}
}
+
+.escalation-email-user-dropdown {
+ .show.dropdown .dropdown-menu {
+ max-height: 300px;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 2002b4d4dff..7f8068e5d56 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -918,7 +918,7 @@ $ide-commit-header-height: 48px;
--svg-status-bg: var(--ide-background, #{$white});
}
- .empty-state {
+ .gl-empty-state {
p {
margin: $grid-size 0;
text-align: center;
diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss
index 5397f3d8895..07614c5271a 100644
--- a/app/assets/stylesheets/page_bundles/issuable.scss
+++ b/app/assets/stylesheets/page_bundles/issuable.scss
@@ -1,18 +1,5 @@
@import 'mixins_and_variables_and_functions';
-$issuable-warning-size: 24px;
-
-.issuable-warning-icon {
- background-color: var(--orange-50, $orange-50);
- border-radius: $border-radius-default;
- color: var(--orange-600, $orange-600);
- width: $issuable-warning-size;
- height: $issuable-warning-size;
- text-align: center;
- line-height: $gl-line-height-24;
- flex: 0 0 auto;
-}
-
.limit-container-width {
.flash-container,
.detail-page-header,
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/page_bundles/labels.scss
index 29f2d15008b..bc0bf4bc490 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/page_bundles/labels.scss
@@ -1,3 +1,5 @@
+@import 'mixins_and_variables_and_functions';
+
.suggest-colors {
padding-top: 3px;
@@ -29,19 +31,19 @@
margin-bottom: -5px;
&:first-of-type {
- border-top-left-radius: $border-radius-base;
+ border-top-left-radius: $gl-border-radius-base;
}
&:nth-of-type(7) {
- border-top-right-radius: $border-radius-base;
+ border-top-right-radius: $gl-border-radius-base;
}
&:nth-last-child(7) {
- border-bottom-left-radius: $border-radius-base;
+ border-bottom-left-radius: $gl-border-radius-base;
}
&:last-of-type {
- border-bottom-right-radius: $border-radius-base;
+ border-bottom-right-radius: $gl-border-radius-base;
}
}
}
@@ -78,7 +80,7 @@
padding: 0 $grid-size;
line-height: 16px;
border-radius: $label-border-radius;
- color: $white;
+ color: var(--white, $white);
}
.manage-labels-list {
@@ -94,7 +96,7 @@
&:hover,
&:focus-within {
- background-color: $blue-50;
+ background-color: var(--blue-50, $blue-50);
}
&:active {
@@ -109,10 +111,6 @@
}
}
-.label-list-item:not(:last-of-type) {
- border-bottom: 1px solid $border-color;
-}
-
.prioritized-labels .add-priority,
.other-labels .remove-priority {
display: none;
@@ -133,7 +131,7 @@
}
.label-badge {
- color: $gray-900;
+ color: var(--gray-900, $gray-900);
display: inline-block;
font-weight: $gl-font-weight-normal;
padding: $gl-padding-4 $gl-padding-8;
@@ -151,15 +149,15 @@
}
.label-action {
- color: $gray-700;
+ color: var(--gray-700, $gray-700);
cursor: pointer;
&:hover {
- color: $blue-600;
+ color: var(--blue-600, $blue-600);
}
&.hover-red:hover {
- color: $red-500;
+ color: var(--red-500, $red-500);
}
}
}
@@ -192,21 +190,3 @@
.priority-labels-empty-state .svg-content img {
max-width: $priority-label-empty-state-width;
}
-
-.scoped-label-tooltip-title {
- color: $indigo-300;
-}
-
-.gl-label-scoped {
- box-shadow: 0 0 0 2px currentColor inset;
-
- &.gl-label-sm {
- box-shadow: 0 0 0 1px inset;
- }
-}
-
-/* Fix scoped label padding in cases where old markdown uses the old label structure */
-.gl-label-text + .gl-label-text {
- @include gl-pl-2;
- @include gl-pr-3;
-}
diff --git a/app/assets/stylesheets/page_bundles/merge_request.scss b/app/assets/stylesheets/page_bundles/merge_request.scss
index f03efb82860..e429c0c149e 100644
--- a/app/assets/stylesheets/page_bundles/merge_request.scss
+++ b/app/assets/stylesheets/page_bundles/merge_request.scss
@@ -205,7 +205,11 @@ $comparison-empty-state-height: 62px;
top: $calc-application-header-height;
z-index: $tabs-holder-z-index;
border-bottom: 1px solid var(--border-color, $border-color);
- background-color: var(--gray-10, $white);
+ background-color: $white;
+
+ .gl-dark & {
+ background-color: var(--gray-10);
+ }
@include media-breakpoint-up(md) {
position: sticky;
diff --git a/app/assets/stylesheets/page_bundles/ml_experiment_tracking.scss b/app/assets/stylesheets/page_bundles/ml_experiment_tracking.scss
index d6f71b12cd9..685719071b5 100644
--- a/app/assets/stylesheets/page_bundles/ml_experiment_tracking.scss
+++ b/app/assets/stylesheets/page_bundles/ml_experiment_tracking.scss
@@ -15,6 +15,6 @@ table.ml-candidate-table {
table.candidate-details {
td {
- padding: $gl-spacing-scale-3;
+ padding: $gl-spacing-scale-3 $gl-spacing-scale-3 $gl-spacing-scale-3 0;
}
}
diff --git a/app/assets/stylesheets/page_bundles/organizations.scss b/app/assets/stylesheets/page_bundles/organizations.scss
new file mode 100644
index 00000000000..1f1d127a82a
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/organizations.scss
@@ -0,0 +1,10 @@
+@import 'mixins_and_variables_and_functions';
+
+// Modeled after projects.scss and groups.scss
+.organization-row .organization-description p {
+ @include gl-mb-0;
+}
+
+.organization-root-path {
+ max-width: 40vw;
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/page_bundles/projects.scss
index 9ce470dbcf2..99c84026762 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/page_bundles/projects.scss
@@ -1,3 +1,5 @@
+@import 'mixins_and_variables_and_functions';
+
.new_project,
.edit-project,
.import-project {
@@ -6,7 +8,7 @@
}
.project-path .form-control {
- border-radius: $border-radius-base;
+ border-radius: $gl-border-radius-base;
}
.input-group {
@@ -41,13 +43,13 @@
&.static-namespace {
height: 35px;
border-radius: 3px;
- border: 1px solid $border-color;
+ border: 1px solid var(--border-color, $border-color);
max-width: 100%;
flex-grow: 1;
}
+ .btn-default {
- border-radius: 0 $border-radius-base $border-radius-base 0;
+ border-radius: 0 $gl-border-radius-base $gl-border-radius-base 0;
}
}
}
@@ -55,7 +57,7 @@
.save-project-loader {
margin-top: 50px;
margin-bottom: 50px;
- color: $gray-700;
+ color: var(--gray-700, $gray-700);
}
.deploy-key {
@@ -85,23 +87,23 @@
}
.vs-public {
- color: $blue-500;
+ color: var(--blue-500, $blue-500);
}
.vs-internal {
- color: $orange-500;
+ color: var(--orange-500, $orange-500);
}
.vs-private {
- color: $green-500;
+ color: var(--green-500, $green-500);
}
.lfs-enabled {
- color: $green-500;
+ color: var(--green-500, $green-500);
}
.lfs-disabled {
- color: $orange-500;
+ color: var(--orange-500, $orange-500);
}
.breadcrumb.repo-breadcrumb {
@@ -113,7 +115,7 @@
margin: 0;
a {
- color: $gl-text-color;
+ color: var(--gl-text-color, $gl-text-color);
}
.dropdown-menu {
@@ -135,7 +137,7 @@
padding: 16px 0;
&:not(:first-child) {
- border-top: 1px solid $border-color;
+ border-top: 1px solid var(--border-color, $border-color);
}
.controls {
@@ -166,7 +168,7 @@
.input-group-text {
width: 100%;
- background-color: $white;
+ background-color: var(--white, $white);
}
.selected-icon {
@@ -235,7 +237,7 @@
.repository-languages-bar {
height: 8px;
margin-bottom: $gl-padding;
- background-color: $white;
+ background-color: var(--white, $white);
border-radius: $border-radius-default;
.progress-bar {
@@ -253,7 +255,7 @@
}
.repository-language-bar-tooltip-share {
- color: $gray-200;
+ color: var(--gray-200, $gray-200);
}
/*
@@ -263,7 +265,7 @@
.project-row {
.description p {
margin-bottom: 0;
- color: $gl-text-color-secondary;
+ color: var(--gl-text-color-secondary, $gl-text-color-secondary);
@include str-truncated(100%);
}
}
@@ -280,7 +282,7 @@
@include gl-display-table-cell;
@include gl-vertical-align-top;
@include gl-py-4;
- border-bottom: 1px solid $gray-50;
+ border-bottom: 1px solid var(--gray-50, $gray-50);
}
.project-row:last-of-type {
@@ -470,8 +472,8 @@
.form-control {
@include gl-font-monospace;
- background-color: $white;
- border-color: $border-color;
+ background-color: var(--white, $white);
+ border-color: var(--border-color, $border-color);
font-size: 14px;
margin-left: -1px;
cursor: auto;
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index f36cbc129a7..01c6fde80da 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -93,6 +93,29 @@ $work-item-sticky-header-height: 52px;
}
}
+ // need to override the listbox styles to match with dropdown
+ // till the dropdown are converted to listbox
+ .gl-new-dropdown-toggle {
+ &:hover,
+ &:focus {
+ background: none !important;
+ box-shadow: $work-item-field-inset-shadow;
+ background-color: $input-bg;
+ }
+
+ .is-not-focused {
+ &.gl-new-dropdown-button-text {
+ margin: 0 0.25rem;
+ }
+ }
+ }
+
+ .gl-new-dropdown-toggle.is-not-focused {
+ .gl-new-dropdown-button-text {
+ margin: 0 0.25rem;
+ }
+ }
+
> .col {
min-width: 0;
}
@@ -167,6 +190,12 @@ $work-item-sticky-header-height: 52px;
}
}
+.work-item-parent-field-value {
+ .work-item-overview & {
+ max-width: 75%;
+ }
+}
+
.token-selector-menu-class {
.work-item-overview & {
width: 100%;
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index cb153122767..490ac15241b 100644
--- a/app/assets/stylesheets/performance_bar.scss
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -10,8 +10,9 @@
height: $performance-bar-height;
background: $black;
+ font-size: $gl-font-size-small;
line-height: $performance-bar-height;
- color: $gray-100;
+ color: $gray-50;
select {
width: 200px;
@@ -24,13 +25,22 @@
select,
input {
color: inherit;
- background-color: inherit;
+ background-color: rgba($white, 0.2);
+
+ &::placeholder {
+ color: rgba($white, 0.7);
+ }
}
option {
color: initial;
}
+ .gl-link,
+ .gl-button {
+ color: $white;
+ }
+
&.disabled {
display: none;
}
@@ -45,6 +55,10 @@
&.development {
background-color: $perf-bar-development;
+
+ .gl-dark & {
+ background-color: $red-950;
+ }
}
// UI Elements
@@ -88,7 +102,6 @@
}
.view {
- margin-right: 15px;
flex-shrink: 0;
&:last-child {
@@ -96,6 +109,22 @@
}
}
+ .view-performance-container,
+ .view-reports-container {
+ margin-right: $gl-padding-24;
+
+ .view:not(:first-child) {
+ margin-right: 0;
+
+ &::before {
+ content: '•';
+ opacity: .5;
+ display: inline-block;
+ margin: 0 $gl-padding-8;
+ }
+ }
+ }
+
.css-truncate {
&.css-truncate-target,
.css-truncate-target {
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index 7616f573412..73877c04c46 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -164,7 +164,7 @@ body.gl-dark {
// rendered and cached in the backend (labels_helper.rb)
&.gl-label-scoped {
.gl-label-text-scoped,
- .gl-label-close {
+ .gl-label-close.gl-button .gl-icon {
color: $gray-900;
}
}
@@ -172,18 +172,24 @@ body.gl-dark {
// white-ish text for light labels
.gl-label-text-light.gl-label-text-light {
- color: $gray-900;
+ &,
+ .gl-label-close .gl-icon {
+ color: $gray-900;
+ }
}
.gl-label-text-dark.gl-label-text-dark {
- color: $gray-10;
+ &,
+ .gl-label-close .gl-icon {
+ color: $gray-10;
+ }
}
// This applies to "gl-labels" from "gitlab-ui"
.gl-label.gl-label-scoped.gl-label-text-dark,
.gl-label.gl-label-scoped.gl-label-text-light {
.gl-label-text-scoped,
- .gl-label-close {
+ .gl-label-close.gl-button .gl-icon {
color: $gray-900;
}
}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index b756e0ed704..8fe45d4bb9d 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -135,3 +135,7 @@
.gl-hover-border-gray-100:hover {
border-color: $gray-100;
}
+
+.gl-last-of-type-border-b-0:last-of-type {
+ @include gl-border-b-0;
+}
diff --git a/app/components/pajamas/alert_component.html.haml b/app/components/pajamas/alert_component.html.haml
index a7be57311bb..ee7d5552455 100644
--- a/app/components/pajamas/alert_component.html.haml
+++ b/app/components/pajamas/alert_component.html.haml
@@ -1,6 +1,7 @@
.gl-alert{ @alert_options, role: 'alert', class: base_class }
- if @show_icon
- = sprite_icon(icon, css_class: icon_classes)
+ .gl-alert-icon-container
+ = sprite_icon(icon, css_class: icon_classes)
- if @dismissible
= render Pajamas::ButtonComponent.new(category: :tertiary,
icon: 'close',
@@ -8,7 +9,7 @@
button_options: dismissible_button_options)
.gl-alert-content{ role: 'alert' }
- if @title
- %h4.gl-alert-title
+ %h2.gl-alert-title
= @title
- if body?
.gl-alert-body
diff --git a/app/components/pajamas/alert_component.rb b/app/components/pajamas/alert_component.rb
index 008d624b7e2..c9397ca56cc 100644
--- a/app/components/pajamas/alert_component.rb
+++ b/app/components/pajamas/alert_component.rb
@@ -24,7 +24,7 @@ module Pajamas
classes = ["gl-alert-#{@variant}"]
classes.push('gl-alert-not-dismissible') unless @dismissible
classes.push('gl-alert-no-icon') unless @show_icon
-
+ classes.push('gl-alert-has-title') if @title
classes.join(' ')
end
diff --git a/app/components/pajamas/banner_component.html.haml b/app/components/pajamas/banner_component.html.haml
index 8a177edddb5..ebb88b305dc 100644
--- a/app/components/pajamas/banner_component.html.haml
+++ b/app/components/pajamas/banner_component.html.haml
@@ -1,22 +1,26 @@
-%section.gl-banner{ @banner_options, class: banner_class }
- - if illustration?
- .gl-banner-illustration
- = illustration
- - elsif @svg_path.present?
- .gl-banner-illustration
- = image_tag @svg_path, alt: ""
+-# This is using gl-card classes to match Vue component
+-# Here's the issue to refactor away from gl-card
+-# https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2324
+.gl-banner.gl-card.gl-pl-6.gl-pr-8.gl-py-6{ @banner_options, class: banner_class }
+ .gl-display-flex
+ - if illustration?
+ .gl-banner-illustration
+ = illustration
+ - elsif @svg_path.present?
+ .gl-banner-illustration
+ = image_tag @svg_path, alt: ""
- .gl-banner-content
- %h1.gl-banner-title= title
+ .gl-banner-content
+ %h1.gl-banner-title= title
- = content
+ = content
- - if primary_action?
- = primary_action
- - else
- = link_button_to @button_text, @button_link, **@button_options, class: 'js-close-callout', variant: :confirm
+ - if primary_action?
+ = primary_action
+ - else
+ = link_button_to @button_text, @button_link, **@button_options, class: 'js-close-callout', variant: :confirm
- - actions.each do |action|
- = action
+ - actions.each do |action|
+ = action
= render Pajamas::ButtonComponent.new(category: :tertiary, variant: close_button_variant, size: :small, icon: 'close', button_options: @close_options)
diff --git a/app/components/pajamas/empty_state_component.html.haml b/app/components/pajamas/empty_state_component.html.haml
index ecd3498c5cd..d7af153db2c 100644
--- a/app/components/pajamas/empty_state_component.html.haml
+++ b/app/components/pajamas/empty_state_component.html.haml
@@ -1,24 +1,24 @@
-- empty_state_class = @compact ? 'gl-flex-direction-row gl-align-items-center' : 'gl-text-center gl-flex-direction-column'
+- empty_state_class = @compact ? 'gl-flex-direction-row' : 'gl-text-center gl-flex-direction-column'
-%section.gl-display-flex.empty-state{ **@empty_state_options, class: empty_state_class }
+%section.gl-display-flex.gl-empty-state{ **@empty_state_options, class: empty_state_class }
- if @svg_path.present?
- image_class = @compact ? 'gl-display-none gl-sm-display-block gl-px-4' : 'gl-max-w-full'
%div{ class: image_class }
= image_tag @svg_path, alt: "", class: 'gl-dark-invert-keep-hue'
- - content_wrapper_class = @compact ? 'gl-flex-grow-1 gl-flex-basis-0 gl-px-4' : 'gl-max-w-full gl-m-auto pl-p-5'
- %div{ class: content_wrapper_class }
- - title_class = @compact ? 'gl-mt-0' : 'gl-my-3'
- %h1.gl-font-size-h-display.gl-line-height-36{ class: title_class }
+ - content_wrapper_class = @compact ? 'gl-flex-grow-1 gl-flex-basis-0 gl-px-4' : 'gl-m-auto gl-p-5'
+ .gl-empty-state-content.gl-mx-auto.gl-my-0{ class: content_wrapper_class }
+ - title_class = @compact ? 'h5' : 'h4'
+ %h1.gl-font-size-h-display.gl-line-height-36.gl-mt-0.gl-mb-0{ class: title_class }
= @title
- if description?
- %p.gl-mt-3{ 'data-testid': 'empty-state-description' }
+ %p.gl-mt-4.gl-mb-0{ 'data-testid': 'empty-state-description' }
= description
- if @primary_button_text.present? || @secondary_button_text.present?
- button_wrapper_class = @compact.present? ? '' : 'gl-justify-content-center'
- .gl-display-flex.gl-flex-wrap{ class: button_wrapper_class }
+ .gl-display-flex.gl-flex-wrap.gl-mt-5.gl-gap-3{ class: button_wrapper_class }
- if @primary_button_text.present?
= render Pajamas::ButtonComponent.new(variant: :confirm, href: @primary_button_link, button_options: { class: 'gl-ml-0!' }) do
diff --git a/app/components/projects/ml/models_index_component.rb b/app/components/projects/ml/models_index_component.rb
index c5c20565195..57900165ad1 100644
--- a/app/components/projects/ml/models_index_component.rb
+++ b/app/components/projects/ml/models_index_component.rb
@@ -3,27 +3,42 @@
module Projects
module Ml
class ModelsIndexComponent < ViewComponent::Base
- attr_reader :models
+ attr_reader :paginator
- def initialize(models:)
- @models = models
+ def initialize(paginator:)
+ @paginator = paginator
end
private
def view_model
- Gitlab::Json.generate({ models: models_view_model })
+ vm = {
+ models: models_view_model,
+ page_info: page_info_view_model
+ }
+
+ Gitlab::Json.generate(vm.deep_transform_keys { |k| k.to_s.camelize(:lower) })
end
def models_view_model
- models.map(&:present).map do |m|
+ paginator.records.map(&:present).map do |m|
{
name: m.name,
version: m.latest_version_name,
+ version_count: m.version_count,
path: m.latest_package_path
}
end
end
+
+ def page_info_view_model
+ {
+ has_next_page: paginator.has_next_page?,
+ has_previous_page: paginator.has_previous_page?,
+ start_cursor: paginator.cursor_for_previous_page,
+ end_cursor: paginator.cursor_for_next_page
+ }
+ end
end
end
end
diff --git a/app/components/projects/ml/show_ml_model_component.html.haml b/app/components/projects/ml/show_ml_model_component.html.haml
new file mode 100644
index 00000000000..20e52246e6d
--- /dev/null
+++ b/app/components/projects/ml/show_ml_model_component.html.haml
@@ -0,0 +1 @@
+#js-mount-show-ml-model{ data: { view_model: view_model } }
diff --git a/app/components/projects/ml/show_ml_model_component.rb b/app/components/projects/ml/show_ml_model_component.rb
new file mode 100644
index 00000000000..2fe2c7e7e9d
--- /dev/null
+++ b/app/components/projects/ml/show_ml_model_component.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Projects
+ module Ml
+ class ShowMlModelComponent < ViewComponent::Base
+ attr_reader :model
+
+ def initialize(model:)
+ @model = model.present
+ end
+
+ private
+
+ def view_model
+ vm = {
+ model: {
+ id: model.id,
+ name: model.name,
+ path: model.path
+ }
+ }
+
+ Gitlab::Json.generate(vm)
+ end
+ end
+ end
+end
diff --git a/app/controllers/acme_challenges_controller.rb b/app/controllers/acme_challenges_controller.rb
index 4a7706db94e..a187e43b3df 100644
--- a/app/controllers/acme_challenges_controller.rb
+++ b/app/controllers/acme_challenges_controller.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Rails/ApplicationController
-class AcmeChallengesController < ActionController::Base
+class AcmeChallengesController < BaseActionController
def show
if acme_order
render plain: acme_order.challenge_file_content, content_type: 'text/plain'
@@ -16,4 +15,3 @@ class AcmeChallengesController < ActionController::Base
@acme_order ||= PagesDomainAcmeOrder.find_by_domain_and_token(params[:domain], params[:token])
end
end
-# rubocop:enable Rails/ApplicationController
diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb
index 15c4103a781..17b0adb868e 100644
--- a/app/controllers/admin/identities_controller.rb
+++ b/app/controllers/admin/identities_controller.rb
@@ -63,6 +63,6 @@ class Admin::IdentitiesController < Admin::ApplicationController
end
def identity_params
- params.require(:identity).permit(:provider, :extern_uid)
+ params.require(:identity).permit(:provider, :extern_uid, :saml_provider_id)
end
end
diff --git a/app/controllers/admin/topics_controller.rb b/app/controllers/admin/topics_controller.rb
index c4de600dd1d..40cad2d26f4 100644
--- a/app/controllers/admin/topics_controller.rb
+++ b/app/controllers/admin/topics_controller.rb
@@ -8,10 +8,6 @@ class Admin::TopicsController < Admin::ApplicationController
feature_category :groups_and_projects
- before_action do
- push_frontend_feature_flag(:content_editor_on_issues, current_user)
- end
-
def index
@topics = Projects::TopicsFinder.new(params: params.permit(:search)).execute.page(params[:page]).without_count
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 7c69f43fa3d..f60da46826a 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -3,7 +3,7 @@
require 'gon'
require 'fogbugz'
-class ApplicationController < ActionController::Base
+class ApplicationController < BaseActionController
include Gitlab::GonHelper
include Gitlab::NoCacheHeaders
include GitlabRoutingHelper
@@ -24,6 +24,7 @@ class ApplicationController < ActionController::Base
include ::Gitlab::EndpointAttributes
include FlocOptOut
include CheckRateLimit
+ include RequestPayloadLogger
extend ContentSecurityPolicyPatch
before_action :limit_session_time, if: -> { !current_user }
@@ -180,29 +181,6 @@ class ApplicationController < ActionController::Base
@workhorse_excluded_content_types ||= %w[text/html application/json]
end
- def append_info_to_payload(payload)
- super
-
- payload[:ua] = request.env["HTTP_USER_AGENT"]
- payload[:remote_ip] = request.remote_ip
-
- payload[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id
- payload[:metadata] = @current_context
- payload[:request_urgency] = urgency&.name
- payload[:target_duration_s] = urgency&.duration
- logged_user = auth_user
- if logged_user.present?
- payload[:user_id] = logged_user.try(:id)
- payload[:username] = logged_user.try(:username)
- end
-
- payload[:queue_duration_s] = request.env[::Gitlab::Middleware::RailsQueueDuration::GITLAB_RAILS_QUEUE_DURATION_KEY]
-
- payload[:response_bytes] = response.body_parts.sum(&:bytesize) if Feature.enabled?(:log_response_length)
-
- store_cloudflare_headers!(payload, request)
- end
-
##
# Controllers such as GitHttpController may use alternative methods
# (e.g. tokens) to authenticate the user, whereas Devise sets current_user.
diff --git a/app/controllers/base_action_controller.rb b/app/controllers/base_action_controller.rb
new file mode 100644
index 00000000000..af2c9e98778
--- /dev/null
+++ b/app/controllers/base_action_controller.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+# GitLab lightweight base action controller
+#
+# This class should be limited to content that
+# is desired/required for *all* controllers in
+# GitLab.
+#
+# Most controllers inherit from `ApplicationController`.
+# Some controllers don't want or need all of that
+# logic and instead inherit from `ActionController::Base`.
+# This makes it difficult to set security headers and
+# handle other critical logic across *all* controllers.
+#
+# Between this controller and `ApplicationController`
+# no controller should ever inherit directly from
+# `ActionController::Base`
+#
+# rubocop:disable Rails/ApplicationController
+# rubocop:disable Gitlab/NamespacedClass
+class BaseActionController < ActionController::Base
+ before_action :security_headers
+
+ private
+
+ def security_headers
+ headers['Cross-Origin-Opener-Policy'] = 'same-origin' if ::Feature.enabled?(:coop_header)
+ end
+end
+# rubocop:enable Gitlab/NamespacedClass
+# rubocop:enable Rails/ApplicationController
diff --git a/app/controllers/chaos_controller.rb b/app/controllers/chaos_controller.rb
index 7328b793b09..b61a8c5ff12 100644
--- a/app/controllers/chaos_controller.rb
+++ b/app/controllers/chaos_controller.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Rails/ApplicationController
-class ChaosController < ActionController::Base
+class ChaosController < BaseActionController
before_action :validate_chaos_secret, unless: :development_or_test?
def leakmem
@@ -95,4 +94,3 @@ class ChaosController < ActionController::Base
Rails.env.development? || Rails.env.test?
end
end
-# rubocop:enable Rails/ApplicationController
diff --git a/app/controllers/concerns/access_tokens_actions.rb b/app/controllers/concerns/access_tokens_actions.rb
index de53fd4d835..84cbdda1581 100644
--- a/app/controllers/concerns/access_tokens_actions.rb
+++ b/app/controllers/concerns/access_tokens_actions.rb
@@ -69,7 +69,6 @@ module AccessTokensActions
resource.members.load
@scopes = Gitlab::Auth.available_scopes_for(resource)
- @scopes.delete(Gitlab::Auth::K8S_PROXY_SCOPE) unless Feature.enabled?(:k8s_proxy_pat, current_user)
@active_access_tokens = active_access_tokens
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index 896004045f4..27f1d1f5528 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -10,18 +10,18 @@ module CreatesCommit
if user_access(target_project).can_push_to_branch?(branch_name_or_ref)
@project_to_commit_into = target_project
+ @different_project = false
@branch_name ||= @ref
else
@project_to_commit_into = current_user.fork_of(target_project)
+ @different_project = true
@branch_name ||= @project_to_commit_into.repository.next_branch('patch')
end
@start_branch ||= @ref || @branch_name
- start_project = @project_to_commit_into
-
commit_params = @commit_params.merge(
- start_project: start_project,
+ start_project: @project_to_commit_into,
start_branch: @start_branch,
source_project: @project,
target_project: target_project,
@@ -74,7 +74,7 @@ module CreatesCommit
nil
else
mr_message =
- if different_project?
+ if @different_project # rubocop:disable Gitlab/ModuleWithInstanceVariables
_("You can now submit a merge request to get this change into the original project.")
else
_("You can now submit a merge request to get this change into the original branch.")
@@ -128,16 +128,12 @@ module CreatesCommit
# rubocop: enable CodeReuse/ActiveRecord
# rubocop:enable Gitlab/ModuleWithInstanceVariables
- def different_project?
- @project_to_commit_into != @project # rubocop:disable Gitlab/ModuleWithInstanceVariables
- end
-
def create_merge_request?
# Even if the field is set, if we're checking the same branch
# as the target branch in the same project,
# we don't want to create a merge request.
params[:create_merge_request].present? &&
- (different_project? || @start_branch != @branch_name) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ (@different_project || @start_branch != @branch_name) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def branch_name_or_ref
diff --git a/app/controllers/concerns/enforces_two_factor_authentication.rb b/app/controllers/concerns/enforces_two_factor_authentication.rb
index 539feb3cf1c..24475909b62 100644
--- a/app/controllers/concerns/enforces_two_factor_authentication.rb
+++ b/app/controllers/concerns/enforces_two_factor_authentication.rb
@@ -73,7 +73,7 @@ module EnforcesTwoFactorAuthentication
end
def skip_two_factor?
- session[:skip_two_factor] && session[:skip_two_factor] > Time.current
+ session[:skip_two_factor] && session[:skip_two_factor].future?
end
def two_factor_verifier
diff --git a/app/controllers/concerns/google_analytics_csp.rb b/app/controllers/concerns/google_analytics_csp.rb
deleted file mode 100644
index 1a8e405928d..00000000000
--- a/app/controllers/concerns/google_analytics_csp.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-module GoogleAnalyticsCSP
- extend ActiveSupport::Concern
-
- included do
- content_security_policy do |policy|
- next unless helpers.google_tag_manager_enabled? || policy.directives.present?
-
- default_script_src = policy.directives['script-src'] || policy.directives['default-src']
- script_src_values = Array.wrap(default_script_src) | ['*.googletagmanager.com']
- policy.script_src(*script_src_values)
-
- default_img_src = policy.directives['img-src'] || policy.directives['default-src']
- img_src_values = Array.wrap(default_img_src) | ['*.google-analytics.com', '*.googletagmanager.com']
- policy.img_src(*img_src_values)
-
- default_connect_src = policy.directives['connect-src'] || policy.directives['default-src']
- connect_src_values =
- Array.wrap(default_connect_src) | ['*.google-analytics.com', '*.analytics.google.com', '*.googletagmanager.com']
- policy.connect_src(*connect_src_values)
- end
- end
-end
diff --git a/app/controllers/concerns/google_syndication_csp.rb b/app/controllers/concerns/google_syndication_csp.rb
deleted file mode 100644
index c55debe448b..00000000000
--- a/app/controllers/concerns/google_syndication_csp.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-module GoogleSyndicationCSP
- extend ActiveSupport::Concern
-
- ALLOWED_SRC = ['*.google.com/pagead/landing', 'pagead2.googlesyndication.com/pagead/landing'].freeze
-
- included do
- content_security_policy do |policy|
- next unless helpers.google_tag_manager_enabled? || policy.directives.present?
-
- connect_src_values = Array.wrap(
- policy.directives['connect-src'] || policy.directives['default-src']
- )
-
- connect_src_values.concat(ALLOWED_SRC) if helpers.google_tag_manager_enabled?
-
- policy.connect_src(*connect_src_values.uniq)
- end
- end
-end
diff --git a/app/controllers/concerns/import/github_oauth.rb b/app/controllers/concerns/import/github_oauth.rb
index dc03a132768..ae5a0401155 100644
--- a/app/controllers/concerns/import/github_oauth.rb
+++ b/app/controllers/concerns/import/github_oauth.rb
@@ -54,23 +54,15 @@ module Import
state = SecureRandom.base64(64)
session[auth_state_key] = state
session[:auth_on_failure_path] = "#{new_project_path}#import_project"
- if Feature.enabled?(:remove_legacy_github_client)
- oauth_client.auth_code.authorize_url(
- redirect_uri: callback_import_url,
- scope: 'repo, user, user:email',
- state: state
- )
- else
- client.authorize_url(callback_import_url, state)
- end
+ oauth_client.auth_code.authorize_url(
+ redirect_uri: callback_import_url,
+ scope: 'repo, user, user:email',
+ state: state
+ )
end
def get_token(code)
- if Feature.enabled?(:remove_legacy_github_client)
- oauth_client.auth_code.get_token(code).token
- else
- client.get_token(code)
- end
+ oauth_client.auth_code.get_token(code).token
end
def missing_oauth_config
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index 0c15c4d0d3f..b4f5589a059 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -5,7 +5,6 @@ module MembershipActions
extend ActiveSupport::Concern
def update
- update_params = params.require(root_params_key).permit(:access_level, :expires_at)
member = members_and_requesters.find(params[:id])
result = Members::UpdateService
.new(current_user, update_params)
@@ -148,6 +147,10 @@ module MembershipActions
membershipable.requesters
end
+ def update_params
+ params.require(root_params_key).permit(:access_level, :expires_at)
+ end
+
def requested_relations(inherited_permissions = :with_inherited_permissions)
case params[inherited_permissions].presence
when 'exclude'
@@ -156,7 +159,8 @@ module MembershipActions
[:inherited]
else
if Feature.enabled?(:webui_members_inherited_users, current_user)
- [:inherited, :direct, :shared_from_groups, (:invited_groups if params[:project_id])].compact
+ project_relations = [:invited_groups, :shared_into_ancestors]
+ [:inherited, :direct, :shared_from_groups, *(project_relations if params[:project_id])]
else
[:inherited, :direct]
end
diff --git a/app/controllers/concerns/onboarding/redirectable.rb b/app/controllers/concerns/onboarding/redirectable.rb
new file mode 100644
index 00000000000..7e669db9199
--- /dev/null
+++ b/app/controllers/concerns/onboarding/redirectable.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Onboarding
+ module Redirectable
+ extend ActiveSupport::Concern
+
+ private
+
+ def after_sign_up_path
+ if onboarding_status.single_invite?
+ flash[:notice] = helpers.invite_accepted_notice(onboarding_status.last_invited_member)
+ onboarding_status.last_invited_member_source.activity_path
+ else
+ # Invites will come here if there is more than 1.
+ path_for_signed_in_user
+ end
+ end
+
+ def path_for_signed_in_user
+ stored_location_for(:user) || last_member_activity_path
+ end
+
+ def last_member_activity_path
+ return dashboard_projects_path unless onboarding_status.last_invited_member_source.present?
+
+ onboarding_status.last_invited_member_source.activity_path
+ end
+ end
+end
+
+Onboarding::Redirectable.prepend_mod
diff --git a/app/controllers/concerns/onboarding/status.rb b/app/controllers/concerns/onboarding/status.rb
index 8a99f5a6c12..ea4dc550149 100644
--- a/app/controllers/concerns/onboarding/status.rb
+++ b/app/controllers/concerns/onboarding/status.rb
@@ -2,21 +2,12 @@
module Onboarding
class Status
- def self.tracking_label
- { free: 'free_registration' }
- end
-
def initialize(params, session, user)
@params = params
@session = session
@user = user
end
- # overridden in EE
- def continue_full_onboarding?
- false
- end
-
def single_invite?
# If there are more than one member it will mean we have been invited to multiple projects/groups and
# are not able to distinguish which one we should putting the user in after registration
diff --git a/app/controllers/concerns/planning_hierarchy.rb b/app/controllers/concerns/planning_hierarchy.rb
index 5df838bc183..51999a87e26 100644
--- a/app/controllers/concerns/planning_hierarchy.rb
+++ b/app/controllers/concerns/planning_hierarchy.rb
@@ -7,7 +7,7 @@ module PlanningHierarchy
def planning_hierarchy
return access_denied! unless can?(current_user, :read_planning_hierarchy, @project)
- render 'shared/planning_hierarchy'
+ route_not_found
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/controllers/concerns/product_analytics_tracking.rb b/app/controllers/concerns/product_analytics_tracking.rb
index e148f5d063a..d4610267897 100644
--- a/app/controllers/concerns/product_analytics_tracking.rb
+++ b/app/controllers/concerns/product_analytics_tracking.rb
@@ -14,7 +14,7 @@ module ProductAnalyticsTracking
end
def track_internal_event(*controller_actions, name:, conditions: nil)
- custom_conditions = [:trackable_html_request?, *conditions]
+ custom_conditions = [:trackable_html_request?, :authenticated?, *conditions]
after_action only: controller_actions, if: custom_conditions do
Gitlab::InternalEvents.track_event(
@@ -70,4 +70,8 @@ module ProductAnalyticsTracking
cookies[:visitor_id] = { value: uuid, expires: 24.months }
uuid
end
+
+ def authenticated?
+ current_user.present?
+ end
end
diff --git a/app/controllers/concerns/renders_projects_list.rb b/app/controllers/concerns/renders_projects_list.rb
index 2d37bc3f9a5..56383658696 100644
--- a/app/controllers/concerns/renders_projects_list.rb
+++ b/app/controllers/concerns/renders_projects_list.rb
@@ -5,7 +5,7 @@ module RendersProjectsList
def prepare_projects_for_rendering(projects)
preload_max_member_access_for_collection(Project, projects)
- current_user.preloaded_member_roles_for_projects(projects) if current_user
+ preload_member_roles(projects) if current_user
# Call the count methods on every project, so the BatchLoader would load them all at
# once when the entities are rendered
@@ -15,4 +15,10 @@ module RendersProjectsList
projects
end
+
+ def preload_member_roles(projects)
+ # overridden in EE
+ end
end
+
+RendersProjectsList.prepend_mod
diff --git a/app/controllers/concerns/request_payload_logger.rb b/app/controllers/concerns/request_payload_logger.rb
new file mode 100644
index 00000000000..b13164e5c57
--- /dev/null
+++ b/app/controllers/concerns/request_payload_logger.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module RequestPayloadLogger
+ extend ActiveSupport::Concern
+ include Gitlab::Logging::CloudflareHelper
+
+ def append_info_to_payload(payload)
+ super
+
+ payload[:ua] = request.env["HTTP_USER_AGENT"]
+ payload[:remote_ip] = request.remote_ip
+ payload[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id
+
+ payload[:metadata] = Gitlab::ApplicationContext.current
+
+ if defined?(urgency)
+ payload[:request_urgency] = urgency&.name
+ payload[:target_duration_s] = urgency&.duration
+ end
+
+ logged_user = auth_user
+ if logged_user.present?
+ payload[:user_id] = logged_user.try(:id)
+ payload[:username] = logged_user.try(:username)
+ end
+
+ payload[:queue_duration_s] = request.env[::Gitlab::Middleware::RailsQueueDuration::GITLAB_RAILS_QUEUE_DURATION_KEY]
+ payload[:response_bytes] = response.body_parts.sum(&:bytesize) if Feature.enabled?(:log_response_length)
+
+ store_cloudflare_headers!(payload, request)
+ end
+end
diff --git a/app/controllers/concerns/snippets/blobs_actions.rb b/app/controllers/concerns/snippets/blobs_actions.rb
index 2a0491b4df8..955debfc209 100644
--- a/app/controllers/concerns/snippets/blobs_actions.rb
+++ b/app/controllers/concerns/snippets/blobs_actions.rb
@@ -4,7 +4,6 @@ module Snippets::BlobsActions
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
- include ExtractsRef
include Snippets::SendBlob
included do
@@ -19,20 +18,14 @@ module Snippets::BlobsActions
private
- def repository_container
- snippet
- end
-
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
def blob
- assign_ref_vars
-
- return unless @commit
+ ref_extractor = ExtractsRef::RefExtractor.new(snippet, params.permit(:id, :ref, :path, :ref_type))
+ ref_extractor.extract!
+ return unless ref_extractor.commit
- @repo.blob_at(@commit.id, @path)
+ snippet.repository.blob_at(ref_extractor.commit.id, ref_extractor.path)
end
strong_memoize_attr :blob
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
def ensure_blob
render_404 unless blob
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 5ceabaa734a..db1cf31d349 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -4,8 +4,6 @@ class ConfirmationsController < Devise::ConfirmationsController
include AcceptsPendingInvitations
include GitlabRecaptcha
include OneTrustCSP
- include GoogleAnalyticsCSP
- include GoogleSyndicationCSP
prepend_before_action :check_recaptcha, only: :create
before_action :load_recaptcha, only: :new
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index 29bc48f93e9..1941920325f 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -255,6 +255,12 @@ class GraphqlController < ApplicationController
end
def authorize_access_api!
+ if current_user.nil? &&
+ request_authenticator.authentication_token_present? &&
+ Feature.enabled?(:invalid_graphql_auth_401)
+ render_error('Invalid token', status: :unauthorized)
+ end
+
return if can?(current_user, :access_api)
render_error('API not accessible for user', status: :forbidden)
@@ -301,6 +307,8 @@ class GraphqlController < ApplicationController
end
def introspection_query_can_use_cache?
+ return false if Gitlab.dev_or_test_env?
+
CACHED_INTROSPECTION_QUERY_STRING == graphql_query_object.query_string.squish
end
diff --git a/app/controllers/groups/autocomplete_sources_controller.rb b/app/controllers/groups/autocomplete_sources_controller.rb
index 414461d9e93..86bf65f4723 100644
--- a/app/controllers/groups/autocomplete_sources_controller.rb
+++ b/app/controllers/groups/autocomplete_sources_controller.rb
@@ -8,6 +8,11 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController
urgency :low, [:issues, :labels, :milestones, :commands, :merge_requests, :members]
def members
+ if Feature.enabled?(:cache_autocomplete_sources_members, current_user)
+ # Cache the response on the frontend
+ expires_in 3.minutes
+ end
+
render json: ::Groups::ParticipantsService.new(@group, current_user).execute(target)
end
diff --git a/app/controllers/groups/custom_emoji_controller.rb b/app/controllers/groups/custom_emoji_controller.rb
new file mode 100644
index 00000000000..f202c9febba
--- /dev/null
+++ b/app/controllers/groups/custom_emoji_controller.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Groups
+ class CustomEmojiController < Groups::ApplicationController
+ feature_category :code_review_workflow
+ urgency :low
+
+ before_action do
+ render_404 unless Feature.enabled?(:custom_emoji)
+ end
+ end
+end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index cbed75019f2..5f6b55ea928 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -9,10 +9,6 @@ class Groups::MilestonesController < Groups::ApplicationController
feature_category :team_planning
urgency :low
- before_action do
- push_frontend_feature_flag(:content_editor_on_issues, group)
- end
-
def index
respond_to do |format|
format.html do
diff --git a/app/controllers/groups/observability_controller.rb b/app/controllers/groups/observability_controller.rb
deleted file mode 100644
index 525407f5849..00000000000
--- a/app/controllers/groups/observability_controller.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-module Groups
- class ObservabilityController < Groups::ApplicationController
- include ::Observability::ContentSecurityPolicy
-
- feature_category :tracing
-
- before_action :check_observability_allowed
-
- def dashboards
- render_observability
- end
-
- def manage
- render_observability
- end
-
- def explore
- render_observability
- end
-
- def datasources
- render_observability
- end
-
- private
-
- def render_observability
- render 'observability', layout: 'group', locals: { base_layout: 'layouts/fullscreen' }
- end
-
- def check_observability_allowed
- render_404 unless Gitlab::Observability.allowed_for_action?(current_user, group, params[:action])
- end
- end
-end
diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb
index 1381999ab4c..2b2db2f950c 100644
--- a/app/controllers/health_controller.rb
+++ b/app/controllers/health_controller.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Rails/ApplicationController
-class HealthController < ActionController::Base
+class HealthController < BaseActionController
protect_from_forgery with: :exception, prepend: true
include RequiresAllowlistedMonitoringClient
@@ -40,4 +39,3 @@ class HealthController < ActionController::Base
render json: result.json, status: result.http_status
end
end
-# rubocop:enable Rails/ApplicationController
diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb
index d7d7ad84bc8..a8ec738caf4 100644
--- a/app/controllers/import/bulk_imports_controller.rb
+++ b/app/controllers/import/bulk_imports_controller.rb
@@ -152,7 +152,7 @@ class Import::BulkImportsController < ApplicationController
allow_local_network: allow_local_requests?,
schemes: %w[http https]
)
- rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e
clear_session_data
redirect_to new_group_path(anchor: 'import-group-pane'), alert: _('Specified URL cannot be used: "%{reason}"') % { reason: e.message }
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index 9ee8e59053f..34fdf513313 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -128,7 +128,7 @@ class Import::FogbugzController < Import::BaseController
allow_local_network: allow_local_requests?,
schemes: %w[http https]
)
- rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e
redirect_to new_import_fogbugz_url, alert: _('Specified URL cannot be used: "%{reason}"') % { reason: e.message }
end
diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb
index 2778b97419a..4e95c6527c3 100644
--- a/app/controllers/import/gitea_controller.rb
+++ b/app/controllers/import/gitea_controller.rb
@@ -85,7 +85,6 @@ class Import::GiteaController < Import::GithubController
@client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], **client_options)
end
- override :client_options
def client_options
verified_url, provider_hostname = verify_blocked_uri
@@ -99,7 +98,7 @@ class Import::GiteaController < Import::GithubController
allow_local_network: allow_local_requests?,
schemes: %w[http https]
)
- rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e
session[access_token_key] = nil
redirect_to new_import_url, alert: _('Specified URL cannot be used: "%{reason}"') % { reason: e.message }
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 28732d58484..2b72ceceb5a 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -192,7 +192,7 @@ class Import::GithubController < Import::BaseController
def client_proxy
@client_proxy ||= Gitlab::GithubImport::Clients::Proxy.new(
- session[access_token_key], client_options
+ session[access_token_key]
)
end
@@ -265,10 +265,6 @@ class Import::GithubController < Import::BaseController
end
# rubocop: enable CodeReuse/ActiveRecord
- def client_options
- { wait_for_rate_limit_reset: false }
- end
-
def rate_limit_threshold_exceeded
head :too_many_requests
end
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index d299613f498..84ccfbc603a 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -87,13 +87,22 @@ class JwtController < ApplicationController
# We have to parse scope here, because Docker Client does not send an array of scopes,
# but rather a flat list and we loose second scope when being processed by Rails:
- # scope=scopeA&scope=scopeB
+ # scope=scopeA&scope=scopeB.
+ #
+ # Additionally, according to RFC6749 (https://datatracker.ietf.org/doc/html/rfc6749#section-3.3), some clients may use
+ # a scope parameter expressed as a list of space-delimited elements. Therefore, we must account for this and split the
+ # scope parameter value(s) appropriately.
#
# This method makes to always return an array of scopes
def scopes_param
return unless params[:scope].present?
- Array(Rack::Utils.parse_query(request.query_string)['scope'])
+ scopes = Array(Rack::Utils.parse_query(request.query_string)['scope'])
+ if Feature.enabled?(:jwt_auth_space_delimited_scopes, Feature.current_request)
+ scopes.flat_map(&:split)
+ else
+ scopes
+ end
end
def auth_user
diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb
index 9f41c092fa0..61851fd1c60 100644
--- a/app/controllers/metrics_controller.rb
+++ b/app/controllers/metrics_controller.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
-# rubocop:disable Rails/ApplicationController
-class MetricsController < ActionController::Base
+class MetricsController < BaseActionController
include RequiresAllowlistedMonitoringClient
protect_from_forgery with: :exception, prepend: true
@@ -36,4 +35,3 @@ class MetricsController < ActionController::Base
)
end
end
-# rubocop:enable Rails/ApplicationController
diff --git a/app/controllers/oauth/tokens_controller.rb b/app/controllers/oauth/tokens_controller.rb
index 012fa318eea..7889e89fc5c 100644
--- a/app/controllers/oauth/tokens_controller.rb
+++ b/app/controllers/oauth/tokens_controller.rb
@@ -2,4 +2,7 @@
class Oauth::TokensController < Doorkeeper::TokensController
include EnforcesTwoFactorAuthentication
+ include RequestPayloadLogger
+
+ alias_method :auth_user, :current_user
end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 72b3516ae3f..a97516fddff 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -7,6 +7,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
include InitializesCurrentUserMode
include KnownSignIn
include AcceptsPendingInvitations
+ include Onboarding::Redirectable
after_action :verify_known_sign_in
@@ -169,38 +170,38 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def sign_in_user_flow(auth_user_class)
auth_user = build_auth_user(auth_user_class)
new_user = auth_user.new?
- user = auth_user.find_and_update!
+ @user = auth_user.find_and_update!
if auth_user.valid_sign_in?
# In this case the `#current_user` would not be set. So we can't fetch it
# from that in `#context_user`. Pushing it manually here makes the information
# available in the logs for this request.
- Gitlab::ApplicationContext.push(user: user)
- track_event(user, oauth['provider'], 'succeeded')
- Gitlab::Tracking.event(self.class.name, "#{oauth['provider']}_sso", user: user) if new_user
+ Gitlab::ApplicationContext.push(user: @user)
+ track_event(@user, oauth['provider'], 'succeeded')
+ Gitlab::Tracking.event(self.class.name, "#{oauth['provider']}_sso", user: @user) if new_user
- set_remember_me(user)
+ set_remember_me(@user)
- if user.two_factor_enabled? && !auth_user.bypass_two_factor?
- prompt_for_two_factor(user)
+ if @user.two_factor_enabled? && !auth_user.bypass_two_factor?
+ prompt_for_two_factor(@user)
store_idp_two_factor_status(false)
else
- if user.deactivated?
- user.activate
+ if @user.deactivated?
+ @user.activate
flash[:notice] = _('Welcome back! Your account had been deactivated due to inactivity but is now reactivated.')
end
# session variable for storing bypass two-factor request from IDP
store_idp_two_factor_status(true)
- accept_pending_invitations(user: user) if new_user
- persist_accepted_terms_if_required(user) if new_user
+ accept_pending_invitations(user: @user) if new_user
+ persist_accepted_terms_if_required(@user) if new_user
- perform_registration_tasks(user, oauth['provider']) if new_user
- sign_in_and_redirect_or_verify_identity(user, auth_user, new_user)
+ perform_registration_tasks(@user, oauth['provider']) if new_user
+ sign_in_and_redirect_or_verify_identity(@user, auth_user, new_user)
end
else
- fail_login(user)
+ fail_login(@user)
end
rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
handle_disabled_provider
@@ -323,9 +324,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
store_location_for(:user, after_sign_up_path)
end
- def after_sign_up_path
- users_sign_up_welcome_path
+ def onboarding_status
+ Onboarding::Status.new(params.to_unsafe_h.deep_symbolize_keys, session, @user)
end
+ strong_memoize_attr :onboarding_status
# overridden in EE
def sign_in_and_redirect_or_verify_identity(user, _, _)
diff --git a/app/controllers/organizations/application_controller.rb b/app/controllers/organizations/application_controller.rb
index d3c3e878bdf..8a99b6804ae 100644
--- a/app/controllers/organizations/application_controller.rb
+++ b/app/controllers/organizations/application_controller.rb
@@ -27,5 +27,9 @@ module Organizations
def authorize_read_organization!
access_denied! unless can?(current_user, :read_organization, organization)
end
+
+ def authorize_admin_organization!
+ access_denied! unless can?(current_user, :admin_organization, organization)
+ end
end
end
diff --git a/app/controllers/organizations/settings_controller.rb b/app/controllers/organizations/settings_controller.rb
new file mode 100644
index 00000000000..a81cbf57a42
--- /dev/null
+++ b/app/controllers/organizations/settings_controller.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Organizations
+ class SettingsController < ApplicationController
+ feature_category :cell
+
+ before_action :authorize_admin_organization!
+
+ def general; end
+ end
+end
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index 57e5ca4d55a..abb6e46394e 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -45,11 +45,17 @@ class Profiles::NotificationsController < Profiles::ApplicationController
projects = project_notifications.map(&:source)
ActiveRecord::Associations::Preloader.new(
records: projects,
- associations: { namespace: [:route, :owner], group: [], creator: [], project_setting: [] }
+ associations: project_associations
).call
Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects, current_user).execute
project_notifications.select { |notification| current_user.can?(:read_project, notification.source) }
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def project_associations
+ { namespace: [:route, :owner], group: [], creator: [], project_setting: [] }
+ end
end
+
+Profiles::NotificationsController.prepend_mod
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 0e4d9f3c154..4b6e2f768fa 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -61,7 +61,6 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
def set_index_vars
@scopes = Gitlab::Auth.available_scopes_for(current_user)
- @scopes.delete(Gitlab::Auth::K8S_PROXY_SCOPE) unless Feature.enabled?(:k8s_proxy_pat, current_user)
@active_access_tokens = active_access_tokens
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index e83b72b71a8..f1646027e8e 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -140,7 +140,10 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def build_qr_code
uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host)
- RQRCode.render_qrcode(uri, :svg, level: :m, unit: 3)
+ RQRCode::QRCode.new(uri, level: :m).as_svg(
+ shape_rendering: "crispEdges",
+ module_size: 3
+ )
end
def account_string
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 62233c8c3c9..30c6f4d865a 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -85,7 +85,7 @@ class Projects::ApplicationController < ApplicationController
end
def require_pages_enabled!
- not_found unless @project.pages_available?
+ not_found unless ::Gitlab::Pages.enabled?
end
def check_issues_available!
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index 480e3408023..60c8fe97e81 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -13,6 +13,11 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
urgency :low, [:issues, :labels, :milestones, :commands, :contacts]
def members
+ if Feature.enabled?(:cache_autocomplete_sources_members, current_user)
+ # Cache the response on the frontend
+ expires_in 3.minutes
+ end
+
render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target)
end
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index f621adbebc7..b37962b850f 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -15,6 +15,7 @@ class Projects::BlameController < Projects::ApplicationController
urgency :low, [:show]
def show
+ @ref_type = ref_type
load_environment
load_blame
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 56e4b22ded2..015e56db012 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -101,8 +101,9 @@ class Projects::BlobController < Projects::ApplicationController
)
rescue Files::UpdateService::FileChangedError
@conflict = true
- @different_project = different_project?
- render :edit
+ render "edit", locals: {
+ commit_to_fork: @different_project
+ }
end
def preview
@@ -164,7 +165,7 @@ class Projects::BlobController < Projects::ApplicationController
@ref_type = ref_type
- if @ref_type == ExtractsRef::BRANCH_REF_TYPE && ambiguous_ref?(@project, @ref)
+ if @ref_type == ExtractsRef::RefExtractor::BRANCH_REF_TYPE && ambiguous_ref?(@project, @ref)
branch = @project.repository.find_branch(@ref)
redirect_to project_blob_path(@project, File.join(branch.target, @path))
end
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index 9d7569047f6..6e4d456ecc1 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -23,6 +23,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
before_action do
push_licensed_feature(:cycle_analytics_for_groups) if project.licensed_feature_available?(:cycle_analytics_for_groups)
push_licensed_feature(:group_level_analytics_dashboard) if project.licensed_feature_available?(:group_level_analytics_dashboard)
+ push_frontend_feature_flag(:vsa_predefined_date_ranges, project)
if project.licensed_feature_available?(:cycle_analytics_for_projects)
push_licensed_feature(:cycle_analytics_for_projects)
diff --git a/app/controllers/projects/find_file_controller.rb b/app/controllers/projects/find_file_controller.rb
index b5099d555ae..1777497ee52 100644
--- a/app/controllers/projects/find_file_controller.rb
+++ b/app/controllers/projects/find_file_controller.rb
@@ -14,7 +14,9 @@ class Projects::FindFileController < Projects::ApplicationController
urgency :low, [:show, :list]
def show
- return render_404 unless @repository.commit(@ref)
+ return render_404 unless @commit
+
+ @ref_type = ref_type
respond_to do |format|
format.html
@@ -22,7 +24,7 @@ class Projects::FindFileController < Projects::ApplicationController
end
def list
- file_paths = @repo.ls_files(@ref)
+ file_paths = @repo.ls_files(@commit.id)
respond_to do |format|
format.json { render json: file_paths }
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
index 69d349b1f1d..bacf3192ee6 100644
--- a/app/controllers/projects/incidents_controller.rb
+++ b/app/controllers/projects/incidents_controller.rb
@@ -11,8 +11,8 @@ class Projects::IncidentsController < Projects::ApplicationController
push_force_frontend_feature_flag(:work_items_mvc, @project&.work_items_mvc_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc_2, @project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:moved_mr_sidebar, project)
- push_frontend_feature_flag(:move_close_into_dropdown, project)
push_force_frontend_feature_flag(:linked_work_items, @project&.linked_work_items_feature_flag_enabled?)
+ push_frontend_feature_flag(:notifications_todos_buttons, project)
end
feature_category :incident_management
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 9abcc108ace..4849cccac52 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -7,7 +7,6 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableCollections
include IssuesCalendar
include RecordUserLastActivity
- include ::Observability::ContentSecurityPolicy
ISSUES_EXCEPT_ACTIONS = %i[index calendar new create bulk_update import_csv export_csv service_desk].freeze
SET_ISSUABLES_INDEX_ONLY_ACTIONS = %i[index calendar service_desk].freeze
@@ -46,13 +45,12 @@ class Projects::IssuesController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:preserve_unchanged_markdown, project)
- push_frontend_feature_flag(:content_editor_on_issues, project&.group)
- push_force_frontend_feature_flag(:content_editor_on_issues, project&.content_editor_on_issues_feature_flag_enabled?)
push_frontend_feature_flag(:service_desk_new_note_email_native_attachments, project)
push_frontend_feature_flag(:saved_replies, current_user)
push_frontend_feature_flag(:issues_grid_view)
push_frontend_feature_flag(:service_desk_ticket)
push_frontend_feature_flag(:issues_list_drawer, project)
+ push_frontend_feature_flag(:linked_work_items, project)
end
before_action only: [:index, :show] do
@@ -71,8 +69,8 @@ class Projects::IssuesController < Projects::ApplicationController
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:epic_widget_edit_confirmation, project)
push_frontend_feature_flag(:moved_mr_sidebar, project)
- push_frontend_feature_flag(:move_close_into_dropdown, project)
push_force_frontend_feature_flag(:linked_work_items, project.linked_work_items_feature_flag_enabled?)
+ push_frontend_feature_flag(:notifications_todos_buttons, project)
end
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
@@ -277,6 +275,12 @@ class Projects::IssuesController < Projects::ApplicationController
@issues = @issuables
end
+ def discussions
+ Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/425834')
+
+ super
+ end
+
protected
def index_html_request?
@@ -450,7 +454,7 @@ class Projects::IssuesController < Projects::ApplicationController
def redirect_if_work_item
return unless use_work_items_path?(issue)
- redirect_to project_work_items_path(project, issue.iid, params: request.query_parameters)
+ redirect_to project_work_item_path(project, issue.iid, params: request.query_parameters)
end
def require_incident_for_incident_routes
diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb
index a4091ebdf4b..9a3e7e31d68 100644
--- a/app/controllers/projects/mattermosts_controller.rb
+++ b/app/controllers/projects/mattermosts_controller.rb
@@ -19,10 +19,10 @@ class Projects::MattermostsController < Projects::ApplicationController
result, message = integration.configure(current_user, configure_params)
if result
- flash[:notice] = 'This service is now configured'
+ flash[:notice] = 'This integration is now configured'
redirect_to edit_project_settings_integration_path(@project, integration)
else
- flash[:alert] = message || 'Failed to configure service'
+ flash[:alert] = message || 'Failed to configure integration'
redirect_to new_project_mattermost_path(@project)
end
end
@@ -31,7 +31,7 @@ class Projects::MattermostsController < Projects::ApplicationController
def configure_params
params.require(:mattermost).permit(:trigger, :team_id).merge(
- url: service_trigger_url(integration),
+ url: integration_trigger_url(integration),
icon_url: asset_url('slash-command-logo.png', skip_pipeline: true))
end
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 81ff6c215f9..1af0ce3c35e 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -7,11 +7,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
feature_category :code_review_workflow
- before_action do
- push_frontend_feature_flag(:content_editor_on_issues, project&.group)
- push_force_frontend_feature_flag(:content_editor_on_issues, project&.content_editor_on_issues_feature_flag_enabled?)
- end
-
private
# Normally the methods with `check_(\w+)_available!` pattern are
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 6a3523b82d9..33a93ed99fb 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -4,7 +4,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
include DiffForPath
include DiffHelper
include RendersCommits
- include ::Observability::ContentSecurityPolicy
skip_before_action :merge_request
before_action :authorize_create_merge_request_from!
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 53fd7256b19..ad7b7221e44 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -11,7 +11,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include SourcegraphDecorator
include DiffHelper
include Gitlab::Cache::Helpers
- include ::Observability::ContentSecurityPolicy
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
skip_before_action :merge_request, only: [:index, :bulk_update, :export_csv]
@@ -37,8 +36,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
before_action only: [:show, :diffs] do
- push_frontend_feature_flag(:content_editor_on_issues, project&.group)
- push_force_frontend_feature_flag(:content_editor_on_issues, project&.content_editor_on_issues_feature_flag_enabled?)
push_frontend_feature_flag(:core_security_mr_widget_counts, project)
push_frontend_feature_flag(:issue_assignees_widget, @project)
push_frontend_feature_flag(:moved_mr_sidebar, project)
@@ -46,9 +43,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:mr_experience_survey, project)
push_frontend_feature_flag(:saved_replies, current_user)
push_force_frontend_feature_flag(:summarize_my_code_review, summarize_my_code_review_enabled?)
- push_frontend_feature_flag(:mr_activity_filters, current_user)
push_frontend_feature_flag(:ci_job_failures_in_mr, project)
push_frontend_feature_flag(:mr_pipelines_graphql, project)
+ push_frontend_feature_flag(:notifications_todos_buttons, project)
end
before_action only: [:edit] do
@@ -159,7 +156,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user)
.with_pagination(request, response)
- .represent(@pipelines, preload: true),
+ .represent(
+ @pipelines,
+ preload: true,
+ disable_failed_builds: ::Feature.enabled?(:ci_fix_performance_pipelines_json_endpoint, @project)
+ ),
count: {
all: @pipelines.count
}
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 1f4e5b54500..35b65dbce7e 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -24,10 +24,6 @@ class Projects::MilestonesController < Projects::ApplicationController
feature_category :team_planning
urgency :low
- before_action do
- push_frontend_feature_flag(:content_editor_on_issues, @project)
- end
-
def index
@sort = params[:sort] || 'due_date_asc'
@milestones = milestones.sort_by_attribute(@sort)
diff --git a/app/controllers/projects/ml/models_controller.rb b/app/controllers/projects/ml/models_controller.rb
index 77855b73cbd..4ff7d014723 100644
--- a/app/controllers/projects/ml/models_controller.rb
+++ b/app/controllers/projects/ml/models_controller.rb
@@ -4,17 +4,30 @@ module Projects
module Ml
class ModelsController < ::Projects::ApplicationController
before_action :check_feature_enabled
+ before_action :set_model, only: [:show]
feature_category :mlops
+ MAX_MODELS_PER_PAGE = 20
+
def index
- @models = ::Projects::Ml::ModelFinder.new(@project).execute
+ @paginator = ::Projects::Ml::ModelFinder.new(@project)
+ .execute
+ .keyset_paginate(cursor: params[:cursor], per_page: MAX_MODELS_PER_PAGE)
end
+ def show; end
+
private
def check_feature_enabled
render_404 unless can?(current_user, :read_model_registry, @project)
end
+
+ def set_model
+ @model = ::Ml::Model.by_project_id_and_id(@project, params[:model_id])
+
+ render_404 unless @model
+ end
end
end
end
diff --git a/app/controllers/projects/prometheus/metrics_controller.rb b/app/controllers/projects/prometheus/metrics_controller.rb
deleted file mode 100644
index 396841e667d..00000000000
--- a/app/controllers/projects/prometheus/metrics_controller.rb
+++ /dev/null
@@ -1,137 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module Prometheus
- class MetricsController < Projects::ApplicationController
- before_action :check_feature_availability!
- before_action :authorize_admin_project!
- before_action :require_prometheus_metrics!
-
- feature_category :metrics
- urgency :low
-
- def active_common
- respond_to do |format|
- format.json do
- matched_metrics = prometheus_adapter.query(:matched_metrics) || {}
-
- if matched_metrics.any?
- render json: matched_metrics
- else
- head :no_content
- end
- end
- end
- end
-
- def validate_query
- respond_to do |format|
- format.json do
- result = prometheus_adapter.query(:validate, params[:query])
-
- if result
- render json: result
- else
- head :accepted
- end
- end
- end
- end
-
- def new
- @metric = project.prometheus_metrics.new
- end
-
- def index
- respond_to do |format|
- format.json do
- metrics = ::PrometheusMetricsFinder.new(
- project: project,
- ordered: true
- ).execute.to_a
-
- response = {}
- if metrics.any?
- response[:metrics] = ::PrometheusMetricSerializer
- .new(project: project)
- .represent(metrics)
- end
-
- render json: response
- end
- end
- end
-
- def create
- @metric = project.prometheus_metrics.create(
- metrics_params.to_h.symbolize_keys
- )
-
- if @metric.persisted?
- redirect_to edit_project_settings_integration_path(project, ::Integrations::Prometheus),
- notice: _('Metric was successfully added.')
- else
- render 'new'
- end
- end
-
- def update
- @metric = prometheus_metric
-
- if @metric.update(metrics_params)
- redirect_to edit_project_settings_integration_path(project, ::Integrations::Prometheus),
- notice: _('Metric was successfully updated.')
- else
- render 'edit'
- end
- end
-
- def edit
- @metric = prometheus_metric
- end
-
- def destroy
- destroy_metrics_service(prometheus_metric).execute
-
- respond_to do |format|
- format.html do
- redirect_to edit_project_settings_integration_path(project, ::Integrations::Prometheus), status: :see_other
- end
- format.json do
- head :ok
- end
- end
- end
-
- private
-
- def prometheus_adapter
- @prometheus_adapter ||= ::Gitlab::Prometheus::Adapter.new(project, project.deployment_platform&.cluster).prometheus_adapter
- end
-
- def require_prometheus_metrics!
- render_404 unless prometheus_adapter&.can_query?
- end
-
- def prometheus_metric
- @prometheus_metric ||= ::PrometheusMetricsFinder.new(id: params[:id]).execute.first
- end
-
- def update_metrics_service(metric)
- ::Projects::Prometheus::Metrics::UpdateService.new(metric, metrics_params)
- end
-
- def destroy_metrics_service(metric)
- ::Projects::Prometheus::Metrics::DestroyService.new(metric)
- end
-
- def metrics_params
- params.require(:prometheus_metric).permit(:title, :query, :y_label, :unit, :legend, :group)
- end
-
- def check_feature_availability!
- render_404 if Feature.enabled?(:remove_monitor_metrics)
- end
- end
- end
-end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 6a246219f7d..fa26601204a 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -41,6 +41,7 @@ class ProjectsController < Projects::ApplicationController
push_frontend_feature_flag(:remove_monitor_metrics, @project)
push_frontend_feature_flag(:explain_code_chat, current_user)
push_frontend_feature_flag(:service_desk_custom_email, @project)
+ push_frontend_feature_flag(:issue_email_participants, @project)
push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
push_licensed_feature(:security_orchestration_policies) if @project.present? && @project.licensed_feature_available?(:security_orchestration_policies)
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
diff --git a/app/controllers/registrations/welcome_controller.rb b/app/controllers/registrations/welcome_controller.rb
deleted file mode 100644
index f7a601ec0bd..00000000000
--- a/app/controllers/registrations/welcome_controller.rb
+++ /dev/null
@@ -1,99 +0,0 @@
-# frozen_string_literal: true
-
-module Registrations
- class WelcomeController < ApplicationController
- include OneTrustCSP
- include GoogleAnalyticsCSP
- include GoogleSyndicationCSP
- include ::Gitlab::Utils::StrongMemoize
-
- layout 'minimal'
- # TODO: Once this is an ee + SaaS only feature, we can remove this.
- # To be completed in https://gitlab.com/gitlab-org/gitlab/-/issues/411858
- skip_before_action :check_two_factor_requirement
-
- helper_method :welcome_update_params
- helper_method :onboarding_status
-
- feature_category :user_management
-
- def show
- return redirect_to path_for_signed_in_user(current_user) if completed_welcome_step?
-
- track_event('render')
- end
-
- def update
- result = ::Users::SignupService.new(current_user, update_params).execute
-
- if result.success?
- track_event('successfully_submitted_form')
- successful_update_hooks
-
- redirect_to update_success_path
- else
- render :show
- end
- end
-
- private
-
- def authenticate_user!
- return if current_user
-
- redirect_to new_user_registration_path
- end
-
- def completed_welcome_step?
- !current_user.setup_for_company.nil?
- end
-
- def update_params
- params.require(:user).permit(:role, :setup_for_company)
- end
-
- def path_for_signed_in_user(user)
- stored_location_for(user) || last_member_activity_path
- end
-
- def last_member_activity_path
- return dashboard_projects_path unless onboarding_status.last_invited_member_source.present?
-
- onboarding_status.last_invited_member_source.activity_path
- end
-
- def update_success_path
- if onboarding_status.continue_full_onboarding? # trials/regular registration on .com
- signup_onboarding_path
- elsif onboarding_status.single_invite? # invites w/o tasks due to order
- flash[:notice] = helpers.invite_accepted_notice(onboarding_status.last_invited_member)
- onboarding_status.last_invited_member_source.activity_path
- else
- # Subscription registrations goes through here as well.
- # Invites will come here too if there is more than 1.
- path_for_signed_in_user(current_user)
- end
- end
-
- # overridden in EE
- def successful_update_hooks; end
-
- # overridden in EE
- def signup_onboarding_path; end
-
- # overridden in EE
- def track_event(action); end
-
- # overridden in EE
- def welcome_update_params
- {}
- end
-
- def onboarding_status
- Onboarding::Status.new(params.to_unsafe_h.deep_symbolize_keys, session, current_user)
- end
- strong_memoize_attr :onboarding_status
- end
-end
-
-Registrations::WelcomeController.prepend_mod_with('Registrations::WelcomeController')
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index a8b5ca81f49..72636a89433 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -7,13 +7,12 @@ class RegistrationsController < Devise::RegistrationsController
include InvisibleCaptchaOnSignup
include OneTrustCSP
include BizibleCSP
- include GoogleAnalyticsCSP
- include GoogleSyndicationCSP
include PreferredLanguageSwitcher
include Gitlab::Tracking::Helpers::WeakPasswordErrorEvent
include SkipsAlreadySignedInMessage
include Gitlab::RackLoadBalancingHelpers
include ::Gitlab::Utils::StrongMemoize
+ include Onboarding::Redirectable
layout 'devise'
@@ -26,11 +25,7 @@ class RegistrationsController < Devise::RegistrationsController
check_rate_limit!(:user_sign_up, scope: request.ip)
end
- before_action only: [:new] do
- push_frontend_feature_flag(:gitlab_gtm_datalayer, type: :ops)
- end
-
- feature_category :user_management
+ feature_category :instance_resiliency
helper_method :arkose_labs_enabled?
helper_method :registration_path_params
@@ -60,7 +55,7 @@ class RegistrationsController < Devise::RegistrationsController
# Devise sets a flash message on both successful & failed signups,
# but we only want to show a message if the resource is blocked by a pending approval.
- flash[:notice] = nil unless resource.blocked_pending_approval?
+ flash[:notice] = nil unless allow_flash_content?(resource)
rescue Gitlab::Access::AccessDeniedError
redirect_to(new_user_session_path)
end
@@ -121,6 +116,9 @@ class RegistrationsController < Devise::RegistrationsController
def after_sign_up_path_for(user)
Gitlab::AppLogger.info(user_created_message(confirmed: user.confirmed?))
+ # Member#accept_invite! operates on the member record to change the association, so the user needs reloaded
+ # to update the collection.
+ user.reset
after_sign_up_path
end
@@ -146,8 +144,13 @@ class RegistrationsController < Devise::RegistrationsController
private
- def after_sign_up_path
- users_sign_up_welcome_path
+ def onboarding_status
+ Onboarding::Status.new(params.to_unsafe_h.deep_symbolize_keys, session, resource)
+ end
+ strong_memoize_attr :onboarding_status
+
+ def allow_flash_content?(user)
+ user.blocked_pending_approval? || onboarding_status.single_invite?
end
# overridden in EE
diff --git a/app/controllers/repositories/lfs_api_controller.rb b/app/controllers/repositories/lfs_api_controller.rb
index da243a0301e..d9ca216b168 100644
--- a/app/controllers/repositories/lfs_api_controller.rb
+++ b/app/controllers/repositories/lfs_api_controller.rb
@@ -26,11 +26,7 @@ module Repositories
end
if download_request?
- if Feature.enabled?(:lfs_batch_direct_downloads, project)
- render json: { objects: download_objects! }, content_type: LfsRequest::CONTENT_TYPE
- else
- render json: { objects: legacy_download_objects! }, content_type: LfsRequest::CONTENT_TYPE
- end
+ render json: { objects: download_objects! }, content_type: LfsRequest::CONTENT_TYPE
elsif upload_request?
render json: { objects: upload_objects! }, content_type: LfsRequest::CONTENT_TYPE
else
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index d247490402f..7fff31c767f 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -35,6 +35,18 @@ class SearchController < ApplicationController
update_scope_for_code_search
end
+ before_action only: :show do
+ push_frontend_feature_flag(:search_notes_hide_archived_projects, current_user)
+ end
+
+ before_action only: :show do
+ push_frontend_feature_flag(:search_issues_hide_archived_projects, current_user)
+ end
+
+ before_action only: :show do
+ push_frontend_feature_flag(:search_merge_requests_hide_archived_projects, current_user)
+ end
+
rescue_from ActiveRecord::QueryCanceled, with: :render_timeout
layout 'search'
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index afbadc7f4ac..595d79abcf2 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -12,8 +12,6 @@ class SessionsController < Devise::SessionsController
include OneTrustCSP
include BizibleCSP
include VerifiesWithEmail
- include GoogleAnalyticsCSP
- include GoogleSyndicationCSP
include PreferredLanguageSwitcher
include SkipsAlreadySignedInMessage
include AcceptsPendingInvitations
diff --git a/app/controllers/users/namespace_visits_controller.rb b/app/controllers/users/namespace_visits_controller.rb
index 7c96d78e26e..d4f536654ca 100644
--- a/app/controllers/users/namespace_visits_controller.rb
+++ b/app/controllers/users/namespace_visits_controller.rb
@@ -5,7 +5,6 @@ module Users
feature_category :navigation
def create
- return head :not_found unless Feature.enabled?(:server_side_frecent_namespaces, current_user)
return head :bad_request unless params[:type].present? && params[:id].present?
Users::TrackNamespaceVisitsWorker.perform_async(params[:type], params[:id], current_user.id, DateTime.now) # rubocop:disable CodeReuse/Worker
diff --git a/app/controllers/users/terms_controller.rb b/app/controllers/users/terms_controller.rb
index f36b140f3a2..f7eb2aad9dc 100644
--- a/app/controllers/users/terms_controller.rb
+++ b/app/controllers/users/terms_controller.rb
@@ -4,7 +4,6 @@ module Users
class TermsController < ApplicationController
include InternalRedirect
include OneTrustCSP
- include GoogleAnalyticsCSP
skip_before_action :authenticate_user!, only: [:index]
skip_before_action :enforce_terms!
@@ -14,10 +13,6 @@ module Users
before_action :terms
- before_action only: [:index] do
- push_frontend_feature_flag(:gitlab_gtm_datalayer, type: :ops)
- end
-
layout 'terms'
feature_category :user_management
diff --git a/app/events/merge_requests/draft_state_change_event.rb b/app/events/merge_requests/draft_state_change_event.rb
new file mode 100644
index 00000000000..ab5f35a9597
--- /dev/null
+++ b/app/events/merge_requests/draft_state_change_event.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class DraftStateChangeEvent < Gitlab::EventStore::Event
+ def schema
+ {
+ 'type' => 'object',
+ 'required' => %w[
+ current_user_id
+ merge_request_id
+ ],
+ 'properties' => {
+ 'current_user_id' => { 'type' => 'integer' },
+ 'merge_request_id' => { 'type' => 'integer' }
+ }
+ }
+ end
+ end
+end
diff --git a/app/events/merge_requests/unblocked_state_event.rb b/app/events/merge_requests/unblocked_state_event.rb
new file mode 100644
index 00000000000..2cf79059cf7
--- /dev/null
+++ b/app/events/merge_requests/unblocked_state_event.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class UnblockedStateEvent < Gitlab::EventStore::Event
+ def schema
+ {
+ 'type' => 'object',
+ 'required' => %w[
+ current_user_id
+ merge_request_id
+ ],
+ 'properties' => {
+ 'current_user_id' => { 'type' => 'integer' },
+ 'merge_request_id' => { 'type' => 'integer' }
+ }
+ }
+ end
+ end
+end
diff --git a/app/experiments/build_ios_app_guide_email_experiment.rb b/app/experiments/build_ios_app_guide_email_experiment.rb
deleted file mode 100644
index d334a6a30d9..00000000000
--- a/app/experiments/build_ios_app_guide_email_experiment.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# frozen_string_literal: true
-
-class BuildIosAppGuideEmailExperiment < ApplicationExperiment
- control { false }
- candidate { true }
-end
diff --git a/app/finders/concerns/packages/finder_helper.rb b/app/finders/concerns/packages/finder_helper.rb
index 0ae99782cd3..585b35981a6 100644
--- a/app/finders/concerns/packages/finder_helper.rb
+++ b/app/finders/concerns/packages/finder_helper.rb
@@ -13,11 +13,13 @@ module Packages
project.packages.installable
end
- def packages_visible_to_user(user, within_group:)
+ def packages_visible_to_user(user, within_group:, with_package_registry_enabled: false)
return ::Packages::Package.none unless within_group
return ::Packages::Package.none unless Ability.allowed?(user, :read_group, within_group)
projects = projects_visible_to_reporters(user, within_group: within_group)
+ projects = projects.with_package_registry_enabled if with_package_registry_enabled
+
::Packages::Package.for_projects(projects.select(:id)).installable
end
diff --git a/app/finders/merge_requests/oldest_per_commit_finder.rb b/app/finders/merge_requests/oldest_per_commit_finder.rb
index 5da7a08e36c..16b5964e242 100644
--- a/app/finders/merge_requests/oldest_per_commit_finder.rb
+++ b/app/finders/merge_requests/oldest_per_commit_finder.rb
@@ -18,8 +18,8 @@ module MergeRequests
mapping = {}
shas = commits.map(&:id)
- # To include merge requests by the merge/squash SHA, we don't need to go
- # through any diff rows.
+ # To include merge requests by the merged/merge/squash SHA, we don't need
+ # to go through any diff rows.
#
# We can't squeeze all this into a single query, as the diff based data
# relies on a GROUP BY. On the other hand, retrieving MRs by their merge
@@ -27,17 +27,19 @@ module MergeRequests
@project
.merge_requests
.preload_target_project
- .by_merge_or_squash_commit_sha(shas)
+ .by_merged_or_merge_or_squash_commit_sha(shas)
.each do |mr|
- # Merge/squash SHAs can't be in the merge request itself. It _is_
- # possible a newer merge request includes the commit, but in that case
- # we still want the oldest merge request.
+ # SHAs for merge commits, squash commits, and rebased source SHAs,
+ # can't be in the merge request source branch. It _is_ possible a
+ # newer merge request includes the commit, but in that case we still
+ # want the oldest merge request.
#
# It's also possible that a merge request produces both a squashed
# commit and a merge commit. In that case we want to store the mapping
# for both the SHAs.
mapping[mr.squash_commit_sha] = mr if mr.squash_commit_sha
mapping[mr.merge_commit_sha] = mr if mr.merge_commit_sha
+ mapping[mr.merged_commit_sha] = mr if mr.merged_commit_sha
end
remaining = shas - mapping.keys
diff --git a/app/finders/packages/maven/package_finder.rb b/app/finders/packages/maven/package_finder.rb
index cc28d951f52..03855afb6e4 100644
--- a/app/finders/packages/maven/package_finder.rb
+++ b/app/finders/packages/maven/package_finder.rb
@@ -4,11 +4,7 @@ module Packages
module Maven
class PackageFinder < ::Packages::GroupOrProjectPackageFinder
def execute
- packages.last
- end
-
- def execute!
- packages.last!
+ packages
end
private
diff --git a/app/finders/packages/npm/packages_for_user_finder.rb b/app/finders/packages/npm/packages_for_user_finder.rb
index f42e49f9184..dc1d3b6e7fe 100644
--- a/app/finders/packages/npm/packages_for_user_finder.rb
+++ b/app/finders/packages/npm/packages_for_user_finder.rb
@@ -3,6 +3,8 @@
module Packages
module Npm
class PackagesForUserFinder < ::Packages::GroupOrProjectPackageFinder
+ extend ::Gitlab::Utils::Override
+
def execute
packages
end
@@ -13,6 +15,11 @@ module Packages
base.npm
.with_name(@params[:package_name])
end
+
+ override :group_packages
+ def group_packages
+ packages_visible_to_user(@current_user, within_group: @project_or_group, with_package_registry_enabled: true)
+ end
end
end
end
diff --git a/app/finders/projects/ml/model_finder.rb b/app/finders/projects/ml/model_finder.rb
index 99c66f53de7..1e407ba4aa4 100644
--- a/app/finders/projects/ml/model_finder.rb
+++ b/app/finders/projects/ml/model_finder.rb
@@ -11,7 +11,7 @@ module Projects
::Ml::Model
.by_project(@project)
.including_latest_version
- .limit(100) # This is a temporary limit before we add pagination
+ .with_version_count
end
end
end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index e6ee4355fd4..87edf36d1ce 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -61,7 +61,7 @@ class ProjectsFinder < UnionFinder
collection = Project.wrap_with_cte(collection) if use_cte
collection = filter_projects(collection)
- sort(collection)
+ sort(collection).allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/427628")
end
private
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index cb824aca33f..e09de1f6612 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -42,6 +42,7 @@ class SnippetsFinder < UnionFinder
include FinderMethods
include Gitlab::Utils::StrongMemoize
include CreatedAtFilter
+ include Gitlab::Allowable
attr_reader :current_user, :params
@@ -79,6 +80,7 @@ class SnippetsFinder < UnionFinder
snippets = all_snippets
snippets = by_ids(snippets)
snippets = snippets.with_optional_visibility(visibility_from_scope)
+ snippets = hide_created_by_banned_user(snippets)
end
by_created_at(snippets)
@@ -87,7 +89,7 @@ class SnippetsFinder < UnionFinder
def return_all_available_and_permited?
# Currently limited to access_levels `admin` and `auditor`
# See policies/base_policy.rb files for specifics.
- params[:all_available] && current_user&.can_read_all_resources?
+ params[:all_available] && can?(current_user, :read_all_resources)
end
def all_snippets
@@ -126,7 +128,7 @@ class SnippetsFinder < UnionFinder
queries = []
queries << personal_snippets unless only_project?
- if Ability.allowed?(current_user, :read_cross_project)
+ if can?(current_user, :read_cross_project)
queries << snippets_of_visible_projects
queries << snippets_of_authorized_projects if current_user
end
@@ -207,6 +209,14 @@ class SnippetsFinder < UnionFinder
snippets.id_in(params[:ids])
end
+ def hide_created_by_banned_user(snippets)
+ # if admin -> return all snippets, if not-admin -> filter out snippets by banned user
+ return snippets if can?(current_user, :read_all_resources)
+ return snippets unless Feature.enabled?(:hide_snippets_of_banned_users)
+
+ snippets.without_created_by_banned_user
+ end
+
def author
strong_memoize(:author) do
next unless params[:author].present?
diff --git a/app/finders/vs_code/settings/settings_finder.rb b/app/finders/vs_code/settings/settings_finder.rb
new file mode 100644
index 00000000000..459ccdbe566
--- /dev/null
+++ b/app/finders/vs_code/settings/settings_finder.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module VsCode
+ module Settings
+ class SettingsFinder
+ def initialize(current_user, setting_types)
+ @current_user = current_user
+ @setting_types = setting_types
+ end
+
+ def execute
+ relation = User.find(current_user.id).vscode_settings
+ return relation unless setting_types.present?
+
+ relation.by_setting_type(setting_types)
+ end
+
+ private
+
+ attr_accessor :current_user, :setting_types
+ end
+ end
+end
diff --git a/app/graphql/mutations/achievements/update_user_achievement_priorities.rb b/app/graphql/mutations/achievements/update_user_achievement_priorities.rb
new file mode 100644
index 00000000000..077b4810fdc
--- /dev/null
+++ b/app/graphql/mutations/achievements/update_user_achievement_priorities.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Achievements
+ class UpdateUserAchievementPriorities < BaseMutation
+ graphql_name 'UserAchievementPrioritiesUpdate'
+
+ field :user_achievements,
+ [::Types::Achievements::UserAchievementType],
+ null: false,
+ description: 'Updated user achievements.'
+
+ argument :user_achievement_ids,
+ [::Types::GlobalIDType[::Achievements::UserAchievement]],
+ required: true,
+ description: 'Global IDs of the user achievements being prioritized, ' \
+ 'ordered from highest to lowest priority.'
+
+ def resolve(args)
+ user_achievements = args.delete(:user_achievement_ids).map { |id| find_object(id) }
+
+ user_achievements.each do |user_achievement|
+ unless Ability.allowed?(current_user, :update_owned_user_achievement, user_achievement)
+ raise_resource_not_available_error!
+ end
+ end
+
+ result = ::Achievements::UpdateUserAchievementPrioritiesService.new(current_user, user_achievements).execute
+ { user_achievements: result.payload, errors: result.errors }
+ end
+
+ def find_object(id)
+ ::Gitlab::Graphql::Lazy.force(GitlabSchema.object_from_id(id, expected_type: ::Achievements::UserAchievement))
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/job/retry.rb b/app/graphql/mutations/ci/job/retry.rb
index bfb9b902cc5..5ccc33de33e 100644
--- a/app/graphql/mutations/ci/job/retry.rb
+++ b/app/graphql/mutations/ci/job/retry.rb
@@ -6,6 +6,12 @@ module Mutations
class Retry < Base
graphql_name 'JobRetry'
+ JobID = ::Types::GlobalIDType[::Ci::Processable]
+
+ argument :id, JobID,
+ required: true,
+ description: 'ID of the job to mutate.'
+
field :job,
Types::Ci::JobType,
null: true,
diff --git a/app/graphql/mutations/merge_requests/accept.rb b/app/graphql/mutations/merge_requests/accept.rb
index 64572091379..220ebea22c7 100644
--- a/app/graphql/mutations/merge_requests/accept.rb
+++ b/app/graphql/mutations/merge_requests/accept.rb
@@ -7,7 +7,7 @@ module Mutations
authorize :accept_merge_request
description <<~DESC
Accepts a merge request.
- When accepted, the source branch will be merged into the target branch, either
+ When accepted, the source branch will be scheduled to merge into the target branch, either
immediately if possible, or using one of the automatic merge strategies.
DESC
@@ -59,7 +59,7 @@ module Mutations
service = AutoMergeService.new(project, current_user, merge_params)
service.execute(merge_request, merge_params[:auto_merge_strategy])
else
- merge_service.execute(merge_request)
+ merge_request.merge_async(current_user.id, merge_params)
end
{
diff --git a/app/graphql/mutations/packages/protection/rule/create.rb b/app/graphql/mutations/packages/protection/rule/create.rb
new file mode 100644
index 00000000000..36eaec334d6
--- /dev/null
+++ b/app/graphql/mutations/packages/protection/rule/create.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Packages
+ module Protection
+ module Rule
+ class Create < ::Mutations::BaseMutation
+ graphql_name 'CreatePackagesProtectionRule'
+ description 'Creates a protection rule to restrict access to project packages. ' \
+ 'Available only when feature flag `packages_protected_packages` is enabled.'
+
+ include FindsProject
+
+ authorize :admin_package
+
+ argument :project_path,
+ GraphQL::Types::ID,
+ required: true,
+ description: 'Full path of the project where a protection rule is located.'
+
+ argument :package_name_pattern,
+ GraphQL::Types::String,
+ required: true,
+ description:
+ 'Package name protected by the protection rule. For example `@my-scope/my-package-*`. ' \
+ 'Wildcard character `*` allowed.'
+
+ argument :package_type,
+ Types::Packages::Protection::RulePackageTypeEnum,
+ required: true,
+ description: 'Package type protected by the protection rule. For example `NPM`.'
+
+ argument :push_protected_up_to_access_level,
+ Types::Packages::Protection::RuleAccessLevelEnum,
+ required: true,
+ description:
+ 'Max GitLab access level unable to push a package. For example `DEVELOPER`, `MAINTAINER`, `OWNER`.'
+
+ field :package_protection_rule,
+ Types::Packages::Protection::RuleType,
+ null: true,
+ description: 'Packages protection rule after mutation.'
+
+ def resolve(project_path:, **kwargs)
+ project = authorized_find!(project_path)
+
+ if Feature.disabled?(:packages_protected_packages, project)
+ raise_resource_not_available_error!("'packages_protected_packages' feature flag is disabled")
+ end
+
+ response = ::Packages::Protection::CreateRuleService.new(project: project, current_user: current_user,
+ params: kwargs).execute
+
+ { package_protection_rule: response.payload[:package_protection_rule], errors: response.errors }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/users/set_namespace_commit_email.rb b/app/graphql/mutations/users/set_namespace_commit_email.rb
index 72ef0635bb3..db1c33595f2 100644
--- a/app/graphql/mutations/users/set_namespace_commit_email.rb
+++ b/app/graphql/mutations/users/set_namespace_commit_email.rb
@@ -20,7 +20,7 @@ module Mutations
null: true,
description: 'User namespace commit email after mutation.'
- authorize :read_namespace
+ authorize :read_namespace_via_membership
def resolve(args)
namespace = authorized_find!(args[:namespace_id])
diff --git a/app/graphql/mutations/work_items/linked_items/add.rb b/app/graphql/mutations/work_items/linked_items/add.rb
index e0c17a61205..4029d17d4ac 100644
--- a/app/graphql/mutations/work_items/linked_items/add.rb
+++ b/app/graphql/mutations/work_items/linked_items/add.rb
@@ -16,8 +16,6 @@ module Mutations
private
def update_links(work_item, params)
- Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/419555')
-
gids = params.delete(:work_items_ids)
work_items = begin
GitlabSchema.parse_gids(gids, expected_type: ::WorkItem).map(&:find)
diff --git a/app/graphql/mutations/work_items/linked_items/base.rb b/app/graphql/mutations/work_items/linked_items/base.rb
index a1d9bced930..8a6201ffdf7 100644
--- a/app/graphql/mutations/work_items/linked_items/base.rb
+++ b/app/graphql/mutations/work_items/linked_items/base.rb
@@ -5,8 +5,7 @@ module Mutations
module LinkedItems
class Base < BaseMutation
# Limit maximum number of items that can be linked at a time to avoid overloading the DB
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/419555
- MAX_WORK_ITEMS = 3
+ MAX_WORK_ITEMS = 10
argument :id, ::Types::GlobalIDType[::WorkItem],
required: true, description: 'Global ID of the work item.'
@@ -33,7 +32,7 @@ module Mutations
def resolve(**args)
work_item = authorized_find!(id: args.delete(:id))
- raise_resource_not_available_error! unless work_item.project.linked_work_items_feature_flag_enabled?
+ raise_resource_not_available_error! unless work_item.resource_parent.linked_work_items_feature_flag_enabled?
service_response = update_links(work_item, args)
diff --git a/app/graphql/mutations/work_items/update.rb b/app/graphql/mutations/work_items/update.rb
index f22e9bcf393..228a9e52355 100644
--- a/app/graphql/mutations/work_items/update.rb
+++ b/app/graphql/mutations/work_items/update.rb
@@ -10,7 +10,7 @@ module Mutations
include Mutations::WorkItems::UpdateArguments
include Mutations::WorkItems::Widgetable
- authorize :update_work_item
+ authorize :read_work_item
field :work_item, Types::WorkItemType,
null: true,
@@ -22,11 +22,13 @@ module Mutations
work_item = authorized_find!(id: id)
widget_params = extract_widget_params!(work_item.work_item_type, attributes)
-
interpret_quick_actions!(work_item, current_user, widget_params, attributes)
+ # Only checks permissions for base attributes because widgets define their own permissions independently
+ raise_resource_not_available_error! unless attributes.empty? || can_update?(work_item)
+
update_result = ::WorkItems::UpdateService.new(
- container: work_item.project,
+ container: work_item.resource_parent,
current_user: current_user,
params: attributes,
widget_params: widget_params,
@@ -62,6 +64,10 @@ module Mutations
widget_params.merge!(parsed_params[:widgets])
attributes.merge!(parsed_params[:common])
end
+
+ def can_update?(work_item)
+ current_user.can?(:update_work_item, work_item)
+ end
end
end
end
diff --git a/app/graphql/resolvers/achievements/user_achievements_for_user_resolver.rb b/app/graphql/resolvers/achievements/user_achievements_for_user_resolver.rb
new file mode 100644
index 00000000000..673babcf14a
--- /dev/null
+++ b/app/graphql/resolvers/achievements/user_achievements_for_user_resolver.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Achievements
+ # rubocop:disable Graphql/ResolverType -- the type is inherited from the parent class
+ class UserAchievementsForUserResolver < UserAchievementsResolver
+ def resolve_with_lookahead
+ super.order_by_priority_asc
+ end
+ end
+ # rubocop:enable Graphql/ResolverType
+ end
+end
diff --git a/app/graphql/resolvers/admin/analytics/usage_trends/measurements_resolver.rb b/app/graphql/resolvers/admin/analytics/usage_trends/measurements_resolver.rb
index b5a19d38b9c..0c9607d9413 100644
--- a/app/graphql/resolvers/admin/analytics/usage_trends/measurements_resolver.rb
+++ b/app/graphql/resolvers/admin/analytics/usage_trends/measurements_resolver.rb
@@ -7,7 +7,7 @@ module Resolvers
class MeasurementsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
- type Types::Admin::Analytics::UsageTrends::MeasurementType, null: true
+ type Types::Admin::Analytics::UsageTrends::MeasurementType.connection_type, null: true
argument :identifier, Types::Admin::Analytics::UsageTrends::MeasurementIdentifierEnum,
required: true,
diff --git a/app/graphql/resolvers/analytics/cycle_analytics/base_issue_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/base_issue_resolver.rb
index 8128023aecb..768265752d5 100644
--- a/app/graphql/resolvers/analytics/cycle_analytics/base_issue_resolver.rb
+++ b/app/graphql/resolvers/analytics/cycle_analytics/base_issue_resolver.rb
@@ -48,3 +48,5 @@ module Resolvers
end
end
end
+
+Resolvers::Analytics::CycleAnalytics::BaseIssueResolver.prepend_mod
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
index 6f847221f1b..17db91a685f 100644
--- a/app/graphql/resolvers/base_resolver.rb
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -11,26 +11,25 @@ module Resolvers
@requires_argument = true
end
+ def self.requires_argument?
+ !!@requires_argument
+ end
+
def self.calls_gitaly!
@calls_gitaly = true
end
+ def self.calls_gitaly?
+ !!@calls_gitaly
+ end
+
# This is a flag to allow us to use `complexity_multiplier` to compute complexity for connection
# fields(see BaseField#connection_complexity_multiplier) in resolvers that do external connection pagination,
- # thus disabling the default `connection` option(see self.field_options method above).
+ # thus disabling the default `connection` option.
def self.calculate_ext_conn_complexity
false
end
- def self.field_options
- extra_options = {
- requires_argument: @requires_argument,
- calls_gitaly: @calls_gitaly
- }.compact
-
- super.merge(extra_options)
- end
-
def self.singular_type
return unless type
@@ -63,8 +62,13 @@ module Resolvers
type parent.singular_type, null: true
def ready?(**args)
- ready, early_return = super
- [ready, select_result(early_return)]
+ value = super
+
+ if value.is_a?(Array)
+ [value[0], select_result(value[1])]
+ else
+ value
+ end
end
def resolve(**args)
diff --git a/app/graphql/resolvers/blobs_resolver.rb b/app/graphql/resolvers/blobs_resolver.rb
index 546eeb76ff5..27a15381b43 100644
--- a/app/graphql/resolvers/blobs_resolver.rb
+++ b/app/graphql/resolvers/blobs_resolver.rb
@@ -36,7 +36,7 @@ module Resolvers
ref ||= repository.root_ref
validate_ref(ref)
- ref = ExtractsRef.qualify_ref(ref, ref_type)
+ ref = ExtractsRef::RefExtractor.qualify_ref(ref, ref_type)
repository.blobs_at(paths.map { |path| [ref, path] }).tap do |blobs|
blobs.each do |blob|
diff --git a/app/graphql/resolvers/ci/config_resolver.rb b/app/graphql/resolvers/ci/config_resolver.rb
index ec6ede58cf5..8c85a6aebea 100644
--- a/app/graphql/resolvers/ci/config_resolver.rb
+++ b/app/graphql/resolvers/ci/config_resolver.rb
@@ -30,11 +30,20 @@ module Resolvers
required: false,
description: 'Run pipeline creation simulation, or only do static check.'
- def resolve(project_path:, content:, sha: nil, dry_run: false)
+ argument :skip_verify_project_sha, GraphQL::Types::Boolean,
+ required: false,
+ alpha: { milestone: '16.5' },
+ description: "If the provided `sha` is found in the project's repository but is not " \
+ "associated with a Git reference (a detached commit), the verification fails and a " \
+ "validation error is returned. Otherwise, verification passes, even if the `sha` is " \
+ "invalid. Set to `true` to skip this verification process."
+
+ def resolve(project_path:, content:, sha: nil, dry_run: false, skip_verify_project_sha: false)
project = authorized_find!(project_path: project_path)
result = ::Gitlab::Ci::Lint
- .new(project: project, current_user: context[:current_user], sha: sha)
+ .new(project: project, current_user: context[:current_user], sha: sha,
+ verify_project_sha: !skip_verify_project_sha)
.validate(content, dry_run: dry_run)
response(result)
diff --git a/app/graphql/resolvers/clusters/agent_tokens_resolver.rb b/app/graphql/resolvers/clusters/agent_tokens_resolver.rb
index 0b9422db2a9..313d71aa345 100644
--- a/app/graphql/resolvers/clusters/agent_tokens_resolver.rb
+++ b/app/graphql/resolvers/clusters/agent_tokens_resolver.rb
@@ -3,7 +3,7 @@
module Resolvers
module Clusters
class AgentTokensResolver < BaseResolver
- type Types::Clusters::AgentTokenType, null: true
+ type Types::Clusters::AgentTokenType.connection_type, null: true
alias_method :agent, :object
diff --git a/app/graphql/resolvers/concerns/caching_array_resolver.rb b/app/graphql/resolvers/concerns/caching_array_resolver.rb
index 62649518142..15bf9a90e46 100644
--- a/app/graphql/resolvers/concerns/caching_array_resolver.rb
+++ b/app/graphql/resolvers/concerns/caching_array_resolver.rb
@@ -22,7 +22,7 @@
#
# **important**: If the cardinality of your collection is likely to be greater than 100,
# then you will want to pass `max_page_size:` as part of the field definition
-# or (ideally) as part of the resolver `field_options`.
+# or (ideally) set `max_page_size` in the resolver.
#
# How to implement:
# --------------------
diff --git a/app/graphql/resolvers/concerns/work_items/look_ahead_preloads.rb b/app/graphql/resolvers/concerns/work_items/look_ahead_preloads.rb
index 92fb9ec5cef..71833fbd2b9 100644
--- a/app/graphql/resolvers/concerns/work_items/look_ahead_preloads.rb
+++ b/app/graphql/resolvers/concerns/work_items/look_ahead_preloads.rb
@@ -14,7 +14,8 @@ module WorkItems
{
work_item_type: :work_item_type,
web_url: { namespace: :route, project: [:project_namespace, { namespace: :route }] },
- widgets: { work_item_type: :enabled_widget_definitions }
+ widgets: { work_item_type: :enabled_widget_definitions },
+ archived: :project
}
end
@@ -48,7 +49,8 @@ module WorkItems
{
project: [:project_feature, :group]
},
- :author
+ :author,
+ *super
]
end
end
diff --git a/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb b/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb
index 793b73342ab..187cb15ccc5 100644
--- a/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb
+++ b/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb
@@ -4,7 +4,6 @@ module Resolvers
module ErrorTracking
class SentryErrorsResolver < BaseResolver
type Types::ErrorTracking::SentryErrorType.connection_type, null: true
- extension Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension
argument :search_term, ::GraphQL::Types::String,
description: 'Search query for the Sentry error details.',
@@ -31,10 +30,6 @@ module Resolvers
Gitlab::Graphql::ExternallyPaginatedArray.new(previous_cursor, next_cursor, *issues)
end
-
- def self.field_options
- super.merge(connection: false) # we manage the pagination manually, so opt out of the connection field extension
- end
end
end
end
diff --git a/app/graphql/resolvers/group_issues_resolver.rb b/app/graphql/resolvers/group_issues_resolver.rb
index 7bbc662c6c8..5e0fb27bafa 100644
--- a/app/graphql/resolvers/group_issues_resolver.rb
+++ b/app/graphql/resolvers/group_issues_resolver.rb
@@ -11,7 +11,11 @@ module Resolvers
before_connection_authorization do |nodes, _|
projects = nodes.map(&:project)
- ActiveRecord::Associations::Preloader.new(records: projects, associations: :namespace).call
+ ActiveRecord::Associations::Preloader.new(records: projects, associations: project_associations).call
+ end
+
+ def self.project_associations
+ [:namespace]
end
def ready?(**args)
@@ -24,3 +28,5 @@ module Resolvers
end
end
# rubocop:enable Graphql/ResolverType
+
+Resolvers::GroupIssuesResolver.prepend_mod
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index 589366ba26d..34f14eee0e5 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -23,7 +23,11 @@ module Resolvers
projects = nodes.map(&:project)
::Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects, current_user).execute
::Preloaders::GroupPolicyPreloader.new(projects.filter_map(&:group), current_user).execute
- ActiveRecord::Associations::Preloader.new(records: projects, associations: :namespace).call
+ ActiveRecord::Associations::Preloader.new(records: projects, associations: project_associations).call
+ end
+
+ def self.project_associations
+ [:namespace]
end
def ready?(**args)
@@ -62,3 +66,5 @@ module Resolvers
end
end
end
+
+Resolvers::IssuesResolver.prepend_mod
diff --git a/app/graphql/resolvers/kas/agent_configurations_resolver.rb b/app/graphql/resolvers/kas/agent_configurations_resolver.rb
index 74c5cbe55f1..6e35f64c2ee 100644
--- a/app/graphql/resolvers/kas/agent_configurations_resolver.rb
+++ b/app/graphql/resolvers/kas/agent_configurations_resolver.rb
@@ -3,7 +3,7 @@
module Resolvers
module Kas
class AgentConfigurationsResolver < BaseResolver
- type Types::Kas::AgentConfigurationType, null: true
+ type Types::Kas::AgentConfigurationType.connection_type, null: true
# Calls Gitaly via KAS
calls_gitaly!
diff --git a/app/graphql/resolvers/last_commit_resolver.rb b/app/graphql/resolvers/last_commit_resolver.rb
index acf7826ab13..ff5701ede8c 100644
--- a/app/graphql/resolvers/last_commit_resolver.rb
+++ b/app/graphql/resolvers/last_commit_resolver.rb
@@ -12,7 +12,7 @@ module Resolvers
# Ensure merge commits can be returned by sending nil to Gitaly instead of '/'
path = tree.path == '/' ? nil : tree.path
commit = Gitlab::Git::Commit.last_for_path(tree.repository,
- ExtractsRef.qualify_ref(tree.sha, tree.ref_type), path, literal_pathspec: true)
+ ExtractsRef::RefExtractor.qualify_ref(tree.sha, tree.ref_type), path, literal_pathspec: true)
::Commit.new(commit, tree.repository.project) if commit
end
diff --git a/app/graphql/resolvers/merge_request_pipelines_resolver.rb b/app/graphql/resolvers/merge_request_pipelines_resolver.rb
index deb698c63e1..45159e0edd5 100644
--- a/app/graphql/resolvers/merge_request_pipelines_resolver.rb
+++ b/app/graphql/resolvers/merge_request_pipelines_resolver.rb
@@ -11,9 +11,7 @@ module Resolvers
# Return at most 500 pipelines for each MR.
# Merge requests generally have many fewer pipelines than this.
- def self.field_options
- super.merge(max_page_size: 500)
- end
+ max_page_size 500
def resolve(**args)
return unless project
diff --git a/app/graphql/resolvers/noteable/notes_resolver.rb b/app/graphql/resolvers/noteable/notes_resolver.rb
index 0d25c747ffb..b4bd1068723 100644
--- a/app/graphql/resolvers/noteable/notes_resolver.rb
+++ b/app/graphql/resolvers/noteable/notes_resolver.rb
@@ -7,6 +7,11 @@ module Resolvers
type Types::Notes::NoteType.connection_type, null: false
+ argument :filter, Types::WorkItems::NotesFilterTypeEnum,
+ required: false,
+ default_value: ::UserPreference::NOTES_FILTERS[:all_notes],
+ description: 'Type of notes collection: ALL_NOTES, ONLY_COMMENTS, ONLY_ACTIVITY.'
+
before_connection_authorization do |nodes, current_user|
next if nodes.blank?
@@ -16,8 +21,9 @@ module Resolvers
::Preloaders::Projects::NotesPreloader.new(project, current_user).call(nodes)
end
- def resolve_with_lookahead(*)
- apply_lookahead(object.notes.fresh)
+ def resolve_with_lookahead(**args)
+ notes = NotesFinder.new(current_user, build_params(args)).execute
+ apply_lookahead(notes)
end
private
@@ -31,6 +37,17 @@ module Resolvers
award_emoji: [:award_emoji]
}
end
+
+ def build_params(args)
+ params = {
+ project: object.project,
+ target: object
+ }
+
+ params[:notes_filter] = args[:filter] if args[:filter].present?
+
+ params
+ end
end
end
end
diff --git a/app/graphql/resolvers/package_pipelines_resolver.rb b/app/graphql/resolvers/package_pipelines_resolver.rb
index 7f610915489..40e5456164a 100644
--- a/app/graphql/resolvers/package_pipelines_resolver.rb
+++ b/app/graphql/resolvers/package_pipelines_resolver.rb
@@ -5,7 +5,7 @@ module Resolvers
include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::Ci::PipelineType.connection_type, null: true
- extension Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension
+ extras [:lookahead]
authorizes_object!
authorize :read_pipeline
@@ -41,14 +41,6 @@ module Resolvers
end
end
- # we manage the pagination manually, so opt out of the connection field extension
- def self.field_options
- super.merge(
- connection: false,
- extras: [:lookahead]
- )
- end
-
private
def lazy_load_pipeline(id)
@@ -59,6 +51,7 @@ module Resolvers
def default_value_for(first:, last:, after:, before:)
Gitlab::Graphql::Pagination::ActiveRecordArrayConnection.new(
[],
+ context: context,
first: first,
last: last,
after: after,
diff --git a/app/graphql/resolvers/paginated_tree_resolver.rb b/app/graphql/resolvers/paginated_tree_resolver.rb
index de48fbafb04..48c94c144dd 100644
--- a/app/graphql/resolvers/paginated_tree_resolver.rb
+++ b/app/graphql/resolvers/paginated_tree_resolver.rb
@@ -3,7 +3,6 @@
module Resolvers
class PaginatedTreeResolver < BaseResolver
type Types::Tree::TreeType.connection_type, null: true
- extension Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension
calls_gitaly!
@@ -50,9 +49,5 @@ module Resolvers
extensions: { code: e.code, gitaly_code: e.status, service: e.service }
)
end
-
- def self.field_options
- super.merge(connection: false) # we manage the pagination manually, so opt out of the connection field extension
- end
end
end
diff --git a/app/graphql/resolvers/project_packages_protection_rules_resolver.rb b/app/graphql/resolvers/project_packages_protection_rules_resolver.rb
new file mode 100644
index 00000000000..5d3d0fbf79d
--- /dev/null
+++ b/app/graphql/resolvers/project_packages_protection_rules_resolver.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class ProjectPackagesProtectionRulesResolver < BaseResolver
+ type Types::Packages::Protection::RuleType.connection_type, null: true
+
+ alias_method :project, :object
+
+ def resolve(**_args)
+ return [] if Feature.disabled?(:packages_protected_packages, project)
+
+ project.package_protection_rules
+ end
+ end
+end
diff --git a/app/graphql/resolvers/projects_resolver.rb b/app/graphql/resolvers/projects_resolver.rb
index 08981f2c441..8dd409a8173 100644
--- a/app/graphql/resolvers/projects_resolver.rb
+++ b/app/graphql/resolvers/projects_resolver.rb
@@ -4,7 +4,7 @@ module Resolvers
class ProjectsResolver < BaseResolver
include ProjectSearchArguments
- type Types::ProjectType, null: true
+ type Types::ProjectType.connection_type, null: true
argument :ids, [GraphQL::Types::ID],
required: false,
diff --git a/app/graphql/resolvers/user_notes_count_resolver.rb b/app/graphql/resolvers/user_notes_count_resolver.rb
index b91815c72f5..ebc54a1c6e8 100644
--- a/app/graphql/resolvers/user_notes_count_resolver.rb
+++ b/app/graphql/resolvers/user_notes_count_resolver.rb
@@ -20,7 +20,7 @@ module Resolvers
def authorized_resource?(object)
ability = "read_#{object.class.name.underscore}".to_sym
- context[:current_user].present? && Ability.allowed?(context[:current_user], ability, object)
+ Ability.allowed?(context[:current_user], ability, object)
end
end
end
diff --git a/app/graphql/resolvers/work_items/ancestors_resolver.rb b/app/graphql/resolvers/work_items/ancestors_resolver.rb
new file mode 100644
index 00000000000..33adbfc9c86
--- /dev/null
+++ b/app/graphql/resolvers/work_items/ancestors_resolver.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module WorkItems
+ class AncestorsResolver < BaseResolver
+ prepend ::WorkItems::LookAheadPreloads
+
+ type Types::WorkItemType.connection_type, null: true
+
+ def resolve_with_lookahead
+ ancestors = object.ancestors
+ return WorkItem.none unless ancestors
+
+ truncate_ancestors(apply_lookahead(ancestors)).reverse!
+ end
+
+ private
+
+ def truncate_ancestors(ancestors)
+ # Iterate from closest ancestor until root or first missing ancestor
+ authorized = authorized_ancestors(ancestors)
+
+ previous_ancestor = object.work_item
+ authorized.take_while do |ancestor|
+ is_direct_parent = previous_ancestor.work_item_parent.id == ancestor.id
+ previous_ancestor = ancestor
+
+ is_direct_parent
+ end
+ end
+
+ def authorized_ancestors(ancestors)
+ preload_resource_parents(ancestors)
+
+ DeclarativePolicy.user_scope do
+ ancestors.select { |ancestor| Ability.allowed?(current_user, :read_work_item, ancestor) }
+ end
+ end
+
+ def preload_resource_parents(work_items)
+ projects = work_items.filter_map(&:project)
+ namespaces = work_items.filter_map(&:namespace)
+ group_namespaces = namespaces.select { |n| n.type == ::Group.sti_name }
+
+ ::Preloaders::GroupPolicyPreloader.new(group_namespaces, current_user).execute if group_namespaces.any?
+ return unless projects.any?
+
+ ::Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects, current_user).execute
+ ::Preloaders::GroupPolicyPreloader.new(projects.filter_map(&:namespace), current_user).execute
+ ActiveRecord::Associations::Preloader.new(records: projects, associations: [:namespace]).call
+ end
+
+ def unconditional_includes
+ [:namespace, :work_item_parent, :work_item_type]
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/work_items/linked_items_resolver.rb b/app/graphql/resolvers/work_items/linked_items_resolver.rb
index 35a6974163a..108d5d41b62 100644
--- a/app/graphql/resolvers/work_items/linked_items_resolver.rb
+++ b/app/graphql/resolvers/work_items/linked_items_resolver.rb
@@ -28,7 +28,7 @@ module Resolvers
private
def related_work_items(type)
- return [] unless work_item.project.linked_work_items_feature_flag_enabled?
+ return [] unless work_item.resource_parent.linked_work_items_feature_flag_enabled?
work_item.linked_work_items(current_user, preload: { project: [:project_feature, :group] }, link_type: type)
end
diff --git a/app/graphql/resolvers/work_items/work_item_discussions_resolver.rb b/app/graphql/resolvers/work_items/work_item_discussions_resolver.rb
index b40d85e8003..0bbd51a537e 100644
--- a/app/graphql/resolvers/work_items/work_item_discussions_resolver.rb
+++ b/app/graphql/resolvers/work_items/work_item_discussions_resolver.rb
@@ -4,7 +4,6 @@ module Resolvers
module WorkItems
class WorkItemDiscussionsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
- extension Gitlab::Graphql::Extensions::ForwardOnlyExternallyPaginatedArrayExtension
authorize :read_work_item
authorizes_object!
@@ -31,11 +30,6 @@ module Resolvers
)
end
- def self.field_options
- # we manage the pagination manually through external array, so opt out of the connection field extension
- super.merge(connection: false)
- end
-
def self.calculate_ext_conn_complexity
true
end
diff --git a/app/graphql/types/achievements/user_achievement_type.rb b/app/graphql/types/achievements/user_achievement_type.rb
index 7cdcb66576c..b92b2c42bee 100644
--- a/app/graphql/types/achievements/user_achievement_type.rb
+++ b/app/graphql/types/achievements/user_achievement_type.rb
@@ -48,6 +48,11 @@ module Types
Types::TimeType,
null: true,
description: 'Timestamp the achievement was revoked.'
+
+ field :priority,
+ GraphQL::Types::Int,
+ null: true,
+ description: 'Priority of the user achievement.'
end
end
end
diff --git a/app/graphql/types/base_argument.rb b/app/graphql/types/base_argument.rb
index d2bc1d55408..cda7fa4a5df 100644
--- a/app/graphql/types/base_argument.rb
+++ b/app/graphql/types/base_argument.rb
@@ -7,7 +7,6 @@ module Types
attr_reader :doc_reference
def initialize(*args, **kwargs, &block)
- init_gitlab_deprecation(kwargs)
@doc_reference = kwargs.delete(:see)
# our custom addition `nullable` which allows us to declare
diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb
index 45e78b330fb..ca86e399f6b 100644
--- a/app/graphql/types/base_enum.rb
+++ b/app/graphql/types/base_enum.rb
@@ -5,12 +5,6 @@ module Types
class BaseEnum < GraphQL::Schema::Enum
class CustomValue < GraphQL::Schema::EnumValue
include Gitlab::Graphql::Deprecations
-
- def initialize(name, desc = nil, **kwargs)
- init_gitlab_deprecation(kwargs)
-
- super(name, desc, **kwargs)
- end
end
enum_value_class(CustomValue)
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index caeb81c95cb..886490ba62f 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -11,13 +11,15 @@ module Types
attr_reader :doc_reference
def initialize(**kwargs, &block)
- init_gitlab_deprecation(kwargs)
- @calls_gitaly = !!kwargs.delete(:calls_gitaly)
+ @requires_argument = kwargs.delete(:requires_argument)
+ @calls_gitaly = kwargs.delete(:calls_gitaly)
@doc_reference = kwargs.delete(:see)
- @constant_complexity = kwargs[:complexity].is_a?(Integer) && kwargs[:complexity] > 0
- @requires_argument = !!kwargs.delete(:requires_argument)
+
+ given_complexity = kwargs[:complexity] || kwargs[:resolver_class].try(:complexity)
+ @constant_complexity = given_complexity.is_a?(Integer) && given_complexity > 0
+ kwargs[:complexity] = field_complexity(kwargs[:resolver_class], given_complexity)
+
@authorize = Array.wrap(kwargs.delete(:authorize))
- kwargs[:complexity] = field_complexity(kwargs[:resolver_class], kwargs[:complexity])
after_connection_extensions = kwargs.delete(:late_extensions) || []
super(**kwargs, &block)
@@ -31,11 +33,12 @@ module Types
end
def may_call_gitaly?
- @constant_complexity || @calls_gitaly
+ @constant_complexity || calls_gitaly?
end
def requires_argument?
- @requires_argument || arguments.values.any? { |argument| argument.type.non_null? }
+ value = @requires_argument.nil? ? @resolver_class.try(:requires_argument?) : @requires_argument
+ !!value || arguments.values.any? { |argument| argument.type.non_null? }
end
# By default fields authorize against the current object, but that is not how our
@@ -82,7 +85,7 @@ module Types
end
def calls_gitaly?
- @calls_gitaly
+ !!(@calls_gitaly.nil? ? @resolver_class.try(:calls_gitaly?) : @calls_gitaly)
end
def constant_complexity?
diff --git a/app/graphql/types/ci/ci_cd_setting_type.rb b/app/graphql/types/ci/ci_cd_setting_type.rb
index 8a49c5a6a95..f01c63d717b 100644
--- a/app/graphql/types/ci/ci_cd_setting_type.rb
+++ b/app/graphql/types/ci/ci_cd_setting_type.rb
@@ -29,12 +29,6 @@ module Types
null: true,
description: 'Whether merge pipelines are enabled.',
method: :merge_pipelines_enabled?
- # TODO(Issue 422295): this is EE only and should be moved to the EE file
- field :merge_trains_enabled,
- GraphQL::Types::Boolean,
- null: true,
- description: 'Whether merge trains are enabled.',
- method: :merge_trains_enabled?
field :project,
Types::ProjectType,
null: true,
diff --git a/app/graphql/types/ci/detailed_status_type.rb b/app/graphql/types/ci/detailed_status_type.rb
index e18770c2708..6882a495259 100644
--- a/app/graphql/types/ci/detailed_status_type.rb
+++ b/app/graphql/types/ci/detailed_status_type.rb
@@ -16,20 +16,34 @@ module Types
field :favicon, GraphQL::Types::String, null: true,
description: 'Favicon of the status.'
field :group, GraphQL::Types::String, null: true,
- description: 'Group of the status.'
+ description: 'Group of the status.',
+ deprecated: {
+ reason: 'The `group` attribute is deprecated. Use `name` instead',
+ milestone: '16.4'
+ }
field :has_details, GraphQL::Types::Boolean, null: true,
description: 'Indicates if the status has further details.',
method: :has_details?
field :icon, GraphQL::Types::String, null: true,
- description: 'Icon of the status.'
+ description: 'Icon of the status.',
+ deprecated: {
+ reason: 'The `icon` attribute is deprecated. Use `name` to ' \
+ 'identify the status to display instead',
+ milestone: '16.4'
+ }
field :id, GraphQL::Types::String, null: false,
description: 'ID for a detailed status.',
extras: [:parent]
field :label, GraphQL::Types::String, null: true,
- calls_gitaly: true,
- description: 'Label of the status.'
+ description: 'Human-readable label of the status (e.g. success).'
+ field :name, GraphQL::Types::String, null: true,
+ description: 'Machine-readable status name (e.g. SUCCESS).'
field :text, GraphQL::Types::String, null: true,
- description: 'Text of the status.'
+ description: 'Text of the status.',
+ deprecated: {
+ reason: 'The `text` attribute is being deprecated. Use `label` instead',
+ milestone: '16.4'
+ }
field :tooltip, GraphQL::Types::String, null: true,
description: 'Tooltip associated with the status.',
method: :status_tooltip
diff --git a/app/graphql/types/ci/job_trace_type.rb b/app/graphql/types/ci/job_trace_type.rb
index 405c640115d..62fb9340b53 100644
--- a/app/graphql/types/ci/job_trace_type.rb
+++ b/app/graphql/types/ci/job_trace_type.rb
@@ -21,7 +21,7 @@ module Types
def html_summary(last_lines:)
object.html(
last_lines: last_lines.clamp(1, 100),
- max_size: Feature.enabled?(:graphql_job_trace_html_summary_max_size) ? MAX_SIZE_B : nil
+ max_size: MAX_SIZE_B
).html_safe
end
end
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index ba638d4bc47..dfdc3752916 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -18,6 +18,9 @@ module Types
field :iid, GraphQL::Types::String, null: false,
description: 'Internal ID of the pipeline.'
+ field :name, GraphQL::Types::String, null: true,
+ description: 'Name of the pipeline.'
+
field :sha, GraphQL::Types::String, null: true,
method: :sha,
description: "SHA of the pipeline's commit." do
@@ -61,7 +64,7 @@ module Types
description: "Timestamp of the pipeline's last activity."
field :started_at, Types::TimeType, null: true,
- description: 'Timestamp when the pipeline was started.'
+ description: 'Timestamp when the pipeline was started.'
field :finished_at, Types::TimeType, null: true,
description: "Timestamp of the pipeline's completion."
@@ -178,6 +181,24 @@ module Types
field :merge_request_event_type, Types::Ci::PipelineMergeRequestEventTypeEnum, null: true,
description: "Event type of the pipeline associated with a merge request."
+ field :total_jobs, GraphQL::Types::Int, null: false, method: :total_size, description: "The total number of jobs in the pipeline"
+
+ field :failure_reason, GraphQL::Types::String, null: true, description: "The reason why the pipeline failed"
+
+ field :triggered_by_path, GraphQL::Types::String, null: true, description: "The path that triggered this pipeline"
+
+ field :source, GraphQL::Types::String, null: true, method: :source, description: "The source of the pipeline"
+
+ field :child, GraphQL::Types::Boolean, null: false, method: :child?, description: "If the pipeline is a child or not"
+
+ field :latest, GraphQL::Types::Boolean, null: false, method: :latest?, calls_gitaly: true, description: "If the pipeline is the latest one or not"
+
+ field :ref_text, GraphQL::Types::String, null: false, method: :ref_text, description: "The reference text from the presenter", calls_gitaly: true
+
+ field :merge_request, Types::MergeRequestType, null: true, description: "The MR which the Pipeline is attached to"
+
+ field :stuck, GraphQL::Types::Boolean, method: :stuck?, null: false, description: "If the pipeline is stuck."
+
def commit
BatchLoader::GraphQL.wrap(object.commit)
end
diff --git a/app/graphql/types/clusters/agent_type.rb b/app/graphql/types/clusters/agent_type.rb
index c0989796141..04a4a719ba1 100644
--- a/app/graphql/types/clusters/agent_type.rb
+++ b/app/graphql/types/clusters/agent_type.rb
@@ -33,7 +33,7 @@ module Types
null: true,
authorize: :read_project
- field :tokens, Types::Clusters::AgentTokenType.connection_type,
+ field :tokens,
description: 'Tokens associated with the cluster agent.',
null: true,
resolver: ::Resolvers::Clusters::AgentTokensResolver
diff --git a/app/graphql/types/custom_emoji_type.rb b/app/graphql/types/custom_emoji_type.rb
index b02cd56e6df..08ac3172f2c 100644
--- a/app/graphql/types/custom_emoji_type.rb
+++ b/app/graphql/types/custom_emoji_type.rb
@@ -7,7 +7,7 @@ module Types
authorize :read_custom_emoji
- connection_type_class(Types::CountableConnectionType)
+ connection_type_class Types::CountableConnectionType
expose_permissions Types::PermissionTypes::CustomEmoji
diff --git a/app/graphql/types/error_tracking/sentry_error_collection_type.rb b/app/graphql/types/error_tracking/sentry_error_collection_type.rb
index 9790560929b..009da29d9c7 100644
--- a/app/graphql/types/error_tracking/sentry_error_collection_type.rb
+++ b/app/graphql/types/error_tracking/sentry_error_collection_type.rb
@@ -16,7 +16,8 @@ module Types
resolver: Resolvers::ErrorTracking::SentryErrorStackTraceResolver
field :errors,
description: "Collection of Sentry Errors.",
- resolver: Resolvers::ErrorTracking::SentryErrorsResolver
+ resolver: Resolvers::ErrorTracking::SentryErrorsResolver,
+ connection_extension: Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension
field :external_url,
GraphQL::Types::String,
null: true,
diff --git a/app/graphql/types/issues/negated_issue_filter_input_type.rb b/app/graphql/types/issues/negated_issue_filter_input_type.rb
index fc39efd2493..12f87509ade 100644
--- a/app/graphql/types/issues/negated_issue_filter_input_type.rb
+++ b/app/graphql/types/issues/negated_issue_filter_input_type.rb
@@ -11,7 +11,7 @@ module Types
argument :assignee_usernames, [GraphQL::Types::String],
required: false,
description: 'Usernames of users not assigned to the issue.'
- argument :author_username, GraphQL::Types::String,
+ argument :author_username, [GraphQL::Types::String],
required: false,
description: "Username of a user who didn't author the issue."
argument :iids, [GraphQL::Types::String],
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 4fd2b245de9..e6625e44508 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -102,6 +102,12 @@ module Types
calls_gitaly: true,
description: 'Detailed merge status of the merge request.'
+ field :mergeability_checks, [::Types::MergeRequests::MergeabilityCheckType],
+ null: false,
+ description: 'Status of all mergeability checks of the merge request.',
+ method: :all_mergeability_checks_results,
+ alpha: { milestone: '16.5' }
+
field :mergeable_discussions_state, GraphQL::Types::Boolean, null: true,
calls_gitaly: true,
description: 'Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged.'
diff --git a/app/graphql/types/merge_requests/mergeability_check_identifier_enum.rb b/app/graphql/types/merge_requests/mergeability_check_identifier_enum.rb
new file mode 100644
index 00000000000..ac25c98941c
--- /dev/null
+++ b/app/graphql/types/merge_requests/mergeability_check_identifier_enum.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Types
+ module MergeRequests
+ class MergeabilityCheckIdentifierEnum < BaseEnum
+ graphql_name 'MergeabilityCheckIdentifier'
+ description 'Representation of mergeability check identifier.'
+
+ MergeRequest.all_mergeability_checks.each do |check_class|
+ identifier = check_class.identifier.to_s
+
+ value identifier.upcase,
+ value: identifier,
+ description: "Mergeability check identifier is #{identifier}."
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/merge_requests/mergeability_check_status_enum.rb b/app/graphql/types/merge_requests/mergeability_check_status_enum.rb
new file mode 100644
index 00000000000..d3b95316b67
--- /dev/null
+++ b/app/graphql/types/merge_requests/mergeability_check_status_enum.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Types
+ module MergeRequests
+ class MergeabilityCheckStatusEnum < BaseEnum
+ graphql_name 'MergeabilityCheckStatus'
+ description 'Representation of whether a mergeability check passed, failed or is inactive.'
+
+ value 'SUCCESS',
+ value: 'success',
+ description: 'Mergeability check has passed.'
+
+ value 'FAILED',
+ value: 'failed',
+ description: 'Mergeability check has failed. The merge request cannot be merged.'
+
+ value 'INACTIVE',
+ value: 'inactive',
+ description: 'Mergeability check is disabled via settings.'
+ end
+ end
+end
diff --git a/app/graphql/types/merge_requests/mergeability_check_type.rb b/app/graphql/types/merge_requests/mergeability_check_type.rb
new file mode 100644
index 00000000000..4ef44c4b511
--- /dev/null
+++ b/app/graphql/types/merge_requests/mergeability_check_type.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Types
+ module MergeRequests
+ class MergeabilityCheckType < BaseObject # rubocop:disable Graphql/AuthorizeTypes
+ graphql_name 'MergeRequestMergeabilityCheck'
+ description 'Mergeability check of the merge request.'
+
+ field :identifier,
+ ::Types::MergeRequests::MergeabilityCheckIdentifierEnum,
+ null: false,
+ description: 'Identifier of the mergeability check.'
+
+ field :status,
+ ::Types::MergeRequests::MergeabilityCheckStatusEnum,
+ null: false,
+ description: 'Status of the mergeability check.'
+
+ def status
+ object.status.to_s
+ end
+
+ def identifier
+ object.identifier.to_s
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 445f26e2fcf..3af7140aed3 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -12,6 +12,7 @@ module Types
mount_mutation Mutations::Achievements::DeleteUserAchievement, alpha: { milestone: '16.1' }
mount_mutation Mutations::Achievements::Revoke, alpha: { milestone: '15.10' }
mount_mutation Mutations::Achievements::Update, alpha: { milestone: '15.11' }
+ mount_mutation Mutations::Achievements::UpdateUserAchievementPriorities, alpha: { milestone: '16.5' }
mount_mutation Mutations::Admin::SidekiqQueues::DeleteJobs
mount_mutation Mutations::AlertManagement::CreateAlertIssue
mount_mutation Mutations::AlertManagement::UpdateAlertStatus
@@ -169,6 +170,7 @@ module Types
mount_mutation Mutations::Packages::BulkDestroy,
extensions: [::Gitlab::Graphql::Limit::FieldCallCount => { limit: 1 }]
mount_mutation Mutations::Packages::DestroyFile
+ mount_mutation Mutations::Packages::Protection::Rule::Create, alpha: { milestone: '16.5' }
mount_mutation Mutations::Packages::DestroyFiles
mount_mutation Mutations::Packages::Cleanup::Policy::Update
mount_mutation Mutations::Echo
diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb
index 3420f16213f..85bda507ff7 100644
--- a/app/graphql/types/namespace_type.rb
+++ b/app/graphql/types/namespace_type.rb
@@ -4,7 +4,7 @@ module Types
class NamespaceType < BaseObject
graphql_name 'Namespace'
- authorize :read_namespace
+ authorize :read_namespace_via_membership
field :id, GraphQL::Types::ID, null: false,
description: 'ID of the namespace.'
diff --git a/app/graphql/types/notes/note_type.rb b/app/graphql/types/notes/note_type.rb
index e7e032c67c6..ffdaab0a5f6 100644
--- a/app/graphql/types/notes/note_type.rb
+++ b/app/graphql/types/notes/note_type.rb
@@ -5,6 +5,8 @@ module Types
class NoteType < BaseObject
graphql_name 'Note'
+ connection_type_class Types::CountableConnectionType
+
authorize :read_note
expose_permissions Types::PermissionTypes::Note
diff --git a/app/graphql/types/packages/helm/dependency_type.rb b/app/graphql/types/packages/helm/dependency_type.rb
index 72a47d0af51..6ba14145fb5 100644
--- a/app/graphql/types/packages/helm/dependency_type.rb
+++ b/app/graphql/types/packages/helm/dependency_type.rb
@@ -12,7 +12,7 @@ module Types
field :alias, GraphQL::Types::String, null: true, description: 'Alias of the dependency.', resolver_method: :resolve_alias
field :condition, GraphQL::Types::String, null: true, description: 'Condition of the dependency.'
field :enabled, GraphQL::Types::Boolean, null: true, description: 'Indicates the dependency is enabled.'
- field :import_values, [GraphQL::Types::JSON], null: true, description: 'Import-values of the dependency.', hash_key: "import-values" # rubocop:disable Graphql/JSONType
+ field :import_values, [GraphQL::Types::JSON], null: true, description: 'Import-values of the dependency.', hash_key: :'import-values' # rubocop:disable Graphql/JSONType
field :name, GraphQL::Types::String, null: true, description: 'Name of the dependency.'
field :repository, GraphQL::Types::String, null: true, description: 'Repository of the dependency.'
field :tags, [GraphQL::Types::String], null: true, description: 'Tags of the dependency.'
diff --git a/app/graphql/types/packages/helm/metadata_type.rb b/app/graphql/types/packages/helm/metadata_type.rb
index ccc5a3029cd..77062a48bc3 100644
--- a/app/graphql/types/packages/helm/metadata_type.rb
+++ b/app/graphql/types/packages/helm/metadata_type.rb
@@ -10,8 +10,8 @@ module Types
# Need to be synced with app/validators/json_schemas/helm_metadata.json
field :annotations, GraphQL::Types::JSON, null: true, description: 'Annotations for the chart.' # rubocop:disable Graphql/JSONType
- field :api_version, GraphQL::Types::String, null: false, description: 'API version of the chart.', hash_key: "apiVersion"
- field :app_version, GraphQL::Types::String, null: true, description: 'App version of the chart.', hash_key: "appVersion"
+ field :api_version, GraphQL::Types::String, null: false, description: 'API version of the chart.', hash_key: :apiVersion
+ field :app_version, GraphQL::Types::String, null: true, description: 'App version of the chart.', hash_key: :appVersion
field :condition, GraphQL::Types::String, null: true, description: 'Condition for the chart.'
field :dependencies, [Types::Packages::Helm::DependencyType], null: true, description: 'Dependencies of the chart.'
field :deprecated, GraphQL::Types::Boolean, null: true, description: 'Indicates if the chart is deprecated.'
@@ -19,12 +19,12 @@ module Types
field :home, GraphQL::Types::String, null: true, description: 'URL of the home page.'
field :icon, GraphQL::Types::String, null: true, description: 'URL to an SVG or PNG image for the chart.'
field :keywords, [GraphQL::Types::String], null: true, description: 'Keywords for the chart.'
- field :kube_version, GraphQL::Types::String, null: true, description: 'Kubernetes versions for the chart.', hash_key: "kubeVersion"
+ field :kube_version, GraphQL::Types::String, null: true, description: 'Kubernetes versions for the chart.', hash_key: :kubeVersion
field :maintainers, [Types::Packages::Helm::MaintainerType], null: true, description: 'Maintainers of the chart.'
field :name, GraphQL::Types::String, null: false, description: 'Name of the chart.'
field :sources, [GraphQL::Types::String], null: true, description: 'URLs of the source code for the chart.'
field :tags, GraphQL::Types::String, null: true, description: 'Tags for the chart.'
- field :type, GraphQL::Types::String, null: true, description: 'Type of the chart.', hash_key: "appVersion"
+ field :type, GraphQL::Types::String, null: true, description: 'Type of the chart.', hash_key: :appVersion
field :version, GraphQL::Types::String, null: false, description: 'Version of the chart.'
end
end
diff --git a/app/graphql/types/packages/package_base_type.rb b/app/graphql/types/packages/package_base_type.rb
index cc41169bcda..aa580d48709 100644
--- a/app/graphql/types/packages/package_base_type.rb
+++ b/app/graphql/types/packages/package_base_type.rb
@@ -23,6 +23,7 @@ module Types
field :package_type, Types::Packages::PackageTypeEnum, null: false, description: 'Package type.'
field :project, Types::ProjectType, null: false, description: 'Project where the package is stored.'
field :status, Types::Packages::PackageStatusEnum, null: false, description: 'Package status.'
+ field :status_message, GraphQL::Types::String, null: true, description: 'Status message.'
field :tags, Types::Packages::PackageTagType.connection_type, null: true, description: 'Package tags.'
field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.'
field :version, GraphQL::Types::String, null: true, description: 'Version string.'
diff --git a/app/graphql/types/packages/package_type.rb b/app/graphql/types/packages/package_type.rb
index f6586670c72..4c5b16cc41e 100644
--- a/app/graphql/types/packages/package_type.rb
+++ b/app/graphql/types/packages/package_type.rb
@@ -10,6 +10,7 @@ module Types
field :pipelines,
resolver: Resolvers::PackagePipelinesResolver,
+ connection_extension: Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension,
description: <<-DESC
Pipelines that built the package. Max page size #{Resolvers::PackagePipelinesResolver::MAX_PAGE_SIZE}.
DESC
diff --git a/app/graphql/types/packages/protection/rule_access_level_enum.rb b/app/graphql/types/packages/protection/rule_access_level_enum.rb
new file mode 100644
index 00000000000..098a3e48100
--- /dev/null
+++ b/app/graphql/types/packages/protection/rule_access_level_enum.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Protection
+ class RuleAccessLevelEnum < BaseEnum
+ graphql_name 'PackagesProtectionRuleAccessLevel'
+ description 'Access level of a package protection rule resource'
+
+ ::Packages::Protection::Rule.push_protected_up_to_access_levels.each_key do |access_level_key|
+ value access_level_key.upcase, value: access_level_key.to_s,
+ description: "#{access_level_key.capitalize} access."
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/protection/rule_package_type_enum.rb b/app/graphql/types/packages/protection/rule_package_type_enum.rb
new file mode 100644
index 00000000000..28e9df76adc
--- /dev/null
+++ b/app/graphql/types/packages/protection/rule_package_type_enum.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Protection
+ class RulePackageTypeEnum < BaseEnum
+ graphql_name 'PackagesProtectionRulePackageType'
+ description 'Package type of a package protection rule resource'
+
+ ::Packages::Protection::Rule.package_types.each_key do |package_type|
+ value package_type.upcase, value: package_type,
+ description: "Packages of the #{package_type} format"
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/protection/rule_type.rb b/app/graphql/types/packages/protection/rule_type.rb
new file mode 100644
index 00000000000..1e969d39ce2
--- /dev/null
+++ b/app/graphql/types/packages/protection/rule_type.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Protection
+ class RuleType < ::Types::BaseObject
+ graphql_name 'PackagesProtectionRule'
+ description 'A packages protection rule designed to protect packages ' \
+ 'from being pushed by users with a certain access level.'
+
+ authorize :admin_package
+
+ field :package_name_pattern,
+ GraphQL::Types::String,
+ null: false,
+ description:
+ 'Package name protected by the protection rule. For example `@my-scope/my-package-*`. ' \
+ 'Wildcard character `*` allowed.'
+
+ field :package_type,
+ Types::Packages::Protection::RulePackageTypeEnum,
+ null: false,
+ description: 'Package type protected by the protection rule. For example `NPM`.'
+
+ field :push_protected_up_to_access_level,
+ Types::Packages::Protection::RuleAccessLevelEnum,
+ null: false,
+ description:
+ 'Max GitLab access level unable to push a package. For example `DEVELOPER`, `MAINTAINER`, `OWNER`.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 2738d4da6c2..95caefc3825 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -311,6 +311,12 @@ module Types
null: true,
description: 'Packages cleanup policy for the project.'
+ field :packages_protection_rules,
+ Types::Packages::Protection::RuleType.connection_type,
+ null: true,
+ description: 'Packages protection rules for the project.',
+ resolver: Resolvers::ProjectPackagesProtectionRulesResolver
+
field :jobs,
type: Types::Ci::JobType.connection_type,
null: true,
@@ -524,7 +530,7 @@ module Types
complexity: 5,
resolver: ::Resolvers::TimelogResolver
- field :agent_configurations, ::Types::Kas::AgentConfigurationType.connection_type,
+ field :agent_configurations,
null: true,
description: 'Agent configurations defined by the project',
resolver: ::Resolvers::Kas::AgentConfigurationsResolver
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index d02b3e4136f..d185007f05b 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -109,7 +109,7 @@ module Types
null: true,
resolver: Resolvers::ProjectResolver,
description: "Find a project."
- field :projects, Types::ProjectType.connection_type,
+ field :projects,
null: true,
resolver: Resolvers::ProjectsResolver,
description: "Find projects visible to the current user."
@@ -154,7 +154,7 @@ module Types
null: true,
resolver: Resolvers::TopicsResolver,
description: "Find project topics."
- field :usage_trends_measurements, Types::Admin::Analytics::UsageTrends::MeasurementType.connection_type,
+ field :usage_trends_measurements,
null: true,
description: 'Get statistics on the instance.',
resolver: Resolvers::Admin::Analytics::UsageTrends::MeasurementsResolver
diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb
index 40eade3a4d1..a012b60b1c6 100644
--- a/app/graphql/types/repository_type.rb
+++ b/app/graphql/types/repository_type.rb
@@ -20,6 +20,7 @@ module Types
field :exists, GraphQL::Types::Boolean, null: false, method: :exists?, calls_gitaly: true,
description: 'Indicates a corresponding Git repository exists on disk.'
field :paginated_tree, Types::Tree::TreeType.connection_type, null: true, resolver: Resolvers::PaginatedTreeResolver, calls_gitaly: true,
+ connection_extension: Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension,
max_page_size: 100,
description: 'Paginated tree of the repository.'
field :root_ref, GraphQL::Types::String, null: true, calls_gitaly: true,
diff --git a/app/graphql/types/security/codequality_reports_comparer_type.rb b/app/graphql/types/security/codequality_reports_comparer_type.rb
index 3b0f790af81..8088bf84627 100644
--- a/app/graphql/types/security/codequality_reports_comparer_type.rb
+++ b/app/graphql/types/security/codequality_reports_comparer_type.rb
@@ -11,7 +11,7 @@ module Types
field :report,
type: CodequalityReportsComparer::ReportType,
null: true,
- hash_key: 'data',
+ hash_key: :data,
description: 'Compared codequality report.'
end
# rubocop: enable Graphql/AuthorizeTypes
diff --git a/app/graphql/types/snippet_type.rb b/app/graphql/types/snippet_type.rb
index 6e6d0edbe15..16f01979a43 100644
--- a/app/graphql/types/snippet_type.rb
+++ b/app/graphql/types/snippet_type.rb
@@ -45,6 +45,11 @@ module Types
description: 'Visibility Level of the snippet.',
null: false
+ field :hidden, GraphQL::Types::Boolean,
+ description: 'Indicates the snippet is hidden because the author has been banned.',
+ null: false,
+ method: :hidden_due_to_author_ban?
+
field :created_at, Types::TimeType,
description: 'Timestamp this snippet was created.',
null: false
diff --git a/app/graphql/types/todo_action_enum.rb b/app/graphql/types/todo_action_enum.rb
index 45b83ea1d64..63f96332eab 100644
--- a/app/graphql/types/todo_action_enum.rb
+++ b/app/graphql/types/todo_action_enum.rb
@@ -13,5 +13,6 @@ module Types
value 'review_requested', value: 9, description: 'Review was requested from the user.'
value 'member_access_requested', value: 10, description: 'Group or project access requested from the user.'
value 'review_submitted', value: 11, description: 'Merge request authored by the user received a review.'
+ value 'okr_checkin_requested', value: 12, description: 'An OKR assigned to the user requires an update.'
end
end
diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb
index 9e5f6810aca..47d486265b0 100644
--- a/app/graphql/types/user_interface.rb
+++ b/app/graphql/types/user_interface.rb
@@ -160,7 +160,7 @@ module Types
description: "Achievements for the user. " \
"Only returns for namespaces where the `achievements` feature flag is enabled.",
extras: [:lookahead],
- resolver: ::Resolvers::Achievements::UserAchievementsResolver
+ resolver: ::Resolvers::Achievements::UserAchievementsForUserResolver
field :bio,
type: ::GraphQL::Types::String,
diff --git a/app/graphql/types/user_state_enum.rb b/app/graphql/types/user_state_enum.rb
index de15fc19682..72503840bf5 100644
--- a/app/graphql/types/user_state_enum.rb
+++ b/app/graphql/types/user_state_enum.rb
@@ -5,8 +5,11 @@ module Types
graphql_name 'UserState'
description 'Possible states of a user'
- value 'active', 'User is active and is able to use the system.', value: 'active'
- value 'blocked', 'User has been blocked and is prevented from using the system.', value: 'blocked'
- value 'deactivated', 'User is no longer active and is unable to use the system.', value: 'deactivated'
+ value 'active', 'User is active and can use the system.', value: 'active'
+ value 'blocked', 'User has been blocked by an administrator and cannot use the system.', value: 'blocked'
+ value 'deactivated', 'User is no longer active and cannot use the system.', value: 'deactivated'
+ value 'banned', 'User is blocked, and their contributions are hidden.', value: 'banned'
+ value 'ldap_blocked', 'User has been blocked by the system.', value: 'ldap_blocked'
+ value 'blocked_pending_approval', 'User is blocked and pending approval.', value: 'blocked_pending_approval'
end
end
diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb
index 170f28103eb..87ca5fddf14 100644
--- a/app/graphql/types/user_type.rb
+++ b/app/graphql/types/user_type.rb
@@ -4,6 +4,9 @@ module Types
class UserType < ::Types::BaseObject
graphql_name 'UserCore'
description 'Core representation of a GitLab user.'
+
+ connection_type_class Types::CountableConnectionType
+
implements ::Types::UserInterface
authorize :read_user
diff --git a/app/graphql/types/work_item_type.rb b/app/graphql/types/work_item_type.rb
index 05798ba3d2f..103a1c0ec9b 100644
--- a/app/graphql/types/work_item_type.rb
+++ b/app/graphql/types/work_item_type.rb
@@ -58,6 +58,10 @@ module Types
field :work_item_type, Types::WorkItems::TypeType, null: false,
description: 'Type assigned to the work item.'
+ field :archived, GraphQL::Types::Boolean, null: false,
+ description: 'Whether the work item belongs to an archived project. Always false for group level work items.',
+ alpha: { milestone: '16.5' }
+
markdown_field :title_html, null: true
markdown_field :description_html, null: true
@@ -70,5 +74,11 @@ module Types
def create_note_email
object.creatable_note_email_address(context[:current_user])
end
+
+ def archived
+ return false if object.project.blank?
+
+ object.project.archived?
+ end
end
end
diff --git a/app/graphql/types/work_items/widgets/hierarchy_type.rb b/app/graphql/types/work_items/widgets/hierarchy_type.rb
index 4ec8ec84779..41c5af2ce63 100644
--- a/app/graphql/types/work_items/widgets/hierarchy_type.rb
+++ b/app/graphql/types/work_items/widgets/hierarchy_type.rb
@@ -20,6 +20,12 @@ module Types
null: true, complexity: 5,
description: 'Child work items.'
+ field :ancestors, ::Types::WorkItemType.connection_type,
+ null: true, complexity: 5,
+ description: 'Ancestors (parents) of the work item.',
+ extras: [:lookahead],
+ resolver: Resolvers::WorkItems::AncestorsResolver
+
field :has_children, GraphQL::Types::Boolean,
null: false, description: 'Indicates if the work item has children.'
diff --git a/app/graphql/types/work_items/widgets/notes_type.rb b/app/graphql/types/work_items/widgets/notes_type.rb
index 7da2777beee..199001649bb 100644
--- a/app/graphql/types/work_items/widgets/notes_type.rb
+++ b/app/graphql/types/work_items/widgets/notes_type.rb
@@ -18,7 +18,8 @@ module Types
field :discussions, Types::Notes::DiscussionType.connection_type,
null: true,
description: "Notes on this work item.",
- resolver: Resolvers::WorkItems::WorkItemDiscussionsResolver
+ resolver: Resolvers::WorkItems::WorkItemDiscussionsResolver,
+ connection_extension: Gitlab::Graphql::Extensions::ForwardOnlyExternallyPaginatedArrayExtension
end
# rubocop:enable Graphql/AuthorizeTypes
end
diff --git a/app/helpers/access_tokens_helper.rb b/app/helpers/access_tokens_helper.rb
index 44200e84afb..4bb6ae29151 100644
--- a/app/helpers/access_tokens_helper.rb
+++ b/app/helpers/access_tokens_helper.rb
@@ -5,7 +5,14 @@ module AccessTokensHelper
include ApplicationHelper
def scope_description(prefix)
- prefix == :project_access_token ? [:doorkeeper, :project_access_token_scope_desc] : [:doorkeeper, :scope_desc]
+ case prefix
+ when :project_access_token
+ [:doorkeeper, :project_access_token_scope_desc]
+ when :group_access_token
+ [:doorkeeper, :group_access_token_scope_desc]
+ else
+ [:doorkeeper, :scope_desc]
+ end
end
def tokens_app_data
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index 5beefbb943c..531ea08791c 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -80,7 +80,7 @@ module AppearancesHelper
add_gitlab_black_text = options[:add_gitlab_black_text] || false
if current_appearance&.header_logo?
- image_tag current_appearance.header_logo_path, class: 'brand-header-logo'
+ image_tag current_appearance.header_logo_path, class: 'brand-header-logo', alt: ''
elsif add_gitlab_white_text
render partial: 'shared/logo_with_white_text', formats: :svg
elsif add_gitlab_black_text
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index e3a630024d9..57937353955 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -123,7 +123,7 @@ module ApplicationHelper
{
page: body_data_page,
page_type_id: controller.params[:id],
- find_file: find_file_path,
+ find_file: find_file_path(ref_type: @ref_type),
group: @group&.path,
group_full_path: @group&.full_path
}.merge(project_data)
@@ -404,6 +404,10 @@ module ApplicationHelper
end
def add_page_specific_style(path, defer: true)
+ @already_added_styles ||= Set.new
+ return if @already_added_styles.include?(path)
+
+ @already_added_styles.add(path)
content_for :page_specific_styles do
if defer
stylesheet_link_tag_defer path
@@ -468,7 +472,7 @@ module ApplicationHelper
end
def hidden_resource_icon(resource, css_class: nil)
- issuable_title = _('This %{issuable} is hidden because its author has been banned')
+ issuable_title = _('This %{issuable} is hidden because its author has been banned.')
case resource
when Issue
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index ef91915ce38..58648a82487 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -282,6 +282,7 @@ module ApplicationSettingsHelper
:external_pipeline_validation_service_timeout,
:external_pipeline_validation_service_token,
:external_pipeline_validation_service_url,
+ :failed_login_attempts_unlock_period_in_minutes,
:first_day_of_week,
:floc_enabled,
:force_pages_access_control,
@@ -314,12 +315,14 @@ module ApplicationSettingsHelper
:jira_connect_application_key,
:jira_connect_public_key_storage_enabled,
:jira_connect_proxy_url,
+ :math_rendering_limits_enabled,
:max_artifacts_size,
:max_attachment_size,
+ :max_decompressed_archive_size,
:max_export_size,
:max_import_size,
:max_import_remote_file_size,
- :max_decompressed_archive_size,
+ :max_login_attempts,
:max_pages_size,
:max_pages_custom_domains_per_project,
:max_terraform_state_size_bytes,
@@ -507,7 +510,8 @@ module ApplicationSettingsHelper
:allow_account_deletion,
:gitlab_shell_operation_limit,
:namespace_aggregation_schedule_lease_duration_in_seconds,
- :ci_max_total_yaml_size_bytes
+ :ci_max_total_yaml_size_bytes,
+ :project_jobs_api_rate_limit
].tap do |settings|
next if Gitlab.com?
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index b7acc562be5..fc157df3891 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -182,26 +182,6 @@ module AuthHelper
current_user.allow_password_authentication_for_web? && !current_user.password_automatically_set?
end
- def google_tag_manager_enabled?
- return false unless Gitlab.com?
-
- if Feature.enabled?(:gtm_nonce, type: :ops)
- extra_config.has_key?('google_tag_manager_nonce_id') &&
- extra_config.google_tag_manager_nonce_id.present?
- else
- extra_config.has_key?('google_tag_manager_id') &&
- extra_config.google_tag_manager_id.present?
- end
- end
-
- def google_tag_manager_id
- return unless google_tag_manager_enabled?
-
- return extra_config.google_tag_manager_nonce_id if Feature.enabled?(:gtm_nonce, type: :ops)
-
- extra_config.google_tag_manager_id
- end
-
def auth_app_owner_text(owner)
return unless owner
diff --git a/app/helpers/blame_helper.rb b/app/helpers/blame_helper.rb
index 56d651a8b65..f00493ddf2a 100644
--- a/app/helpers/blame_helper.rb
+++ b/app/helpers/blame_helper.rb
@@ -1,13 +1,6 @@
# frozen_string_literal: true
module BlameHelper
- BODY_FONT_SIZE = "0.875rem"
- COMMIT_LINE_HEIGHT = 3 # 150% * 2 lines of text
- COMMIT_PADDING = "10px" # 5px from both top and bottom
- COMMIT_BLOCK_HEIGHT_EXP = "(#{BODY_FONT_SIZE} * #{COMMIT_LINE_HEIGHT}) + #{COMMIT_PADDING}"
- CODE_LINE_HEIGHT = 1.1875
- CODE_PADDING = "20px" # 10px from both top and bottom
-
def age_map_duration(blame_groups, project)
now = Time.zone.now
start_date = blame_groups.map { |blame_group| blame_group[:commit].committed_date }
@@ -32,14 +25,6 @@ module BlameHelper
end
end
- def intrinsic_row_css(line_count)
- # using rems here because the size of the row depends on the text size
- # which can be customized via user agent styles and browser preferences
- total_line_height_exp = "#{line_count * CODE_LINE_HEIGHT}rem + #{CODE_PADDING}"
- row_height_exp = line_count == 1 ? COMMIT_BLOCK_HEIGHT_EXP : total_line_height_exp
- "contain-intrinsic-size: 1px calc(#{row_height_exp})"
- end
-
def blame_pages_streaming_url(id, project)
namespace_project_blame_page_url(namespace_id: project.namespace, project_id: project, id: id, streaming: true)
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 6746e6549ec..0d5b8755a37 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -268,14 +268,6 @@ module BlobHelper
}.compact
end
- def edit_modify_file_fork_params(action)
- {
- to: request.fullpath,
- notice: edit_in_new_fork_notice_action(action),
- notice_now: edit_in_new_fork_notice_now
- }
- end
-
def edit_fork_button_tag(common_classes, project, label, params, action = 'edit')
fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: params)
@@ -322,11 +314,6 @@ module BlobHelper
@project.team.human_max_access(current_user&.id).try(:downcase)
end
- def editing_ci_config?
- @path.to_s.end_with?(Ci::Pipeline::CONFIG_EXTENSION) ||
- @path.to_s == @project.ci_config_path_or_default
- end
-
def vue_blob_app_data(project, blob, ref)
{
blob_path: blob.path,
diff --git a/app/helpers/ci/builds_helper.rb b/app/helpers/ci/builds_helper.rb
index 8a00c0f3eb0..001b316fcf2 100644
--- a/app/helpers/ci/builds_helper.rb
+++ b/app/helpers/ci/builds_helper.rb
@@ -9,15 +9,6 @@ module Ci
build_class.join(' ')
end
- def javascript_build_options
- {
- page_path: project_job_path(@project, @build),
- build_status: @build.status,
- build_stage: @build.stage_name,
- log_state: ''
- }
- end
-
def build_failed_issue_options
{
title: _("Job Failed #%{build_id}") % { build_id: @build.id },
diff --git a/app/helpers/ci/jobs_helper.rb b/app/helpers/ci/jobs_helper.rb
index 991b1f4d74e..216a8bc8fa1 100644
--- a/app/helpers/ci/jobs_helper.rb
+++ b/app/helpers/ci/jobs_helper.rb
@@ -5,15 +5,13 @@ module Ci
def jobs_data(project, build)
{
"endpoint" => project_job_path(project, build, format: :json),
+ "page_path" => project_job_path(project, build),
"project_path" => project.full_path,
"artifact_help_url" => help_page_path('user/gitlab_com/index.md', anchor: 'gitlab-cicd'),
"deployment_help_url" => help_page_path('user/project/clusters/deploy_to_cluster.md', anchor: 'troubleshooting'),
"runner_settings_url" => project_runners_path(build.project, anchor: 'js-runners-settings'),
- "page_path" => project_job_path(project, build),
"build_status" => build.status,
"build_stage" => build.stage_name,
- "log_state" => '',
- "build_options" => javascript_build_options,
"retry_outdated_job_docs_url" => help_page_path('ci/pipelines/settings', anchor: 'retry-outdated-jobs')
}
end
diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb
index a034e4331c0..510c7cd5fb6 100644
--- a/app/helpers/ci/pipelines_helper.rb
+++ b/app/helpers/ci/pipelines_helper.rb
@@ -78,10 +78,7 @@ module Ci
params: params.to_json,
artifacts_endpoint: downloadable_artifacts_project_pipeline_path(project, artifacts_endpoint_placeholder, format: :json),
artifacts_endpoint_placeholder: artifacts_endpoint_placeholder,
- pipeline_schedule_url: pipeline_schedules_path(project),
- empty_state_svg_path: image_path('illustrations/empty-state/empty-pipeline-md.svg'),
- error_state_svg_path: image_path('illustrations/pipelines_failed.svg'),
- no_pipelines_svg_path: image_path('illustrations/empty-state/empty-pipeline-md.svg'),
+ pipeline_schedules_path: pipeline_schedules_path(project),
can_create_pipeline: can?(current_user, :create_pipeline, project).to_s,
new_pipeline_path: can?(current_user, :create_pipeline, project) && new_project_pipeline_path(project),
ci_lint_path: can?(current_user, :create_pipeline, project) && project_ci_lint_path(project),
diff --git a/app/helpers/ci/status_helper.rb b/app/helpers/ci/status_helper.rb
index 5d526a6abb6..86f48b51f76 100644
--- a/app/helpers/ci/status_helper.rb
+++ b/app/helpers/ci/status_helper.rb
@@ -72,20 +72,19 @@ module Ci
status,
path,
tooltip_placement: tooltip_placement,
- icon_size: 24)
+ icon_size: 16)
end
def render_status_with_link(status, path = nil, type: _('pipeline'), tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16)
- klass = "ci-status-link #{ci_icon_class_for_status(status)} d-inline-flex #{cssclass}"
+ variant = badge_variant(status)
+ klass = "ci-status-link #{ci_icon_class_for_status(status)} d-inline-flex gl-line-height-1 #{cssclass}"
title = "#{type.titleize}: #{ci_label_for_status(status)}"
- data = { toggle: 'tooltip', placement: tooltip_placement, container: container }
+ data = { toggle: 'tooltip', placement: tooltip_placement, container: container, testid: 'ci-status-badge-legacy' }
+ badge_classes = 'gl-px-2 gl-ml-3'
- if path
- link_to ci_icon_for_status(status, size: icon_size), path,
- class: klass, title: title, data: data
- else
+ gl_badge_tag(variant: variant, size: :md, href: path, class: badge_classes, title: title, data: data) do
content_tag :span, ci_icon_for_status(status, size: icon_size),
- class: klass, title: title, data: data
+ class: klass
end
end
@@ -118,5 +117,24 @@ module Ci
translation = "CiStatusLabel|#{label}"
s_(translation)
end
+
+ def badge_variant(status)
+ variant = detailed_status?(status) ? status.group : status.dasherize
+
+ case variant
+ when 'success'
+ :success
+ when 'success-with-warnings', 'pending'
+ :warning
+ when 'failed'
+ :danger
+ when 'running'
+ :info
+ when 'canceled', 'manual'
+ :neutral
+ else
+ :muted
+ end
+ end
end
end
diff --git a/app/helpers/ci/triggers_helper.rb b/app/helpers/ci/triggers_helper.rb
index 01555b6e2cc..56b64d6049b 100644
--- a/app/helpers/ci/triggers_helper.rb
+++ b/app/helpers/ci/triggers_helper.rb
@@ -9,7 +9,7 @@ module Ci::TriggersHelper
end
end
- def service_trigger_url(service)
- "#{Settings.gitlab.url}/api/v4/projects/#{service.project_id}/services/#{service.to_param}/trigger"
+ def integration_trigger_url(integration)
+ "#{Settings.gitlab.url}/api/v4/projects/#{integration.project_id}/integrations/#{integration.to_param}/trigger"
end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 42871dcc56f..6ffef1b612b 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -42,7 +42,7 @@ module CommitsHelper
crumbs = content_tag(:li, class: 'breadcrumb-item') do
link_to(
@project.path,
- project_commits_path(@project, @ref)
+ project_commits_path(@project, @ref, ref_type: @ref_type)
)
end
@@ -56,7 +56,8 @@ module CommitsHelper
part,
project_commits_path(
@project,
- tree_join(@ref, parts[0..i].join('/'))
+ tree_join(@ref, parts[0..i].join('/')),
+ ref_type: @ref_type
)
)
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 9a78d4d9ad5..9031d0556da 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -138,24 +138,18 @@ module DiffHelper
def submodule_diff_compare_link(diff_file)
compare_url = submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository, diff_file)&.compare
+ return '' unless compare_url
- link = ""
+ link_text = [
+ _('Compare'),
+ ' ',
+ content_tag(:span, Commit.truncate_sha(diff_file.old_blob.id), class: 'commit-sha'),
+ '...',
+ content_tag(:span, Commit.truncate_sha(diff_file.blob.id), class: 'commit-sha')
+ ].join('').html_safe
- if compare_url
-
- link_text = [
- _('Compare'),
- ' ',
- content_tag(:span, Commit.truncate_sha(diff_file.old_blob.id), class: 'commit-sha'),
- '...',
- content_tag(:span, Commit.truncate_sha(diff_file.blob.id), class: 'commit-sha')
- ].join('').html_safe
-
- tooltip = _('Compare submodule commit revisions')
- link = content_tag(:span, link_to(link_text, compare_url, class: 'btn gl-button has-tooltip', title: tooltip), class: 'submodule-compare')
- end
-
- link
+ tooltip = _('Compare submodule commit revisions')
+ link_button_to link_text, compare_url, class: 'has-tooltip submodule-compare', title: tooltip
end
def diff_file_blob_raw_url(diff_file, only_path: false)
@@ -270,11 +264,6 @@ module DiffHelper
toggle_whitespace_link(url, options)
end
- def diff_merge_request_whitespace_link(project, merge_request, options)
- url = diffs_project_merge_request_path(project, merge_request, params_with_whitespace)
- toggle_whitespace_link(url, options)
- end
-
def diff_compare_whitespace_link(project, from, to, options)
url = project_compare_path(project, from, to, params_with_whitespace)
toggle_whitespace_link(url, options)
@@ -285,9 +274,8 @@ module DiffHelper
end
def toggle_whitespace_link(url, options)
- options[:class] = [*options[:class], 'btn gl-button btn-default'].join(' ')
toggle_text = hide_whitespace? ? s_('Diffs|Show whitespace changes') : s_('Diffs|Hide whitespace changes')
- link_to toggle_text, url, class: options[:class]
+ link_button_to toggle_text, url, class: options[:class]
end
def code_navigation_path(diffs)
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index ce18bedd25f..cc91b70758f 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -36,7 +36,7 @@ module DropdownsHelper
output << dropdown_filter(options[:placeholder])
end
- output << content_tag(:div, data: { qa_selector: "dropdown_list_content" }, class: "dropdown-content #{options[:content_class] if options.key?(:content_class)}") do
+ output << content_tag(:div, data: { testid: "dropdown-list-content" }, class: "dropdown-content #{options[:content_class] if options.key?(:content_class)}") do
capture(&block) if block && !options.key?(:footer_content)
end
diff --git a/app/helpers/groups/observability_helper.rb b/app/helpers/groups/observability_helper.rb
deleted file mode 100644
index 7661817da7b..00000000000
--- a/app/helpers/groups/observability_helper.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-module Groups
- module ObservabilityHelper
- ACTION_TO_PATH = {
- 'dashboards' => {
- path: '/',
- title: -> { s_('Observability|Dashboards') }
- },
- 'manage' => {
- path: '/dashboards',
- title: -> { s_('Observability|Manage dashboards') }
- },
- 'explore' => {
- path: '/explore',
- title: -> { s_('Observability|Explore telemetry data') }
- },
- 'datasources' => {
- path: '/datasources',
- title: -> { s_('Observability|Data sources') }
- }
- }.freeze
-
- def observability_iframe_src(group)
- Gitlab::Observability.build_full_url(group, params[:observability_path],
- observability_config_for(params).fetch(:path))
- end
-
- def observability_page_title
- observability_config_for(params).fetch(:title).call
- end
-
- private
-
- def observability_config_for(params)
- ACTION_TO_PATH.fetch(params[:action], ACTION_TO_PATH['dashboards'])
- end
- end
-end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index e552b01f7ba..f48157cb65a 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -174,7 +174,9 @@ module GroupsHelper
end
def show_group_readme?(group)
- group.group_readme
+ return false unless group.group_readme
+
+ can?(current_user, :read_code, group.readme_project)
end
def group_settings_readme_app_data(group)
@@ -186,7 +188,7 @@ module GroupsHelper
}
end
- def enabled_git_access_protocol_options_for_group
+ def enabled_git_access_protocol_options_for_group(_)
case ::Gitlab::CurrentSettings.enabled_git_access_protocol
when nil, ""
[[_("Both SSH and HTTP(S)"), "all"], [_("Only SSH"), "ssh"], [_("Only HTTP(S)"), "http"]]
@@ -197,6 +199,14 @@ module GroupsHelper
end
end
+ def new_custom_emoji_path(group)
+ return unless Feature.enabled?(:custom_emoji)
+ return unless group
+ return unless can?(current_user, :create_custom_emoji, group)
+
+ new_group_custom_emoji_path(group)
+ end
+
private
def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false)
diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb
index 696790b9dcb..2582d6fcc34 100644
--- a/app/helpers/ide_helper.rb
+++ b/app/helpers/ide_helper.rb
@@ -7,10 +7,7 @@ module IdeHelper
'use-new-web-ide' => use_new_web_ide?.to_s,
'new-web-ide-help-page-path' => help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'),
'sign-in-path' => new_session_path(current_user),
- 'user-preferences-path' => profile_preferences_path,
- 'editor-font-src-url' => font_url('gitlab-mono/GitLabMono.woff2'),
- 'editor-font-family' => 'GitLab Mono',
- 'editor-font-format' => 'woff2'
+ 'user-preferences-path' => profile_preferences_path
}.merge(use_new_web_ide? ? new_ide_data(project: project) : legacy_ide_data(project: project))
return base_data unless project
@@ -29,6 +26,28 @@ module IdeHelper
private
+ def new_ide_fonts
+ {
+ fallback_font_family: 'monospace',
+ font_faces: [{
+ family: 'GitLab Mono',
+ display: 'block',
+ src: [{
+ url: font_url('gitlab-mono/GitLabMono.woff2'),
+ format: 'woff2'
+ }]
+ }, {
+ family: 'GitLab Mono',
+ display: 'block',
+ style: 'italic',
+ src: [{
+ url: font_url('gitlab-mono/GitLabMono-Italic.woff2'),
+ format: 'woff2'
+ }]
+ }]
+ }
+ end
+
def new_ide_code_suggestions_data
{}
end
@@ -38,7 +57,8 @@ module IdeHelper
'project-path' => project&.path_with_namespace,
'csp-nonce' => content_security_policy_nonce,
# We will replace these placeholders in the FE
- 'ide-remote-path' => ide_remote_path(remote_host: ':remote_host', remote_path: ':remote_path')
+ 'ide-remote-path' => ide_remote_path(remote_host: ':remote_host', remote_path: ':remote_path'),
+ 'editor-font' => new_ide_fonts.to_json
}.merge(new_ide_code_suggestions_data)
end
diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb
index a88be976337..510561ec614 100644
--- a/app/helpers/integrations_helper.rb
+++ b/app/helpers/integrations_helper.rb
@@ -30,10 +30,6 @@ module IntegrationsHelper
_("Alert")
when "incident"
_("Incident")
- when "group_mention"
- _("Group mention in public")
- when "group_confidential_mention"
- _("Group mention in private")
end
end
# rubocop:enable Metrics/CyclomaticComplexity
@@ -295,10 +291,6 @@ module IntegrationsHelper
s_("ProjectService|Trigger event when a new, unique alert is recorded.")
when "incident", "incident_events"
s_("ProjectService|Trigger event when an incident is created.")
- when "group_mention"
- s_("ProjectService|Trigger event when a group is mentioned in a public context.")
- when "group_confidential_mention"
- s_("ProjectService|Trigger event when a group is mentioned in a confidential context.")
when "build_events"
s_("ProjectService|Trigger event when a build is created.")
when "archive_trace_events"
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 7f948db2f71..f2f20fa1b50 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -177,7 +177,6 @@ module IssuablesHelper
markdownPreviewPath: preview_markdown_path(parent, target_type: issuable.model_name, target_id: issuable.iid),
markdownDocsPath: help_page_path('user/markdown'),
lockVersion: issuable.lock_version,
- state: issuable.state,
issuableTemplateNamesPath: template_names_path(parent, issuable),
initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title,
@@ -231,22 +230,6 @@ module IssuablesHelper
end
end
- def state_name_with_icon(issuable)
- if issuable.is_a?(MergeRequest)
- if issuable.open?
- [_("Open"), "merge-request-open"]
- elsif issuable.merged?
- [_("Merged"), "merge"]
- else
- [_("Closed"), "merge-request-close"]
- end
- elsif issuable.open?
- [_("Open"), "issues"]
- else
- [_("Closed"), "issue-closed"]
- end
- end
-
def issuable_type_selector_data(issuable)
{
selected_type: issuable.issue_type,
@@ -374,7 +357,6 @@ module IssuablesHelper
issuableId: issuable.id,
issueType: issuable.issue_type,
isHidden: issue_hidden?(issuable),
- sentryIssueIdentifier: SentryIssue.find_by(issue: issuable)&.sentry_issue_identifier, # rubocop:disable CodeReuse/ActiveRecord
zoomMeetingUrl: ZoomMeeting.canonical_meeting_url(issuable),
**incident_only_initial_data(issuable),
**issue_header_data(issuable),
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 06eb3fcc233..131cd7cd969 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -78,10 +78,6 @@ module MergeRequestsHelper
.execute(include_routes: true)
end
- def merge_request_button_visibility(merge_request, closed)
- return 'hidden' if merge_request_button_hidden?(merge_request, closed)
- end
-
def merge_request_button_hidden?(merge_request, closed)
merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_or_merged_without_fork?
end
@@ -150,12 +146,6 @@ module MergeRequestsHelper
end
end
- def toggle_draft_merge_request_path(issuable)
- wip_event = issuable.draft? ? 'ready' : 'draft'
-
- issuable_path(issuable, { merge_request: { wip_event: wip_event } })
- end
-
def user_merge_requests_counts
@user_merge_requests_counts ||= begin
assigned_count = assigned_issuables_count(:merge_requests)
@@ -185,6 +175,10 @@ module MergeRequestsHelper
Feature.enabled?(:moved_mr_sidebar, @project)
end
+ def notifications_todos_buttons_enabled?
+ Feature.enabled?(:notifications_todos_buttons, @project)
+ end
+
def diffs_tab_pane_data(project, merge_request, params)
{
"is-locked": merge_request.discussion_locked?,
@@ -207,7 +201,8 @@ module MergeRequestsHelper
source_project_default_url: merge_request.source_project && default_url_to_repo(merge_request.source_project),
source_project_full_path: merge_request.source_project&.full_path,
is_forked: project.forked?.to_s,
- new_comment_template_path: profile_comment_templates_path
+ new_comment_template_path: profile_comment_templates_path,
+ iid: merge_request.iid
}
end
@@ -273,14 +268,14 @@ module MergeRequestsHelper
''
end
- link_to branch, branch_path, title: branch_title, class: 'gl-text-blue-500! gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mx-2'
+ link_to branch, branch_path, title: branch_title, class: 'ref-container gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mx-2'
end
def merge_request_header(project, merge_request)
link_to_author = link_to_member(project, merge_request.author, size: 24, extra_class: 'gl-font-weight-bold gl-mr-2', avatar: false)
copy_button = clipboard_button(text: merge_request.source_branch, title: _('Copy branch name'), class: 'gl-display-none! gl-md-display-inline-block! js-source-branch-copy')
- target_branch = link_to merge_request.target_branch, project_tree_path(merge_request.target_project, merge_request.target_branch), title: merge_request.target_branch, class: 'gl-text-blue-500! gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mx-2'
+ target_branch = link_to merge_request.target_branch, project_tree_path(merge_request.target_project, merge_request.target_branch), title: merge_request.target_branch, class: 'ref-container gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mx-2'
_('%{author} requested to merge %{source_branch} %{copy_button} into %{target_branch} %{created_at}').html_safe % { author: link_to_author.html_safe, source_branch: merge_request_source_branch(merge_request).html_safe, copy_button: copy_button.html_safe, target_branch: target_branch.html_safe, created_at: time_ago_with_tooltip(merge_request.created_at, html_class: 'gl-display-inline-block').html_safe }
end
diff --git a/app/helpers/organizations/organization_helper.rb b/app/helpers/organizations/organization_helper.rb
index 6b5c4342c5c..5d89bb93000 100644
--- a/app/helpers/organizations/organization_helper.rb
+++ b/app/helpers/organizations/organization_helper.rb
@@ -16,10 +16,24 @@ module Organizations
}.merge(shared_groups_and_projects_app_data).to_json
end
+ def organization_new_app_data
+ {
+ organizations_path: organizations_path,
+ root_url: root_url
+ }.to_json
+ end
+
def organization_groups_and_projects_app_data
shared_groups_and_projects_app_data.to_json
end
+ def organization_index_app_data
+ {
+ new_organization_url: new_organization_path,
+ organizations_empty_state_svg_path: image_path('illustrations/empty-state/empty-organizations-md.svg')
+ }
+ end
+
private
def shared_groups_and_projects_app_data
diff --git a/app/helpers/projects/ml/experiments_helper.rb b/app/helpers/projects/ml/experiments_helper.rb
index d5b2c3cd36a..6c7b6eb6fbc 100644
--- a/app/helpers/projects/ml/experiments_helper.rb
+++ b/app/helpers/projects/ml/experiments_helper.rb
@@ -14,17 +14,17 @@ module Projects
Gitlab::Json.generate(data)
end
- def candidates_table_items(candidates, user)
+ def candidates_table_items(candidates, current_user)
items = candidates.map do |candidate|
{
**candidate.params.to_h { |p| [p.name, p.value] },
**candidate.latest_metrics.to_h { |m| [m.name, number_with_precision(m.value, precision: 4)] },
- ci_job: job_info(candidate, user),
+ ci_job: job_info(candidate, current_user),
artifact: link_to_artifact(candidate),
details: link_to_details(candidate),
name: candidate.name,
created_at: candidate.created_at,
- user: user_info(candidate)
+ user: user_info(candidate, current_user)
}
end
@@ -87,8 +87,13 @@ module Projects
project_ml_experiment_path(project, experiment.iid)
end
- def user_info(candidate)
- user = candidate.user
+ def user_info(candidate, current_user)
+ user =
+ if candidate.from_ci?
+ candidate.ci_build.user if can?(current_user, :read_build, candidate.ci_build)
+ else
+ candidate.user
+ end
return unless user.present?
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index e45b38f2266..04fe0a4450c 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -195,27 +195,6 @@ module ProjectsHelper
{ branch_name: tag.strong(truncate(sanitize(branch_name))), link_to_autodeploy_doc: link_to_autodeploy_doc }
end
- def project_list_cache_key(project, pipeline_status: true)
- key = [
- project.star_count,
- project.route.cache_key,
- project.cache_key,
- project.last_activity_date,
- controller.controller_name,
- controller.action_name,
- Gitlab::CurrentSettings.cache_key,
- "cross-project:#{can?(current_user, :read_cross_project)}",
- max_project_member_access_cache_key(project),
- pipeline_status,
- Gitlab::I18n.locale,
- 'v2.6'
- ]
-
- key << pipeline_status_cache_key(project.pipeline_status) if pipeline_status && project.pipeline_status.has_status?
-
- key
- end
-
def load_pipeline_status(projects)
Gitlab::Cache::Ci::ProjectPipelineStatus
.load_in_batch_for_projects(projects)
@@ -252,8 +231,6 @@ module ProjectsHelper
end
def show_mobile_devops_project_promo?(project)
- return false unless ::Feature.enabled?(:mobile_devops_projects_promo, project)
-
return false unless (project.project_setting.target_platforms & ::ProjectSetting::ALLOWED_TARGET_PLATFORMS).any?
cookies["hide_mobile_devops_promo_#{project.id}".to_sym].blank?
@@ -373,18 +350,6 @@ module ProjectsHelper
false
end
- def grafana_integration_url
- @project.grafana_integration&.grafana_url
- end
-
- def grafana_integration_masked_token
- @project.grafana_integration&.masked_token
- end
-
- def grafana_integration_enabled?
- @project.grafana_integration&.enabled?
- end
-
def project_license_name(project)
key = "project:#{project.id}:license_name"
@@ -479,10 +444,6 @@ module ProjectsHelper
configure_oauth_import_message('Bitbucket', help_page_path("integration/bitbucket"))
end
- def import_from_gitlab_message
- configure_oauth_import_message('GitLab.com', help_page_path("integration/gitlab"))
- end
-
def show_inactive_project_deletion_banner?(project)
return false unless project.present? && project.saved?
return false unless delete_inactive_projects?
@@ -674,30 +635,6 @@ module ProjectsHelper
end
end
- def project_last_activity(project)
- if project.last_activity_at
- time_ago_with_tooltip(project.last_activity_at, placement: 'bottom', html_class: 'last_activity_time_ago')
- else
- s_("ProjectLastActivity|Never")
- end
- end
-
- def project_status_css_class(status)
- case status
- when "started"
- "table-active"
- when "failed"
- "table-danger"
- when "finished"
- "table-success"
- end
- end
-
- def readme_cache_key
- sha = @project.commit.try(:sha) || 'nil'
- [@project.full_path, sha, "readme"].join('-')
- end
-
def current_ref
@ref || @repository.try(:root_ref)
end
@@ -756,13 +693,13 @@ module ProjectsHelper
end
end
- def find_file_path
+ def find_file_path(ref_type: nil)
return unless @project && !@project.empty_repo?
return unless can?(current_user, :read_code, @project)
ref = @ref || @project.repository.root_ref
- project_find_file_path(@project, ref)
+ project_find_file_path(@project, ref, ref_type: ref_type)
end
def can_show_last_commit_in_list?(project)
diff --git a/app/helpers/registrations_helper.rb b/app/helpers/registrations_helper.rb
index c2c142bca4d..363c38ffe59 100644
--- a/app/helpers/registrations_helper.rb
+++ b/app/helpers/registrations_helper.rb
@@ -16,6 +16,9 @@ module RegistrationsHelper
end
# overridden in EE
+ def oauth_tracking_label; end
+
+ # overridden in EE
def register_omniauth_params(_local_assigns)
{}
end
diff --git a/app/helpers/resource_events/abuse_report_events_helper.rb b/app/helpers/resource_events/abuse_report_events_helper.rb
index 8adbc891184..207ec73454b 100644
--- a/app/helpers/resource_events/abuse_report_events_helper.rb
+++ b/app/helpers/resource_events/abuse_report_events_helper.rb
@@ -10,6 +10,8 @@ module ResourceEvents
s_('AbuseReportEvent|Successfully blocked the user')
when 'delete_user'
s_('AbuseReportEvent|Successfully scheduled the user for deletion')
+ when 'trust_user'
+ s_('AbuseReportEvent|Successfully trusted the user')
when 'close_report'
s_('AbuseReportEvent|Successfully closed the report')
when 'ban_user_and_close_report'
@@ -18,6 +20,8 @@ module ResourceEvents
s_('AbuseReportEvent|Successfully blocked the user and closed the report')
when 'delete_user_and_close_report'
s_('AbuseReportEvent|Successfully scheduled the user for deletion and closed the report')
+ when 'trust_user_and_close_report'
+ s_('AbuseReportEvent|Successfully trusted the user and closed the report')
end
end
end
diff --git a/app/helpers/routing/projects_helper.rb b/app/helpers/routing/projects_helper.rb
index 06de9022be4..1f00d283a30 100644
--- a/app/helpers/routing/projects_helper.rb
+++ b/app/helpers/routing/projects_helper.rb
@@ -43,10 +43,12 @@ module Routing
end
def work_item_url(entity, *args)
- if entity.project.present?
- project_work_items_url(entity.project, entity.iid, *args)
+ return group_work_item_url(entity.namespace, entity.iid, *args) unless entity.project.present?
+
+ if use_issue_path?(entity)
+ project_issue_url(entity.project, entity.iid, *args)
else
- group_work_item_url(entity.namespace, entity.iid, *args)
+ project_work_item_url(entity.project, entity.iid, *args)
end
end
@@ -97,6 +99,10 @@ module Routing
issue.issue_type == 'task'
end
+
+ def use_issue_path?(work_item)
+ work_item.issue_type == 'issue'
+ end
end
end
diff --git a/app/helpers/safe_format_helper.rb b/app/helpers/safe_format_helper.rb
index 71bfc9ecb40..9f8c5082c26 100644
--- a/app/helpers/safe_format_helper.rb
+++ b/app/helpers/safe_format_helper.rb
@@ -25,8 +25,8 @@ module SafeFormatHelper
# Use `Kernel.format` to avoid conflicts with ViewComponent's `format`.
Kernel.format(
- html_escape_once(format),
- args.transform_values { |value| html_escape(value) }
+ ERB::Util.html_escape_once(format),
+ args.transform_values { |value| ERB::Util.html_escape(value) }
).html_safe
end
diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb
deleted file mode 100644
index 21aa82aff1c..00000000000
--- a/app/helpers/sidekiq_helper.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-module SidekiqHelper
- SIDEKIQ_PS_REGEXP = %r{\A
- (?<pid>\d+)\s+
- (?<cpu>[\d\.,]+)\s+
- (?<mem>[\d\.,]+)\s+
- (?<state>[DIEKNRSTVWXZLpsl\+<>/\d]+)\s+
- (?<start>.+?)\s+
- (?<command>(?:ruby\d+:\s+)?sidekiq.*\].*)
- \z}x
-
- def parse_sidekiq_ps(line)
- match = line.strip.match(SIDEKIQ_PS_REGEXP)
- match ? match[1..6] : Array.new(6, '?')
- end
-end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 1405bc7be37..94445564c22 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -64,15 +64,6 @@ module SortingHelper
options
end
- def forks_sort_options_hash
- {
- sort_value_recently_created => sort_title_created_date,
- sort_value_oldest_created => sort_title_created_date,
- sort_value_latest_activity => sort_title_latest_activity,
- sort_value_oldest_activity => sort_title_latest_activity
- }
- end
-
def forks_reverse_sort_options_hash
{
sort_value_recently_created => sort_value_oldest_created,
@@ -93,12 +84,6 @@ module SortingHelper
}
end
- def subgroups_sort_options_hash
- groups_sort_options_hash.merge(
- sort_value_stars_desc => sort_title_most_stars
- )
- end
-
def admin_groups_sort_options_hash
groups_sort_options_hash.merge(
sort_value_largest_group => sort_title_largest_group
@@ -199,19 +184,6 @@ module SortingHelper
}.merge(issuable_sort_option_overrides)
end
- def audit_logs_sort_order_hash
- {
- sort_value_recently_created => sort_title_recently_created,
- sort_value_oldest_created => sort_title_oldest_created
- }
- end
-
- def issuable_sort_option_title(sort_value)
- sort_value = issuable_sort_option_overrides[sort_value] || sort_value
-
- sort_options_hash[sort_value]
- end
-
def issuable_sort_options(viewing_issues, viewing_merge_requests)
options = [
{ value: sort_value_priority, text: sort_title_priority, href: page_filter_path(sort: sort_value_priority) },
@@ -321,17 +293,6 @@ module SortingHelper
}
end
- def packages_sort_option_title(sort_value)
- packages_sort_options_hash[sort_value] || sort_title_created_date
- end
-
- def packages_sort_direction_button(sort_value)
- reverse_sort = packages_reverse_sort_order_hash[sort_value]
- url = package_sort_path(sort: reverse_sort)
-
- sort_direction_button(url, reverse_sort, sort_value)
- end
-
def forks_sort_direction_button(sort_value, without = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id])
reverse_sort = forks_reverse_sort_options_hash[sort_value]
url = page_filter_path(sort: reverse_sort, without: without)
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 0d885621b6c..d053aeb7bfe 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -31,6 +31,9 @@ module TodosHelper
s_("Todos|has requested access to %{what} %{which}"), what: _(todo.member_access_type), which: _(todo.target.name)
)
when Todo::REVIEW_SUBMITTED then s_('Todos|reviewed your merge request')
+ when Todo::OKR_CHECKIN_REQUESTED then format(
+ s_("Todos|requested an OKR update for %{what}"), what: todo.target.title
+ )
end
end
@@ -163,6 +166,10 @@ module TodosHelper
todos_filter_params.values.none?
end
+ def todos_has_filtered_results?
+ params[:group_id] || params[:project_id] || params[:author_id] || params[:type] || params[:action_id]
+ end
+
def no_todos_messages
[
s_('Todos|Good job! Looks like you don\'t have anything left on your To-Do List'),
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 30f8f6fdfe5..a892b6e6ac6 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -67,6 +67,8 @@ module UsersHelper
project_ids = projects.pluck(:id)
# rubocop: enable CodeReuse/ActiveRecord
+ preload_project_associations(projects)
+
Preloaders::UserMaxAccessLevelInProjectsPreloader
.new(project_ids, current_user)
.execute
@@ -371,6 +373,10 @@ module UsersHelper
def saved_replies_enabled?
Feature.enabled?(:saved_replies, current_user)
end
+
+ def preload_project_associations(_)
+ # Overridden in EE
+ end
end
UsersHelper.prepend_mod_with('UsersHelper')
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
index b2b8ca2a120..bd63381e9d1 100644
--- a/app/helpers/wiki_helper.rb
+++ b/app/helpers/wiki_helper.rb
@@ -22,9 +22,7 @@ module WikiHelper
end
def wiki_sidebar_toggle_button
- content_tag :button, class: 'gl-button btn btn-default btn-icon sidebar-toggle js-sidebar-wiki-toggle', role: 'button', type: 'button' do
- sprite_icon('chevron-double-lg-left')
- end
+ render Pajamas::ButtonComponent.new(icon: 'chevron-double-lg-left', button_options: { class: 'sidebar-toggle js-sidebar-wiki-toggle' })
end
# Produces a pure text breadcrumb for a given page.
@@ -60,17 +58,14 @@ module WikiHelper
end
def wiki_sort_controls(wiki, direction)
- link_class = 'gl-button btn btn-default btn-icon has-tooltip reverse-sort-btn rspec-reverse-sort'
+ link_class = 'has-tooltip reverse-sort-btn rspec-reverse-sort'
reversed_direction = direction == 'desc' ? 'asc' : 'desc'
icon_class = direction == 'desc' ? 'highest' : 'lowest'
title = direction == 'desc' ? _('Sort direction: Descending') : _('Sort direction: Ascending')
link_options = { action: :pages, direction: reversed_direction }
- link_to(wiki_path(wiki, **link_options),
- type: 'button', class: link_class, title: title) do
- sprite_icon("sort-#{icon_class}")
- end
+ render Pajamas::ButtonComponent.new(href: wiki_path(wiki, **link_options), icon: "sort-#{icon_class}", button_options: { class: link_class, title: title })
end
def wiki_sort_title(key)
diff --git a/app/helpers/work_items_helper.rb b/app/helpers/work_items_helper.rb
index 1969c98de8b..d2e3d41377a 100644
--- a/app/helpers/work_items_helper.rb
+++ b/app/helpers/work_items_helper.rb
@@ -1,10 +1,11 @@
# frozen_string_literal: true
module WorkItemsHelper
- def work_items_index_data(project)
+ def work_items_index_data(resource_parent)
{
- full_path: project.full_path,
- issues_list_path: project_issues_path(project),
+ full_path: resource_parent.full_path,
+ issues_list_path:
+ resource_parent.is_a?(Group) ? issues_group_path(resource_parent) : project_issues_path(resource_parent),
register_path: new_user_registration_path(redirect_to_referer: 'yes'),
sign_in_path: new_session_path(:user, redirect_to_referer: 'yes'),
new_comment_template_path: profile_comment_templates_path,
diff --git a/app/mailers/emails/in_product_marketing.rb b/app/mailers/emails/in_product_marketing.rb
deleted file mode 100644
index 92743dc1926..00000000000
--- a/app/mailers/emails/in_product_marketing.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-module Emails
- module InProductMarketing
- FROM_ADDRESS = 'GitLab <team@gitlab.com>'
- CUSTOM_HEADERS = {
- from: FROM_ADDRESS,
- reply_to: FROM_ADDRESS,
- 'X-Mailgun-Track' => 'yes',
- 'X-Mailgun-Track-Clicks' => 'yes',
- 'X-Mailgun-Track-Opens' => 'yes',
- 'X-Mailgun-Tag' => 'marketing'
- }.freeze
-
- def build_ios_app_guide_email(recipient_email)
- @message = ::Gitlab::Email::Message::BuildIosAppGuide.new
-
- mail_to(to: recipient_email, subject: @message.subject_line)
- end
-
- private
-
- def mail_to(to:, subject:)
- custom_headers = Gitlab.com? ? CUSTOM_HEADERS : {}
- mail_with_locale(to: to, subject: subject, **custom_headers) do |format|
- format.html do
- @message.format = :html
-
- render layout: 'in_product_marketing_mailer'
- end
-
- format.text do
- @message.format = :text
-
- render layout: nil
- end
- end
- end
- end
-end
-
-Emails::InProductMarketing.prepend_mod
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index a9e1efbdd5d..2be4cdf734a 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -65,11 +65,13 @@ module Emails
@token_names = token_names
@days_to_expire = PersonalAccessToken::DAYS_TO_EXPIRE
@resource = resource
- @target_url = if resource.is_a?(Group)
- group_settings_access_tokens_url(resource)
- else
- project_settings_access_tokens_url(resource)
- end
+ if resource.is_a?(Group)
+ @target_url = group_settings_access_tokens_url(resource)
+ @reason_text = _('You are receiving this email because you are an Owner of the Group.')
+ else
+ @target_url = project_settings_access_tokens_url(resource)
+ @reason_text = _('You are receiving this email because you are a Maintainer of the Project.')
+ end
mail_with_locale(
to: recipient.notification_email_or_default,
@@ -100,7 +102,7 @@ module Emails
@target_url = profile_personal_access_tokens_url
@days_to_expire = PersonalAccessToken::DAYS_TO_EXPIRE
- mail_with_locale(to: @user.notification_email_or_default, subject: subject(_("Your personal access tokens will expire in %{days_to_expire} days or less") % { days_to_expire: @days_to_expire }))
+ email_with_layout(to: @user.notification_email_or_default, subject: subject(_("Your personal access tokens will expire in %{days_to_expire} days or less") % { days_to_expire: @days_to_expire }))
end
def access_token_expired_email(user, token_names = [])
@@ -121,7 +123,7 @@ module Emails
@target_url = profile_personal_access_tokens_url
@source = source
- mail_with_locale(to: @user.notification_email_or_default, subject: subject(_("A personal access token has been revoked")))
+ email_with_layout(to: @user.notification_email_or_default, subject: subject(_("Your personal access token has been revoked")))
end
def ssh_key_expired_email(user, fingerprints)
@@ -170,7 +172,7 @@ module Emails
@user = user
- mail_with_locale(to: @user.notification_email_or_default, subject: subject(_("Two-factor authentication disabled")))
+ email_with_layout(to: @user.notification_email_or_default, subject: subject(_("Two-factor authentication disabled")))
end
def new_email_address_added_email(user, email)
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 77d32a55941..2f90579a5c2 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -22,7 +22,6 @@ class Notify < ApplicationMailer
include Emails::Groups
include Emails::Reviews
include Emails::ServiceDesk
- include Emails::InProductMarketing
include Emails::AdminNotification
include Emails::IdentityVerification
include Emails::Imports
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index 638df56b770..6548b6d1088 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -81,6 +81,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.access_token_revoked_email(user, 'token_name').message
end
+ def access_token_about_to_expire_email
+ Notify.access_token_about_to_expire_email(user, ['%w', '%w']).message
+ end
+
def ssh_key_expired_email
fingerprints = []
Notify.ssh_key_expired_email(user, fingerprints).message
@@ -222,6 +226,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.two_factor_otp_attempt_failed_email(user, '127.0.0.1').message
end
+ def disabled_two_factor_email
+ Notify.disabled_two_factor_email(user).message
+ end
+
def new_email_address_added_email
Notify.new_email_address_added_email(user, 'someone@gitlab.com').message
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index d8510524c1f..b8433191d84 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -166,3 +166,5 @@ class Ability
end
end
end
+
+Ability.prepend_mod_with('AbilityPrepend')
diff --git a/app/models/abuse/reports/user_mention.rb b/app/models/abuse/reports/user_mention.rb
new file mode 100644
index 00000000000..e8091089ede
--- /dev/null
+++ b/app/models/abuse/reports/user_mention.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Abuse
+ module Reports
+ class UserMention < UserMention
+ self.table_name = 'abuse_report_user_mentions'
+
+ belongs_to :abuse_report, optional: false
+ belongs_to :note, optional: false
+ end
+ end
+end
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index bf25c539830..872dedf07b1 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -6,6 +6,8 @@ class AbuseReport < ApplicationRecord
include Gitlab::FileTypeDetection
include WithUploads
include Gitlab::Utils::StrongMemoize
+ include Mentionable
+ include Noteable
MAX_CHAR_LIMIT_URL = 512
MAX_FILE_SIZE = 1.megabyte
@@ -23,6 +25,9 @@ class AbuseReport < ApplicationRecord
has_many :abuse_events, class_name: 'Abuse::Event', inverse_of: :abuse_report
+ has_many :notes, as: :noteable
+ has_many :user_mentions, class_name: 'Abuse::Reports::UserMention'
+
validates :reporter, presence: true, on: :create
validates :user, presence: true, on: :create
validates :message, presence: true
@@ -158,6 +163,10 @@ class AbuseReport < ApplicationRecord
Project.find_by_full_path(route_hash.values_at(:namespace_id, :project_id).join('/'))
end
+ def group
+ Group.find_by_full_path(route_hash[:group_id])
+ end
+
def route_hash
match = Rails.application.routes.recognize_path(reported_from_url)
return {} if match[:unmatched_route].present?
@@ -200,7 +209,7 @@ class AbuseReport < ApplicationRecord
format(_('contains URLs that exceed the %{limit} character limit'), limit: MAX_CHAR_LIMIT_URL)
)
end
- rescue ::Gitlab::UrlBlocker::BlockedUrlError
+ rescue ::Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError
errors.add(:links_to_spam, _('only supports valid HTTP(S) URLs'))
end
diff --git a/app/models/achievements/user_achievement.rb b/app/models/achievements/user_achievement.rb
index 08ebadaa6b0..8b15b25c183 100644
--- a/app/models/achievements/user_achievement.rb
+++ b/app/models/achievements/user_achievement.rb
@@ -15,6 +15,23 @@ module Achievements
optional: true
scope :not_revoked, -> { where(revoked_by_user_id: nil) }
+ scope :order_by_priority_asc, -> {
+ keyset_order = Gitlab::Pagination::Keyset::Order.build([
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'priority',
+ order_expression: ::Achievements::UserAchievement.arel_table[:priority].asc,
+ nullable: :nulls_last,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'id',
+ order_expression: ::Achievements::UserAchievement.arel_table[:id].asc,
+ nullable: :not_nullable,
+ distinct: true
+ )
+ ])
+ reorder(keyset_order)
+ }
scope :order_by_id_asc, -> { order(id: :asc) }
def revoked?
diff --git a/app/models/analytics/cycle_analytics/issue_stage_event.rb b/app/models/analytics/cycle_analytics/issue_stage_event.rb
index 837eb35c839..1a8f1b7c84a 100644
--- a/app/models/analytics/cycle_analytics/issue_stage_event.rb
+++ b/app/models/analytics/cycle_analytics/issue_stage_event.rb
@@ -17,13 +17,44 @@ module Analytics
where(condition.arel.exists)
end
- def self.issuable_id_column
- :issue_id
- end
+ class << self
+ def project_column
+ :project_id
+ end
+
+ def issuable_id_column
+ :issue_id
+ end
+
+ def issuable_model
+ ::Issue
+ end
+
+ def select_columns
+ [
+ *super,
+ issuable_model.arel_table[:weight],
+ issuable_model.arel_table[:sprint_id]
+ ]
+ end
+
+ def column_list
+ [
+ *super,
+ :weight,
+ :sprint_id
+ ]
+ end
- def self.issuable_model
- ::Issue
+ def insert_column_list
+ [
+ *super,
+ :weight,
+ :sprint_id
+ ]
+ end
end
end
end
end
+Analytics::CycleAnalytics::IssueStageEvent.prepend_mod_with('Analytics::CycleAnalytics::IssueStageEvent')
diff --git a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
index 0dfa322b2c3..7f85d284034 100644
--- a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
+++ b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
@@ -17,6 +17,10 @@ module Analytics
where(condition.arel.exists)
end
+ def self.project_column
+ :target_project_id
+ end
+
def self.issuable_id_column
:merge_request_id
end
diff --git a/app/models/analytics/cycle_analytics/value_stream.rb b/app/models/analytics/cycle_analytics/value_stream.rb
index 16446a5b463..7f8c6eef704 100644
--- a/app/models/analytics/cycle_analytics/value_stream.rb
+++ b/app/models/analytics/cycle_analytics/value_stream.rb
@@ -51,3 +51,4 @@ module Analytics
end
end
end
+Analytics::CycleAnalytics::ValueStream.prepend_mod_with('Analytics::CycleAnalytics::ValueStream')
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 7058bfd5650..15e44296635 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -6,7 +6,7 @@ class ApplicationRecord < ActiveRecord::Base
include LegacyBulkInsert
include CrossDatabaseModification
include SensitiveSerializableHash
- include ResetOnUnionError
+ include ResetOnColumnErrors
self.abstract_class = true
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 153257636ba..824a2bd9fa4 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -16,12 +16,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
ignore_columns %i[instance_administration_project_id instance_administrators_group_id], remove_with: '16.2', remove_after: '2023-06-22'
ignore_column :database_apdex_settings, remove_with: '16.4', remove_after: '2023-08-22'
- ignore_columns %i[
- dashboard_notification_limit
- dashboard_enforcement_limit
- dashboard_limit_new_namespace_creation_enforcement_date
- ], remove_with: '16.5', remove_after: '2023-08-22'
-
ignore_column %i[
relay_state_domain_allowlist
in_product_marketing_emails_enabled
@@ -36,7 +30,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
jitsu_project_xid
jitsu_administrator_email
], remove_with: '16.5', remove_after: '2023-09-22'
- ignore_columns %i[ai_access_token ai_access_token_iv], remove_with: '16.6', remove_after: '2023-10-22'
+ ignore_columns %i[encrypted_ai_access_token encrypted_ai_access_token_iv], remove_with: '16.6', remove_after: '2023-10-22'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
@@ -122,6 +116,10 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
validates :default_branch_protection_defaults, json_schema: { filename: 'default_branch_protection_defaults' }
validates :default_branch_protection_defaults, bytesize: { maximum: -> { DEFAULT_BRANCH_PROTECTIONS_DEFAULT_MAX_SIZE } }
+ validates :failed_login_attempts_unlock_period_in_minutes,
+ allow_nil: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
validates :grafana_url,
system_hook_url: ADDRESSABLE_URL_VALIDATION_OPTIONS.merge({
blocked_message: "is blocked: %{exception_message}. #{GRAFANA_URL_ERROR_MESSAGE}"
@@ -269,6 +267,10 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :max_login_attempts,
+ allow_nil: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
validates :max_pages_size,
presence: true,
numericality: {
@@ -311,7 +313,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
if: :auto_devops_enabled?
validates :enabled_git_access_protocol,
- inclusion: { in: %w[ssh http], allow_blank: true }
+ inclusion: { in: ->(_) { enabled_git_access_protocol_values }, allow_blank: true }
validates :domain_denylist,
presence: { message: 'Domain denylist cannot be empty if denylist is enabled.' },
@@ -657,6 +659,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
validates :throttle_authenticated_deprecated_api_period_in_seconds
validates :throttle_protected_paths_requests_per_period
validates :throttle_protected_paths_period_in_seconds
+ validates :project_jobs_api_rate_limit
end
with_options(numericality: { only_integer: true, greater_than_or_equal_to: 0 }) do
@@ -805,11 +808,12 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
attr_encrypted :openai_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
attr_encrypted :anthropic_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
attr_encrypted :vertex_ai_credentials, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
+ attr_encrypted :vertex_ai_access_token, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
# Restricting the validation to `on: :update` only to avoid cyclical dependencies with
# License <--> ApplicationSetting. This method calls a license check when we create
# ApplicationSetting from defaults which in turn depends on ApplicationSetting record.
- # The currect default is defined in the `defaults` method so we don't need to validate
+ # The correct default is defined in the `defaults` method so we don't need to validate
# it here.
validates :disable_feed_token,
inclusion: { in: [true, false], message: N_('must be a boolean value') }, on: :update
@@ -834,6 +838,9 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
allow_nil: false,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
+ validates :math_rendering_limits_enabled,
+ inclusion: { in: [true, false], message: N_('must be a boolean value') }
+
before_validation :ensure_uuid!
before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed?
before_validation :normalize_default_branch_name
@@ -958,19 +965,31 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
false
end
+ def max_login_attempts_column_exists?
+ self.class.database.cached_column_exists?(:max_login_attempts)
+ end
+
+ def failed_login_attempts_unlock_period_in_minutes_column_exists?
+ self.class.database.cached_column_exists?(:failed_login_attempts_unlock_period_in_minutes)
+ end
+
private
def self.human_attribute_name(attribute, *options)
HUMANIZED_ATTRIBUTES[attribute.to_sym] || super
end
+ def self.enabled_git_access_protocol_values
+ %w[ssh http]
+ end
+
def parsed_grafana_url
@parsed_grafana_url ||= Gitlab::Utils.parse_url(grafana_url)
end
def parsed_kroki_url
@parsed_kroki_url ||= Gitlab::UrlBlocker.validate!(kroki_url, schemes: %w[http https], enforce_sanitization: true)[0]
- rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e
self.errors.add(
:kroki_url,
"is not valid. #{e}"
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 5a90e246499..1bd15a56de5 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -51,6 +51,7 @@ module ApplicationSettingImplementation
container_registry_token_expire_delay: 5,
container_registry_vendor: '',
container_registry_version: '',
+ container_registry_db_enabled: false,
custom_http_clone_url_root: nil,
decompress_archive_file_timeout: 210,
default_artifacts_expire_in: '30 days',
@@ -87,6 +88,7 @@ module ApplicationSettingImplementation
external_pipeline_validation_service_timeout: nil,
external_pipeline_validation_service_token: nil,
external_pipeline_validation_service_url: nil,
+ failed_login_attempts_unlock_period_in_minutes: nil,
first_day_of_week: 0,
floc_enabled: false,
gitaly_timeout_default: 55,
@@ -117,12 +119,14 @@ module ApplicationSettingImplementation
login_recaptcha_protection_enabled: false,
mailgun_signing_key: nil,
mailgun_events_enabled: false,
+ math_rendering_limits_enabled: true,
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
+ max_decompressed_archive_size: 25600,
max_export_size: 0,
max_import_size: 0,
max_import_remote_file_size: 10240,
- max_decompressed_archive_size: 25600,
+ max_login_attempts: nil,
max_terraform_state_size_bytes: 0,
max_yaml_size_bytes: 1.megabyte,
max_yaml_depth: 100,
@@ -267,7 +271,8 @@ module ApplicationSettingImplementation
gitlab_dedicated_instance: false,
ci_max_includes: 150,
allow_account_deletion: true,
- gitlab_shell_operation_limit: 600
+ gitlab_shell_operation_limit: 600,
+ project_jobs_api_rate_limit: 600
}.tap do |hsh|
hsh.merge!(non_production_defaults) unless Rails.env.production?
end
diff --git a/app/models/approval.rb b/app/models/approval.rb
index ecc15077c8d..c3992994dd3 100644
--- a/app/models/approval.rb
+++ b/app/models/approval.rb
@@ -14,4 +14,7 @@ class Approval < ApplicationRecord
validates :user_id, presence: true, uniqueness: { scope: [:merge_request_id] }
scope :with_user, -> { joins(:user) }
+ scope :with_invalid_patch_id_sha, ->(patch_id_sha) do
+ where.not(patch_id_sha: patch_id_sha).or(where(patch_id_sha: nil))
+ end
end
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index 73e3fa709b0..e445d08a096 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -9,7 +9,7 @@ class AwardEmoji < ApplicationRecord
include Importable
include IgnorableColumns
- ignore_column :awardable_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :awardable_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
belongs_to :awardable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user
diff --git a/app/models/badges/group_badge.rb b/app/models/badges/group_badge.rb
index c0712f452df..f74c9f89e9f 100644
--- a/app/models/badges/group_badge.rb
+++ b/app/models/badges/group_badge.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class GroupBadge < Badge
+ include EachBatch
+
belongs_to :group
validates :group, presence: true
diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb
index fde528e3fa0..a7ace7429d7 100644
--- a/app/models/bulk_import.rb
+++ b/app/models/bulk_import.rb
@@ -76,4 +76,8 @@ class BulkImport < ApplicationRecord
def supports_batched_export?
source_version_info >= self.class.min_gl_version_for_migration_in_batches
end
+
+ def completed?
+ finished? || failed? || timeout?
+ end
end
diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb
index d1a6f3b9a80..d9efd489af5 100644
--- a/app/models/bulk_imports/tracker.rb
+++ b/app/models/bulk_imports/tracker.rb
@@ -33,11 +33,9 @@ class BulkImports::Tracker < ApplicationRecord
entity_scope.where(stage: next_stage_scope).with_status(:created)
}
- def self.stage_running?(entity_id, stage)
- where(stage: stage, bulk_import_entity_id: entity_id)
- .with_status(:created, :enqueued, :started)
- .exists?
- end
+ scope :running_trackers, -> (entity_id) {
+ where(bulk_import_entity_id: entity_id).with_status(:enqueued, :started)
+ }
def pipeline_class
unless entity.pipeline_exists?(pipeline_name)
diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb
index d3fbfe3aa55..38e6273bf20 100644
--- a/app/models/chat_name.rb
+++ b/app/models/chat_name.rb
@@ -27,6 +27,6 @@ class ChatName < ApplicationRecord
end
def update_last_used_at?
- last_used_at.nil? || last_used_at > LAST_USED_AT_INTERVAL.ago
+ last_used_at.nil? || last_used_at.before?(LAST_USED_AT_INTERVAL.ago)
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 2abb8e4be48..d2cf9058976 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -11,6 +11,7 @@ module Ci
include Importable
include Ci::HasRef
include Ci::TrackEnvironmentUsage
+ include EachBatch
extend ::Gitlab::Utils::Override
@@ -414,7 +415,7 @@ module Ci
end
def options_scheduled_at
- ChronicDuration.parse(options[:start_in], use_complete_matcher: true)&.seconds&.from_now
+ ChronicDuration.parse(options[:start_in])&.seconds&.from_now
end
def action?
@@ -738,7 +739,7 @@ module Ci
def artifacts_expire_in=(value)
self.artifacts_expire_at =
if value
- ChronicDuration.parse(value, use_complete_matcher: true)&.seconds&.from_now
+ ChronicDuration.parse(value)&.seconds&.from_now
end
end
@@ -1090,7 +1091,7 @@ module Ci
end
def has_expiring_artifacts?
- artifacts_expire_at.present? && artifacts_expire_at > Time.current
+ artifacts_expire_at.present? && artifacts_expire_at.future?
end
def job_jwt_variables
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 4c723bb7c0c..555565ff621 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -14,7 +14,7 @@ module Ci
self.table_name = 'p_ci_builds_metadata'
self.primary_key = 'id'
- partitionable scope: :build
+ partitionable scope: :build, partitioned: true
belongs_to :build, class_name: 'CommitStatus'
belongs_to :project
diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb
index 00241908644..1831b7868f9 100644
--- a/app/models/ci/build_need.rb
+++ b/app/models/ci/build_need.rb
@@ -7,7 +7,7 @@ module Ci
include SafelyChangeColumnDefault
include BulkInsertSafe
- MAX_JOB_NAME_LENGTH = 128
+ MAX_JOB_NAME_LENGTH = 255
columns_changing_default :partition_id
diff --git a/app/models/ci/catalog/components_project.rb b/app/models/ci/catalog/components_project.rb
new file mode 100644
index 00000000000..2bc33a6f050
--- /dev/null
+++ b/app/models/ci/catalog/components_project.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module Ci
+ module Catalog
+ class ComponentsProject
+ # ComponentsProject is a type of Catalog Resource which contains one or more
+ # CI/CD components.
+ # It is responsible for retrieving the data of a component file, including the content, name, and file path.
+
+ TEMPLATE_FILE = 'template.yml'
+ TEMPLATES_DIR = 'templates'
+ TEMPLATE_PATH_REGEX = '^templates\/\w+\-?\w+(?:\/template)?\.yml$'
+
+ ComponentData = Struct.new(:content, :path, keyword_init: true)
+
+ def initialize(project, sha = project&.default_branch)
+ @project = project
+ @sha = sha
+ end
+
+ def fetch_component_paths(sha)
+ project.repository.search_files_by_regexp(TEMPLATE_PATH_REGEX, sha)
+ end
+
+ def extract_component_name(path)
+ return unless path.match?(TEMPLATE_PATH_REGEX)
+
+ dirname = File.dirname(path)
+ filename = File.basename(path, '.*')
+
+ if dirname == TEMPLATES_DIR
+ filename
+ else
+ File.basename(dirname)
+ end
+ end
+
+ def extract_inputs(blob)
+ result = Gitlab::Ci::Config::Yaml::Loader.new(blob).load_uninterpolated_yaml
+
+ raise result.error_class, result.error unless result.valid?
+
+ result.inputs
+ end
+
+ def fetch_component(component_name)
+ path = simple_template_path(component_name)
+ content = fetch_content(path)
+
+ if content.nil?
+ path = complex_template_path(component_name)
+ content = fetch_content(path)
+ end
+
+ if content.nil?
+ path = legacy_template_path(component_name)
+ content = fetch_content(path)
+ end
+
+ ComponentData.new(content: content, path: path)
+ end
+
+ private
+
+ attr_reader :project, :sha
+
+ def fetch_content(component_path)
+ project.repository.blob_data_at(sha, component_path)
+ end
+
+ # A simple template consists of a single file
+ def simple_template_path(component_name)
+ # TODO: Extract this line and move to fetch_content once we remove legacy fetching
+ return unless component_name.index('/').nil?
+
+ File.join(TEMPLATES_DIR, "#{component_name}.yml")
+ end
+
+ # A complex template is directory-based and may consist of multiple files.
+ # Given a path like "my-org/sub-group/the-project/templates/component"
+ # returns the entry point path: "templates/component/template.yml".
+ def complex_template_path(component_name)
+ # TODO: Extract this line and move to fetch_content once we remove legacy fetching
+ return unless component_name.index('/').nil?
+
+ File.join(TEMPLATES_DIR, component_name, TEMPLATE_FILE)
+ end
+
+ def legacy_template_path(component_name)
+ File.join(component_name, TEMPLATE_FILE).delete_prefix('/')
+ end
+ end
+ end
+end
diff --git a/app/models/ci/catalog/listing.rb b/app/models/ci/catalog/listing.rb
index 1cb030c67c3..c3b18af8c3f 100644
--- a/app/models/ci/catalog/listing.rb
+++ b/app/models/ci/catalog/listing.rb
@@ -18,6 +18,8 @@ module Ci
case sort.to_s
when 'name_desc' then all_resources.order_by_name_desc
when 'name_asc' then all_resources.order_by_name_asc
+ when 'latest_released_at_desc' then all_resources.order_by_latest_released_at_desc
+ when 'latest_released_at_asc' then all_resources.order_by_latest_released_at_asc
else
all_resources.order_by_created_at_desc
end
diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb
index 799cdce4af7..8ffc0292a69 100644
--- a/app/models/ci/catalog/resource.rb
+++ b/app/models/ci/catalog/resource.rb
@@ -18,6 +18,8 @@ module Ci
scope :order_by_created_at_desc, -> { reorder(created_at: :desc) }
scope :order_by_name_desc, -> { joins(:project).merge(Project.sorted_by_name_desc) }
scope :order_by_name_asc, -> { joins(:project).merge(Project.sorted_by_name_asc) }
+ scope :order_by_latest_released_at_desc, -> { reorder(arel_table[:latest_released_at].desc.nulls_last) }
+ scope :order_by_latest_released_at_asc, -> { reorder(arel_table[:latest_released_at].asc.nulls_last) }
delegate :avatar_path, :description, :name, :star_count, :forks_count, to: :project
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 3f9d8f07b06..2a346f97958 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -310,7 +310,7 @@ module Ci
end
def expiring?
- expire_at.present? && expire_at > Time.current
+ expire_at.present? && expire_at.future?
end
def expire_in
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 5bf4e846304..0a876d26cc9 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -1366,6 +1366,11 @@ module Ci
merge_request.merge_request_diff_for(merge_request_diff_sha)
end
+ def reduced_build_attributes_list_for_rules?
+ ::Feature.enabled?(:reduced_build_attributes_list_for_rules, project)
+ end
+ strong_memoize_attr :reduced_build_attributes_list_for_rules?
+
private
def add_message(severity, content)
diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb
index 199e1cd07e7..8655e8eb9b8 100644
--- a/app/models/ci/ref.rb
+++ b/app/models/ci/ref.rb
@@ -36,7 +36,7 @@ module Ci
next unless ci_ref.artifacts_locked?
ci_ref.run_after_commit do
- Ci::PipelineSuccessUnlockArtifactsWorker.perform_async(ci_ref.last_finished_pipeline_id)
+ Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(ci_ref.id)
end
end
end
@@ -52,7 +52,11 @@ module Ci
end
def last_finished_pipeline_id
- Ci::Pipeline.last_finished_for_ref_id(self.id)&.id
+ last_finished_pipeline&.id
+ end
+
+ def last_finished_pipeline
+ Ci::Pipeline.last_finished_for_ref_id(self.id)
end
def artifacts_locked?
diff --git a/app/models/ci/unlock_pipeline_request.rb b/app/models/ci/unlock_pipeline_request.rb
new file mode 100644
index 00000000000..c8fc82f3e55
--- /dev/null
+++ b/app/models/ci/unlock_pipeline_request.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Ci
+ class UnlockPipelineRequest
+ QUEUE_REDIS_KEY = 'ci_unlock_pipeline_requests:queue'
+
+ def self.enqueue(pipeline_id)
+ unix_timestamp = Time.current.utc.to_i
+ pipeline_ids = Array(pipeline_id).uniq
+ pipeline_ids_with_scores = pipeline_ids.map do |id|
+ # The order of values per pair is `[score, key]`, so in this case, the unix timestamp is the score.
+ # By default, the sort order of sorted sets is from lowest to highest, though this does not matter much
+ # because we use `ZPOPMIN` to make sure to return the lowest/oldest request in terms of unix timestamp score.
+ [unix_timestamp, id]
+ end
+
+ with_redis do |redis|
+ added = redis.zadd(QUEUE_REDIS_KEY, pipeline_ids_with_scores, nx: true)
+ log_event(:enqueued, pipeline_ids) if added > 0
+ added
+ end
+ end
+
+ def self.next!
+ with_redis do |redis|
+ pipeline_id, enqueue_timestamp = redis.zpopmin(QUEUE_REDIS_KEY)
+ break unless pipeline_id
+
+ pipeline_id = pipeline_id.to_i
+ log_event(:picked_next, pipeline_id)
+
+ [pipeline_id, enqueue_timestamp.to_i]
+ end
+ end
+
+ def self.total_pending
+ with_redis do |redis|
+ redis.zcard(QUEUE_REDIS_KEY)
+ end
+ end
+
+ def self.with_redis(&block)
+ Gitlab::Redis::SharedState.with(&block)
+ end
+
+ def self.log_event(event, pipeline_id)
+ Gitlab::AppLogger.info(
+ message: "Pipeline unlock - #{event}",
+ pipeline_id: pipeline_id
+ )
+ end
+ end
+end
diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb
index f4c497a42cc..e2754db73b9 100644
--- a/app/models/clusters/agent_token.rb
+++ b/app/models/clusters/agent_token.rb
@@ -33,6 +33,10 @@ module Clusters
revoked: 1
}
+ def revoke!
+ update(status: :revoked)
+ end
+
def to_ability_name
:cluster
end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index f9a34959675..5bd55fd6f4c 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -24,7 +24,6 @@ module Clusters
has_one :cluster_project, -> { order(id: :desc) }, class_name: 'Clusters::Project'
has_many :deployment_clusters
has_many :deployments, inverse_of: :cluster, through: :deployment_clusters
- has_many :successful_deployments, -> { success }, class_name: 'Deployment'
has_many :environments, -> { distinct }, through: :deployments
has_many :cluster_groups, class_name: 'Clusters::Group'
diff --git a/app/models/clusters/concerns/prometheus_client.rb b/app/models/clusters/concerns/prometheus_client.rb
index d2f69b813aa..b4234e9cc0a 100644
--- a/app/models/clusters/concerns/prometheus_client.rb
+++ b/app/models/clusters/concerns/prometheus_client.rb
@@ -35,7 +35,7 @@ module Clusters
def configured?
kube_client.present? && available?
- rescue Gitlab::UrlBlocker::BlockedUrlError
+ rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError
false
end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 5efbec45561..6ae0cd8e3fd 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -12,7 +12,7 @@ module Clusters
REQUIRED_K8S_MIN_VERSION = 23
IGNORED_CONNECTION_EXCEPTIONS = [
- Gitlab::UrlBlocker::BlockedUrlError,
+ Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError,
Kubeclient::HttpError,
Errno::ECONNREFUSED,
URI::InvalidURIError,
diff --git a/app/models/commit_user_mention.rb b/app/models/commit_user_mention.rb
index 9215e15f07d..fa7f065b6b4 100644
--- a/app/models/commit_user_mention.rb
+++ b/app/models/commit_user_mention.rb
@@ -3,7 +3,7 @@
class CommitUserMention < UserMention
include IgnorableColumns
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
belongs_to :note
end
diff --git a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
index d268c32c088..1d9cf5729cd 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
@@ -16,6 +16,7 @@ module Analytics
scope :start_event_timestamp_before, -> (date) { where(arel_table[:start_event_timestamp].lteq(date)) }
scope :authored, ->(user) { where(author_id: user) }
scope :with_milestone_id, ->(milestone_id) { where(milestone_id: milestone_id) }
+ scope :without_milestone_id, -> (milestone_id) { where('milestone_id <> ? or milestone_id IS NULL', milestone_id) }
scope :end_event_is_not_happened_yet, -> { where(end_event_timestamp: nil) }
scope :order_by_end_event, -> (direction) do
# ORDER BY end_event_timestamp, merge_request_id/issue_id, start_event_timestamp
@@ -57,45 +58,19 @@ module Analytics
class_methods do
def upsert_data(data)
- upsert_values = data.map do |row|
- row.values_at(
- :stage_event_hash_id,
- :issuable_id,
- :group_id,
- :project_id,
- :milestone_id,
- :author_id,
- :state_id,
- :start_event_timestamp,
- :end_event_timestamp
- )
- end
+ upsert_values = data.map { |row| row.values_at(*column_list) }
value_list = Arel::Nodes::ValuesList.new(upsert_values).to_sql
query = <<~SQL
INSERT INTO #{quoted_table_name}
(
- stage_event_hash_id,
- #{connection.quote_column_name(issuable_id_column)},
- group_id,
- project_id,
- milestone_id,
- author_id,
- state_id,
- start_event_timestamp,
- end_event_timestamp
+ #{insert_column_list.join(",\n")}
)
#{value_list}
ON CONFLICT(stage_event_hash_id, #{issuable_id_column})
DO UPDATE SET
- group_id = excluded.group_id,
- project_id = excluded.project_id,
- milestone_id = excluded.milestone_id,
- author_id = excluded.author_id,
- state_id = excluded.state_id,
- start_event_timestamp = excluded.start_event_timestamp,
- end_event_timestamp = excluded.end_event_timestamp
+ #{column_updates.join(",\n")}
SQL
result = connection.execute(query)
@@ -113,6 +88,51 @@ module Analytics
def arel_order(arel_node, direction)
direction.to_sym == :desc ? arel_node.desc : arel_node.asc
end
+
+ def select_columns
+ [
+ issuable_model.arel_table[:id],
+ issuable_model.arel_table[project_column].as('project_id'),
+ issuable_model.arel_table[:milestone_id],
+ issuable_model.arel_table[:author_id],
+ issuable_model.arel_table[:state_id],
+ Project.arel_table[:parent_id].as('group_id')
+ ]
+ end
+
+ def column_list
+ [
+ :stage_event_hash_id,
+ :issuable_id,
+ :group_id,
+ :project_id,
+ :milestone_id,
+ :author_id,
+ :state_id,
+ :start_event_timestamp,
+ :end_event_timestamp
+ ]
+ end
+
+ def insert_column_list
+ [
+ :stage_event_hash_id,
+ connection.quote_column_name(issuable_id_column),
+ :group_id,
+ :project_id,
+ :milestone_id,
+ :author_id,
+ :state_id,
+ :start_event_timestamp,
+ :end_event_timestamp
+ ]
+ end
+
+ def column_updates
+ insert_column_list.map do |column|
+ "#{column} = excluded.#{column}"
+ end
+ end
end
end
end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index e830594af11..22e71c4fa13 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -13,26 +13,26 @@ module Awardable
end
class_methods do
- def awarded(user, name = nil)
+ def awarded(user, name = nil, base_class_name = base_class.name, awardable_id_column = :id)
award_emoji_table = Arel::Table.new('award_emoji')
inner_query = award_emoji_table
.project('true')
.where(award_emoji_table[:user_id].eq(user.id))
- .where(award_emoji_table[:awardable_type].eq(base_class.name))
- .where(award_emoji_table[:awardable_id].eq(self.arel_table[:id]))
+ .where(award_emoji_table[:awardable_type].eq(base_class_name))
+ .where(award_emoji_table[:awardable_id].eq(self.arel_table[awardable_id_column]))
inner_query = inner_query.where(award_emoji_table[:name].eq(name)) if name.present?
where(inner_query.exists)
end
- def not_awarded(user, name = nil)
+ def not_awarded(user, name = nil, base_class_name = base_class.name, awardable_id_column = :id)
award_emoji_table = Arel::Table.new('award_emoji')
inner_query = award_emoji_table
.project('true')
.where(award_emoji_table[:user_id].eq(user.id))
- .where(award_emoji_table[:awardable_type].eq(base_class.name))
- .where(award_emoji_table[:awardable_id].eq(self.arel_table[:id]))
+ .where(award_emoji_table[:awardable_type].eq(base_class_name))
+ .where(award_emoji_table[:awardable_id].eq(self.arel_table[awardable_id_column]))
inner_query = inner_query.where(award_emoji_table[:name].eq(name)) if name.present?
@@ -52,14 +52,14 @@ module Awardable
end
# Order votes by emoji, optional sort order param `descending` defaults to true
- def order_votes(emoji_name, direction)
+ def order_votes(emoji_name, direction, base_class_name = base_class.name, awardable_id_column = :id)
awardable_table = self.arel_table
awards_table = AwardEmoji.arel_table
join_clause = awardable_table
.join(awards_table, Arel::Nodes::OuterJoin)
- .on(awards_table[:awardable_id].eq(awardable_table[:id])
- .and(awards_table[:awardable_type].eq(base_class.name).and(awards_table[:name].eq(emoji_name))))
+ .on(awards_table[:awardable_id].eq(awardable_table[awardable_id_column])
+ .and(awards_table[:awardable_type].eq(base_class_name).and(awards_table[:name].eq(emoji_name))))
.join_sources
joins(join_clause).group(awardable_table[:id]).reorder(
diff --git a/app/models/concerns/bulk_users_by_email_load.rb b/app/models/concerns/bulk_users_by_email_load.rb
index edbd3e21458..55143ead30a 100644
--- a/app/models/concerns/bulk_users_by_email_load.rb
+++ b/app/models/concerns/bulk_users_by_email_load.rb
@@ -7,7 +7,7 @@ module BulkUsersByEmailLoad
def users_by_emails(emails)
Gitlab::SafeRequestLoader.execute(resource_key: user_by_email_resource_key, resource_ids: emails) do |emails|
# have to consider all emails - even secondary, so use all_emails here
- grouped_users_by_email = User.by_any_email(emails).preload(:emails).group_by(&:all_emails)
+ grouped_users_by_email = User.by_any_email(emails, confirmed: true).preload(:emails).group_by(&:all_emails)
grouped_users_by_email.each_with_object({}) do |(found_emails, users), h|
found_emails.each { |e| h[e] = users.first if emails.include?(e) } # don't include all emails for an account, only the ones we want
diff --git a/app/models/concerns/chronic_duration_attribute.rb b/app/models/concerns/chronic_duration_attribute.rb
index 7b7b61fdf06..44b34cf9b2f 100644
--- a/app/models/concerns/chronic_duration_attribute.rb
+++ b/app/models/concerns/chronic_duration_attribute.rb
@@ -18,7 +18,7 @@ module ChronicDurationAttribute
begin
new_value = if value.present?
- ChronicDuration.parse(value, use_complete_matcher: true).to_i
+ ChronicDuration.parse(value).to_i
else
parameters[:default].presence
end
diff --git a/app/models/concerns/ci/deployable.rb b/app/models/concerns/ci/deployable.rb
index d25151f9a34..844c8a1fa7d 100644
--- a/app/models/concerns/ci/deployable.rb
+++ b/app/models/concerns/ci/deployable.rb
@@ -4,6 +4,7 @@
module Ci
module Deployable
extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
included do
prepend_mod_with('Ci::Deployable') # rubocop: disable Cop/InjectEnterpriseEditionModule
@@ -17,8 +18,16 @@ module Ci
end
end
+ after_transition any => [:failed] do |job|
+ next unless job.stops_environment?
+
+ job.run_after_commit do
+ Environments::StopJobFailedWorker.perform_async(id)
+ end
+ end
+
# Synchronize Deployment Status
- # Please note that the data integirty is not assured because we can't use
+ # Please note that the data integrity is not assured because we can't use
# a database transaction due to DB decomposition.
after_transition do |job, transition|
next if transition.loopback?
@@ -32,13 +41,12 @@ module Ci
end
def outdated_deployment?
- strong_memoize(:outdated_deployment) do
- deployment_job? &&
- project.ci_forward_deployment_enabled? &&
- (!project.ci_forward_deployment_rollback_allowed? || incomplete?) &&
- deployment&.older_than_last_successful_deployment?
- end
+ deployment_job? &&
+ project.ci_forward_deployment_enabled? &&
+ (!project.ci_forward_deployment_rollback_allowed? || incomplete?) &&
+ deployment&.older_than_last_successful_deployment?
end
+ strong_memoize_attr :outdated_deployment?
# Virtual deployment status depending on the environment status.
def deployment_status
@@ -106,10 +114,10 @@ module Ci
namespace = options.dig(:environment, :kubernetes, :namespace)
- if namespace.present? # rubocop:disable Style/GuardClause
- strong_memoize(:expanded_kubernetes_namespace) do
- ExpandVariables.expand(namespace, -> { simple_variables })
- end
+ return unless namespace.present?
+
+ strong_memoize(:expanded_kubernetes_namespace) do
+ ExpandVariables.expand(namespace, -> { simple_variables })
end
end
@@ -146,12 +154,11 @@ module Ci
end
def environment_status
- strong_memoize(:environment_status) do
- if has_environment_keyword? && merge_request
- EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha)
- end
- end
+ return unless has_environment_keyword? && merge_request
+
+ EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha)
end
+ strong_memoize_attr :environment_status
def on_stop
options&.dig(:environment, :on_stop)
diff --git a/app/models/concerns/enums/issuable_link.rb b/app/models/concerns/enums/issuable_link.rb
new file mode 100644
index 00000000000..ca5728c2600
--- /dev/null
+++ b/app/models/concerns/enums/issuable_link.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Enums
+ module IssuableLink
+ TYPE_RELATES_TO = 'relates_to'
+ TYPE_BLOCKS = 'blocks'
+
+ def self.link_types
+ { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1 }
+ end
+ end
+end
diff --git a/app/models/concerns/import_state/sidekiq_job_tracker.rb b/app/models/concerns/import_state/sidekiq_job_tracker.rb
index b7d0ed0f51b..9c892acb158 100644
--- a/app/models/concerns/import_state/sidekiq_job_tracker.rb
+++ b/app/models/concerns/import_state/sidekiq_job_tracker.rb
@@ -19,7 +19,7 @@ module ImportState
end
def self.jid_by(project_id:, status:)
- select(:jid).where(status: status).find_by(project_id: project_id)
+ select(:id, :jid).where(status: status).find_by(project_id: project_id)
end
end
end
diff --git a/app/models/concerns/integrations/enable_ssl_verification.rb b/app/models/concerns/integrations/enable_ssl_verification.rb
index 9735a9bf5f6..cb20955488a 100644
--- a/app/models/concerns/integrations/enable_ssl_verification.rb
+++ b/app/models/concerns/integrations/enable_ssl_verification.rb
@@ -5,7 +5,11 @@ module Integrations
extend ActiveSupport::Concern
prepended do
- boolean_accessor :enable_ssl_verification
+ field :enable_ssl_verification,
+ type: :checkbox,
+ title: -> { s_('Integrations|SSL verification') },
+ checkbox_label: -> { s_('Integrations|Enable SSL verification') },
+ help: -> { s_('Integrations|Clear if using a self-signed certificate.') }
end
def initialize_properties
@@ -17,18 +21,11 @@ module Integrations
def fields
super.tap do |fields|
url_index = fields.index { |field| field[:name].ends_with?('_url') }
- insert_index = url_index ? url_index + 1 : -1
+ insert_index = url_index || -1
- fields.insert(insert_index,
- Field.new(
- name: 'enable_ssl_verification',
- integration_class: self,
- type: :checkbox,
- title: s_('Integrations|SSL verification'),
- checkbox_label: s_('Integrations|Enable SSL verification'),
- help: s_('Integrations|Clear if using a self-signed certificate.')
- )
- )
+ enable_ssl_verification_index = fields.index { |field| field[:name] == 'enable_ssl_verification' }
+
+ fields.insert(insert_index, fields.delete_at(enable_ssl_verification_index))
end
end
end
diff --git a/app/models/concerns/issuable_link.rb b/app/models/concerns/issuable_link.rb
index e884e5acecf..dcd2705185f 100644
--- a/app/models/concerns/issuable_link.rb
+++ b/app/models/concerns/issuable_link.rb
@@ -9,8 +9,8 @@
module IssuableLink
extend ActiveSupport::Concern
- TYPE_RELATES_TO = 'relates_to'
- TYPE_BLOCKS = 'blocks' ## EE-only. Kept here to be used on link_type enum.
+ MAX_LINKS_COUNT = 100
+ TYPE_RELATES_TO = Enums::IssuableLink::TYPE_RELATES_TO
class_methods do
def inverse_link_type(type)
@@ -38,10 +38,11 @@ module IssuableLink
validates :source, uniqueness: { scope: :target_id, message: 'is already related' }
validate :check_self_relation
validate :check_opposite_relation
+ validate :validate_max_number_of_links, on: :create
scope :for_source_or_target, ->(issuable) { where(source: issuable).or(where(target: issuable)) }
- enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1 }
+ enum link_type: Enums::IssuableLink.link_types
private
@@ -60,6 +61,27 @@ module IssuableLink
errors.add(:source, "is already related to this #{self.class.issuable_name}")
end
end
+
+ def validate_max_number_of_links
+ return unless source && target
+
+ validate_max_number_of_links_for(source, :source)
+ validate_max_number_of_links_for(target, :target)
+ end
+
+ def validate_max_number_of_links_for(item, attribute_name)
+ return unless item.linked_items_count >= MAX_LINKS_COUNT
+
+ errors.add(
+ attribute_name,
+ format(
+ s_('This %{issuable} would exceed the maximum number of linked %{issuables} (%{limit}).'),
+ issuable: self.class.issuable_name,
+ issuables: self.class.issuable_name.pluralize,
+ limit: MAX_LINKS_COUNT
+ )
+ )
+ end
end
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 06cee46645b..971089edc45 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -12,12 +12,12 @@ module Noteable
class_methods do
# `Noteable` class names that support replying to individual notes.
def replyable_types
- %w[Issue MergeRequest]
+ %w[Issue MergeRequest AbuseReport]
end
# `Noteable` class names that support resolvable notes.
def resolvable_types
- %w[Issue MergeRequest DesignManagement::Design]
+ %w[Issue MergeRequest DesignManagement::Design AbuseReport]
end
# `Noteable` class names that support creating/forwarding individual notes.
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index f0bb1cc359b..a5994b538ce 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -71,6 +71,8 @@ module ProtectedRefAccess
return false if current_user.nil? || no_access?
return current_user.admin? if admin_access?
+ return false if Feature.enabled?(:check_membership_in_protected_ref_access) && !project.member?(current_user)
+
yield if block_given?
user_can_access?(current_user)
diff --git a/app/models/concerns/repository_storage_movable.rb b/app/models/concerns/repository_storage_movable.rb
index 87ff413f2c1..77edabb9706 100644
--- a/app/models/concerns/repository_storage_movable.rb
+++ b/app/models/concerns/repository_storage_movable.rb
@@ -49,6 +49,7 @@ module RepositoryStorageMovable
begin
storage_move.container.set_repository_read_only!(skip_git_transfer_check: true)
rescue StandardError => e
+ storage_move.do_fail!
storage_move.add_error(e.message)
next false
end
diff --git a/app/models/concerns/reset_on_column_errors.rb b/app/models/concerns/reset_on_column_errors.rb
new file mode 100644
index 00000000000..8ace52ebff5
--- /dev/null
+++ b/app/models/concerns/reset_on_column_errors.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module ResetOnColumnErrors
+ extend ActiveSupport::Concern
+
+ MAX_RESET_PERIOD = 10.minutes
+
+ included do |base|
+ base.rescue_from ActiveRecord::StatementInvalid, with: :reset_on_union_error
+ base.rescue_from ActiveModel::UnknownAttributeError, with: :reset_on_unknown_attribute_error
+
+ base.class_attribute :previous_reset_columns_from_error
+ end
+
+ class_methods do
+ def do_reset(exception)
+ class_to_be_reset = base_class
+
+ class_to_be_reset.reset_column_information
+ Gitlab::ErrorTracking.log_exception(exception, { reset_model_name: class_to_be_reset.name })
+
+ class_to_be_reset.previous_reset_columns_from_error = Time.current
+ end
+
+ def reset_on_union_error(exception)
+ if exception.message.include?("each UNION query must have the same number of columns") && should_reset?
+ do_reset(exception)
+ end
+
+ raise
+ end
+
+ def should_reset?
+ return false if base_class.previous_reset_columns_from_error? &&
+ base_class.previous_reset_columns_from_error > MAX_RESET_PERIOD.ago
+
+ Feature.enabled?(:reset_column_information_on_statement_invalid, type: :ops)
+ end
+ end
+
+ def reset_on_union_error(exception)
+ self.class.reset_on_union_error(exception)
+ end
+
+ def reset_on_unknown_attribute_error(exception)
+ self.class.do_reset(exception) if self.class.should_reset?
+
+ raise
+ end
+end
diff --git a/app/models/concerns/reset_on_union_error.rb b/app/models/concerns/reset_on_union_error.rb
deleted file mode 100644
index 42e350b0bed..00000000000
--- a/app/models/concerns/reset_on_union_error.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-module ResetOnUnionError
- extend ActiveSupport::Concern
-
- MAX_RESET_PERIOD = 10.minutes
-
- included do |base|
- base.rescue_from ActiveRecord::StatementInvalid, with: :reset_on_union_error
-
- base.class_attribute :previous_reset_columns_from_error
- end
-
- class_methods do
- def reset_on_union_error(exception)
- if reset_on_statement_invalid?(exception)
- class_to_be_reset = base_class
-
- class_to_be_reset.reset_column_information
- Gitlab::ErrorTracking.log_exception(exception, { reset_model_name: class_to_be_reset.name })
-
- class_to_be_reset.previous_reset_columns_from_error = Time.current
- end
-
- raise
- end
-
- def reset_on_statement_invalid?(exception)
- return false unless exception.message.include?("each UNION query must have the same number of columns")
-
- return false if base_class.previous_reset_columns_from_error? &&
- base_class.previous_reset_columns_from_error > MAX_RESET_PERIOD.ago
-
- Feature.enabled?(:reset_column_information_on_statement_invalid, type: :ops)
- end
- end
-end
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index ef14ff5fbe2..4c16ba18823 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -15,16 +15,7 @@ module Routable
#
# Returns a single object, or nil.
- # rubocop:disable Metrics/CyclomaticComplexity
- # rubocop:disable Metrics/PerceivedComplexity
- def self.find_by_full_path(
- path,
- follow_redirects: false,
- route_scope: Route,
- redirect_route_scope: RedirectRoute,
- optimize_routable: Routable.optimize_routable_enabled?
- )
-
+ def self.find_by_full_path(path, follow_redirects: false, route_scope: nil)
return unless path.present?
# Convert path to string to prevent DB error: function lower(integer) does not exist
@@ -35,49 +26,22 @@ module Routable
#
# We need to qualify the columns with the table name, to support both direct lookups on
# Route/RedirectRoute, and scoped lookups through the Routable classes.
- if optimize_routable
- path_condition = { path: path }
-
- source_type_condition = if route_scope == Route
- {}
- else
- { source_type: route_scope.klass.base_class }
- end
+ path_condition = { path: path }
- route =
- Route.where(source_type_condition).find_by(path_condition) ||
- Route.where(source_type_condition).iwhere(path_condition).take
+ source_type_condition = route_scope ? { source_type: route_scope.klass.base_class } : {}
- if follow_redirects
- route ||= RedirectRoute.where(source_type_condition).iwhere(path_condition).take
- end
+ route =
+ Route.where(source_type_condition).find_by(path_condition) ||
+ Route.where(source_type_condition).iwhere(path_condition).take
- return unless route
- return route.source if route_scope == Route
-
- route_scope.find_by(id: route.source_id)
- else
- Gitlab::Database.allow_cross_joins_across_databases(url:
- "https://gitlab.com/gitlab-org/gitlab/-/issues/420046") do
- route =
- route_scope.find_by(routes: { path: path }) ||
- route_scope.iwhere(Route.arel_table[:path] => path).take
-
- if follow_redirects
- route ||= redirect_route_scope.iwhere(RedirectRoute.arel_table[:path] => path).take
- end
-
- next unless route
-
- route.is_a?(Routable) ? route : route.source
- end
+ if follow_redirects
+ route ||= RedirectRoute.where(source_type_condition).iwhere(path_condition).take
end
- end
- # rubocop:enable Metrics/PerceivedComplexity
- # rubocop:enable Metrics/CyclomaticComplexity
- def self.optimize_routable_enabled?
- Feature.enabled?(:optimize_routable)
+ return unless route
+ return route.source unless route_scope
+
+ route_scope.find_by(id: route.source_id)
end
included do
@@ -107,22 +71,12 @@ module Routable
#
# Returns a single object, or nil.
def find_by_full_path(path, follow_redirects: false)
- optimize_routable = Routable.optimize_routable_enabled?
-
- if optimize_routable
- route_scope = all
- redirect_route_scope = RedirectRoute
- else
- route_scope = includes(:route).references(:routes)
- redirect_route_scope = joins(:redirect_routes)
- end
+ route_scope = all
Routable.find_by_full_path(
path,
follow_redirects: follow_redirects,
- route_scope: route_scope,
- redirect_route_scope: redirect_route_scope,
- optimize_routable: optimize_routable
+ route_scope: route_scope
)
end
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
deleted file mode 100644
index 5455a2159cd..00000000000
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ /dev/null
@@ -1,112 +0,0 @@
-# frozen_string_literal: true
-
-module Storage
- module LegacyNamespace
- extend ActiveSupport::Concern
-
- include Gitlab::ShellAdapter
-
- def move_dir
- proj_with_tags = first_project_with_container_registry_tags
-
- if proj_with_tags
- raise Gitlab::UpdatePathError, "Namespace #{name} (#{id}) cannot be moved because at least one project (e.g. #{proj_with_tags.name} (#{proj_with_tags.id})) has tags in container registry"
- end
-
- parent_was = if saved_change_to_parent? && parent_id_before_last_save.present?
- Namespace.find(parent_id_before_last_save) # raise NotFound early if needed
- end
-
- if saved_change_to_parent?
- former_parent_full_path = parent_was&.full_path
- parent_full_path = parent&.full_path
- Gitlab::UploadsTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path)
- else
- Gitlab::UploadsTransfer.new.rename_namespace(full_path_before_last_save, full_path)
- end
-
- # If repositories moved successfully we need to
- # send update instructions to users.
- # However we cannot allow rollback since we moved namespace dir
- # So we basically we mute exceptions in next actions
- begin
- send_update_instructions
- write_projects_repository_config
- rescue StandardError => e
- Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e,
- full_path_before_last_save: full_path_before_last_save,
- full_path: full_path,
- action: 'move_dir')
- end
-
- true # false would cancel later callbacks but not rollback
- end
-
- # Hooks
-
- # Save the storages before the projects are destroyed to use them on after destroy
- def prepare_for_destroy
- old_repository_storages
- end
-
- private
-
- def move_repositories
- # Move the namespace directory in all storages used by member projects
- repository_storages(legacy_only: true).each do |repository_storage|
- # Ensure old directory exists before moving it
- Gitlab::GitalyClient::NamespaceService.allow do
- gitlab_shell.add_namespace(repository_storage, full_path_before_last_save)
-
- # Ensure new directory exists before moving it (if there's a parent)
- gitlab_shell.add_namespace(repository_storage, parent.full_path) if parent
-
- unless gitlab_shell.mv_namespace(repository_storage, full_path_before_last_save, full_path)
-
- Gitlab::AppLogger.error("Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}")
-
- # if we cannot move namespace directory we should rollback
- # db changes in order to prevent out of sync between db and fs
- raise Gitlab::UpdatePathError, 'namespace directory cannot be moved'
- end
- end
- end
- end
-
- def old_repository_storages
- @old_repository_storage_paths ||= repository_storages(legacy_only: true)
- end
-
- def repository_storages(legacy_only: false)
- # We need to get the storage paths for all the projects, even the ones that are
- # pending delete. Unscoping also get rids of the default order, which causes
- # problems with SELECT DISTINCT.
- Project.unscoped do
- namespace_projects = all_projects
- namespace_projects = namespace_projects.without_storage_feature(:repository) if legacy_only
- namespace_projects.pluck(Arel.sql('distinct(repository_storage)'))
- end
- end
-
- def rm_dir
- # Remove the namespace directory in all storages paths used by member projects
- old_repository_storages.each do |repository_storage|
- # Move namespace directory into trash.
- # We will remove it later async
- new_path = "#{full_path}+#{id}+deleted"
-
- Gitlab::GitalyClient::NamespaceService.allow do
- if gitlab_shell.mv_namespace(repository_storage, full_path, new_path)
- Gitlab::AppLogger.info %(Namespace directory "#{full_path}" moved to "#{new_path}")
-
- # Remove namespace directory async with delay so
- # GitLab has time to remove all projects first
- run_after_commit do
- GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path)
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/concerns/vulnerability_finding_helpers.rb b/app/models/concerns/vulnerability_finding_helpers.rb
index e8a50497b20..94d091e8459 100644
--- a/app/models/concerns/vulnerability_finding_helpers.rb
+++ b/app/models/concerns/vulnerability_finding_helpers.rb
@@ -50,6 +50,7 @@ module VulnerabilityFindingHelpers
finding_data = report_finding.to_hash.except(
:compare_key, :identifiers, :location, :scanner, :links, :signatures, :flags, :evidence
)
+
identifiers = report_finding.identifiers.uniq(&:fingerprint).map do |identifier|
Vulnerabilities::Identifier.new(identifier.to_hash.merge({ project: project }))
end
diff --git a/app/models/container_expiration_policy.rb b/app/models/container_expiration_policy.rb
index f643fa7730b..a7ed5e28695 100644
--- a/app/models/container_expiration_policy.rb
+++ b/app/models/container_expiration_policy.rb
@@ -80,7 +80,7 @@ class ContainerExpirationPolicy < ApplicationRecord
end
def set_next_run_at
- cadence_seconds = ChronicDuration.parse(cadence, use_complete_matcher: true).seconds
+ cadence_seconds = ChronicDuration.parse(cadence).seconds
self.next_run_at = Time.zone.now + cadence_seconds
end
diff --git a/app/models/container_registry/protection.rb b/app/models/container_registry/protection.rb
new file mode 100644
index 00000000000..33c94c0c893
--- /dev/null
+++ b/app/models/container_registry/protection.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module ContainerRegistry
+ module Protection
+ def self.table_name_prefix
+ 'container_registry_protection_'
+ end
+ end
+end
diff --git a/app/models/container_registry/protection/rule.rb b/app/models/container_registry/protection/rule.rb
new file mode 100644
index 00000000000..a91f3633d75
--- /dev/null
+++ b/app/models/container_registry/protection/rule.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module ContainerRegistry
+ module Protection
+ class Rule < ApplicationRecord
+ enum delete_protected_up_to_access_level:
+ Gitlab::Access.sym_options_with_owner.slice(:maintainer, :owner, :developer),
+ _prefix: :delete_protected_up_to
+ enum push_protected_up_to_access_level:
+ Gitlab::Access.sym_options_with_owner.slice(:maintainer, :owner, :developer),
+ _prefix: :push_protected_up_to
+
+ belongs_to :project, inverse_of: :container_registry_protection_rules
+
+ validates :container_path_pattern, presence: true, uniqueness: { scope: :project_id }, length: { maximum: 255 }
+ validates :delete_protected_up_to_access_level, presence: true
+ validates :push_protected_up_to_access_level, presence: true
+ end
+ end
+end
diff --git a/app/models/design_user_mention.rb b/app/models/design_user_mention.rb
index 7d0cd72e9eb..ba1ef1b5712 100644
--- a/app/models/design_user_mention.rb
+++ b/app/models/design_user_mention.rb
@@ -3,7 +3,7 @@
class DesignUserMention < UserMention
include IgnorableColumns
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
belongs_to :design, class_name: 'DesignManagement::Design'
belongs_to :note
diff --git a/app/models/discussion_note.rb b/app/models/discussion_note.rb
index a1dfa0e72ec..fa830179022 100644
--- a/app/models/discussion_note.rb
+++ b/app/models/discussion_note.rb
@@ -9,7 +9,7 @@ class DiscussionNote < Note
# Names of all implementers of `Noteable` that support discussions.
def self.noteable_types
- %w[MergeRequest Issue Commit Snippet]
+ %w[MergeRequest Issue Commit Snippet AbuseReport]
end
validates :noteable_type, inclusion: { in: noteable_types }
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 29394c37e2c..efdcf7174aa 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -195,6 +195,10 @@ class Environment < ApplicationRecord
transition %i[available stopping] => :stopped
end
+ event :recover_stuck_stopping do
+ transition stopping: :available
+ end
+
state :available
state :stopping
state :stopped
diff --git a/app/models/event.rb b/app/models/event.rb
index 9e4a662aaa5..7de7ad8ccd6 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -12,7 +12,7 @@ class Event < ApplicationRecord
include IgnorableColumns
include EachBatch
- ignore_column :target_id_convert_to_bigint, remove_with: '16.4', remove_after: '2023-09-22'
+ ignore_column :target_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
ACTIONS = HashWithIndifferentAccess.new(
created: 1,
diff --git a/app/models/group.rb b/app/models/group.rb
index 9330ffef156..c83dd24e98e 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -423,15 +423,13 @@ class Group < Namespace
owners.include?(user)
end
- def add_members(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
+ def add_members(users, access_level, current_user: nil, expires_at: nil)
Members::Groups::CreatorService.add_members( # rubocop:disable CodeReuse/ServiceClass
self,
users,
access_level,
current_user: current_user,
- expires_at: expires_at,
- tasks_to_be_done: tasks_to_be_done,
- tasks_project_id: tasks_project_id
+ expires_at: expires_at
)
end
@@ -512,9 +510,15 @@ class Group < Namespace
members_with_parents(only_active_users: false)
end
- members_from_hiearchy.all_owners.left_outer_joins(:user)
- .merge(User.without_project_bot)
- .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455")
+ owners = []
+
+ members_from_hiearchy.all_owners.non_invite.each_batch do |relation|
+ owners += relation.preload(:user).load.reject do |member|
+ member.user.project_bot?
+ end
+ end
+
+ owners
end
def ldap_synced?
@@ -657,12 +661,6 @@ class Group < Namespace
.non_invite
end
- def users_with_parents
- User
- .where(id: members_with_parents.select(:user_id))
- .reorder(nil)
- end
-
def users_with_descendants
User
.where(id: members_with_descendants.select(:user_id))
@@ -694,7 +692,7 @@ class Group < Namespace
return GroupMember::NO_ACCESS unless user
return GroupMember::OWNER if user.can_admin_all_resources? && !only_concrete_membership
- max_member_access([user.id])[user.id]
+ max_member_access(user)
end
def mattermost_team_params
@@ -879,10 +877,6 @@ class Group < Namespace
].compact.min
end
- def content_editor_on_issues_feature_flag_enabled?
- feature_flag_enabled_for_self_or_ancestor?(:content_editor_on_issues)
- end
-
def work_items_feature_flag_enabled?
feature_flag_enabled_for_self_or_ancestor?(:work_items)
end
@@ -953,16 +947,16 @@ class Group < Namespace
end
end
- def max_member_access(user_ids)
- ::Gitlab::Database.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455") do
- Gitlab::SafeRequestLoader.execute(
- resource_key: max_member_access_for_resource_key(User),
- resource_ids: user_ids,
- default_value: Gitlab::Access::NO_ACCESS
- ) do |user_ids|
- members_with_parents.where(user_id: user_ids).group(:user_id).maximum(:access_level)
- end
- end
+ def max_member_access(user)
+ Gitlab::SafeRequestLoader.execute(
+ resource_key: max_member_access_for_resource_key(User),
+ resource_ids: [user.id],
+ default_value: Gitlab::Access::NO_ACCESS
+ ) do |_|
+ next {} unless user.active?
+
+ members_with_parents(only_active_users: false).where(user_id: user.id).group(:user_id).maximum(:access_level)
+ end.fetch(user.id)
end
def update_two_factor_requirement
diff --git a/app/models/identity.rb b/app/models/identity.rb
index a4c59694050..1a3a9a300b6 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -14,6 +14,7 @@ class Identity < MainClusterwide::ApplicationRecord
after_destroy :clear_user_synced_attributes, if: :user_synced_attributes_metadata_from_provider?
scope :for_user, ->(user) { where(user: user) }
+ scope :for_user_ids, ->(user_ids) { where(user_id: user_ids) }
scope :with_provider, ->(provider) { where(provider: provider) }
scope :with_extern_uid, ->(provider, extern_uid) do
iwhere(extern_uid: normalize_uid(provider, extern_uid)).with_provider(provider)
diff --git a/app/models/integration.rb b/app/models/integration.rb
index d4c76f743a3..b4408301c6d 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -47,6 +47,9 @@ class Integration < ApplicationRecord
Integrations::BaseThirdPartyWiki
].freeze
+ BASE_ATTRIBUTES = %w[id instance project_id group_id created_at updated_at
+ encrypted_properties encrypted_properties_iv properties].freeze
+
SECTION_TYPE_CONFIGURATION = 'configuration'
SECTION_TYPE_CONNECTION = 'connection'
SECTION_TYPE_TRIGGER = 'trigger'
@@ -111,18 +114,18 @@ class Integration < ApplicationRecord
validate :validate_belongs_to_project_or_group
scope :external_issue_trackers, -> { where(category: 'issue_tracker').active }
- # TODO: Will be modified in 15.0
- # Details: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74501#note_744393645
- scope :third_party_wikis, -> { where(type: %w[Integrations::Confluence Integrations::Shimo]).active }
+ scope :third_party_wikis, -> { where(category: 'third_party_wiki').active }
scope :by_name, ->(name) { by_type(integration_name_to_type(name)) }
scope :external_wikis, -> { by_name(:external_wiki).active }
scope :active, -> { where(active: true) }
scope :by_type, ->(type) { where(type: type) } # INTERNAL USE ONLY: use by_name instead
- scope :by_active_flag, -> (flag) { where(active: flag) }
- scope :inherit_from_id, -> (id) { where(inherit_from_id: id) }
+ scope :by_active_flag, ->(flag) { where(active: flag) }
+ scope :inherit_from_id, ->(id) { where(inherit_from_id: id) }
scope :with_default_settings, -> { where.not(inherit_from_id: nil) }
scope :with_custom_settings, -> { where(inherit_from_id: nil) }
- scope :for_group, -> (group) { where(group_id: group, type: available_integration_types(include_project_specific: false)) }
+ scope :for_group, ->(group) {
+ where(group_id: group, type: available_integration_types(include_project_specific: false))
+ }
scope :for_instance, -> { where(instance: true, type: available_integration_types(include_project_specific: false)) }
scope :push_hooks, -> { where(push_events: true, active: true) }
@@ -216,13 +219,6 @@ class Integration < ApplicationRecord
# Also keep track of updated properties in a similar way as ActiveModel::Dirty
def self.boolean_accessor(*args)
args.each do |arg|
- # TODO: Allow legacy usage of `.boolean_accessor`, once all integrations
- # are converted to the field DSL we can remove this and only call
- # `.boolean_accessor` through `.field`.
- #
- # See https://gitlab.com/groups/gitlab-org/-/epics/7652
- prop_accessor(arg) unless method_defined?(arg)
-
class_eval <<~RUBY, __FILE__, __LINE__ + 1
# Make the original getter available as a private method.
alias_method :#{arg}_before_type_cast, :#{arg}
@@ -239,13 +235,14 @@ class Integration < ApplicationRecord
RUBY
end
end
+ private_class_method :boolean_accessor
def self.to_param
raise NotImplementedError
end
def self.event_names
- self.supported_events.map { |event| IntegrationsHelper.integration_event_field_name(event) }
+ supported_events.map { |event| IntegrationsHelper.integration_event_field_name(event) }
end
def self.supported_events
@@ -406,7 +403,7 @@ class Integration < ApplicationRecord
from_union([active.where(instance: true), active.where(group_id: group_ids, inherit_from_id: nil)])
.order(order)
.group_by(&:type)
- .count { |type, parents| build_from_integration(parents.first, association => owner.id).save }
+ .count { |_type, parents| build_from_integration(parents.first, association => owner.id).save }
end
def self.inherited_descendants_from_self_or_ancestors_from(integration)
@@ -415,9 +412,10 @@ class Integration < ApplicationRecord
.or(where(type: integration.type, instance: true)).select(:id)
from_union([
- where(type: integration.type, inherit_from_id: inherit_from_ids, group: integration.group.descendants),
- where(type: integration.type, inherit_from_id: inherit_from_ids, project: Project.in_namespace(integration.group.self_and_descendants))
- ])
+ where(type: integration.type, inherit_from_id: inherit_from_ids, group: integration.group.descendants),
+ where(type: integration.type, inherit_from_id: inherit_from_ids,
+ project: Project.in_namespace(integration.group.self_and_descendants))
+ ])
end
def activated?
@@ -490,10 +488,9 @@ class Integration < ApplicationRecord
def to_database_hash
column = self.class.attribute_aliases.fetch('type', 'type')
- as_json(
- except: %w[id instance project_id group_id created_at updated_at]
- ).merge(column => type)
- .merge(reencrypt_properties)
+ attributes_for_database.except(*BASE_ATTRIBUTES)
+ .merge(column => type)
+ .merge(reencrypt_properties)
end
def reencrypt_properties
diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb
index 859522670ef..77555996cd9 100644
--- a/app/models/integrations/asana.rb
+++ b/app/models/integrations/asana.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
-require 'asana'
-
module Integrations
class Asana < Integration
+ TASK_URL_TEMPLATE = 'https://app.asana.com/api/1.0/tasks/%{task_gid}'
+ STORY_URL_TEMPLATE = 'https://app.asana.com/api/1.0/tasks/%{task_gid}/stories'
+
validates :api_key, presence: true, if: :activated?
field :api_key,
@@ -40,12 +41,6 @@ module Integrations
%w[push]
end
- def client
- @_client ||= ::Asana::Client.new do |c|
- c.authentication :access_token, api_key
- end
- end
-
def execute(data)
return unless supported_events.include?(data[:object_kind])
@@ -78,11 +73,12 @@ module Integrations
taskid = tuple[2] || tuple[1]
begin
- task = ::Asana::Resources::Task.find_by_id(client, taskid)
- task.add_comment(text: "#{push_msg} #{message}")
+ story_on_task_url = format(STORY_URL_TEMPLATE, task_gid: taskid)
+ Gitlab::HTTP.post(story_on_task_url, headers: { "Authorization" => "Bearer #{api_key}" }, body: { text: "#{push_msg} #{message}" })
if tuple[0]
- task.update(completed: true)
+ task_url = format(TASK_URL_TEMPLATE, task_gid: taskid)
+ Gitlab::HTTP.put(task_url, headers: { "Authorization" => "Bearer #{api_key}" }, body: { completed: true })
end
rescue StandardError => e
log_error(e.message)
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index 0b8432136dd..9f15532a0b0 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -28,14 +28,13 @@ module Integrations
non_empty_password_title: -> { s_('ProjectService|Enter new password') },
non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current password') }
- validates :bamboo_url, presence: true, public_url: true, if: :activated?
- validates :build_key, presence: true, if: :activated?
- validates :username,
- presence: true,
- if: ->(service) { service.activated? && service.password }
- validates :password,
- presence: true,
- if: ->(service) { service.activated? && service.username }
+ with_options if: :activated? do
+ validates :bamboo_url, presence: true, public_url: true
+ validates :build_key, presence: true
+ end
+
+ validates :username, presence: true, if: ->(integration) { integration.activated? && integration.password }
+ validates :password, presence: true, if: ->(integration) { integration.activated? && integration.username }
attr_accessor :response
@@ -48,8 +47,16 @@ module Integrations
end
def help
- docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'), target: '_blank', rel: 'noopener noreferrer'
- s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo. You must set up automatic revision labeling and a repository trigger in Bamboo. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+ docs_link = ActionController::Base.helpers.link_to(
+ _('Learn more.'),
+ Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'),
+ target: '_blank',
+ rel: 'noopener noreferrer'
+ )
+ format(
+ s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo. You must set up automatic revision labeling and ' \
+ 'a repository trigger in Bamboo. %{docs_link}').html_safe,
+ docs_link: docs_link.html_safe)
end
def self.to_param
@@ -70,12 +77,18 @@ module Integrations
get_path("updateAndBuild.action", { buildKey: build_key })
end
- def calculate_reactive_cache(sha, ref)
+ def calculate_reactive_cache(sha, _ref)
response = try_get_path("rest/api/latest/result/byChangeset/#{sha}")
{ build_page: read_build_page(response), commit_status: read_commit_status(response) }
end
+ def avatar_url
+ ActionController::Base.helpers.image_path(
+ 'illustrations/third-party-logos/integrations-logos/atlassian-bamboo.svg'
+ )
+ end
+
private
def get_build_result(response)
@@ -112,7 +125,7 @@ module Integrations
if result.blank?
'Pending'
else
- result.dig('buildState')
+ result['buildState']
end
return :error unless status.present?
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index 2c929dc2cb3..b75801335bd 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -13,6 +13,8 @@ module Integrations
tag_push pipeline wiki_page deployment incident
].freeze
+ GROUP_ONLY_SUPPORTED_EVENTS = %w[group_mention group_confidential_mention].freeze
+
SUPPORTED_EVENTS_FOR_LABEL_FILTER = %w[issue confidential_issue merge_request note confidential_note].freeze
EVENT_CHANNEL = proc { |event| "#{event}_channel" }
@@ -26,12 +28,12 @@ module Integrations
attribute :category, default: 'chat'
- prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified, :labels_to_be_notified_behavior
+ prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified,
+ :labels_to_be_notified_behavior, :notify_only_default_branch
# Custom serialized properties initialization
prop_accessor(*SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] })
-
- boolean_accessor :notify_only_default_branch
+ prop_accessor(*GROUP_ONLY_SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] })
validates :webhook,
presence: true,
@@ -44,10 +46,10 @@ module Integrations
super
if properties.empty?
- self.notify_only_broken_pipelines = true if self.respond_to?(:notify_only_broken_pipelines)
+ self.notify_only_broken_pipelines = true if respond_to?(:notify_only_broken_pipelines)
self.branches_to_be_notified = "default"
self.labels_to_be_notified_behavior = MATCH_ANY_LABEL
- elsif !self.notify_only_default_branch.nil?
+ elsif !notify_only_default_branch.nil?
# In older versions, there was only a boolean property named
# `notify_only_default_branch`. Now we have a string property named
# `branches_to_be_notified`. Instead of doing a background migration, we
@@ -55,7 +57,7 @@ module Integrations
# users haven't specified one already. When users edit the integration and
# select a value for this new property, it will override everything.
- self.branches_to_be_notified ||= notify_only_default_branch? ? "default" : "all"
+ self.branches_to_be_notified ||= notify_only_default_branch == 'true' ? "default" : "all"
end
end
@@ -237,7 +239,7 @@ module Integrations
case object_kind
when "push", "tag_push"
Integrations::ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
- when "issue"
+ when "issue", "incident"
Integrations::ChatMessage::IssueMessage.new(data) unless update?(data)
when "merge_request"
Integrations::ChatMessage::MergeMessage.new(data) unless update?(data)
@@ -249,8 +251,8 @@ module Integrations
Integrations::ChatMessage::WikiPageMessage.new(data)
when "deployment"
Integrations::ChatMessage::DeploymentMessage.new(data) if notify_for_ref?(data)
- when "incident"
- Integrations::ChatMessage::IssueMessage.new(data) unless update?(data)
+ when "group_mention"
+ Integrations::ChatMessage::GroupMentionMessage.new(data)
end
end
# rubocop:enable Metrics/CyclomaticComplexity
diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb
index 65aec8b278f..09a0c9ba361 100644
--- a/app/models/integrations/base_slack_notification.rb
+++ b/app/models/integrations/base_slack_notification.rb
@@ -7,8 +7,6 @@ module Integrations
].freeze
prop_accessor EVENT_CHANNEL['alert']
- prop_accessor EVENT_CHANNEL['group_mention']
- prop_accessor EVENT_CHANNEL['group_confidential_mention']
override :default_channel_placeholder
def default_channel_placeholder
@@ -18,7 +16,6 @@ module Integrations
override :get_message
def get_message(object_kind, data)
return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert'
- return Integrations::ChatMessage::GroupMentionMessage.new(data) if object_kind == 'group_mention'
super
end
diff --git a/app/models/integrations/chat_message/alert_message.rb b/app/models/integrations/chat_message/alert_message.rb
index e2c689f9435..6c7ea9aed7c 100644
--- a/app/models/integrations/chat_message/alert_message.rb
+++ b/app/models/integrations/chat_message/alert_message.rb
@@ -34,12 +34,12 @@ module Integrations
"Alert firing in #{strip_markup(project_name)}"
end
- private
-
def attachment_color
"#C95823"
end
+ private
+
def attachment_fields
[
{
diff --git a/app/models/integrations/chat_message/deployment_message.rb b/app/models/integrations/chat_message/deployment_message.rb
index 0367459dfcb..4d3e962d885 100644
--- a/app/models/integrations/chat_message/deployment_message.rb
+++ b/app/models/integrations/chat_message/deployment_message.rb
@@ -30,7 +30,7 @@ module Integrations
[{
text: format(description_message),
- color: color
+ color: attachment_color
}]
end
@@ -38,17 +38,7 @@ module Integrations
{}
end
- private
-
- def message
- if running?
- "Starting deploy to #{strip_markup(environment)}"
- else
- "Deploy to #{strip_markup(environment)} #{humanized_status}"
- end
- end
-
- def color
+ def attachment_color
case status
when 'success'
'good'
@@ -61,6 +51,16 @@ module Integrations
end
end
+ private
+
+ def message
+ if running?
+ "Starting deploy to #{strip_markup(environment)}"
+ else
+ "Deploy to #{strip_markup(environment)} #{humanized_status}"
+ end
+ end
+
def project_link
link(project_name, project_url)
end
diff --git a/app/models/integrations/chat_message/issue_message.rb b/app/models/integrations/chat_message/issue_message.rb
index dd516362491..4c144bc2f68 100644
--- a/app/models/integrations/chat_message/issue_message.rb
+++ b/app/models/integrations/chat_message/issue_message.rb
@@ -41,6 +41,10 @@ module Integrations
}
end
+ def attachment_color
+ '#C95823'
+ end
+
private
def message
@@ -56,7 +60,7 @@ module Integrations
title: issue_title,
title_link: issue_url,
text: format(SlackMarkdownSanitizer.sanitize_slack_link(description)),
- color: '#C95823'
+ color: attachment_color
}]
end
diff --git a/app/models/integrations/chat_message/pipeline_message.rb b/app/models/integrations/chat_message/pipeline_message.rb
index f8a634be336..2abe4a6e9c7 100644
--- a/app/models/integrations/chat_message/pipeline_message.rb
+++ b/app/models/integrations/chat_message/pipeline_message.rb
@@ -89,6 +89,15 @@ module Integrations
}
end
+ def attachment_color
+ case status
+ when 'success'
+ detailed_status == 'passed with warnings' ? 'warning' : 'good'
+ else
+ 'danger'
+ end
+ end
+
private
def actually_failed_jobs(builds)
@@ -180,15 +189,6 @@ module Integrations
end
end
- def attachment_color
- case status
- when 'success'
- detailed_status == 'passed with warnings' ? 'warning' : 'good'
- else
- 'danger'
- end
- end
-
def ref_url
if ref_type == 'tag'
"#{project_url}/-/tags/#{ref}"
diff --git a/app/models/integrations/chat_message/push_message.rb b/app/models/integrations/chat_message/push_message.rb
index b17e28bb6c6..ee44fc98791 100644
--- a/app/models/integrations/chat_message/push_message.rb
+++ b/app/models/integrations/chat_message/push_message.rb
@@ -35,6 +35,10 @@ module Integrations
}
end
+ def attachment_color
+ '#345'
+ end
+
private
def humanized_action(short: false)
@@ -111,10 +115,6 @@ module Integrations
['pushed to', ref_link, "of #{project_link} (#{compare_link})"]
end
end
-
- def attachment_color
- '#345'
- end
end
end
end
diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb
index 815e3669d78..33b2b52fa62 100644
--- a/app/models/integrations/discord.rb
+++ b/app/models/integrations/discord.rb
@@ -42,8 +42,15 @@ module Integrations
s_('DiscordService|Override the default webhook (e.g. https://discord.com/api/webhooks/…)')
end
+ override :supported_events
+ def supported_events
+ additional = group_level? ? %w[group_mention group_confidential_mention] : []
+
+ (self.class.supported_events + additional).freeze
+ end
+
def self.supported_events
- %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page]
+ %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page deployment]
end
def configurable_channels?
@@ -68,7 +75,7 @@ module Integrations
builder.add_embed do |embed|
embed.author = Discordrb::Webhooks::EmbedAuthor.new(name: message.user_name, icon_url: message.user_avatar)
embed.description = (message.pretext + "\n" + Array.wrap(message.attachments).join("\n")).gsub(ATTACHMENT_REGEX, " \\k<entry> - \\k<name>\n")
- embed.colour = 16543014 # The hex "fc6d26" as an Integer
+ embed.colour = embed_color(message)
embed.timestamp = Time.now.utc
end
end
@@ -77,6 +84,33 @@ module Integrations
false
end
+ COLOR_OVERRIDES = {
+ 'good' => '#0d532a',
+ 'warning' => '#703800',
+ 'danger' => '#8d1300'
+ }.freeze
+
+ def embed_color(message)
+ return 'fc6d26'.hex unless message.respond_to?(:attachment_color)
+
+ color = message.attachment_color
+
+ color = COLOR_OVERRIDES[color] if COLOR_OVERRIDES.key?(color)
+
+ color = color.delete_prefix('#')
+
+ normalize_color(color).hex
+ end
+
+ # Expands the short notation to the full colorcode notation
+ # 123456 -> 123456
+ # 123 -> 112233
+ def normalize_color(color)
+ return (color[0, 1] * 2) + (color[1, 1] * 2) + (color[2, 1] * 2) if color.length == 3
+
+ color
+ end
+
def custom_data(data)
super(data).merge(markdown: true)
end
diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb
index 680752c3d56..6e4753470a3 100644
--- a/app/models/integrations/hangouts_chat.rb
+++ b/app/models/integrations/hangouts_chat.rb
@@ -30,12 +30,15 @@ module Integrations
end
def help
- docs_link = ActionController::Base.helpers.link_to _('How do I set up a Google Chat webhook?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/hangouts_chat'), target: '_blank', rel: 'noopener noreferrer'
- s_('Before enabling this integration, create a webhook for the room in Google Chat where you want to receive notifications from this project. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+ docs_link = ActionController::Base.helpers.link_to(_('How do I set up a Google Chat webhook?'),
+ Rails.application.routes.url_helpers.help_page_url('user/project/integrations/hangouts_chat'),
+ target: '_blank', rel: 'noopener noreferrer')
+ format(
+ s_('Before enabling this integration, create a webhook for the room in Google Chat where you want to receive ' \
+ 'notifications from this project. %{docs_link}').html_safe, docs_link: docs_link.html_safe)
end
- def default_channel_placeholder
- end
+ def default_channel_placeholder; end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page]
@@ -43,14 +46,20 @@ module Integrations
private
- def notify(message, opts)
+ def notify(message, _opts)
url = webhook.dup
key = parse_thread_key(message)
url = Gitlab::Utils.add_url_parameters(url, { threadKey: key }) if key
- simple_text = parse_simple_text_message(message)
- ::HangoutsChat::Sender.new(url).simple(simple_text)
+ payload = { text: parse_simple_text_message(message) }
+
+ Gitlab::HTTP.post(
+ url,
+ body: payload.to_json,
+ headers: { 'Content-Type' => 'application/json' },
+ parse: nil
+ ).response
end
# Returns an appropriate key for threading messages in google chat
diff --git a/app/models/integrations/integration_list.rb b/app/models/integrations/integration_list.rb
new file mode 100644
index 00000000000..ab03e5c0e0a
--- /dev/null
+++ b/app/models/integrations/integration_list.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Integrations
+ class IntegrationList
+ def initialize(batch, integration_hash, association)
+ @batch = batch
+ @integration_hash = integration_hash
+ @association = association
+ end
+
+ def to_array
+ [Integration, columns, values]
+ end
+
+ private
+
+ attr_reader :batch, :integration_hash, :association
+
+ def columns
+ integration_hash.keys << "#{association}_id"
+ end
+
+ def values
+ batch.select(:id).map do |record|
+ integration_hash.values << record.id
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index d8d1f860e9a..f6e99454cb1 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -11,8 +11,12 @@ module Integrations
PROJECTS_PER_PAGE = 50
JIRA_CLOUD_HOST = '.atlassian.net'
- ATLASSIAN_REFERRER_GITLAB_COM = { atlOrigin: 'eyJpIjoiY2QyZTJiZDRkNGZhNGZlMWI3NzRkNTBmZmVlNzNiZTkiLCJwIjoianN3LWdpdGxhYi1pbnQifQ' }.freeze
- ATLASSIAN_REFERRER_SELF_MANAGED = { atlOrigin: 'eyJpIjoiYjM0MTA4MzUyYTYxNDVkY2IwMzVjOGQ3ZWQ3NzMwM2QiLCJwIjoianN3LWdpdGxhYlNNLWludCJ9' }.freeze
+ ATLASSIAN_REFERRER_GITLAB_COM = {
+ atlOrigin: 'eyJpIjoiY2QyZTJiZDRkNGZhNGZlMWI3NzRkNTBmZmVlNzNiZTkiLCJwIjoianN3LWdpdGxhYi1pbnQifQ'
+ }.freeze
+ ATLASSIAN_REFERRER_SELF_MANAGED = {
+ atlOrigin: 'eyJpIjoiYjM0MTA4MzUyYTYxNDVkY2IwMzVjOGQ3ZWQ3NzMwM2QiLCJwIjoianN3LWdpdGxhYlNNLWludCJ9'
+ }.freeze
API_ENDPOINTS = {
find_issue: "/rest/api/2/issue/%s",
@@ -28,11 +32,13 @@ module Integrations
AUTH_TYPE_BASIC = 0
AUTH_TYPE_PAT = 1
- SNOWPLOW_EVENT_CATEGORY = self.name
+ SNOWPLOW_EVENT_CATEGORY = name
validates :url, public_url: true, presence: true, if: :activated?
validates :api_url, public_url: true, allow_blank: true
- validates :username, presence: true, if: ->(object) { object.activated? && !object.personal_access_token_authorization? }
+ validates :username, presence: true, if: ->(object) {
+ object.activated? && !object.personal_access_token_authorization?
+ }
validates :password, presence: true, if: :activated?
validates :jira_auth_type, presence: true, inclusion: { in: [AUTH_TYPE_BASIC, AUTH_TYPE_PAT] }, if: :activated?
validates :jira_issue_prefix, untrusted_regexp: true, length: { maximum: 255 }, if: :activated?
@@ -130,7 +136,7 @@ module Integrations
end
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
- def reference_pattern(only_long: true)
+ def reference_pattern(*)
@reference_pattern ||= jira_issue_match_regex
end
@@ -144,7 +150,7 @@ module Integrations
end
def data_fields
- jira_tracker_data || self.build_jira_tracker_data
+ jira_tracker_data || build_jira_tracker_data
end
def set_default_data
@@ -186,8 +192,13 @@ module Integrations
end
def help
- jira_doc_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('integration/jira/index') }
- s_("JiraService|You must configure Jira before enabling this integration. %{jira_doc_link_start}Learn more.%{link_end}") % { jira_doc_link_start: jira_doc_link_start, link_end: '</a>'.html_safe }
+ jira_doc_link_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe,
+ url: help_page_path('integration/jira/index'))
+ format(
+ s_("JiraService|You must configure Jira before enabling this integration. " \
+ "%{jira_doc_link_start}Learn more.%{link_end}"),
+ jira_doc_link_start: jira_doc_link_start,
+ link_end: '</a>'.html_safe)
end
def title
@@ -212,7 +223,8 @@ module Integrations
{
type: SECTION_TYPE_JIRA_TRIGGER,
title: _('Trigger'),
- description: s_('JiraService|When a Jira issue is mentioned in a commit or merge request, a remote link and comment (if enabled) will be created.')
+ description: s_('JiraService|When a Jira issue is mentioned in a commit or merge request, a remote link ' \
+ 'and comment (if enabled) will be created.')
},
{
type: SECTION_TYPE_CONFIGURATION,
@@ -313,7 +325,8 @@ module Integrations
override :create_cross_reference_note
def create_cross_reference_note(external_issue, mentioned_in, author)
unless can_cross_reference?(mentioned_in)
- return s_("JiraService|Events for %{noteable_model_name} are disabled.") % { noteable_model_name: mentioned_in.model_name.plural.humanize(capitalize: false) }
+ return format(s_("JiraService|Events for %{noteable_model_name} are disabled."),
+ noteable_model_name: mentioned_in.model_name.plural.humanize(capitalize: false))
end
jira_issue = find_issue(external_issue.id)
@@ -381,6 +394,10 @@ module Integrations
jira_auth_type == AUTH_TYPE_PAT
end
+ def avatar_url
+ ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/jira.svg')
+ end
+
private
def jira_issue_match_regex
@@ -398,10 +415,9 @@ module Integrations
end
def server_info
- strong_memoize(:server_info) do
- client_url.present? ? jira_request(API_ENDPOINTS[:server_info]) { client.ServerInfo.all.attrs } : nil
- end
+ client_url.present? ? jira_request(API_ENDPOINTS[:server_info]) { client.ServerInfo.all.attrs } : nil
end
+ strong_memoize_attr :server_info
def can_cross_reference?(mentioned_in)
case mentioned_in
@@ -430,7 +446,8 @@ module Integrations
true
rescue StandardError => e
path = API_ENDPOINTS[:transition_issue] % issue.id
- log_exception(e, message: 'Issue transition failed', client_url: client_url, client_path: path, client_status: '400')
+ log_exception(e, message: 'Issue transition failed', client_url: client_url, client_path: path,
+ client_status: '400')
false
end
@@ -488,9 +505,9 @@ module Integrations
link_title = "#{entity_name.capitalize} - #{entity_title}"
link_props = build_remote_link_props(url: entity_url, title: link_title)
- unless comment_exists?(issue, message)
- send_message(issue, message, link_props)
- end
+ return if comment_exists?(issue, message)
+
+ send_message(issue, message, link_props)
end
def comment_message(data)
@@ -503,21 +520,22 @@ module Integrations
project_link = build_jira_link(project.full_name, Gitlab::Routing.url_helpers.project_url(project))
branch =
if entity[:branch].present?
- s_('JiraService| on branch %{branch_link}') % {
- branch_link: build_jira_link(entity[:branch], project_tree_url(project, entity[:branch]))
- }
+ format(s_('JiraService| on branch %{branch_link}'),
+ branch_link: build_jira_link(entity[:branch], project_tree_url(project, entity[:branch])))
end
entity_message = entity[:description].presence if all_details?
entity_message ||= entity[:title].chomp
- s_('JiraService|%{user_link} mentioned this issue in %{entity_link} of %{project_link}%{branch}:{quote}%{entity_message}{quote}') % {
+ format(
+ s_('JiraService|%{user_link} mentioned this issue in %{entity_link} of ' \
+ '%{project_link}%{branch}:{quote}%{entity_message}{quote}'),
user_link: user_link,
entity_link: entity_link,
project_link: project_link,
branch: branch,
entity_message: entity_message
- }
+ )
end
def build_jira_link(title, url)
@@ -586,13 +604,13 @@ module Integrations
end
def resource_url(resource)
- "#{Settings.gitlab.base_url.chomp("/")}#{resource}"
+ "#{Settings.gitlab.base_url.chomp('/')}#{resource}"
end
def build_entity_url(entity_type, entity_id)
polymorphic_url(
[
- self.project,
+ project,
entity_type.to_sym
],
id: entity_id,
@@ -631,7 +649,8 @@ module Integrations
yield
rescue StandardError => e
@error = e
- log_exception(e, message: 'Error sending message', client_url: client_url, client_path: path, client_status: e.try(:code))
+ log_exception(e, message: 'Error sending message', client_url: client_url, client_path: path,
+ client_status: e.try(:code))
nil
end
@@ -648,7 +667,8 @@ module Integrations
results = server_info
unless results.present?
- Gitlab::AppLogger.warn(message: "Jira API returned no ServerInfo, setting deployment_type from URL", server_info: results, url: client_url)
+ Gitlab::AppLogger.warn(message: "Jira API returned no ServerInfo, setting deployment_type from URL",
+ server_info: results, url: client_url)
return set_deployment_type_from_url
end
@@ -681,13 +701,25 @@ module Integrations
end
def jira_issues_section_description
- jira_issues_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('integration/jira/issues') }
- description = s_('JiraService|Work on Jira issues without leaving GitLab. Add a Jira menu to access a read-only list of your Jira issues. %{jira_issues_link_start}Learn more.%{link_end}') % { jira_issues_link_start: jira_issues_link_start, link_end: '</a>'.html_safe }
+ jira_issues_link_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe,
+ url: help_page_path('integration/jira/issues'))
+ description = format(
+ s_('JiraService|Work on Jira issues without leaving GitLab. Add a Jira menu to access a read-only list of ' \
+ 'your Jira issues. %{jira_issues_link_start}Learn more.%{link_end}'),
+ jira_issues_link_start: jira_issues_link_start,
+ link_end: '</a>'.html_safe
+ )
if project&.issues_enabled?
- gitlab_issues_link_start = '<a href="%{url}">'.html_safe % { url: edit_project_path(project, anchor: 'js-shared-permissions') }
+ gitlab_issues_link_start = format('<a href="%{url}">'.html_safe, url: edit_project_path(project,
+ anchor: 'js-shared-permissions'))
description += '<br><br>'.html_safe
- description += s_("JiraService|Displaying Jira issues while leaving GitLab issues also enabled might be confusing. Consider %{gitlab_issues_link_start}disabling GitLab issues%{link_end} if they won't otherwise be used.") % { gitlab_issues_link_start: gitlab_issues_link_start, link_end: '</a>'.html_safe }
+ description += format(
+ s_("JiraService|Displaying Jira issues while leaving GitLab issues also enabled might be confusing. " \
+ "Consider %{gitlab_issues_link_start}disabling GitLab issues%{link_end} if they won't otherwise be used."),
+ gitlab_issues_link_start: gitlab_issues_link_start,
+ link_end: '</a>'.html_safe
+ )
end
description
diff --git a/app/models/integrations/pipelines_email.rb b/app/models/integrations/pipelines_email.rb
index fa22bd1a73c..01efbc3e4a4 100644
--- a/app/models/integrations/pipelines_email.rb
+++ b/app/models/integrations/pipelines_email.rb
@@ -37,8 +37,8 @@ module Integrations
# `notify_only_default_branch`. Now we have a string property named
# `branches_to_be_notified`. Instead of doing a background migration, we
# opted to set a value for the new property based on the old one, if
- # users hasn't specified one already. When users edit the service and
- # selects a value for this new property, it will override everything.
+ # users haven't specified one already. When users edit the integration and
+ # select a value for this new property, it will override everything.
self.branches_to_be_notified ||= notify_only_default_branch? ? "default" : "all"
end
diff --git a/app/models/integrations/pivotaltracker.rb b/app/models/integrations/pivotaltracker.rb
index f42a872c49e..b3cbc988dd6 100644
--- a/app/models/integrations/pivotaltracker.rb
+++ b/app/models/integrations/pivotaltracker.rb
@@ -65,6 +65,10 @@ module Integrations
end
end
+ def avatar_url
+ ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/pivotal-tracker.svg')
+ end
+
private
def allowed_branch?(ref)
diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb
index 8474a5b7adf..ff8d07a1b4c 100644
--- a/app/models/integrations/prometheus.rb
+++ b/app/models/integrations/prometheus.rb
@@ -185,7 +185,7 @@ module Integrations
# Remove in next required stop after %16.4
# https://gitlab.com/gitlab-org/gitlab/-/issues/338838
def sync_http_integration!
- return unless manual_configuration_changed?
+ return unless manual_configuration_changed? && !manual_configuration_was.nil?
project.alert_management_http_integrations
.for_endpoint_identifier('legacy-prometheus')
diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb
index e97c7e5e738..2feae29f627 100644
--- a/app/models/integrations/pushover.rb
+++ b/app/models/integrations/pushover.rb
@@ -125,5 +125,9 @@ module Integrations
Gitlab::HTTP.post('/messages.json', base_uri: BASE_URI, body: pushover_data)
end
+
+ def avatar_url
+ ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/pushover.svg')
+ end
end
end
diff --git a/app/models/integrations/telegram.rb b/app/models/integrations/telegram.rb
index 7c196720386..71fe6f8d6ef 100644
--- a/app/models/integrations/telegram.rb
+++ b/app/models/integrations/telegram.rb
@@ -26,6 +26,12 @@ module Integrations
section: SECTION_TYPE_CONFIGURATION,
help: 'If selected, successful pipelines do not trigger a notification event.'
+ field :branches_to_be_notified,
+ type: :select,
+ section: SECTION_TYPE_CONFIGURATION,
+ title: -> { s_('Integrations|Branches for which notifications are to be sent') },
+ choices: -> { branch_choices }
+
with_options if: :activated? do
validates :token, :room, presence: true
end
@@ -60,6 +66,10 @@ module Integrations
super - ['deployment']
end
+ def avatar_url
+ ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/telegram.svg')
+ end
+
private
def set_webhook
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 58383a6a329..b207785021d 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -543,7 +543,9 @@ class Issue < ApplicationRecord
end
end
- def related_issues(current_user, preload: nil)
+ def related_issues(current_user = nil, authorize: true, preload: nil)
+ return [] if new_record?
+
related_issues =
linked_issues_select
.joins("INNER JOIN issue_links ON
@@ -554,6 +556,7 @@ class Issue < ApplicationRecord
.reorder('issue_link_id')
related_issues = yield related_issues if block_given?
+ return related_issues unless authorize
cross_project_filter = -> (issues) { issues.where(project: project) }
Ability.issues_readable_by_user(related_issues,
@@ -561,6 +564,10 @@ class Issue < ApplicationRecord
filters: { read_cross_project: cross_project_filter })
end
+ def linked_items_count
+ related_issues(authorize: false).size
+ end
+
def can_be_worked_on?
!self.closed? && !self.project.forked?
end
@@ -688,20 +695,14 @@ class Issue < ApplicationRecord
# for performance reasons, check commit: 002ad215818450d2cbbc5fa065850a953dc7ada8
# Make sure to sync this method with issue_policy.rb
def readable_by?(user)
- if !project.issues_enabled?
- false
- elsif user.can_read_all_resources?
- true
- elsif project.personal? && project.team.owner?(user)
+ if user.can_read_all_resources?
true
- elsif confidential? && !assignee_or_author?(user)
- project.member?(user, Gitlab::Access::REPORTER)
elsif hidden?
false
- elsif project.public? || (project.internal? && !user.external?)
- project.feature_available?(:issues, user)
+ elsif project
+ project_level_readable_by?(user)
else
- project.member?(user)
+ group_level_readable_by?(user)
end
end
@@ -754,6 +755,31 @@ class Issue < ApplicationRecord
private
+ def project_level_readable_by?(user)
+ if !project.issues_enabled?
+ false
+ elsif project.personal? && project.team.owner?(user)
+ true
+ elsif confidential? && !assignee_or_author?(user)
+ project.member?(user, Gitlab::Access::REPORTER)
+ elsif project.public? || (project.internal? && !user.external?)
+ project.feature_available?(:issues, user)
+ else
+ project.member?(user)
+ end
+ end
+
+ def group_level_readable_by?(user)
+ # This should never happen as we don't support personal namespace level issues. Just additional safety.
+ return false unless namespace.is_a?(::Group)
+
+ if confidential? && !assignee_or_author?(user)
+ namespace.member?(user, Gitlab::Access::REPORTER)
+ else
+ namespace.member?(user)
+ end
+ end
+
def due_date_after_start_date
return unless start_date.present? && due_date.present?
diff --git a/app/models/issue_user_mention.rb b/app/models/issue_user_mention.rb
index ad0df0dca78..6c3bedfccca 100644
--- a/app/models/issue_user_mention.rb
+++ b/app/models/issue_user_mention.rb
@@ -5,5 +5,5 @@ class IssueUserMention < UserMention
belongs_to :note
include IgnorableColumns
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
end
diff --git a/app/models/lfs_download_object.rb b/app/models/lfs_download_object.rb
index 046e47262dd..ec190ebf5d8 100644
--- a/app/models/lfs_download_object.rb
+++ b/app/models/lfs_download_object.rb
@@ -19,6 +19,15 @@ class LfsDownloadObject
@headers = headers || {}
end
+ def to_hash
+ {
+ oid: oid,
+ size: size,
+ link: link,
+ headers: headers
+ }.stringify_keys
+ end
+
def sanitized_uri
@sanitized_uri ||= Gitlab::UrlSanitizer.new(link)
end
diff --git a/app/models/loose_foreign_keys/deleted_record.rb b/app/models/loose_foreign_keys/deleted_record.rb
index 1d26c3c11e4..6af80686ec2 100644
--- a/app/models/loose_foreign_keys/deleted_record.rb
+++ b/app/models/loose_foreign_keys/deleted_record.rb
@@ -36,34 +36,24 @@ class LooseForeignKeys::DeletedRecord < Gitlab::Database::SharedModel
enum status: { pending: 1, processed: 2 }, _prefix: :status
def self.load_batch_for_table(table, batch_size)
- if Feature.enabled?("loose_foreign_keys_batch_load_using_union")
- partition_names = Gitlab::Database::PostgresPartitionedTable.each_partition(table_name).map(&:name)
-
- unions = partition_names.map do |partition_name|
- partition_number = partition_name[/\d+/].to_i
-
- select(arel_table[Arel.star], arel_table[:partition].as('partition_number'))
- .from("#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{partition_name} AS #{table_name}")
- .for_table(table)
- .where(partition: partition_number)
- .status_pending
- .consume_order
- .limit(batch_size)
- end
-
- select(arel_table[Arel.star])
- .from_union(unions, remove_duplicates: false, remove_order: false)
- .limit(batch_size)
- .to_a
- else
- # selecting partition as partition_number to workaround the sliding partitioning column ignore
+ partition_names = Gitlab::Database::PostgresPartitionedTable.each_partition(table_name).map(&:name)
+
+ unions = partition_names.map do |partition_name|
+ partition_number = partition_name[/\d+/].to_i
+
select(arel_table[Arel.star], arel_table[:partition].as('partition_number'))
+ .from("#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{partition_name} AS #{table_name}")
.for_table(table)
+ .where(partition: partition_number)
.status_pending
.consume_order
.limit(batch_size)
- .to_a
end
+
+ select(arel_table[Arel.star])
+ .from_union(unions, remove_duplicates: false, remove_order: false)
+ .limit(batch_size)
+ .to_a
end
def self.mark_records_processed(records)
diff --git a/app/models/member.rb b/app/models/member.rb
index cdf40eaa8f5..77e283044ea 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -29,10 +29,8 @@ class Member < ApplicationRecord
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :member_namespace, inverse_of: :namespace_members, foreign_key: 'member_namespace_id', class_name: 'Namespace'
belongs_to :member_role
- has_one :member_task
delegate :name, :username, :email, :last_activity_on, to: :user, prefix: true
- delegate :tasks_to_be_done, to: :member_task, allow_nil: true
validates :expires_at, allow_blank: true, future_date: true
validates :user, presence: true, unless: :invite?
@@ -525,6 +523,7 @@ class Member < ApplicationRecord
def validate_access_level_locked_for_member_role
return unless member_role_id
+ return if member_role_changed? # it is ok to change the access level when changing member role
if access_level_changed?
errors.add(:access_level, _("cannot be changed since member is associated with a custom role"))
@@ -577,12 +576,6 @@ class Member < ApplicationRecord
def after_accept_invite
post_create_hook
-
- run_after_commit_or_now do
- if member_task
- TasksToBeDone::CreateWorker.perform_async(member_task.id, created_by_id, [user_id.to_i])
- end
- end
end
def after_decline_invite
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 52b9c3a80e3..b5a590d646e 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -62,9 +62,13 @@ class GroupMember < Member
return false unless access_level == Gitlab::Access::OWNER
return last_owner unless last_owner.nil?
- group.member_owners_excluding_project_bots.where.not(
- group: group, user_id: user_id
- ).empty?
+ owners = group.member_owners_excluding_project_bots
+
+ owners.reject! do |member|
+ member.group == group && member.user_id == user_id
+ end
+
+ owners.empty?
end
private
diff --git a/app/models/members/last_group_owner_assigner.rb b/app/models/members/last_group_owner_assigner.rb
index 45cd8d8b000..707cd7bf31c 100644
--- a/app/models/members/last_group_owner_assigner.rb
+++ b/app/models/members/last_group_owner_assigner.rb
@@ -22,7 +22,7 @@ class LastGroupOwnerAssigner
end
def owner_ids
- @owner_ids ||= owners.where(id: member_ids).ids
+ @owner_ids ||= member_ids & owners.map(&:id)
end
def member_ids
@@ -30,6 +30,6 @@ class LastGroupOwnerAssigner
end
def owners
- @owners ||= group.member_owners_excluding_project_bots.load
+ @owners ||= group.member_owners_excluding_project_bots
end
end
diff --git a/app/models/members/member_task.rb b/app/models/members/member_task.rb
deleted file mode 100644
index 6cf6b1adb45..00000000000
--- a/app/models/members/member_task.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-class MemberTask < ApplicationRecord
- TASKS = {
- code: 0,
- ci: 1,
- issues: 2
- }.freeze
-
- belongs_to :member
- belongs_to :project
-
- validates :member, :project, presence: true
- validates :tasks, inclusion: { in: TASKS.values }
- validate :tasks_uniqueness
- validate :project_in_member_source
-
- scope :for_members, -> (members) { joins(:member).where(member: members) }
-
- def tasks_to_be_done
- Array(self[:tasks]).map { |task| TASKS.key(task) }
- end
-
- def tasks_to_be_done=(tasks)
- self[:tasks] = Array(tasks).map do |task|
- TASKS[task.to_sym]
- end.uniq
- end
-
- private
-
- def tasks_uniqueness
- errors.add(:tasks, 'are not unique') unless Array(tasks).length == Array(tasks).uniq.length
- end
-
- def project_in_member_source
- case member
- when GroupMember
- errors.add(:project, _('is not in the member group')) unless project.namespace == member.source
- when ProjectMember
- errors.add(:project, _('is not the member project')) unless project == member.source
- end
- end
-end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 6a72ed6476e..d9726e76c4b 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -337,15 +337,19 @@ class MergeRequest < ApplicationRecord
scope :by_squash_commit_sha, -> (sha) do
where(squash_commit_sha: sha)
end
- scope :by_merge_or_squash_commit_sha, -> (sha) do
- from_union([by_squash_commit_sha(sha), by_merge_commit_sha(sha)])
+ scope :by_merged_commit_sha, -> (sha) do
+ where(merged_commit_sha: sha)
+ end
+ scope :by_merged_or_merge_or_squash_commit_sha, -> (sha) do
+ from_union([by_squash_commit_sha(sha), by_merge_commit_sha(sha), by_merged_commit_sha(sha)])
end
scope :by_related_commit_sha, -> (sha) do
from_union(
[
by_commit_sha(sha),
by_squash_commit_sha(sha),
- by_merge_commit_sha(sha)
+ by_merge_commit_sha(sha),
+ by_merged_commit_sha(sha)
]
)
end
@@ -1231,19 +1235,23 @@ class MergeRequest < ApplicationRecord
}
end
- def mergeable?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, check_mergeability_retry_lease: false, skip_rebase_check: false)
+ def mergeable?(
+ skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, check_mergeability_retry_lease: false,
+ skip_draft_check: false, skip_rebase_check: false, skip_blocked_check: false)
+
return false unless mergeable_state?(
skip_ci_check: skip_ci_check,
skip_discussions_check: skip_discussions_check,
- skip_approved_check: skip_approved_check
+ skip_draft_check: skip_draft_check,
+ skip_approved_check: skip_approved_check,
+ skip_blocked_check: skip_blocked_check
)
check_mergeability(sync_retry_lease: check_mergeability_retry_lease)
-
- can_be_merged? && (!should_be_rebased? || skip_rebase_check)
+ mergeable_git_state?(skip_rebase_check: skip_rebase_check)
end
- def mergeability_checks
+ def self.mergeable_state_checks
# We want to have the cheapest checks first in the list, that way we can
# fail fast before running the more expensive ones.
#
@@ -1256,17 +1264,52 @@ class MergeRequest < ApplicationRecord
]
end
- def mergeable_state?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false)
+ def self.mergeable_git_state_checks
+ [
+ ::MergeRequests::Mergeability::CheckConflictStatusService,
+ ::MergeRequests::Mergeability::CheckRebaseStatusService
+ ]
+ end
+
+ def self.all_mergeability_checks
+ mergeable_state_checks + mergeable_git_state_checks
+ end
+
+ def mergeable_state?(
+ skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false,
+ skip_draft_check: false, skip_blocked_check: false)
additional_checks = execute_merge_checks(
+ self.class.mergeable_state_checks,
params: {
skip_ci_check: skip_ci_check,
skip_discussions_check: skip_discussions_check,
- skip_approved_check: skip_approved_check
+ skip_approved_check: skip_approved_check,
+ skip_draft_check: skip_draft_check,
+ skip_blocked_check: skip_blocked_check
}
)
additional_checks.success?
end
+ def mergeable_git_state?(skip_rebase_check: false)
+ checks = execute_merge_checks(
+ self.class.mergeable_git_state_checks,
+ params: {
+ skip_rebase_check: skip_rebase_check
+ }
+ )
+
+ checks.success?
+ end
+
+ def all_mergeability_checks_results
+ execute_merge_checks(
+ self.class.all_mergeability_checks,
+ params: {},
+ execute_all: true
+ ).payload[:results]
+ end
+
def ff_merge_possible?
project.repository.ancestor?(target_branch_sha, diff_head_sha)
end
@@ -1689,7 +1732,7 @@ class MergeRequest < ApplicationRecord
end
def has_terraform_reports?
- actual_head_pipeline&.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:terraform))
+ actual_head_pipeline&.has_reports?(Ci::JobArtifact.of_report_type(:terraform))
end
def compare_accessibility_reports
@@ -1957,15 +2000,11 @@ class MergeRequest < ApplicationRecord
end
def base_pipeline
- @base_pipeline ||= project.ci_pipelines
- .order(id: :desc)
- .find_by(sha: diff_base_sha, ref: target_branch)
+ @base_pipeline ||= base_pipelines.last
end
def merge_base_pipeline
- @merge_base_pipeline ||= project.ci_pipelines
- .order(id: :desc)
- .find_by(sha: actual_head_pipeline.target_sha, ref: target_branch)
+ @merge_base_pipeline ||= merge_base_pipelines.last
end
def discussions_rendered_on_frontend?
@@ -2081,9 +2120,11 @@ class MergeRequest < ApplicationRecord
false # Overridden in EE
end
- def execute_merge_checks(params: {})
+ def execute_merge_checks(checks, params: {}, execute_all: false)
# rubocop: disable CodeReuse/ServiceClass
- MergeRequests::Mergeability::RunChecksService.new(merge_request: self, params: params).execute
+ MergeRequests::Mergeability::RunChecksService
+ .new(merge_request: self, params: params)
+ .execute(checks, execute_all: execute_all)
# rubocop: enable CodeReuse/ServiceClass
end
@@ -2115,10 +2156,35 @@ class MergeRequest < ApplicationRecord
!squash && target_project.squash_always?
end
+ def current_patch_id_sha
+ return merge_request_diff.patch_id_sha if merge_request_diff.patch_id_sha.present?
+
+ base_sha = diff_refs&.base_sha
+ head_sha = diff_refs&.head_sha
+
+ return unless base_sha && head_sha
+ return if base_sha == head_sha
+
+ project.repository.get_patch_id(base_sha, head_sha)
+ end
+
private
attr_accessor :skip_fetch_ref
+ def merge_base_pipelines
+ target_branch_pipelines_for(sha: actual_head_pipeline.target_sha)
+ end
+
+ def base_pipelines
+ target_branch_pipelines_for(sha: diff_base_sha)
+ end
+
+ def target_branch_pipelines_for(sha:)
+ project.ci_pipelines
+ .where(sha: sha, ref: target_branch)
+ end
+
def set_draft_status
self.draft = draft?
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index bddc03d8b21..900f4bcfeb2 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -210,6 +210,8 @@ class MergeRequestDiff < ApplicationRecord
# and save it to the database as serialized data
def save_git_content
ensure_commit_shas
+ set_patch_id_sha
+
save_commits
save_diffs
@@ -223,6 +225,16 @@ class MergeRequestDiff < ApplicationRecord
keep_around_commits unless importing?
end
+ def set_patch_id_sha
+ return unless base_commit_sha && head_commit_sha
+ return if base_commit_sha == head_commit_sha
+
+ self.patch_id_sha = project.repository&.get_patch_id(
+ base_commit_sha,
+ head_commit_sha
+ )
+ end
+
def set_as_latest_diff
# Don't set merge_head diff as latest so it won't get considered as the
# MergeRequest#merge_request_diff.
diff --git a/app/models/merge_request_user_mention.rb b/app/models/merge_request_user_mention.rb
index 3157f1ca2aa..548a91162cd 100644
--- a/app/models/merge_request_user_mention.rb
+++ b/app/models/merge_request_user_mention.rb
@@ -3,7 +3,7 @@
class MergeRequestUserMention < UserMention
include IgnorableColumns
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
belongs_to :merge_request
belongs_to :note
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index eb0da368c7b..d5b9a4dc30f 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -54,6 +54,7 @@ class Milestone < ApplicationRecord
scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) }
scope :reorder_by_due_date_asc, -> { reorder(arel_table[:due_date].asc.nulls_last) }
scope :with_api_entity_associations, -> { preload(project: [:project_feature, :route, namespace: :route]) }
+ scope :preload_for_indexing, -> { includes(project: [:project_feature]) }
scope :order_by_dates_and_title, -> { order(due_date: :asc, start_date: :asc, title: :asc) }
validates :group, presence: true, unless: :project
diff --git a/app/models/ml/model.rb b/app/models/ml/model.rb
index fb15b9fea72..27f03ed5857 100644
--- a/app/models/ml/model.rb
+++ b/app/models/ml/model.rb
@@ -19,6 +19,11 @@ module Ml
has_one :latest_version, -> { latest_by_model }, class_name: 'Ml::ModelVersion', inverse_of: :model
scope :including_latest_version, -> { includes(:latest_version) }
+ scope :with_version_count, -> {
+ left_outer_joins(:versions)
+ .select("ml_models.*, count(ml_model_versions.id) as version_count")
+ .group(:id)
+ }
scope :by_project, ->(project) { where(project_id: project.id) }
def valid_default_experiment?
@@ -32,5 +37,9 @@ module Ml
create_with(default_experiment: experiment)
.find_or_create_by(project: project, name: name)
end
+
+ def self.by_project_id_and_id(project_id, id)
+ find_by(project_id: project_id, id: id)
+ end
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index ea0ea4de5b5..733b89fcaf2 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -6,7 +6,6 @@ class Namespace < ApplicationRecord
include Gitlab::VisibilityLevel
include Routable
include AfterCommitQueue
- include Storage::LegacyNamespace
include Gitlab::SQL::Pattern
include FeatureGate
include FromUnion
@@ -18,6 +17,9 @@ class Namespace < ApplicationRecord
include Ci::NamespaceSettings
include Referable
include CrossDatabaseIgnoredTables
+ include IgnorableColumns
+
+ ignore_column :unlock_membership_to_ldap, remove_with: '16.7', remove_after: '2023-11-16'
cross_database_ignore_tables %w[routes redirect_routes], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424277'
@@ -97,7 +99,10 @@ class Namespace < ApplicationRecord
validates :path,
presence: true,
length: { maximum: URL_MAX_LENGTH }
- validate :container_registry_namespace_path_validation
+
+ validates :path,
+ format: { with: Gitlab::Regex.oci_repository_path_regex, message: Gitlab::Regex.oci_repository_path_regex_message },
+ if: :path_changed?
validates :path, namespace_path: true, if: ->(n) { !n.project_namespace? }
# Project path validator is used for project namespaces for now to assure
@@ -147,7 +152,6 @@ class Namespace < ApplicationRecord
before_update :sync_share_with_group_lock_with_parent, if: :parent_changed?
after_update :force_share_with_group_lock_on_descendants, if: -> { saved_change_to_share_with_group_lock? && share_with_group_lock? }
after_update :expire_first_auto_devops_config_cache, if: -> { saved_change_to_auto_devops_enabled? }
- after_update :move_dir, if: :saved_change_to_path_or_parent?, unless: -> { is_a?(Namespaces::ProjectNamespace) }
after_save :reload_namespace_details
@@ -155,8 +159,6 @@ class Namespace < ApplicationRecord
after_sync_traversal_ids :schedule_sync_event_worker # custom callback defined in Namespaces::Traversal::Linear
- # Legacy Storage specific hooks
-
after_commit :expire_child_caches, on: :update, if: -> {
Feature.enabled?(:cached_route_lookups, self, type: :ops) &&
saved_change_to_name? || saved_change_to_path? || saved_change_to_parent_id?
@@ -289,13 +291,6 @@ class Namespace < ApplicationRecord
"#{self.class.reference_prefix}#{full_path}"
end
- def container_registry_namespace_path_validation
- return if Feature.disabled?(:restrict_special_characters_in_namespace_path, self)
- return if !path_changed? || path.match?(Gitlab::Regex.oci_repository_path_regex)
-
- errors.add(:path, Gitlab::Regex.oci_repository_path_regex_message)
- end
-
def package_settings
package_setting_relation || build_package_setting_relation
end
@@ -313,7 +308,7 @@ class Namespace < ApplicationRecord
end
def human_name
- owner_name
+ owner_name || path
end
def any_project_has_container_registry_tags?
diff --git a/app/models/namespace/detail.rb b/app/models/namespace/detail.rb
index a65027733e9..f5e850830bc 100644
--- a/app/models/namespace/detail.rb
+++ b/app/models/namespace/detail.rb
@@ -1,13 +1,6 @@
# frozen_string_literal: true
class Namespace::Detail < ApplicationRecord
- include IgnorableColumns
-
- ignore_column :dashboard_notification_at, remove_with: '16.5', remove_after: '2023-08-22'
- ignore_column :dashboard_enforcement_at, remove_with: '16.5', remove_after: '2023-08-22'
- ignore_column :next_over_limit_check_at, remove_with: '16.5', remove_after: '2023-08-22'
- ignore_column :free_user_cap_over_limit_notified_at, remove_with: '16.5', remove_after: '2023-08-22'
-
belongs_to :namespace, inverse_of: :namespace_details
validates :namespace, presence: true
validates :description, length: { maximum: 255 }
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 8d5d788c738..3befcdeaec5 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -10,7 +10,7 @@ class NamespaceSetting < ApplicationRecord
belongs_to :namespace, inverse_of: :namespace_settings
enum jobs_to_be_done: { basics: 0, move_repository: 1, code_storage: 2, exploring: 3, ci: 4, other: 5 }, _suffix: true
- enum enabled_git_access_protocol: { all: 0, ssh: 1, http: 2 }, _suffix: true
+ enum enabled_git_access_protocol: { all: 0, ssh: 1, http: 2, ssh_certificates: 3 }, _suffix: true
attribute :default_branch_protection_defaults, default: -> { {} }
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index 1ca3c8e85f3..c3348c49ea1 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -61,8 +61,6 @@ module Namespaces
# INPUT: [[4909902], [4909902,51065789], [4909902,51065793], [7135830], [15599674, 1], [15599674, 1, 3], [15599674, 2]]
# RESULT: [[4909902], [7135830], [15599674, 1], [15599674, 2]]
def shortest_traversal_ids_prefixes
- raise ArgumentError, 'Feature not supported since the `:use_traversal_ids` is disabled' unless use_traversal_ids?
-
prefixes = []
# The array needs to be sorted (O(nlogn)) to ensure shortest elements are always first
@@ -91,8 +89,6 @@ module Namespaces
end
def use_traversal_ids?
- return false unless Feature.enabled?(:use_traversal_ids)
-
traversal_ids.present?
end
diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb
index 6e79e3ac9a1..c63639e721a 100644
--- a/app/models/namespaces/traversal/linear_scopes.rb
+++ b/app/models/namespaces/traversal/linear_scopes.rb
@@ -12,14 +12,10 @@ module Namespaces
# list of namespace IDs, it can be faster to reference the ID in
# traversal_ids than the primary key ID column.
def as_ids
- return super unless use_traversal_ids?
-
select(Arel.sql('namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)]').as('id'))
end
def roots
- return super unless use_traversal_ids?
-
root_ids = all.select("#{quoted_table_name}.traversal_ids[1]").distinct
unscoped.where(id: root_ids)
end
@@ -37,20 +33,14 @@ module Namespaces
end
def self_and_descendants(include_self: true)
- return super unless use_traversal_ids?
-
self_and_descendants_with_comparison_operators(include_self: include_self)
end
def self_and_descendant_ids(include_self: true)
- return super unless use_traversal_ids?
-
self_and_descendants(include_self: include_self).as_ids
end
def self_and_hierarchy
- return super unless use_traversal_ids_for_self_and_hierarchy_scopes?
-
unscoped.from_union([all.self_and_ancestors, all.self_and_descendants(include_self: false)])
end
@@ -74,15 +64,6 @@ module Namespaces
private
- def use_traversal_ids?
- Feature.enabled?(:use_traversal_ids)
- end
-
- def use_traversal_ids_for_self_and_hierarchy_scopes?
- Feature.enabled?(:use_traversal_ids_for_self_and_hierarchy_scopes) &&
- use_traversal_ids?
- end
-
def self_and_ancestors_from_inner_join(include_self: true, upto: nil, hierarchy_order: nil)
base_cte = all.reselect('namespaces.traversal_ids').as_cte(:base_ancestors_cte)
diff --git a/app/models/note.rb b/app/models/note.rb
index 8fc45436dc7..eae7a40fb4e 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -26,7 +26,7 @@ class Note < ApplicationRecord
include IgnorableColumns
include Spammable
- ignore_column :id_convert_to_bigint, remove_with: '16.3', remove_after: '2023-08-22'
+ ignore_column :id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
ISSUE_TASK_SYSTEM_NOTE_PATTERN = /\A.*marked\sthe\stask.+as\s(completed|incomplete).*\z/
@@ -105,7 +105,7 @@ class Note < ApplicationRecord
validates :note, presence: true
validates :note, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }
validates :project, presence: true, if: :for_project_noteable?
- validates :namespace, presence: true
+ validates :namespace, presence: true, unless: :for_abuse_report?
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
@@ -383,7 +383,7 @@ class Note < ApplicationRecord
end
def for_project_noteable?
- !for_personal_snippet?
+ !(for_personal_snippet? || for_abuse_report?)
end
def for_design?
@@ -394,6 +394,10 @@ class Note < ApplicationRecord
for_issue? || for_merge_request?
end
+ def for_abuse_report?
+ noteable_type == AbuseReport.name
+ end
+
def skip_project_check?
!for_project_noteable?
end
@@ -830,7 +834,11 @@ class Note < ApplicationRecord
def ensure_namespace_id
return if namespace_id.present? && !noteable_changed? && !project_changed?
- self.namespace_id = if for_project_noteable?
+ self.namespace_id = if for_issue?
+ # Some issues are not project noteables (e.g. group-level work items)
+ # so we need this separate condition
+ noteable&.namespace_id
+ elsif for_project_noteable?
project&.project_namespace_id
elsif for_personal_snippet?
noteable&.author&.namespace&.id
diff --git a/app/models/note_diff_file.rb b/app/models/note_diff_file.rb
index b0f6af0d853..624a722e369 100644
--- a/app/models/note_diff_file.rb
+++ b/app/models/note_diff_file.rb
@@ -4,7 +4,7 @@ class NoteDiffFile < ApplicationRecord
include DiffFile
include IgnorableColumns
- ignore_column :diff_note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :diff_note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
scope :referencing_sha, -> (oids, project_id:) do
joins(:diff_note).where(notes: { project_id: project_id, commit_id: oids })
diff --git a/app/models/packages/protection/rule.rb b/app/models/packages/protection/rule.rb
index bb65be92b90..582b51475c2 100644
--- a/app/models/packages/protection/rule.rb
+++ b/app/models/packages/protection/rule.rb
@@ -4,18 +4,43 @@ module Packages
module Protection
class Rule < ApplicationRecord
enum package_type: Packages::Package.package_types.slice(:npm)
+ enum push_protected_up_to_access_level:
+ Gitlab::Access.sym_options_with_owner.slice(:developer, :maintainer, :owner),
+ _prefix: :push_protected_up_to
belongs_to :project, inverse_of: :package_protection_rules
validates :package_name_pattern, presence: true, uniqueness: { scope: [:project_id, :package_type] },
length: { maximum: 255 }
validates :package_type, presence: true
- validates :push_protected_up_to_access_level, presence: true,
- inclusion: { in: [
- Gitlab::Access::DEVELOPER,
- Gitlab::Access::MAINTAINER,
- Gitlab::Access::OWNER
- ] }
+ validates :push_protected_up_to_access_level, presence: true
+
+ before_save :set_package_name_pattern_ilike_query, if: :package_name_pattern_changed?
+
+ scope :for_package_name, ->(package_name) {
+ return none if package_name.blank?
+
+ where(":package_name ILIKE package_name_pattern_ilike_query", package_name: package_name)
+ }
+
+ def self.push_protected_from?(access_level:, package_name:, package_type:)
+ return true if [access_level, package_name, package_type].any?(&:blank?)
+
+ where(package_type: package_type, push_protected_up_to_access_level: access_level..)
+ .for_package_name(package_name)
+ .exists?
+ end
+
+ private
+
+ # We want to allow wildcard pattern (`*`) for the field `package_name_pattern`
+ # , e.g. `@my-scope/my-package-*`, etc.
+ # Therefore, we need to preprocess the field value before we can use the field in the ILIKE clause.
+ # E.g. convert wildcard character (`*`) to LIKE match character (`%`), escape certain characters, etc.
+ def set_package_name_pattern_ilike_query
+ self.package_name_pattern_ilike_query = self.class.sanitize_sql_like(package_name_pattern)
+ .tr('*', '%')
+ end
end
end
end
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index e8becc833ca..8a02415aef4 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -55,6 +55,12 @@ module Pages
strong_memoize_attr :prefix
def unique_host
+ # When serving custom domain we don't present the unique host to avoid
+ # GitLab Pages auto-redirect to the unique domain instead of keeping serving
+ # from the custom domain.
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/426435
+ return if domain.present?
+
url_builder.unique_host
end
strong_memoize_attr :unique_host
diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb
index de7b2416258..f05ed2aac6e 100644
--- a/app/models/pages_deployment.rb
+++ b/app/models/pages_deployment.rb
@@ -6,8 +6,6 @@ class PagesDeployment < ApplicationRecord
include FileStoreMounter
include Gitlab::Utils::StrongMemoize
- MIGRATED_FILE_NAME = "_migrated.zip"
-
attribute :file_store, :integer, default: -> { ::Pages::DeploymentUploader.default_store }
belongs_to :project, optional: false
@@ -16,11 +14,11 @@ class PagesDeployment < ApplicationRecord
belongs_to :ci_build, class_name: 'Ci::Build', optional: true
scope :older_than, ->(id) { where('id < ?', id) }
- scope :migrated_from_legacy_storage, -> { where(file: MIGRATED_FILE_NAME) }
scope :with_files_stored_locally, -> { where(file_store: ::ObjectStorage::Store::LOCAL) }
scope :with_files_stored_remotely, -> { where(file_store: ::ObjectStorage::Store::REMOTE) }
scope :project_id_in, ->(ids) { where(project_id: ids) }
scope :active, -> { where(deleted_at: nil) }
+ scope :deactivated, -> { where('deleted_at < ?', Time.now.utc) }
validates :file, presence: true
validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES }
@@ -43,10 +41,6 @@ class PagesDeployment < ApplicationRecord
.update_all(updated_at: now, deleted_at: time || now)
end
- def migrated?
- file.filename == MIGRATED_FILE_NAME
- end
-
private
def set_size
diff --git a/app/models/plan_limits.rb b/app/models/plan_limits.rb
index 245c0719439..478fc1c418a 100644
--- a/app/models/plan_limits.rb
+++ b/app/models/plan_limits.rb
@@ -7,7 +7,6 @@ class PlanLimits < ApplicationRecord
ignore_column :ci_max_artifact_size_running_container_scanning, remove_with: '14.3', remove_after: '2021-08-22'
ignore_column :web_hook_calls_high, remove_with: '15.10', remove_after: '2022-02-22'
- ignore_column :ci_active_pipelines, remove_with: '16.3', remove_after: '2022-07-22'
attribute :limits_history, :ind_jsonb, default: -> { {} }
validates :limits_history, json_schema: { filename: 'plan_limits_history' }
diff --git a/app/models/preloaders/group_root_ancestor_preloader.rb b/app/models/preloaders/group_root_ancestor_preloader.rb
index 29c60e90964..410f48c8176 100644
--- a/app/models/preloaders/group_root_ancestor_preloader.rb
+++ b/app/models/preloaders/group_root_ancestor_preloader.rb
@@ -8,8 +8,6 @@ module Preloaders
end
def execute
- return unless ::Feature.enabled?(:use_traversal_ids)
-
# type == 'Group' condition located on subquery to prevent a filter in the query
root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
.select('namespaces.*, root_query.id as source_id')
diff --git a/app/models/preloaders/project_root_ancestor_preloader.rb b/app/models/preloaders/project_root_ancestor_preloader.rb
index ccb9d2eab98..1e96e139f94 100644
--- a/app/models/preloaders/project_root_ancestor_preloader.rb
+++ b/app/models/preloaders/project_root_ancestor_preloader.rb
@@ -10,7 +10,6 @@ module Preloaders
def execute
return unless @projects.is_a?(ActiveRecord::Relation)
- return unless ::Feature.enabled?(:use_traversal_ids)
root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
.select('namespaces.*, root_query.id as source_id')
diff --git a/app/models/preloaders/user_max_access_level_in_groups_preloader.rb b/app/models/preloaders/user_max_access_level_in_groups_preloader.rb
index 16d46facb96..aaa54e0228b 100644
--- a/app/models/preloaders/user_max_access_level_in_groups_preloader.rb
+++ b/app/models/preloaders/user_max_access_level_in_groups_preloader.rb
@@ -10,27 +10,11 @@ module Preloaders
end
def execute
- if ::Feature.enabled?(:use_traversal_ids)
- preload_with_traversal_ids
- else
- preload_direct_memberships
- end
+ preload_with_traversal_ids
end
private
- def preload_direct_memberships
- group_memberships = GroupMember.active_without_invites_and_requests
- .where(user: @user, source_id: @groups)
- .group(:source_id)
- .maximum(:access_level)
-
- @groups.each do |group|
- access_level = group_memberships[group.id]
- group.merge_value_to_request_store(User, @user.id, access_level) if access_level.present?
- end
- end
-
def preload_with_traversal_ids
# Diagrammatic representation of this step:
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111157#note_1271550140
diff --git a/app/models/project.rb b/app/models/project.rb
index 5989584ce43..fd226d23e77 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -390,6 +390,7 @@ class Project < ApplicationRecord
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :project
has_many :alert_management_http_integrations, class_name: 'AlertManagement::HttpIntegration', inverse_of: :project
+ has_many :container_registry_protection_rules, class_name: 'ContainerRegistry::Protection::Rule', inverse_of: :project
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
# here.
@@ -559,13 +560,16 @@ class Project < ApplicationRecord
allow_blank: true
validates :name,
presence: true,
- length: { maximum: 255 },
- format: { with: Gitlab::Regex.project_name_regex,
- message: Gitlab::Regex.project_name_regex_message }
+ length: { maximum: 255 }
validates :path,
presence: true,
project_path: true,
length: { maximum: 255 }
+
+ validates :name,
+ format: { with: Gitlab::Regex.project_name_regex,
+ message: Gitlab::Regex.project_name_regex_message },
+ if: :name_changed?
validates :path,
format: { with: Gitlab::Regex.oci_repository_path_regex,
message: Gitlab::Regex.oci_repository_path_regex_message },
@@ -749,6 +753,7 @@ class Project < ApplicationRecord
scope :service_desk_enabled, -> { where(service_desk_enabled: true) }
scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
+ scope :with_package_registry_enabled, -> { with_feature_enabled(:package_registry) }
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
scope :with_merge_requests_available_for_user, ->(current_user) { with_feature_available_for_user(:merge_requests, current_user) }
scope :with_issues_or_mrs_available_for_user, -> (user) do
@@ -1449,7 +1454,7 @@ class Project < ApplicationRecord
super(import_url.sanitized_url)
credentials = import_url.credentials.to_h.transform_values { |value| CGI.unescape(value.to_s) }
- create_or_update_import_data(credentials: credentials)
+ build_or_assign_import_data(credentials: credentials)
else
super(value)
end
@@ -1470,9 +1475,7 @@ class Project < ApplicationRecord
valid?(:import_url) || errors.messages[:import_url].nil?
end
- # TODO: rename to build_or_assign_import_data as it doesn't save record
- # https://gitlab.com/gitlab-org/gitlab/-/issues/377319
- def create_or_update_import_data(data: nil, credentials: nil)
+ def build_or_assign_import_data(data: nil, credentials: nil)
return if data.nil? && credentials.nil?
project_import_data = import_data || build_import_data
@@ -2236,15 +2239,6 @@ class Project < ApplicationRecord
pages_metadatum&.deployed?
end
- def pages_path
- # TODO: when we migrate Pages to work with new storage types, change here to use disk_path
- File.join(Settings.pages.path, full_path)
- end
-
- def pages_available?
- Gitlab.config.pages.enabled
- end
-
def pages_show_onboarding?
!(pages_metadatum&.onboarding_complete || pages_metadatum&.deployed)
end
@@ -2693,26 +2687,6 @@ class Project < ApplicationRecord
self.merge_requests_ff_only_enabled || self.merge_requests_rebase_enabled
end
- def migrate_to_hashed_storage!
- return unless storage_upgradable?
-
- if git_transfer_in_progress?
- HashedStorage::ProjectMigrateWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id)
- else
- HashedStorage::ProjectMigrateWorker.perform_async(id)
- end
- end
-
- def rollback_to_legacy_storage!
- return if legacy_storage?
-
- if git_transfer_in_progress?
- HashedStorage::ProjectRollbackWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id)
- else
- HashedStorage::ProjectRollbackWorker.perform_async(id)
- end
- end
-
override :git_transfer_in_progress?
def git_transfer_in_progress?
GL_REPOSITORY_TYPES.any? do |type|
@@ -3195,10 +3169,6 @@ class Project < ApplicationRecord
creator.banned? && team.max_member_access(creator.id) == Gitlab::Access::OWNER
end
- def content_editor_on_issues_feature_flag_enabled?
- group&.content_editor_on_issues_feature_flag_enabled? || Feature.enabled?(:content_editor_on_issues, self)
- end
-
def work_items_feature_flag_enabled?
group&.work_items_feature_flag_enabled? || Feature.enabled?(:work_items, self)
end
@@ -3346,7 +3316,7 @@ class Project < ApplicationRecord
end
def merge_requests_allowing_collaboration(source_branch = nil)
- relation = source_of_merge_requests.opened.where(allow_collaboration: true)
+ relation = source_of_merge_requests.from_fork.opened.where(allow_collaboration: true)
relation = relation.where(source_branch: source_branch) if source_branch
relation
end
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
index c328e7d37c8..4d0c6029235 100644
--- a/app/models/project_authorization.rb
+++ b/app/models/project_authorization.rb
@@ -11,6 +11,7 @@ class ProjectAuthorization < ApplicationRecord
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validates :user, uniqueness: { scope: :project }, presence: true
+ scope :for_project, ->(projects) { where(project: projects) }
scope :non_guests, -> { where('access_level > ?', ::Gitlab::Access::GUEST) }
# TODO: To be removed after https://gitlab.com/gitlab-org/gitlab/-/issues/418205
diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb
index 7e0722ab68c..96c1ad7def8 100644
--- a/app/models/project_import_data.rb
+++ b/app/models/project_import_data.rb
@@ -5,6 +5,11 @@ require 'carrierwave/orm/activerecord'
class ProjectImportData < ApplicationRecord
prepend_mod_with('ProjectImportData') # rubocop: disable Cop/InjectEnterpriseEditionModule
+ # Timeout strategy can only be changed via API, currently only with GitHub and BitBucket Server
+ OPTIMISTIC_TIMEOUT = "optimistic"
+ PESSIMISTIC_TIMEOUT = "pessimistic"
+ TIMEOUT_STRATEGIES = [OPTIMISTIC_TIMEOUT, PESSIMISTIC_TIMEOUT].freeze
+
belongs_to :project, inverse_of: :import_data
attr_encrypted :credentials,
key: Settings.attr_encrypted_db_key_base,
diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb
index 7a3ece4bc92..eca2e5a740e 100644
--- a/app/models/project_pages_metadatum.rb
+++ b/app/models/project_pages_metadatum.rb
@@ -12,6 +12,5 @@ class ProjectPagesMetadatum < ApplicationRecord
belongs_to :pages_deployment
scope :deployed, -> { where(deployed: true) }
- scope :only_on_legacy_storage, -> { deployed.where(pages_deployment: nil) }
scope :with_project_route_and_deployment, -> { preload(:pages_deployment, project: [:namespace, :route]) }
end
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index 69d1a9f4aeb..d16fe996672 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -21,6 +21,8 @@ class ProjectSetting < ApplicationRecord
jitsu_administrator_email
], remove_with: '16.5', remove_after: '2023-09-22'
+ ignore_column :jitsu_key, remove_with: '16.7', remove_after: '2023-11-17'
+
attr_encrypted :cube_api_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_32,
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 38521ae6090..586294f0dd0 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -43,15 +43,13 @@ class ProjectTeam
member
end
- def add_members(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
+ def add_members(users, access_level, current_user: nil, expires_at: nil)
Members::Projects::CreatorService.add_members( # rubocop:disable CodeReuse/ServiceClass
project,
users,
access_level,
current_user: current_user,
- expires_at: expires_at,
- tasks_to_be_done: tasks_to_be_done,
- tasks_project_id: tasks_project_id
+ expires_at: expires_at
)
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 1c27a7a64cf..e565de9c4ba 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -688,7 +688,7 @@ class Repository
def head_tree(skip_flat_paths: true)
return if empty? || root_ref.nil?
- @head_tree ||= Tree.new(self, root_ref, nil, skip_flat_paths: skip_flat_paths)
+ @head_tree ||= Tree.new(self, root_ref, nil, skip_flat_paths: skip_flat_paths, ref_type: 'heads')
end
def tree(sha = :head, path = nil, recursive: false, skip_flat_paths: true, pagination_params: nil, ref_type: nil, rescue_not_found: true)
@@ -1244,7 +1244,14 @@ class Repository
def get_patch_id(old_revision, new_revision)
raw_repository.get_patch_id(old_revision, new_revision)
- rescue Gitlab::Git::CommandError
+ rescue Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository => e
+ Gitlab::ErrorTracking.track_exception(
+ e,
+ project_id: project.id,
+ old_revision: old_revision,
+ new_revision: new_revision
+ )
+
nil
end
@@ -1258,6 +1265,12 @@ class Repository
Gitlab::Git::ObjectPool.init_from_gitaly(gitaly_object_pool, source_project&.repository)
end
+ def get_file_attributes(revision, paths, attributes)
+ raw_repository
+ .get_file_attributes(revision, paths, attributes)
+ .map(&:to_h)
+ end
+
private
def ancestor_cache_key(ancestor_id, descendant_id)
diff --git a/app/models/resource_events/abuse_report_event.rb b/app/models/resource_events/abuse_report_event.rb
index 59f88a63998..5881f87241d 100644
--- a/app/models/resource_events/abuse_report_event.rb
+++ b/app/models/resource_events/abuse_report_event.rb
@@ -16,7 +16,9 @@ module ResourceEvents
close_report: 4,
ban_user_and_close_report: 5,
block_user_and_close_report: 6,
- delete_user_and_close_report: 7
+ delete_user_and_close_report: 7,
+ trust_user: 8,
+ trust_user_and_close_report: 9
}
enum reason: {
@@ -28,7 +30,8 @@ module ResourceEvents
copyright: 6,
malware: 7,
other: 8,
- unconfirmed: 9
+ unconfirmed: 9,
+ trusted: 10
}
def success_message
diff --git a/app/models/service_desk/custom_email_credential.rb b/app/models/service_desk/custom_email_credential.rb
index 8ccdd6f2261..5986ac8a43f 100644
--- a/app/models/service_desk/custom_email_credential.rb
+++ b/app/models/service_desk/custom_email_credential.rb
@@ -59,7 +59,7 @@ module ServiceDesk
allow_localhost: false,
allow_local_network: false
)
- rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e
errors.add(:smtp_address, e)
end
end
diff --git a/app/models/service_list.rb b/app/models/service_list.rb
deleted file mode 100644
index 8a52539d128..00000000000
--- a/app/models/service_list.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-class ServiceList
- def initialize(batch, service_hash, association)
- @batch = batch
- @service_hash = service_hash
- @association = association
- end
-
- def to_array
- [Integration, columns, values]
- end
-
- private
-
- attr_reader :batch, :service_hash, :association
-
- def columns
- service_hash.keys << "#{association}_id"
- end
-
- def values
- batch.select(:id).map do |record|
- service_hash.values << record.id
- end
- end
-end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index d4f8c1b3b0b..78b0c0849e3 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -79,6 +79,10 @@ class Snippet < ApplicationRecord
scope :with_statistics, -> { joins(:statistics) }
scope :inc_projects_namespace_route, -> { includes(project: [:route, :namespace]) }
+ scope :without_created_by_banned_user, -> do
+ where_not_exists(Users::BannedUser.where('snippets.author_id = banned_users.user_id'))
+ end
+
attr_mentionable :description
participant :author
@@ -365,6 +369,10 @@ class Snippet < ApplicationRecord
def multiple_files?
list_files.size > 1
end
+
+ def hidden_due_to_author_ban?
+ Feature.enabled?(:hide_snippets_of_banned_users) && author.banned?
+ end
end
Snippet.prepend_mod_with('Snippet')
diff --git a/app/models/snippet_user_mention.rb b/app/models/snippet_user_mention.rb
index 8ef2c579a5a..2b6845495bc 100644
--- a/app/models/snippet_user_mention.rb
+++ b/app/models/snippet_user_mention.rb
@@ -3,7 +3,7 @@
class SnippetUserMention < UserMention
include IgnorableColumns
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
belongs_to :snippet
belongs_to :note
diff --git a/app/models/ssh_host_key.rb b/app/models/ssh_host_key.rb
index daa64f4e087..672a6d64127 100644
--- a/app/models/ssh_host_key.rb
+++ b/app/models/ssh_host_key.rb
@@ -157,7 +157,7 @@ class SshHostKey
url.port = url.inferred_port
[url, ip]
- rescue Gitlab::UrlBlocker::BlockedUrlError
+ rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError
raise ArgumentError, "Invalid URL"
end
diff --git a/app/models/storage/hashed.rb b/app/models/storage/hashed.rb
index 05e93f00912..5cef033e672 100644
--- a/app/models/storage/hashed.rb
+++ b/app/models/storage/hashed.rb
@@ -31,10 +31,6 @@ module Storage
"#{base_dir}/#{disk_hash}" if disk_hash
end
- def rename_repo(old_full_path: nil, new_full_path: nil)
- true
- end
-
private
# Generates the hash for the repository path and name on disk
diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb
index 0d12a629b8e..700314e277a 100644
--- a/app/models/storage/legacy_project.rb
+++ b/app/models/storage/legacy_project.rb
@@ -23,27 +23,5 @@ module Storage
def disk_path
project.full_path
end
-
- def rename_repo(old_full_path: nil, new_full_path: nil)
- old_full_path ||= project.full_path_before_last_save
- new_full_path ||= project.build_full_path
-
- if gitlab_shell.mv_repository(repository_storage, old_full_path, new_full_path)
- # If repository moved successfully we need to send update instructions to users.
- # However we cannot allow rollback since we moved repository
- # So we basically we mute exceptions in next actions
- begin
- gitlab_shell.mv_repository(repository_storage, "#{old_full_path}.wiki", "#{new_full_path}.wiki")
- return true
- rescue StandardError => e
- Gitlab::AppLogger.error("Exception renaming #{old_full_path} -> #{new_full_path}: #{e}")
- # Returning false does not rollback after_* transaction but gives
- # us information about failing some of tasks
- return false
- end
- end
-
- false
- end
end
end
diff --git a/app/models/suggestion.rb b/app/models/suggestion.rb
index 58a154b8986..c4178d3c5f1 100644
--- a/app/models/suggestion.rb
+++ b/app/models/suggestion.rb
@@ -5,7 +5,7 @@ class Suggestion < ApplicationRecord
include Suggestible
include IgnorableColumns
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
belongs_to :note, inverse_of: :suggestions
validates :note, presence: true, unless: :importing?
diff --git a/app/models/system/broadcast_message.rb b/app/models/system/broadcast_message.rb
index 332baea4449..06f0115ade6 100644
--- a/app/models/system/broadcast_message.rb
+++ b/app/models/system/broadcast_message.rb
@@ -125,7 +125,7 @@ module System
end
def future?
- starts_at > Time.current
+ starts_at.future?
end
def now_or_future?
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 4e71a13a3a1..dc93decce5e 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -4,7 +4,7 @@ class SystemNoteMetadata < ApplicationRecord
include Importable
include IgnorableColumns
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
# These notes's action text might contain a reference that is external.
# We should always force a deep validation upon references that are found
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index eb72456b435..b6b4decc64b 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -5,7 +5,7 @@ class Timelog < ApplicationRecord
include IgnorableColumns
include Sortable
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
before_save :set_project
diff --git a/app/models/todo.rb b/app/models/todo.rb
index d159b51a0eb..e64dbf83a4c 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -6,7 +6,7 @@ class Todo < ApplicationRecord
include EachBatch
include IgnorableColumns
- ignore_column :note_id_convert_to_bigint, remove_with: '16.2', remove_after: '2023-07-22'
+ ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
# Time to wait for todos being removed when not visible for user anymore.
# Prevents TODOs being removed by mistake, for example, removing access from a user
@@ -25,6 +25,7 @@ class Todo < ApplicationRecord
REVIEW_REQUESTED = 9
MEMBER_ACCESS_REQUESTED = 10
REVIEW_SUBMITTED = 11 # This is an EE-only feature
+ OKR_CHECKIN_REQUESTED = 12 # This is an EE-only feature
ACTION_NAMES = {
ASSIGNED => :assigned,
@@ -37,7 +38,8 @@ class Todo < ApplicationRecord
DIRECTLY_ADDRESSED => :directly_addressed,
MERGE_TRAIN_REMOVED => :merge_train_removed,
MEMBER_ACCESS_REQUESTED => :member_access_requested,
- REVIEW_SUBMITTED => :review_submitted
+ REVIEW_SUBMITTED => :review_submitted,
+ OKR_CHECKIN_REQUESTED => :okr_checkin_requested
}.freeze
ACTIONS_MULTIPLE_ALLOWED = [Todo::MENTIONED, Todo::DIRECTLY_ADDRESSED, Todo::MEMBER_ACCESS_REQUESTED].freeze
@@ -78,6 +80,7 @@ class Todo < ApplicationRecord
scope :for_type, -> (type) { where(target_type: type) }
scope :for_target, -> (id) { where(target_id: id) }
scope :for_commit, -> (id) { where(commit_id: id) }
+ scope :not_in_users, -> (user_ids) { where.not('todos.user_id' => user_ids) }
scope :with_entity_associations, -> do
preload(:target, :author, :note, group: :route, project: [:route, :group, { namespace: [:route, :owner] }, :project_setting])
end
diff --git a/app/models/tree.rb b/app/models/tree.rb
index 4d62334800d..030e7d9e85f 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -13,10 +13,10 @@ class Tree
@repository = repository
@sha = sha
@path = path
- @ref_type = ExtractsRef.ref_type(ref_type)
+ @ref_type = ExtractsRef::RefExtractor.ref_type(ref_type)
git_repo = @repository.raw_repository
- ref = ExtractsRef.qualify_ref(@sha, ref_type)
+ ref = ExtractsRef::RefExtractor.qualify_ref(@sha, ref_type)
@entries, @cursor = Gitlab::Git::Tree.where(git_repo, ref, @path, recursive, skip_flat_paths, rescue_not_found,
pagination_params)
diff --git a/app/models/upload.rb b/app/models/upload.rb
index a4fbc703146..59ce9a1f37a 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -2,6 +2,7 @@
class Upload < ApplicationRecord
include Checksummable
+ include EachBatch
# Upper limit for foreground checksum processing
CHECKSUM_THRESHOLD = 100.megabytes
diff --git a/app/models/user.rb b/app/models/user.rb
index c4e867ab571..4034677509f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -271,6 +271,7 @@ class User < MainClusterwide::ApplicationRecord
has_many :bulk_imports
has_many :custom_attributes, class_name: 'UserCustomAttribute'
+ has_one :trusted_with_spam_attribute, -> { UserCustomAttribute.trusted_with_spam }, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'Users::Callout'
has_many :group_callouts, class_name: 'Users::GroupCallout'
has_many :project_callouts, class_name: 'Users::ProjectCallout'
@@ -306,6 +307,7 @@ class User < MainClusterwide::ApplicationRecord
has_many :awarded_user_achievements, class_name: 'Achievements::UserAchievement', foreign_key: 'awarded_by_user_id', inverse_of: :awarded_by_user
has_many :revoked_user_achievements, class_name: 'Achievements::UserAchievement', foreign_key: 'revoked_by_user_id', inverse_of: :revoked_by_user
has_many :achievements, through: :user_achievements, class_name: 'Achievements::Achievement', inverse_of: :users
+ has_many :vscode_settings, class_name: 'VsCode::Settings::VsCodeSetting', inverse_of: :user
#
# Validations
@@ -1234,10 +1236,6 @@ class User < MainClusterwide::ApplicationRecord
authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
end
- def preloaded_member_roles_for_projects(projects)
- # overridden in EE
- end
-
# rubocop: disable CodeReuse/ServiceClass
def require_ssh_key?
count = Users::KeysCountService.new(self).count
@@ -2226,8 +2224,8 @@ class User < MainClusterwide::ApplicationRecord
}
end
- def allow_possible_spam?
- custom_attributes.by_key(UserCustomAttribute::ALLOW_POSSIBLE_SPAM).exists?
+ def trusted?
+ trusted_with_spam_attribute.present?
end
def namespace_commit_email_for_namespace(namespace)
@@ -2511,14 +2509,6 @@ class User < MainClusterwide::ApplicationRecord
def ci_namespace_mirrors_for_group_members(level)
search_members = group_members.where('access_level >= ?', level)
- # This reduces searched prefixes to only shortest ones
- # to avoid querying descendants since they are already covered
- # by ancestor namespaces. If the FF is not available fallback to
- # inefficient search: https://gitlab.com/gitlab-org/gitlab/-/issues/336436
- unless Feature.enabled?(:use_traversal_ids)
- return Ci::NamespaceMirror.contains_any_of_namespaces(search_members.pluck(:source_id))
- end
-
traversal_ids = Group.joins(:all_group_members)
.merge(search_members)
.shortest_traversal_ids_prefixes
diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb
index 15d50071bf6..728c1f4844a 100644
--- a/app/models/user_custom_attribute.rb
+++ b/app/models/user_custom_attribute.rb
@@ -10,13 +10,15 @@ class UserCustomAttribute < ApplicationRecord
scope :by_user_id, ->(user_id) { where(user_id: user_id) }
scope :by_updated_at, ->(updated_at) { where(updated_at: updated_at) }
scope :arkose_sessions, -> { by_key('arkose_session') }
+ scope :trusted_with_spam, -> { by_key(TRUSTED_BY) }
BLOCKED_BY = 'blocked_by'
UNBLOCKED_BY = 'unblocked_by'
ARKOSE_RISK_BAND = 'arkose_risk_band'
AUTO_BANNED_BY_ABUSE_REPORT_ID = 'auto_banned_by_abuse_report_id'
AUTO_BANNED_BY_SPAM_LOG_ID = 'auto_banned_by_spam_log_id'
- ALLOW_POSSIBLE_SPAM = 'allow_possible_spam'
+ TRUSTED_BY = 'trusted_by'
+ AUTO_BANNED_BY = 'auto_banned_by'
IDENTITY_VERIFICATION_PHONE_EXEMPT = 'identity_verification_phone_exempt'
class << self
@@ -50,6 +52,17 @@ class UserCustomAttribute < ApplicationRecord
return unless spam_log
custom_attribute = { user_id: spam_log.user_id, key: AUTO_BANNED_BY_SPAM_LOG_ID, value: spam_log.id }
+ upsert_custom_attributes([custom_attribute])
+ end
+
+ def set_trusted_by(user:, trusted_by:)
+ return unless user && trusted_by
+
+ custom_attribute = {
+ user_id: user.id,
+ key: UserCustomAttribute::TRUSTED_BY,
+ value: "#{trusted_by.username}/#{trusted_by.id}+#{Time.current}"
+ }
upsert_custom_attributes([custom_attribute])
end
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index def0765560e..60dd89c3ee7 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -73,9 +73,10 @@ module Users
new_navigation_callout: 71,
# 72 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129022
namespace_over_storage_users_combined_alert: 73, # EE-only
- rich_text_editor: 74,
+ # 74 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132751
vsd_feedback_banner: 75, # EE-only
- security_policy_protected_branch_modification: 76 # EE-only
+ security_policy_protected_branch_modification: 76, # EE-only
+ vulnerability_report_grouping: 77 # EE-only
}
validates :feature_name,
diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb
index 086943884a5..276d549006f 100644
--- a/app/models/users/credit_card_validation.rb
+++ b/app/models/users/credit_card_validation.rb
@@ -23,18 +23,18 @@ module Users
scope :find_or_initialize_by_user, ->(user_id) { where(user_id: user_id).first_or_initialize }
scope :by_banned_user, -> { joins(:banned_user) }
- scope :similar_by_holder_name, ->(holder_name) do
- if holder_name.present?
- where('lower(holder_name) = lower(:value)', value: holder_name)
+ scope :similar_by_holder_name, ->(holder_name_hash) do
+ if holder_name_hash.present?
+ where(holder_name_hash: holder_name_hash)
else
none
end
end
scope :similar_to, ->(credit_card_validation) do
where(
- expiration_date: credit_card_validation.expiration_date,
- last_digits: credit_card_validation.last_digits,
- network: credit_card_validation.network
+ expiration_date_hash: credit_card_validation.expiration_date_hash,
+ last_digits_hash: credit_card_validation.last_digits_hash,
+ network_hash: credit_card_validation.network_hash
)
end
@@ -48,11 +48,11 @@ module Users
end
def similar_holder_names_count
- self.class.similar_by_holder_name(holder_name).count
+ self.class.similar_by_holder_name(holder_name_hash).count
end
def used_by_banned_user?
- self.class.by_banned_user.similar_to(self).similar_by_holder_name(holder_name).exists?
+ self.class.by_banned_user.similar_to(self).similar_by_holder_name(holder_name_hash).exists?
end
def set_last_digits_hash
diff --git a/app/models/users/in_product_marketing_email.rb b/app/models/users/in_product_marketing_email.rb
index f220cfd17c5..5b9255f93b1 100644
--- a/app/models/users/in_product_marketing_email.rb
+++ b/app/models/users/in_product_marketing_email.rb
@@ -3,30 +3,21 @@
module Users
class InProductMarketingEmail < ApplicationRecord
include BulkInsertSafe
+ include IgnorableColumns
- BUILD_IOS_APP_GUIDE = 'build_ios_app_guide'
- CAMPAIGNS = [BUILD_IOS_APP_GUIDE].freeze
+ ignore_column :campaign, remove_with: '16.7', remove_after: '2023-11-15'
belongs_to :user
validates :user, presence: true
-
- validates :track, :series, presence: true, if: -> { campaign.blank? }
- validates :campaign, presence: true, if: -> { track.blank? && series.blank? }
- validates :campaign, inclusion: { in: CAMPAIGNS }, allow_nil: true
+ validates :track, presence: true
+ validates :series, presence: true
validates :user_id, uniqueness: {
scope: [:track, :series],
message: 'track series email has already been sent'
}, if: -> { track.present? }
- validates :user_id, uniqueness: {
- scope: :campaign,
- message: 'campaign email has already been sent'
- }, if: -> { campaign.present? }
-
- validate :campaign_or_track_series
-
enum track: {
create: 0,
verify: 1,
@@ -44,20 +35,15 @@ module Users
INACTIVE_TRACK_NAMES = %w[invite_team experience].freeze
ACTIVE_TRACKS = tracks.except(*INACTIVE_TRACK_NAMES)
- scope :for_user_with_track_and_series, -> (user, track, series) do
+ scope :for_user_with_track_and_series, ->(user, track, series) do
where(user: user, track: track, series: series)
end
- scope :without_track_and_series, -> (track, series) do
+ scope :without_track_and_series, ->(track, series) do
join_condition = for_user.and(for_track_and_series(track, series))
users_without_records(join_condition)
end
- scope :without_campaign, -> (campaign) do
- join_condition = for_user.and(for_campaign(campaign))
- users_without_records(join_condition)
- end
-
def self.users_table
User.arel_table
end
@@ -78,10 +64,6 @@ module Users
arel_table[:user_id].eq(users_table[:id])
end
- def self.for_campaign(campaign)
- arel_table[:campaign].eq(campaign)
- end
-
def self.for_track_and_series(track, series)
arel_table[:track].eq(ACTIVE_TRACKS[track])
.and(arel_table[:series]).eq(series)
@@ -92,13 +74,5 @@ module Users
email.update(cta_clicked_at: Time.zone.now) if email && email.cta_clicked_at.blank?
end
-
- private
-
- def campaign_or_track_series
- if campaign.present? && (track.present? || series.present?)
- errors.add(:campaign, 'should be a campaign or a track and series but not both')
- end
- end
end
end
diff --git a/app/models/users/phone_number_validation.rb b/app/models/users/phone_number_validation.rb
index 52f16a7861f..e033445d76b 100644
--- a/app/models/users/phone_number_validation.rb
+++ b/app/models/users/phone_number_validation.rb
@@ -2,9 +2,13 @@
module Users
class PhoneNumberValidation < ApplicationRecord
+ include IgnorableColumns
+
self.primary_key = :user_id
self.table_name = 'user_phone_number_validations'
+ ignore_column :verification_attempts, remove_with: '16.7', remove_after: '2023-11-17'
+
belongs_to :user, foreign_key: :user_id
belongs_to :banned_user, class_name: '::Users::BannedUser', foreign_key: :user_id
diff --git a/app/models/vs_code/settings/vs_code_setting.rb b/app/models/vs_code/settings/vs_code_setting.rb
new file mode 100644
index 00000000000..e55d958d2b4
--- /dev/null
+++ b/app/models/vs_code/settings/vs_code_setting.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module VsCode
+ module Settings
+ class VsCodeSetting < ApplicationRecord
+ belongs_to :user, inverse_of: :vscode_settings
+
+ validates :setting_type, presence: true
+ validates :content, presence: true
+
+ scope :by_setting_type, ->(setting_type) { where(setting_type: setting_type) }
+ scope :by_user, ->(user) { where(user: user) }
+ end
+ end
+end
diff --git a/app/models/vulnerability.rb b/app/models/vulnerability.rb
index 650e8942132..0e3fe2cc8ac 100644
--- a/app/models/vulnerability.rb
+++ b/app/models/vulnerability.rb
@@ -9,6 +9,9 @@ class Vulnerability < ApplicationRecord
scope :with_projects, -> { includes(:project) }
+ validates :cvss, json_schema: { filename: "vulnerability_cvss_vectors", draft: 7 }
+ attribute :cvss, :ind_jsonb
+
def self.link_reference_pattern
nil
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index a7e2be0eae5..2eed693ca76 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -205,7 +205,7 @@ class WikiPage
update_attributes(attrs)
save do
- wiki.create_page(title, content, format, attrs[:message])
+ wiki.create_page(title, raw_content, format, attrs[:message])
end
end
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 62b837eeeb6..0761a213532 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -148,6 +148,8 @@ class WorkItem < Issue
end
def linked_work_items(current_user = nil, authorize: true, preload: nil, link_type: nil)
+ return [] if new_record?
+
linked_work_items = linked_work_items_query(link_type).preload(preload).reorder('issue_link_id')
return linked_work_items unless authorize
@@ -159,6 +161,10 @@ class WorkItem < Issue
)
end
+ def linked_items_count
+ linked_work_items(authorize: false).size
+ end
+
private
override :parent_link_confidentiality
diff --git a/app/models/work_items/parent_link.rb b/app/models/work_items/parent_link.rb
index ea7755b03b4..32232c93d11 100644
--- a/app/models/work_items/parent_link.rb
+++ b/app/models/work_items/parent_link.rb
@@ -15,7 +15,6 @@ module WorkItems
validates :work_item, presence: true, uniqueness: true
validate :validate_hierarchy_restrictions
validate :validate_cyclic_reference
- validate :validate_same_project
validate :validate_max_children
validate :validate_confidentiality
validate :check_existing_related_link
@@ -50,14 +49,6 @@ module WorkItems
private
- def validate_same_project
- return if work_item.nil? || work_item_parent.nil?
-
- if work_item.resource_parent != work_item_parent.resource_parent
- errors.add :work_item_parent, _('parent must be in the same project as child.')
- end
- end
-
def validate_max_children
return unless work_item_parent
@@ -88,6 +79,14 @@ module WorkItems
end
validate_depth(restriction.maximum_depth)
+ validate_cross_hierarchy(restriction.cross_hierarchy_enabled)
+ end
+
+ def validate_cross_hierarchy(cross_hierarchy_enabled)
+ return if cross_hierarchy_enabled
+ return if work_item.resource_parent == work_item_parent.resource_parent
+
+ errors.add :work_item_parent, _('parent must be in the same project or group as child.')
end
def validate_depth(depth)
diff --git a/app/models/work_items/related_link_restriction.rb b/app/models/work_items/related_link_restriction.rb
new file mode 100644
index 00000000000..d4a66c95ffb
--- /dev/null
+++ b/app/models/work_items/related_link_restriction.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module WorkItems
+ class RelatedLinkRestriction < ApplicationRecord
+ self.table_name = 'work_item_related_link_restrictions'
+
+ belongs_to :source_type, class_name: 'WorkItems::Type'
+ belongs_to :target_type, class_name: 'WorkItems::Type'
+
+ validates :source_type, presence: true
+ validates :target_type, presence: true
+ validates :target_type, uniqueness: { scope: [:source_type_id, :link_type] }
+
+ enum link_type: Enums::IssuableLink.link_types
+ end
+end
diff --git a/app/models/work_items/related_work_item_link.rb b/app/models/work_items/related_work_item_link.rb
index a911ef5f05d..fb0069541fb 100644
--- a/app/models/work_items/related_work_item_link.rb
+++ b/app/models/work_items/related_work_item_link.rb
@@ -11,7 +11,7 @@ module WorkItems
belongs_to :source, class_name: 'WorkItem'
belongs_to :target, class_name: 'WorkItem'
- validate :validate_max_number_of_links, on: :create
+ validate :validate_related_link_restrictions
class << self
extend ::Gitlab::Utils::Override
@@ -28,14 +28,39 @@ module WorkItems
end
end
- def validate_max_number_of_links
- if source && source.linked_work_items(authorize: false).size >= MAX_LINKS_COUNT
- errors.add :source, s_('WorkItems|This work item would exceed the maximum number of linked items.')
- end
+ private
+
+ def validate_related_link_restrictions
+ return unless source && target
+
+ source_type = source.work_item_type
+ target_type = target.work_item_type
- return unless target && target.linked_work_items(authorize: false).size >= MAX_LINKS_COUNT
+ return if link_restriction_exists?(source_type.id, target_type.id)
- errors.add :target, s_('WorkItems|This work item would exceed the maximum number of linked items.')
+ errors.add :source, format(
+ s_('%{source_type} cannot be related to %{type_type}'),
+ source_type: source_type.name.downcase.pluralize,
+ type_type: target_type.name.downcase.pluralize
+ )
+ end
+
+ def link_restriction_exists?(source_type_id, target_type_id)
+ source_restriction = find_restriction(source_type_id, target_type_id)
+ return true if source_restriction.present?
+ return false if source_type_id == target_type_id
+
+ find_restriction(target_type_id, source_type_id).present?
+ end
+
+ def find_restriction(source_type_id, target_type_id)
+ ::WorkItems::RelatedLinkRestriction.find_by_source_type_id_and_target_type_id_and_link_type(
+ source_type_id,
+ target_type_id,
+ link_type
+ )
end
end
end
+
+WorkItems::RelatedWorkItemLink.prepend_mod
diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb
index b7ceeecbc7f..4ccef4c93d3 100644
--- a/app/models/work_items/type.rb
+++ b/app/models/work_items/type.rb
@@ -73,6 +73,7 @@ module WorkItems
Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.upsert_types
Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter.upsert_restrictions
+ Gitlab::DatabaseImporters::WorkItems::RelatedLinksRestrictionsImporter.upsert_restrictions
find_by(namespace_id: nil, base_type: type)
end
diff --git a/app/models/work_items/widgets/hierarchy.rb b/app/models/work_items/widgets/hierarchy.rb
index 8f54cb32f43..fc6714f1e08 100644
--- a/app/models/work_items/widgets/hierarchy.rb
+++ b/app/models/work_items/widgets/hierarchy.rb
@@ -10,6 +10,26 @@ module WorkItems
def children
work_item.work_item_children_by_relative_position
end
+
+ def ancestors
+ work_item.ancestors
+ end
+
+ def self.quick_action_commands
+ [:set_parent, :add_child]
+ end
+
+ def self.quick_action_params
+ [:set_parent, :add_child]
+ end
+
+ def self.process_quick_action_param(param_name, value)
+ return super unless param_name.in?(quick_action_params) && value.present?
+
+ return { parent: value } if param_name == :set_parent
+
+ return { children: value } if param_name == :add_child
+ end
end
end
end
diff --git a/app/policies/achievements/user_achievement_policy.rb b/app/policies/achievements/user_achievement_policy.rb
index 05650a05490..2710c9c0a5b 100644
--- a/app/policies/achievements/user_achievement_policy.rb
+++ b/app/policies/achievements/user_achievement_policy.rb
@@ -5,8 +5,17 @@ module Achievements
delegate { @subject.achievement.namespace }
delegate { @subject.user }
+ condition(:user_is_owner) { @subject.user == @user }
+
rule { can?(:read_user_profile) | can?(:admin_achievement) }.enable :read_user_achievement
- rule { ~can?(:read_achievement) }.prevent :read_user_achievement
+ rule { user_is_owner }.enable :update_owned_user_achievement
+
+ rule { can?(:update_owned_user_achievement) }.enable :update_user_achievement
+
+ rule { ~can?(:read_achievement) }.policy do
+ prevent :read_user_achievement
+ prevent :update_user_achievement
+ end
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index faa83019bda..2ab59f5a34d 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -72,10 +72,6 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
access_level(for_any_session: true) >= GroupMember::GUEST || valid_dependency_proxy_deploy_token
end
- condition(:observability_enabled, scope: :subject) do
- Feature.enabled?(:observability_group_tab, @subject)
- end
-
desc "Deploy token with read_package_registry scope"
condition(:read_package_registry_deploy_token) do
@user.is_a?(DeployToken) && @user.groups.include?(@subject) && @user.read_package_registry
@@ -174,7 +170,9 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
prevent :read_design_activity
end
- rule { has_access }.enable :read_namespace
+ rule { has_access }.enable :read_namespace_via_membership
+
+ rule { can?(:read_namespace_via_membership) }.enable :read_namespace
rule { developer }.policy do
enable :admin_metrics_dashboard_annotation
@@ -364,14 +362,6 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :destroy_resource_access_tokens
end
- rule { can?(:developer_access) & observability_enabled }.policy do
- enable :read_observability
- end
-
- rule { can?(:maintainer_access) & observability_enabled }.policy do
- enable :admin_observability
- end
-
# Should be matched with ProjectPolicy#read_internal_note
rule { admin | reporter }.enable :read_internal_note
diff --git a/app/policies/identity_provider_policy.rb b/app/policies/identity_provider_policy.rb
index 1e748c78555..5baa96b37ee 100644
--- a/app/policies/identity_provider_policy.rb
+++ b/app/policies/identity_provider_policy.rb
@@ -14,4 +14,6 @@ class IdentityProviderPolicy < BasePolicy
rule { protected_provider }.prevent(:unlink)
end
-IdentityProviderPolicy.prepend_mod_with('IdentityProviderPolicy')
+# Added for JiHu
+# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127672#note_1568398967
+IdentityProviderPolicy.prepend_mod
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index 538959c92bd..6114785a851 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -29,6 +29,12 @@ class IssuePolicy < IssuablePolicy
@subject.work_item_type.widgets.include?(::WorkItems::Widgets::Notes)
end
+ condition(:group_issue, scope: :subject) { subject_container.is_a?(Group) }
+
+ rule { group_issue & can?(:read_group) }.policy do
+ enable :create_note
+ end
+
rule { ~notes_widget_enabled }.policy do
prevent :create_note
prevent :read_note
diff --git a/app/policies/namespaces/group_project_namespace_shared_policy.rb b/app/policies/namespaces/group_project_namespace_shared_policy.rb
index 2214839fb62..b24cb5be607 100644
--- a/app/policies/namespaces/group_project_namespace_shared_policy.rb
+++ b/app/policies/namespaces/group_project_namespace_shared_policy.rb
@@ -23,6 +23,7 @@ module Namespaces
enable :read_work_item
enable :read_issue
enable :read_namespace
+ enable :read_namespace_via_membership
end
rule { can?(:create_work_item) }.enable :create_task
diff --git a/app/policies/namespaces/user_namespace_policy.rb b/app/policies/namespaces/user_namespace_policy.rb
index bfed61e72d3..f2ac0f0403d 100644
--- a/app/policies/namespaces/user_namespace_policy.rb
+++ b/app/policies/namespaces/user_namespace_policy.rb
@@ -14,6 +14,7 @@ module Namespaces
enable :import_projects
enable :admin_namespace
enable :read_namespace
+ enable :read_namespace_via_membership
enable :read_statistics
enable :create_jira_connect_subscription
enable :admin_package
diff --git a/app/policies/packages/protection/rule_policy.rb b/app/policies/packages/protection/rule_policy.rb
new file mode 100644
index 00000000000..fdf269e04cf
--- /dev/null
+++ b/app/policies/packages/protection/rule_policy.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Packages
+ module Protection
+ class RulePolicy < BasePolicy
+ delegate { @subject.project }
+ end
+ end
+end
diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb
index 205dad6ea5f..7ee258f962a 100644
--- a/app/policies/personal_snippet_policy.rb
+++ b/app/policies/personal_snippet_policy.rb
@@ -4,6 +4,7 @@ class PersonalSnippetPolicy < BasePolicy
condition(:public_snippet, scope: :subject) { @subject.public? }
condition(:is_author) { @user && @subject.author == @user }
condition(:internal_snippet, scope: :subject) { @subject.internal? }
+ condition(:hidden, scope: :subject) { @subject.hidden_due_to_author_ban? }
rule { public_snippet }.policy do
enable :read_snippet
@@ -29,5 +30,13 @@ class PersonalSnippetPolicy < BasePolicy
rule { can?(:create_note) }.enable :award_emoji
+ rule { hidden & ~can?(:read_all_resources) }.policy do
+ prevent :read_snippet
+ prevent :update_snippet
+ prevent :admin_snippet
+ prevent :read_note
+ prevent :create_note
+ end
+
rule { can?(:read_all_resources) }.enable :read_snippet
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index a57b6f8daf7..20f88577d67 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -642,7 +642,6 @@ class ProjectPolicy < BasePolicy
prevent(*create_read_update_admin_destroy(:build))
prevent(*create_read_update_admin_destroy(:pipeline_schedule))
prevent(*create_read_update_admin_destroy(:environment))
- prevent(*create_read_update_admin_destroy(:cluster))
prevent(*create_read_update_admin_destroy(:deployment))
end
@@ -666,6 +665,7 @@ class ProjectPolicy < BasePolicy
prevent :read_pipeline_schedule
prevent(*create_read_update_admin_destroy(:feature_flag))
prevent(:admin_feature_flags_user_lists)
+ prevent(*create_read_update_admin_destroy(:cluster))
end
rule { container_registry_disabled }.policy do
@@ -695,7 +695,6 @@ class ProjectPolicy < BasePolicy
enable :read_merge_request
enable :read_note
enable :read_pipeline
- enable :read_pipeline_schedule
enable :read_environment
enable :read_deployment
enable :read_commit_status
@@ -712,7 +711,10 @@ class ProjectPolicy < BasePolicy
enable :read_issue
end
- rule { can?(:public_access) & public_builds }.enable :read_ci_cd_analytics
+ rule { can?(:public_access) & public_builds }.policy do
+ enable :read_ci_cd_analytics
+ enable :read_pipeline_schedule
+ end
rule { public_builds }.policy do
enable :read_build
diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb
index e11c1a39757..214e48d8841 100644
--- a/app/policies/project_snippet_policy.rb
+++ b/app/policies/project_snippet_policy.rb
@@ -8,7 +8,7 @@ class ProjectSnippetPolicy < BasePolicy
condition(:internal_snippet, scope: :subject) { @subject.internal? }
condition(:private_snippet, scope: :subject) { @subject.private? }
condition(:public_project, scope: :subject) { @subject.project.public? }
-
+ condition(:hidden, scope: :subject) { @subject.hidden_due_to_author_ban? }
condition(:is_author) { @user && @subject.author == @user }
# We have to check both project feature visibility and a snippet visibility and take the stricter one
@@ -50,6 +50,13 @@ class ProjectSnippetPolicy < BasePolicy
enable :admin_snippet
end
+ rule { hidden & ~can?(:read_all_resources) }.policy do
+ prevent :read_snippet
+ prevent :update_snippet
+ prevent :admin_snippet
+ prevent :read_note
+ end
+
rule { ~can?(:read_snippet) }.prevent :create_note
end
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index 6f32f4de62c..c52fc168c55 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -90,11 +90,11 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
end
def blame_path
- url_helpers.project_blame_path(project, ref_qualified_path)
+ url_helpers.project_blame_path(*path_params)
end
def history_path
- url_helpers.project_commits_path(project, ref_qualified_path)
+ url_helpers.project_commits_path(*path_params)
end
def permalink_path
@@ -193,7 +193,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
def commit_id
# If `ref_type` is present the commit_id will include the ref qualifier e.g. `refs/heads/`.
# We only accept/return unqualified refs so we need to remove the qualifier from the `commit_id`.
- ExtractsRef.unqualify_ref(blob.commit_id, ref_type)
+ ExtractsRef::RefExtractor.unqualify_ref(blob.commit_id, ref_type)
end
def ref_qualified_path
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index 762ee0d92cd..0bf4a99dcba 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -90,7 +90,7 @@ module Ci
def link_to_pipeline_ref
ApplicationController.helpers.link_to(pipeline.ref,
project_commits_path(pipeline.project, pipeline.ref),
- class: "ref-name gl-link gl-bg-blue-50 gl-rounded-base gl-px-2")
+ class: "ref-container gl-link")
end
def link_to_merge_request
@@ -98,7 +98,7 @@ module Ci
ApplicationController.helpers.link_to(merge_request_presenter.to_reference,
project_merge_request_path(merge_request_presenter.project, merge_request_presenter),
- class: 'mr-iid')
+ class: 'mr-iid ref-container')
end
def link_to_merge_request_source_branch
@@ -120,6 +120,10 @@ module Ci
end
end
+ def triggered_by_path
+ pipeline.child? ? project_pipeline_path(pipeline.triggered_by_pipeline.project, pipeline.triggered_by_pipeline) : ''
+ end
+
private
def plain_ref_name
@@ -133,26 +137,6 @@ module Ci
end
end
end
-
- def all_related_merge_request_links(limit: nil)
- limit ||= all_related_merge_requests.count
-
- all_related_merge_requests.first(limit).map do |merge_request|
- mr_path = project_merge_request_path(merge_request.project, merge_request)
-
- ApplicationController.helpers.link_to "#{merge_request.to_reference} #{merge_request.title}", mr_path, class: 'mr-iid'
- end
- end
-
- def all_related_merge_requests
- strong_memoize(:all_related_merge_requests) do
- if pipeline.ref && can?(current_user, :read_merge_request, pipeline.project)
- pipeline.all_merge_requests_by_recency.to_a
- else
- []
- end
- end
- end
end
end
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 5c23af6e821..09845574aa1 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -197,7 +197,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
def source_branch_link
if source_branch_exists?
- link_to(source_branch, source_branch_commits_path, class: 'ref-name gl-link gl-bg-blue-50 gl-rounded-base gl-px-2')
+ link_to(source_branch, source_branch_commits_path, class: 'ref-container gl-link')
else
content_tag(:span, source_branch, class: 'ref-name')
end
@@ -205,7 +205,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
def target_branch_link
if target_branch_exists?
- link_to(target_branch, target_branch_commits_path, class: 'ref-name gl-link gl-bg-blue-50 gl-rounded-base gl-px-2')
+ link_to(target_branch, target_branch_commits_path, class: 'ref-container gl-link')
else
content_tag(:span, target_branch, class: 'ref-name')
end
diff --git a/app/presenters/ml/candidate_details_presenter.rb b/app/presenters/ml/candidate_details_presenter.rb
index 29d4617903f..057d3bd19d9 100644
--- a/app/presenters/ml/candidate_details_presenter.rb
+++ b/app/presenters/ml/candidate_details_presenter.rb
@@ -23,7 +23,7 @@ module Ml
ci_job: job_info
},
params: candidate.params,
- metrics: candidate.latest_metrics,
+ metrics: candidate.metrics,
metadata: candidate.metadata
}
}
diff --git a/app/presenters/ml/model_presenter.rb b/app/presenters/ml/model_presenter.rb
index 1317a13351b..388e2b73bc1 100644
--- a/app/presenters/ml/model_presenter.rb
+++ b/app/presenters/ml/model_presenter.rb
@@ -13,5 +13,9 @@ module Ml
Gitlab::Routing.url_helpers.project_package_path(model.project, model.latest_version.package_id)
end
+
+ def path
+ Gitlab::Routing.url_helpers.project_ml_model_path(model.project, model.id)
+ end
end
end
diff --git a/app/presenters/tree_entry_presenter.rb b/app/presenters/tree_entry_presenter.rb
index 3f4a9f13c36..674fc3ee322 100644
--- a/app/presenters/tree_entry_presenter.rb
+++ b/app/presenters/tree_entry_presenter.rb
@@ -19,7 +19,7 @@ class TreeEntryPresenter < Gitlab::View::Presenter::Delegated
# If `ref_type` is present the commit_id will include the ref qualifier e.g. `refs/heads/`.
# We only accept/return unqualified refs so we need to remove the qualifier from the `commit_id`.
- commit_id = ExtractsRef.unqualify_ref(tree.commit_id, ref_type)
+ commit_id = ExtractsRef::RefExtractor.unqualify_ref(tree.commit_id, ref_type)
File.join(commit_id, tree.path)
end
diff --git a/app/presenters/vs_code/settings/vs_code_manifest_presenter.rb b/app/presenters/vs_code/settings/vs_code_manifest_presenter.rb
new file mode 100644
index 00000000000..a8cb5de10a2
--- /dev/null
+++ b/app/presenters/vs_code/settings/vs_code_manifest_presenter.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module VsCode
+ module Settings
+ class VsCodeManifestPresenter < Gitlab::View::Presenter::Simple
+ attr_reader :settings
+
+ def initialize(settings)
+ @settings = settings
+ end
+
+ def latest
+ latest_settings_map = {}
+ # There is a default machine stored
+ latest_settings_map['machines'] = DEFAULT_MACHINE[:uuid]
+
+ return latest_settings_map if settings.empty?
+
+ persisted_settings = settings.each_with_object({}) do |setting, hash|
+ hash[setting.setting_type] = setting.uuid
+ end
+
+ latest_settings_map.merge(persisted_settings)
+ end
+
+ def session
+ DEFAULT_SESSION
+ end
+ end
+ end
+end
diff --git a/app/presenters/vs_code/settings/vs_code_setting_presenter.rb b/app/presenters/vs_code/settings/vs_code_setting_presenter.rb
new file mode 100644
index 00000000000..246bebf6099
--- /dev/null
+++ b/app/presenters/vs_code/settings/vs_code_setting_presenter.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module VsCode
+ module Settings
+ class VsCodeSettingPresenter < Gitlab::View::Presenter::Simple
+ attr_reader :setting
+
+ def initialize(setting)
+ @setting = setting
+ end
+
+ def content
+ @setting[:setting_type] == 'machines' ? nil : @setting.content
+ end
+
+ def machines
+ @setting[:setting_type] == 'machines' ? @setting[:machines] : nil
+ end
+
+ def version
+ @setting[:version]
+ end
+
+ def machine_id
+ DEFAULT_MACHINE[:uuid] if @setting[:setting_type] != 'machines'
+ end
+ end
+ end
+end
diff --git a/app/serializers/admin/abuse_report_details_entity.rb b/app/serializers/admin/abuse_report_details_entity.rb
index 8a67aabda9e..77b85f239f7 100644
--- a/app/serializers/admin/abuse_report_details_entity.rb
+++ b/app/serializers/admin/abuse_report_details_entity.rb
@@ -35,10 +35,7 @@ module Admin
end
end
- expose :credit_card, if: ->(report) { report.user.credit_card_validation&.holder_name } do
- expose :name do |report|
- report.user.credit_card_validation.holder_name
- end
+ expose :credit_card, if: ->(report) { report.user.credit_card_validation.present? } do
expose :similar_records_count do |report|
report.user.credit_card_validation.similar_records.count
end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 741643f7989..9aee031328b 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -139,10 +139,11 @@ class BuildDetailsEntity < Ci::JobEntity
return super unless build.failure_reason.to_sym == :missing_dependency_failure
docs_url = "https://docs.gitlab.com/ee/ci/yaml/index.html#dependencies"
+ troubleshooting_url = "https://docs.gitlab.com/ee/ci/jobs/job_artifacts_troubleshooting.html#error-message-this-job-could-not-start-because-it-could-not-retrieve-the-needed-artifacts"
[
failure_message,
- help_message(docs_url).html_safe
+ help_message(docs_url, troubleshooting_url).html_safe
].join("<br />")
end
@@ -157,8 +158,8 @@ class BuildDetailsEntity < Ci::JobEntity
{ invalid_dependencies: html_escape(invalid_dependencies), punctuation: punctuation }
end
- def help_message(docs_url)
- html_escape(_("<a href=\"#{docs_url}\">Learn more.</a>".html_safe))
+ def help_message(docs_url, troubleshooting_url)
+ html_escape(_("Learn more about <a href=\"#{docs_url}\">dependencies</a> and <a href=\"#{troubleshooting_url}\">common causes</a> of this error.</a>".html_safe))
end
end
diff --git a/app/serializers/ci/pipeline_entity.rb b/app/serializers/ci/pipeline_entity.rb
index 28baa64bc7c..832ca619edc 100644
--- a/app/serializers/ci/pipeline_entity.rb
+++ b/app/serializers/ci/pipeline_entity.rb
@@ -33,7 +33,8 @@ class Ci::PipelineEntity < Grape::Entity
expose :can_cancel?, as: :cancelable
expose :failure_reason?, as: :failure_reason
expose :detached_merge_request_pipeline?, as: :detached_merge_request_pipeline
- expose :merged_result_pipeline?, as: :merge_request_pipeline
+ expose :merged_result_pipeline?, as: :merge_request_pipeline # deprecated, use merged_result_pipeline going forward
+ expose :merged_result_pipeline?, as: :merged_result_pipeline
end
expose :details do
@@ -83,12 +84,18 @@ class Ci::PipelineEntity < Grape::Entity
project_pipeline_path(pipeline.project, pipeline)
end
- expose :failed_builds, if: -> (*) { can_retry? }, using: Ci::JobEntity do |pipeline|
+ expose :failed_builds,
+ if: -> (_, options) { !options[:disable_failed_builds] && can_retry? },
+ using: Ci::JobEntity do |pipeline|
pipeline.failed_builds.each do |build|
build.project = pipeline.project
end
end
+ expose :failed_builds_count do |pipeline|
+ pipeline.failed_builds.size
+ end
+
private
alias_method :pipeline, :object
diff --git a/app/serializers/merge_request_noteable_entity.rb b/app/serializers/merge_request_noteable_entity.rb
index 306bac7daae..aac90c20b53 100644
--- a/app/serializers/merge_request_noteable_entity.rb
+++ b/app/serializers/merge_request_noteable_entity.rb
@@ -15,8 +15,8 @@ class MergeRequestNoteableEntity < IssuableEntity
project_tree_path(merge_request.source_project, merge_request.source_branch)
end
- expose :target_branch_path, if: -> (merge_request) { merge_request.source_project } do |merge_request|
- project_tree_path(merge_request.source_project, merge_request.target_branch)
+ expose :target_branch_path, if: -> (merge_request) { merge_request.target_project } do |merge_request|
+ project_tree_path(merge_request.target_project, merge_request.target_branch)
end
expose :diff_head_sha
diff --git a/app/serializers/merge_requests/pipeline_entity.rb b/app/serializers/merge_requests/pipeline_entity.rb
index 500dc435526..83f168682db 100644
--- a/app/serializers/merge_requests/pipeline_entity.rb
+++ b/app/serializers/merge_requests/pipeline_entity.rb
@@ -12,7 +12,8 @@ class MergeRequests::PipelineEntity < Grape::Entity
end
expose :flags do
- expose :merged_result_pipeline?, as: :merge_request_pipeline
+ expose :merged_result_pipeline?, as: :merge_request_pipeline # deprecated, use merged_result_pipeline going forward
+ expose :merged_result_pipeline?, as: :merged_result_pipeline
end
expose :commit, using: CommitEntity
diff --git a/app/serializers/project_import_entity.rb b/app/serializers/project_import_entity.rb
index 302086143c1..e5d1b84b7e4 100644
--- a/app/serializers/project_import_entity.rb
+++ b/app/serializers/project_import_entity.rb
@@ -19,7 +19,7 @@ class ProjectImportEntity < ProjectEntity
# Only for GitHub importer where we pass client through
expose :relation_type do |project, options|
- next nil if options[:client].nil? || Feature.disabled?(:remove_legacy_github_client)
+ next nil if options[:client].nil?
::Gitlab::GithubImport::ProjectRelationType.new(options[:client]).for(project.import_source)
end
diff --git a/app/services/achievements/update_user_achievement_priorities_service.rb b/app/services/achievements/update_user_achievement_priorities_service.rb
new file mode 100644
index 00000000000..1165a1b3bf6
--- /dev/null
+++ b/app/services/achievements/update_user_achievement_priorities_service.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Achievements
+ class UpdateUserAchievementPrioritiesService
+ attr_reader :current_user, :user_achievements
+
+ def initialize(current_user, user_achievements)
+ @current_user = current_user
+ @user_achievements = user_achievements
+ end
+
+ def execute
+ return error_no_permissions unless allowed?
+
+ prioritized_user_achievements_map = Hash[user_achievements.map.with_index { |ua, idx| [ua.id, idx] }]
+
+ user_achievements_priorities_mapping = current_user.user_achievements.each_with_object({}) do |ua, result|
+ next if ua.priority.nil? && !prioritized_user_achievements_map.key?(ua.id)
+
+ result[ua] = { priority: prioritized_user_achievements_map.fetch(ua.id, nil) }
+ end
+
+ return ServiceResponse.success(payload: []) if user_achievements_priorities_mapping.empty?
+
+ ::Gitlab::Database::BulkUpdate.execute(%i[priority], user_achievements_priorities_mapping)
+
+ ServiceResponse.success(payload: user_achievements_priorities_mapping.keys.map(&:reload))
+ end
+
+ private
+
+ def allowed?
+ user_achievements.all? { |user_achievement| current_user&.can?(:update_owned_user_achievement, user_achievement) }
+ end
+
+ def error(message)
+ ServiceResponse.error(payload: user_achievements, message: Array(message))
+ end
+
+ def error_no_permissions
+ error("You can't update at least one of the given user achievements.")
+ end
+ end
+end
diff --git a/app/services/admin/abuse_reports/moderate_user_service.rb b/app/services/admin/abuse_reports/moderate_user_service.rb
index 823568d9db8..1e14806c694 100644
--- a/app/services/admin/abuse_reports/moderate_user_service.rb
+++ b/app/services/admin/abuse_reports/moderate_user_service.rb
@@ -42,6 +42,7 @@ module Admin
when :block_user then block_user
when :delete_user then delete_user
when :close_report then close_report
+ when :trust_user then trust_user
end
end
@@ -66,6 +67,10 @@ module Admin
success
end
+ def trust_user
+ Users::TrustService.new(current_user).execute(abuse_report.user)
+ end
+
def close_similar_open_reports
# admins see the abuse report and other open reports for the same user in one page
# hence, if the request is to close the report, close other open reports for the same user too
diff --git a/app/services/audit_events/build_service.rb b/app/services/audit_events/build_service.rb
index f5322fa5ff4..9eab2f836db 100644
--- a/app/services/audit_events/build_service.rb
+++ b/app/services/audit_events/build_service.rb
@@ -11,7 +11,10 @@ module AuditEvents
def initialize(
author:, scope:, target:, message:,
created_at: DateTime.current, additional_details: {}, ip_address: nil, target_details: nil)
- raise MissingAttributeError if missing_attribute?(author, scope, target, message)
+ raise MissingAttributeError, "author" if author.blank?
+ raise MissingAttributeError, "scope" if scope.blank?
+ raise MissingAttributeError, "target" if target.blank?
+ raise MissingAttributeError, "message" if message.blank?
@author = build_author(author)
@scope = scope
@@ -32,10 +35,6 @@ module AuditEvents
private
- def missing_attribute?(author, scope, target, message)
- author.blank? || scope.blank? || target.blank? || message.blank?
- end
-
def payload
base_payload.merge(details: base_details_payload)
end
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 9b010272995..363510a41a1 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -39,32 +39,45 @@ module Auth
end
def self.full_access_token(*names)
- access_token(%w[*], names)
+ names_and_actions = names.index_with { %w[*] }
+ access_token(names_and_actions)
end
def self.import_access_token
- access_token(%w[*], ['import'], 'registry')
+ access_token({ 'import' => %w[*] }, 'registry')
end
def self.pull_access_token(*names)
- access_token(['pull'], names)
+ names_and_actions = names.index_with { %w[pull] }
+ access_token(names_and_actions)
end
def self.pull_nested_repositories_access_token(name)
- name = name.chomp('/') if name.end_with?('/')
- paths = [name, "#{name}/*"]
- access_token(['pull'], paths)
+ name = name.chomp('/')
+
+ access_token({
+ name => %w[pull],
+ "#{name}/*" => %w[pull]
+ })
+ end
+
+ def self.push_pull_nested_repositories_access_token(name)
+ name = name.chomp('/')
+
+ access_token({
+ name => %w[pull push],
+ "#{name}/*" => %w[pull]
+ })
end
- def self.access_token(actions, names, type = 'repository')
- names = names.flatten
+ def self.access_token(names_and_actions, type = 'repository')
registry = Gitlab.config.registry
token = JSONWebToken::RSAToken.new(registry.key)
token.issuer = registry.issuer
token.audience = AUDIENCE
token.expire_time = token_expire_at
- token[:access] = names.map do |name|
+ token[:access] = names_and_actions.map do |name, actions|
{
type: type,
name: name,
@@ -219,7 +232,6 @@ module Auth
# Overridden in EE
def can_access?(requested_project, requested_action)
return false unless requested_project.container_registry_enabled?
- return false if requested_project.repository_access_level == ::ProjectFeature::DISABLED
case requested_action
when 'pull'
diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb
index 77ed0369624..d0fde43138a 100644
--- a/app/services/auto_merge/base_service.rb
+++ b/app/services/auto_merge/base_service.rb
@@ -61,9 +61,9 @@ module AutoMerge
merge_request.can_be_merged_by?(current_user) &&
merge_request.open? &&
!merge_request.broken? &&
- !merge_request.draft? &&
- merge_request.mergeable_discussions_state? &&
- !merge_request.merge_blocked_by_other_mrs? &&
+ (skip_draft_check(merge_request) || !merge_request.draft?) &&
+ (skip_discussions_check(merge_request) || merge_request.mergeable_discussions_state?) &&
+ (skip_blocked_check(merge_request) || !merge_request.merge_blocked_by_other_mrs?) &&
yield
end
end
@@ -109,5 +109,20 @@ module AutoMerge
def track_exception(error, merge_request)
Gitlab::ErrorTracking.track_exception(error, merge_request_id: merge_request&.id)
end
+
+ # Will skip the draft check or not when checking if strategy is available
+ def skip_draft_check(merge_request)
+ false
+ end
+
+ # Will skip the blocked check or not when checking if strategy is available
+ def skip_blocked_check(merge_request)
+ false
+ end
+
+ # Will skip the discussions check or not when checking if strategy is available
+ def skip_discussions_check(merge_request)
+ false
+ end
end
end
diff --git a/app/services/branches/delete_service.rb b/app/services/branches/delete_service.rb
index 6efbdd161a1..e396d784ca6 100644
--- a/app/services/branches/delete_service.rb
+++ b/app/services/branches/delete_service.rb
@@ -37,3 +37,5 @@ module Branches
end
end
end
+
+Branches::DeleteService.prepend_mod
diff --git a/app/services/bulk_create_integration_service.rb b/app/services/bulk_create_integration_service.rb
index 8fbb7f4f347..70c77444f13 100644
--- a/app/services/bulk_create_integration_service.rb
+++ b/app/services/bulk_create_integration_service.rb
@@ -10,10 +10,10 @@ class BulkCreateIntegrationService
end
def execute
- service_list = ServiceList.new(batch, integration_hash(:create), association).to_array
+ integration_list = Integrations::IntegrationList.new(batch, integration_hash(:create), association).to_array
Integration.transaction do
- results = bulk_insert(*service_list)
+ results = bulk_insert(*integration_list)
if integration.data_fields_present?
data_list = DataList.new(results, data_fields_hash(:create), integration.data_fields.class).to_array
diff --git a/app/services/bulk_imports/process_service.rb b/app/services/bulk_imports/process_service.rb
new file mode 100644
index 00000000000..14c5545cfd5
--- /dev/null
+++ b/app/services/bulk_imports/process_service.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class ProcessService
+ PERFORM_DELAY = 5.seconds
+ DEFAULT_BATCH_SIZE = 5
+
+ attr_reader :bulk_import
+
+ def initialize(bulk_import)
+ @bulk_import = bulk_import
+ end
+
+ def execute
+ return unless bulk_import
+ return if bulk_import.completed?
+ return bulk_import.fail_op! if all_entities_failed?
+ return bulk_import.finish! if all_entities_processed? && bulk_import.started?
+ return re_enqueue if max_batch_size_exceeded? # Do not start more jobs if max allowed are already running
+
+ process_bulk_import
+ re_enqueue
+ rescue StandardError => e
+ Gitlab::ErrorTracking.track_exception(e, bulk_import_id: bulk_import.id)
+
+ bulk_import.fail_op
+ end
+
+ private
+
+ def process_bulk_import
+ bulk_import.start! if bulk_import.created?
+
+ created_entities.first(next_batch_size).each do |entity|
+ create_tracker(entity)
+
+ entity.start!
+
+ BulkImports::ExportRequestWorker.perform_async(entity.id)
+ end
+ end
+
+ def entities
+ @entities ||= bulk_import.entities
+ end
+
+ def created_entities
+ entities.with_status(:created)
+ end
+
+ def all_entities_processed?
+ entities.all? { |entity| entity.finished? || entity.failed? }
+ end
+
+ def all_entities_failed?
+ entities.all?(&:failed?)
+ end
+
+ # A new BulkImportWorker job is enqueued to either
+ # - Process the new BulkImports::Entity created during import (e.g. for the subgroups)
+ # - Or to mark the `bulk_import` as finished
+ def re_enqueue
+ BulkImportWorker.perform_in(PERFORM_DELAY, bulk_import.id)
+ end
+
+ def started_entities
+ entities.with_status(:started)
+ end
+
+ def max_batch_size_exceeded?
+ started_entities.count >= DEFAULT_BATCH_SIZE
+ end
+
+ def next_batch_size
+ [DEFAULT_BATCH_SIZE - started_entities.count, 0].max
+ end
+
+ def create_tracker(entity)
+ entity.class.transaction do
+ entity.pipelines.each do |pipeline|
+ status = skip_pipeline?(pipeline, entity) ? :skipped : :created
+
+ entity.trackers.create!(
+ stage: pipeline[:stage],
+ pipeline_name: pipeline[:pipeline],
+ status: BulkImports::Tracker.state_machine.states[status].value
+ )
+ end
+ end
+ end
+
+ def skip_pipeline?(pipeline, entity)
+ return false unless entity.source_version.valid?
+
+ minimum_version, maximum_version = pipeline.values_at(:minimum_source_version, :maximum_source_version)
+
+ if source_version_out_of_range?(minimum_version, maximum_version, entity.source_version.without_patch)
+ log_skipped_pipeline(pipeline, entity, minimum_version, maximum_version)
+ return true
+ end
+
+ false
+ end
+
+ def source_version_out_of_range?(minimum_version, maximum_version, non_patch_source_version)
+ (minimum_version && non_patch_source_version < Gitlab::VersionInfo.parse(minimum_version)) ||
+ (maximum_version && non_patch_source_version > Gitlab::VersionInfo.parse(maximum_version))
+ end
+
+ def log_skipped_pipeline(pipeline, entity, minimum_version, maximum_version)
+ logger.info(
+ message: 'Pipeline skipped as source instance version not compatible with pipeline',
+ bulk_import_entity_id: entity.id,
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
+ pipeline_name: pipeline[:pipeline],
+ minimum_source_version: minimum_version,
+ maximum_source_version: maximum_version,
+ source_version: entity.source_version.to_s,
+ importer: 'gitlab_migration'
+ )
+ end
+
+ def logger
+ @logger ||= Gitlab::Import::Logger.build
+ end
+ end
+end
diff --git a/app/services/bulk_imports/relation_batch_export_service.rb b/app/services/bulk_imports/relation_batch_export_service.rb
index 19eb550216d..c7164d7c304 100644
--- a/app/services/bulk_imports/relation_batch_export_service.rb
+++ b/app/services/bulk_imports/relation_batch_export_service.rb
@@ -14,6 +14,7 @@ module BulkImports
start_batch!
export_service.export_batch(relation_batch_ids)
+ ensure_export_file_exists!
compress_exported_relation
upload_compressed_file
@@ -76,5 +77,15 @@ module BulkImports
batch.update!(status_event: 'fail_op', error: exception.message.truncate(255))
end
+
+ def exported_filepath
+ File.join(export_path, exported_filename)
+ end
+
+ # Create empty file on disk
+ # if relation is empty and nothing was exported
+ def ensure_export_file_exists!
+ FileUtils.touch(exported_filepath)
+ end
end
end
diff --git a/app/services/bulk_imports/relation_export_service.rb b/app/services/bulk_imports/relation_export_service.rb
index ed71c09420b..91640496440 100644
--- a/app/services/bulk_imports/relation_export_service.rb
+++ b/app/services/bulk_imports/relation_export_service.rb
@@ -18,6 +18,7 @@ module BulkImports
find_or_create_export! do |export|
export.remove_existing_upload!
export_service.execute
+ ensure_export_file_exists!
compress_exported_relation
upload_compressed_file(export)
end
@@ -91,5 +92,15 @@ module BulkImports
export&.update(status_event: 'fail_op', error: exception.class, batched: false)
end
+
+ def exported_filepath
+ File.join(export_path, export_service.exported_filename)
+ end
+
+ # Create empty file on disk
+ # if relation is empty and nothing was exported
+ def ensure_export_file_exists!
+ FileUtils.touch(exported_filepath)
+ end
end
end
diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb
index 3b204d51bab..a7dc6a47a6b 100644
--- a/app/services/chat_names/find_user_service.rb
+++ b/app/services/chat_names/find_user_service.rb
@@ -11,7 +11,7 @@ module ChatNames
chat_name = find_chat_name
return unless chat_name
- chat_name.update_last_used_at
+ record_chat_activity(chat_name)
chat_name
end
@@ -27,5 +27,10 @@ module ChatNames
)
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def record_chat_activity(chat_name)
+ chat_name.update_last_used_at
+ Users::ActivityService.new(author: chat_name.user).execute
+ end
end
end
diff --git a/app/services/ci/catalog/resources/validate_service.rb b/app/services/ci/catalog/resources/validate_service.rb
new file mode 100644
index 00000000000..9e8986ba6fc
--- /dev/null
+++ b/app/services/ci/catalog/resources/validate_service.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Ci
+ module Catalog
+ module Resources
+ class ValidateService
+ attr_reader :project
+
+ def initialize(project, ref)
+ @project = project
+ @ref = ref
+ @errors = []
+ end
+
+ def execute
+ check_project_readme
+ check_project_description
+
+ if errors.empty?
+ ServiceResponse.success
+ else
+ ServiceResponse.error(message: errors.join(' , '))
+ end
+ end
+
+ private
+
+ attr_reader :ref, :errors
+
+ def check_project_description
+ return if project.description.present?
+
+ errors << 'Project must have a description'
+ end
+
+ def check_project_readme
+ return if project_has_readme?
+
+ errors << 'Project must have a README'
+ end
+
+ def project_has_readme?
+ project.repository.blob_data_at(ref, 'README.md')
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/ci/catalog/validate_resource_service.rb b/app/services/ci/catalog/validate_resource_service.rb
deleted file mode 100644
index f166c220869..00000000000
--- a/app/services/ci/catalog/validate_resource_service.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- module Catalog
- class ValidateResourceService
- attr_reader :project
-
- def initialize(project, ref)
- @project = project
- @ref = ref
- @errors = []
- end
-
- def execute
- check_project_readme
- check_project_description
-
- if errors.empty?
- ServiceResponse.success
- else
- ServiceResponse.error(message: errors.join(' , '))
- end
- end
-
- private
-
- attr_reader :ref, :errors
-
- def check_project_description
- return if project.description.present?
-
- errors << 'Project must have a description'
- end
-
- def check_project_readme
- return if project_has_readme?
-
- errors << 'Project must have a README'
- end
-
- def project_has_readme?
- project.repository.blob_data_at(ref, 'README.md')
- end
- end
- end
-end
diff --git a/app/services/ci/components/fetch_service.rb b/app/services/ci/components/fetch_service.rb
index 45abb415174..4f09d47b530 100644
--- a/app/services/ci/components/fetch_service.rb
+++ b/app/services/ci/components/fetch_service.rb
@@ -5,8 +5,6 @@ module Ci
class FetchService
include Gitlab::Utils::StrongMemoize
- TEMPLATE_FILE = 'template.yml'
-
COMPONENT_PATHS = [
::Gitlab::Ci::Components::InstancePath
].freeze
@@ -23,11 +21,16 @@ module Ci
reason: :unsupported_path)
end
- component_path = component_path_class.new(address: address, content_filename: TEMPLATE_FILE)
- content = component_path.fetch_content!(current_user: current_user)
+ component_path = component_path_class.new(address: address)
+ result = component_path.fetch_content!(current_user: current_user)
- if content.present?
- ServiceResponse.success(payload: { content: content, path: component_path })
+ if result
+ ServiceResponse.success(payload: {
+ content: result.content,
+ path: result.path,
+ project: component_path.project,
+ sha: component_path.sha
+ })
else
ServiceResponse.error(message: "#{error_prefix} content not found", reason: :content_not_found)
end
diff --git a/app/services/ci/job_artifacts/destroy_all_expired_service.rb b/app/services/ci/job_artifacts/destroy_all_expired_service.rb
index 57b95e59d7d..4d688d79982 100644
--- a/app/services/ci/job_artifacts/destroy_all_expired_service.rb
+++ b/app/services/ci/job_artifacts/destroy_all_expired_service.rb
@@ -44,10 +44,6 @@ module Ci
def destroy_batch(artifacts)
Ci::JobArtifacts::DestroyBatchService.new(artifacts, skip_projects_on_refresh: true).execute
end
-
- def loop_timeout?
- Time.current > @start_at + LOOP_TIMEOUT
- end
end
end
end
diff --git a/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb b/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb
index 05cd20a152b..c18984953a1 100644
--- a/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb
+++ b/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb
@@ -19,20 +19,11 @@ module Ci
return if pipeline.parent_pipeline? # skip if child pipeline
return unless project.auto_cancel_pending_pipelines?
- if Feature.enabled?(:use_offset_pagination_for_canceling_redundant_pipelines, project)
- paginator.each do |ids|
- pipelines = parent_and_child_pipelines(ids)
+ paginator.each do |ids|
+ pipelines = parent_and_child_pipelines(ids)
- Gitlab::OptimisticLocking.retry_lock(pipelines, name: 'cancel_pending_pipelines') do |cancelables|
- auto_cancel_interruptible_pipelines(cancelables.ids)
- end
- end
- else
- Gitlab::OptimisticLocking
- .retry_lock(parent_and_child_pipelines, name: 'cancel_pending_pipelines') do |cancelables|
- cancelables.select(:id).each_batch(of: BATCH_SIZE) do |cancelables_batch|
- auto_cancel_interruptible_pipelines(cancelables_batch.ids)
- end
+ Gitlab::OptimisticLocking.retry_lock(pipelines, name: 'cancel_pending_pipelines') do |cancelables|
+ auto_cancel_interruptible_pipelines(cancelables.ids)
end
end
end
@@ -61,7 +52,7 @@ module Ci
end
end
- def parent_auto_cancelable_pipelines(ids = nil)
+ def parent_auto_cancelable_pipelines(ids)
scope = project.all_pipelines
.created_after(pipelines_created_after)
.for_ref(pipeline.ref)
@@ -70,11 +61,10 @@ module Ci
.for_status(CommitStatus::AVAILABLE_STATUSES) # Force usage of project_id_and_status_and_created_at_index
.ci_sources
- scope = scope.id_in(ids) if ids.present?
- scope
+ scope.id_in(ids)
end
- def parent_and_child_pipelines(ids = nil)
+ def parent_and_child_pipelines(ids)
Ci::Pipeline.object_hierarchy(parent_auto_cancelable_pipelines(ids), project_condition: :same)
.base_and_descendants
.alive_or_scheduled
diff --git a/app/services/ci/pipeline_processing/atomic_processing_service.rb b/app/services/ci/pipeline_processing/atomic_processing_service.rb
index 750272c3ecb..84e5089b0d5 100644
--- a/app/services/ci/pipeline_processing/atomic_processing_service.rb
+++ b/app/services/ci/pipeline_processing/atomic_processing_service.rb
@@ -93,6 +93,8 @@ module Ci
# We do not continue to process the job if the previous status is not completed
return unless Ci::HasStatus::COMPLETED_STATUSES.include?(previous_status)
+ ::Deployments::CreateForJobService.new.execute(job)
+
Gitlab::OptimisticLocking.retry_lock(job, name: 'atomic_processing_update_job') do |subject|
Ci::ProcessBuildService.new(project, subject.user)
.execute(subject, previous_status)
diff --git a/app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb b/app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb
new file mode 100644
index 00000000000..319186ce030
--- /dev/null
+++ b/app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Ci
+ module Refs
+ class EnqueuePipelinesToUnlockService
+ include BaseServiceUtility
+
+ BATCH_SIZE = 50
+ ENQUEUE_INTERVAL_SECONDS = 0.1
+
+ def execute(ci_ref, before_pipeline: nil)
+ pipelines_scope = ci_ref.pipelines.artifacts_locked
+ pipelines_scope = pipelines_scope.before_pipeline(before_pipeline) if before_pipeline
+ total_new_entries = 0
+
+ pipelines_scope.each_batch(of: BATCH_SIZE) do |batch|
+ pipeline_ids = batch.pluck(:id) # rubocop: disable CodeReuse/ActiveRecord
+ total_added = Ci::UnlockPipelineRequest.enqueue(pipeline_ids)
+ total_new_entries += total_added
+
+ # Take a little rest to avoid overloading Redis
+ sleep ENQUEUE_INTERVAL_SECONDS
+ end
+
+ success(
+ total_pending_entries: Ci::UnlockPipelineRequest.total_pending,
+ total_new_entries: total_new_entries
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/ci/retry_job_service.rb b/app/services/ci/retry_job_service.rb
index 14ea09f17a0..d7c3e9e7f64 100644
--- a/app/services/ci/retry_job_service.rb
+++ b/app/services/ci/retry_job_service.rb
@@ -39,7 +39,9 @@ module Ci
::Ci::CopyCrossDatabaseAssociationsService.new.execute(job, new_job)
- ::Deployments::CreateForJobService.new.execute(new_job)
+ if Feature.disabled?(:create_deployment_only_for_processable_jobs, project)
+ ::Deployments::CreateForJobService.new.execute(new_job)
+ end
::MergeRequests::AddTodoWhenBuildFailsService
.new(project: project)
diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb
index 85f910d05d7..f6b2c90c6ec 100644
--- a/app/services/ci/retry_pipeline_service.rb
+++ b/app/services/ci/retry_pipeline_service.rb
@@ -26,9 +26,7 @@ module Ci
.new(project: project, current_user: current_user)
.close_all(pipeline)
- Ci::ProcessPipelineService
- .new(pipeline)
- .execute
+ start_pipeline(pipeline)
ServiceResponse.success
rescue Gitlab::Access::AccessDeniedError => e
@@ -52,6 +50,10 @@ module Ci
def can_be_retried?(build)
can?(current_user, :update_build, build)
end
+
+ def start_pipeline(pipeline)
+ Ci::PipelineCreation::StartPipelineService.new(pipeline).execute
+ end
end
end
diff --git a/app/services/ci/unlock_pipeline_service.rb b/app/services/ci/unlock_pipeline_service.rb
new file mode 100644
index 00000000000..88d4a8fd0be
--- /dev/null
+++ b/app/services/ci/unlock_pipeline_service.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+module Ci
+ class UnlockPipelineService
+ include BaseServiceUtility
+ include ::Gitlab::ExclusiveLeaseHelpers
+
+ ExecutionTimeoutError = Class.new(StandardError)
+
+ BATCH_SIZE = 100
+ MAX_EXEC_DURATION = 10.minutes.freeze
+ LOCK_TIMEOUT = (MAX_EXEC_DURATION + 1.minute).freeze
+
+ def initialize(pipeline)
+ @pipeline = pipeline
+ @already_leased = false
+ @already_unlocked = false
+ @exec_timeout = false
+ @unlocked_job_artifacts_count = 0
+ @unlocked_pipeline_artifacts_count = 0
+ end
+
+ def execute
+ unlock_pipeline_exclusively
+
+ success(
+ skipped_already_leased: already_leased,
+ skipped_already_unlocked: already_unlocked,
+ exec_timeout: exec_timeout,
+ unlocked_job_artifacts: unlocked_job_artifacts_count,
+ unlocked_pipeline_artifacts: unlocked_pipeline_artifacts_count
+ )
+ end
+
+ private
+
+ attr_reader :pipeline, :already_leased, :already_unlocked, :exec_timeout,
+ :unlocked_job_artifacts_count, :unlocked_pipeline_artifacts_count
+
+ def unlock_pipeline_exclusively
+ in_lock(lock_key, ttl: LOCK_TIMEOUT, retries: 0) do
+ # Even though we enforce uniqueness when enqueueing pipelines, there is still a rare race condition chance that
+ # a pipeline can be re-enqueued right after a worker pops off the same pipeline ID from the queue, and then just
+ # after it completing the unlock process and releasing the lock, another worker picks up the re-enqueued
+ # pipeline ID. So let's make sure to only unlock artifacts if the pipeline has not been unlocked.
+ if pipeline.unlocked?
+ @already_unlocked = true
+ break
+ end
+
+ unlock_job_artifacts
+ unlock_pipeline_artifacts
+
+ # Marking the row in `ci_pipelines` to unlocked signifies that all artifacts have
+ # already been unlocked. This must always happen last.
+ unlock_pipeline
+ end
+ rescue ExecutionTimeoutError
+ @exec_timeout = true
+ rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
+ @already_leased = true
+ ensure
+ if pipeline.unlocked?
+ Ci::UnlockPipelineRequest.log_event(:completed, pipeline.id) unless already_unlocked
+ else
+ # This is to ensure to re-enqueue the pipeline in 2 occasions:
+ # 1. When an unexpected error happens.
+ # 2. When the execution timeout has been reached in the case of a pipeline having a lot of
+ # job artifacts. This allows us to continue unlocking the rest of the artifacts from
+ # where we left off. This is why we unlock the pipeline last.
+ Ci::UnlockPipelineRequest.enqueue(pipeline.id)
+ Ci::UnlockPipelineRequest.log_event(:re_enqueued, pipeline.id)
+ end
+ end
+
+ def lock_key
+ "ci:unlock_pipeline_service:lock:#{pipeline.id}"
+ end
+
+ def unlock_pipeline
+ pipeline.update_column(:locked, Ci::Pipeline.lockeds[:unlocked])
+ end
+
+ def unlock_job_artifacts
+ start = Time.current
+
+ pipeline.builds.each_batch(of: BATCH_SIZE) do |builds|
+ # rubocop: disable CodeReuse/ActiveRecord
+ Ci::JobArtifact.where(job_id: builds.pluck(:id)).each_batch(of: BATCH_SIZE) do |job_artifacts|
+ unlocked_count = Ci::JobArtifact
+ .where(id: job_artifacts.pluck(:id))
+ .update_all(locked: :unlocked)
+
+ @unlocked_job_artifacts_count ||= 0
+ @unlocked_job_artifacts_count += unlocked_count
+
+ raise ExecutionTimeoutError if (Time.current - start) > MAX_EXEC_DURATION
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+
+ def unlock_pipeline_artifacts
+ @unlocked_pipeline_artifacts_count = pipeline.pipeline_artifacts.update_all(locked: :unlocked)
+ end
+ end
+end
diff --git a/app/services/clusters/agent_tokens/revoke_service.rb b/app/services/clusters/agent_tokens/revoke_service.rb
index 5d89b405969..46873fbbc47 100644
--- a/app/services/clusters/agent_tokens/revoke_service.rb
+++ b/app/services/clusters/agent_tokens/revoke_service.rb
@@ -13,7 +13,7 @@ module Clusters
def execute
return error_no_permissions unless current_user.can?(:create_cluster, token.agent.project)
- if token.update(status: token.class.statuses[:revoked])
+ if token.revoke!
log_activity_event(token)
ServiceResponse.success
diff --git a/app/services/clusters/cleanup/project_namespace_service.rb b/app/services/clusters/cleanup/project_namespace_service.rb
index f6ac06d0594..3d5a4f85d10 100644
--- a/app/services/clusters/cleanup/project_namespace_service.rb
+++ b/app/services/clusters/cleanup/project_namespace_service.rb
@@ -40,7 +40,7 @@ module Clusters
cluster.kubeclient&.delete_namespace(kubernetes_namespace.namespace)
rescue Kubeclient::ResourceNotFoundError
# The resources have already been deleted, possibly on a previous attempt that timed out
- rescue Gitlab::UrlBlocker::BlockedUrlError
+ rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError
# User gave an invalid cluster from the start, or deleted the endpoint before this job ran
end
end
diff --git a/app/services/clusters/cleanup/service_account_service.rb b/app/services/clusters/cleanup/service_account_service.rb
index 0ce4bf9bb9c..0358a5412b3 100644
--- a/app/services/clusters/cleanup/service_account_service.rb
+++ b/app/services/clusters/cleanup/service_account_service.rb
@@ -22,7 +22,7 @@ module Clusters
)
rescue Kubeclient::ResourceNotFoundError
# The resources have already been deleted, possibly on a previous attempt that timed out
- rescue Gitlab::UrlBlocker::BlockedUrlError
+ rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError
# User gave an invalid cluster from the start, or deleted the endpoint before this job ran
rescue Kubeclient::HttpError => e
# unauthorized, forbidden: GitLab's access has been revoked
diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb
index 89370bd8abb..5fc84e5aad7 100644
--- a/app/services/commits/create_service.rb
+++ b/app/services/commits/create_service.rb
@@ -40,11 +40,7 @@ module Commits
Gitlab::Git::CommandError => ex
Gitlab::ErrorTracking.log_exception(ex)
- if Feature.enabled?(:errors_utf_8_encoding)
- error(Gitlab::EncodingHelper.encode_utf8_no_detect(ex.message))
- else
- error(ex.message)
- end
+ error(Gitlab::EncodingHelper.encode_utf8_no_detect(ex.message))
end
private
diff --git a/app/services/concerns/update_repository_storage_methods.rb b/app/services/concerns/update_repository_storage_methods.rb
index f14c79ecd7e..50963cc58b2 100644
--- a/app/services/concerns/update_repository_storage_methods.rb
+++ b/app/services/concerns/update_repository_storage_methods.rb
@@ -82,7 +82,6 @@ module UpdateRepositoryStorageMethods
repository = type.repository_for(type.design? ? container.design_management_repository : container)
full_path = repository.full_path
raw_repository = repository.raw
- checksum = repository.checksum
# Initialize a git repository on the target path
new_repository = Gitlab::Git::Repository.new(
@@ -92,12 +91,7 @@ module UpdateRepositoryStorageMethods
full_path
)
- new_repository.replicate(raw_repository)
- new_checksum = new_repository.checksum
-
- if checksum != new_checksum
- raise Error, s_('UpdateRepositoryStorage|Failed to verify %{type} repository checksum from %{old} to %{new}') % { type: type.name, old: checksum, new: new_checksum }
- end
+ Repositories::ReplicateService.new(raw_repository).execute(new_repository, type.name)
end
def same_filesystem?
diff --git a/app/services/concerns/users/participable_service.rb b/app/services/concerns/users/participable_service.rb
index 1a03b444b68..a54c4947b0b 100644
--- a/app/services/concerns/users/participable_service.rb
+++ b/app/services/concerns/users/participable_service.rb
@@ -34,7 +34,7 @@ module Users
def groups
return [] unless current_user
- current_user.authorized_groups.with_route.sort_by(&:path)
+ current_user.authorized_groups.with_route.sort_by(&:full_path)
end
def render_participants_as_hash(participants)
diff --git a/app/services/deployments/create_for_job_service.rb b/app/services/deployments/create_for_job_service.rb
index e230515ce27..fb07efe8694 100644
--- a/app/services/deployments/create_for_job_service.rb
+++ b/app/services/deployments/create_for_job_service.rb
@@ -38,8 +38,6 @@ module Deployments
return unless deployment.valid? && deployment.environment.persisted?
if cluster = deployment.environment.deployment_platform&.cluster # rubocop: disable Lint/AssignmentInCondition
- # double write cluster_id until 12.9: https://gitlab.com/gitlab-org/gitlab/issues/202628
- deployment.cluster_id = cluster.id
deployment.deployment_cluster = ::DeploymentCluster.new(
cluster_id: cluster.id,
kubernetes_namespace: cluster.kubernetes_namespace_for(deployment.environment, deployable: job)
diff --git a/app/services/deployments/create_service.rb b/app/services/deployments/create_service.rb
index ebf2b077bca..8ef9982f41b 100644
--- a/app/services/deployments/create_service.rb
+++ b/app/services/deployments/create_service.rb
@@ -28,7 +28,6 @@ module Deployments
# We use explicit parameters here so we never by accident allow parameters
# to be set that one should not be able to set (e.g. the row ID).
{
- cluster_id: environment.deployment_platform&.cluster_id,
project_id: environment.project_id,
environment_id: environment.id,
ref: params[:ref],
diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb
index 31da099d078..a2eb4f1f396 100644
--- a/app/services/git/branch_hooks_service.rb
+++ b/app/services/git/branch_hooks_service.rb
@@ -156,7 +156,7 @@ module Git
return if branch_to_sync.nil? && commits_to_sync.empty?
- if commits_to_sync.any? && Feature.enabled?(:batch_delay_jira_branch_sync_worker, project)
+ if commits_to_sync.any?
commits_to_sync.each_slice(JIRA_SYNC_BATCH_SIZE).with_index do |commits, i|
JiraConnect::SyncBranchWorker.perform_in(
JIRA_SYNC_BATCH_DELAY * i,
diff --git a/app/services/import/bitbucket_server_service.rb b/app/services/import/bitbucket_server_service.rb
index 3d961780889..e628e88eaa9 100644
--- a/app/services/import/bitbucket_server_service.rb
+++ b/app/services/import/bitbucket_server_service.rb
@@ -42,7 +42,8 @@ module Import
project_name,
target_namespace,
current_user,
- credentials
+ credentials,
+ timeout_strategy
).execute
end
@@ -74,6 +75,10 @@ module Import
@url ||= params[:bitbucket_server_url]
end
+ def timeout_strategy
+ @timeout_strategy ||= params[:timeout_strategy] || ProjectImportData::PESSIMISTIC_TIMEOUT
+ end
+
def allow_local_requests?
Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
end
diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb
index 73e0c229a9c..86c62145a87 100644
--- a/app/services/import/github_service.rb
+++ b/app/services/import/github_service.rb
@@ -138,6 +138,7 @@ module Import
Gitlab::GithubImport::Settings
.new(project)
.write(
+ timeout_strategy: params[:timeout_strategy] || ProjectImportData::PESSIMISTIC_TIMEOUT,
optional_stages: params[:optional_stages],
additional_access_tokens: access_params[:additional_access_tokens]
)
diff --git a/app/services/import/validate_remote_git_endpoint_service.rb b/app/services/import/validate_remote_git_endpoint_service.rb
index a994072c4aa..2177238fddf 100644
--- a/app/services/import/validate_remote_git_endpoint_service.rb
+++ b/app/services/import/validate_remote_git_endpoint_service.rb
@@ -13,6 +13,8 @@ module Import
GIT_PROTOCOL_PKT_LEN = 4
GIT_MINIMUM_RESPONSE_LENGTH = GIT_PROTOCOL_PKT_LEN + GIT_EXPECTED_FIRST_PACKET_LINE.length
EXPECTED_CONTENT_TYPE = "application/x-#{GIT_SERVICE_NAME}-advertisement"
+ INVALID_BODY_MESSAGE = 'Not a git repository: Invalid response body'
+ INVALID_CONTENT_TYPE_MESSAGE = 'Not a git repository: Invalid content-type'
def initialize(params)
@params = params
@@ -30,32 +32,35 @@ module Import
uri.fragment = nil
url = Gitlab::Utils.append_path(uri.to_s, "/info/refs?service=#{GIT_SERVICE_NAME}")
- response_body = ''
- result = nil
- Gitlab::HTTP.try_get(url, stream_body: true, follow_redirects: false, basic_auth: auth) do |fragment|
- response_body += fragment
- next if response_body.length < GIT_MINIMUM_RESPONSE_LENGTH
-
- result = if status_code_is_valid(fragment) && content_type_is_valid(fragment) && response_body_is_valid(response_body)
- :success
- else
- :error
- end
-
- # We are interested only in the first chunks of the response
- # So we're using stream_body: true and breaking when receive enough body
- break
- end
+ response, response_body = http_get_and_extract_first_chunks(url)
- if result == :success
- ServiceResponse.success
- else
- ServiceResponse.error(message: "#{uri} is not a valid HTTP Git repository")
- end
+ validate(uri, response, response_body)
+ rescue *Gitlab::HTTP::HTTP_ERRORS => err
+ error_result("HTTP #{err.class.name.underscore} error: #{err.message}")
+ rescue StandardError => err
+ ServiceResponse.error(
+ message: "Internal #{err.class.name.underscore} error: #{err.message}",
+ reason: 500
+ )
end
private
+ def http_get_and_extract_first_chunks(url)
+ # We are interested only in the first chunks of the response
+ # So we're using stream_body: true and breaking when receive enough body
+ response = nil
+ response_body = ''
+
+ Gitlab::HTTP.get(url, stream_body: true, follow_redirects: false, basic_auth: auth) do |response_chunk|
+ response = response_chunk
+ response_body += response_chunk
+ break if GIT_MINIMUM_RESPONSE_LENGTH <= response_body.length
+ end
+
+ [response, response_body]
+ end
+
def auth
unless @params[:user].to_s.blank?
{
@@ -65,16 +70,38 @@ module Import
end
end
- def status_code_is_valid(fragment)
- fragment.http_response.code == '200'
+ def validate(uri, response, response_body)
+ return status_code_error(uri, response) unless status_code_is_valid?(response)
+ return error_result(INVALID_CONTENT_TYPE_MESSAGE) unless content_type_is_valid?(response)
+ return error_result(INVALID_BODY_MESSAGE) unless response_body_is_valid?(response_body)
+
+ ServiceResponse.success
+ end
+
+ def status_code_error(uri, response)
+ http_code = response.http_response.code.to_i
+ message = response.http_response.message || Rack::Utils::HTTP_STATUS_CODES[http_code]
+
+ error_result(
+ "#{uri} endpoint error: #{http_code}#{message.presence&.prepend(' ')}",
+ http_code
+ )
+ end
+
+ def error_result(message, reason = nil)
+ ServiceResponse.error(message: message, reason: reason)
+ end
+
+ def status_code_is_valid?(response)
+ response.http_response.code == '200'
end
- def content_type_is_valid(fragment)
- fragment.http_response['content-type'] == EXPECTED_CONTENT_TYPE
+ def content_type_is_valid?(response)
+ response.http_response['content-type'] == EXPECTED_CONTENT_TYPE
end
- def response_body_is_valid(response_body)
- response_body.match?(GIT_BODY_MESSAGE_REGEXP)
+ def response_body_is_valid?(response_body)
+ response_body.length <= GIT_MINIMUM_RESPONSE_LENGTH && response_body.match?(GIT_BODY_MESSAGE_REGEXP)
end
end
end
diff --git a/app/services/issuable/clone/base_service.rb b/app/services/issuable/clone/base_service.rb
index a4e815e70fc..d7fdd235db1 100644
--- a/app/services/issuable/clone/base_service.rb
+++ b/app/services/issuable/clone/base_service.rb
@@ -93,5 +93,3 @@ module Issuable
end
end
end
-
-Issuable::Clone::BaseService.prepend_mod_with('Issuable::Clone::BaseService')
diff --git a/app/services/issuable_links/create_service.rb b/app/services/issuable_links/create_service.rb
index 761ba3f74aa..c855a58522c 100644
--- a/app/services/issuable_links/create_service.rb
+++ b/app/services/issuable_links/create_service.rb
@@ -2,13 +2,14 @@
module IssuableLinks
class CreateService < BaseService
- attr_reader :issuable, :current_user, :params
+ attr_reader :issuable, :current_user, :params, :new_links
def initialize(issuable, user, params)
@issuable = issuable
@current_user = user
@params = params.dup
@errors = []
+ @new_links = []
end
def execute
@@ -45,6 +46,7 @@ module IssuableLinks
set_link_type(link)
if link.changed? && link.save
+ new_links << link
create_notes(link)
end
diff --git a/app/services/issue_links/create_service.rb b/app/services/issue_links/create_service.rb
index db05920678e..3523e945d37 100644
--- a/app/services/issue_links/create_service.rb
+++ b/app/services/issue_links/create_service.rb
@@ -9,7 +9,7 @@ module IssueLinks
end
def previous_related_issuables
- @related_issues ||= issuable.related_issues(current_user).to_a
+ @related_issues ||= issuable.related_issues(authorize: false).to_a
end
private
diff --git a/app/services/issues/set_crm_contacts_service.rb b/app/services/issues/set_crm_contacts_service.rb
index 5836097f1fd..c2ed7c554be 100644
--- a/app/services/issues/set_crm_contacts_service.rb
+++ b/app/services/issues/set_crm_contacts_service.rb
@@ -13,7 +13,7 @@ module Issues
return error_invalid_params unless valid_params?
@existing_ids = issue.customer_relations_contact_ids
- determine_changes if params[:replace_ids].present?
+ determine_changes if set_present?
return error_too_many if too_many?
@added_count = 0
@@ -108,7 +108,7 @@ module Issues
end
def set_present?
- params[:replace_ids].present?
+ !params[:replace_ids].nil?
end
def add_or_remove_present?
diff --git a/app/services/jira_connect/sync_service.rb b/app/services/jira_connect/sync_service.rb
index 497c282072d..5d9292a6967 100644
--- a/app/services/jira_connect/sync_service.rb
+++ b/app/services/jira_connect/sync_service.rb
@@ -9,6 +9,8 @@ module JiraConnect
# Parameters: see Atlassian::JiraConnect::Client#send_info
# Includes: update_sequence_id, commits, branches, merge_requests, pipelines
def execute(**args)
+ preload_reviewers_for_merge_requests(args[:merge_requests]) if args.key?(:merge_requests)
+
JiraConnectInstallation.for_project(project).flat_map do |installation|
client = Atlassian::JiraConnect::Client.new(installation.base_url, installation.shared_secret)
@@ -43,5 +45,11 @@ module JiraConnect
def logger
Gitlab::IntegrationsLogger
end
+
+ def preload_reviewers_for_merge_requests(merge_requests)
+ ActiveRecord::Associations::Preloader.new(
+ records: merge_requests, associations: [:approvals, { merge_request_reviewers: :reviewer }]
+ ).call
+ end
end
end
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index aba075c3644..9cedc7ee3a5 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -16,7 +16,6 @@ module Members
@errors = []
@invites = invites_from_params
@source = params[:source]
- @tasks_to_be_done_members = []
end
def execute
@@ -31,13 +30,13 @@ module Members
validate_invitable!
add_members
- create_tasks_to_be_done
enqueue_onboarding_progress_action
publish_event!
result
rescue BlankInvitesError, TooManyInvitesError, MembershipLockedError => e
+ Gitlab::ErrorTracking.log_exception(e, class: self.class.to_s, user_id: current_user.id)
error(e.message)
end
@@ -47,8 +46,7 @@ module Members
private
- attr_reader :source, :errors, :invites, :member_created_namespace_id, :members,
- :tasks_to_be_done_members, :member_created_member_task_id
+ attr_reader :source, :errors, :invites, :member_created_namespace_id, :members
def adding_at_least_one_owner
params[:access_level] == Gitlab::Access::OWNER
@@ -88,9 +86,7 @@ module Members
invites,
params[:access_level],
expires_at: params[:expires_at],
- current_user: current_user,
- tasks_to_be_done: params[:tasks_to_be_done],
- tasks_project_id: params[:tasks_project_id]
+ current_user: current_user
)
members.each { |member| process_result(member) }
@@ -123,7 +119,6 @@ module Members
def after_execute(member:)
super
- build_tasks_to_be_done_members(member)
track_invite_source(member)
end
@@ -146,30 +141,6 @@ module Members
member.invite? ? 'net_new_user' : 'existing_user'
end
- def build_tasks_to_be_done_members(member)
- return unless tasks_to_be_done?(member)
-
- @tasks_to_be_done_members << member
- # We can take the first `member_task` here, since all tasks will have the same attributes needed
- # for the `TasksToBeDone::CreateWorker`, ie. `project` and `tasks_to_be_done`.
- @member_created_member_task_id ||= member.member_task.id
- end
-
- def tasks_to_be_done?(member)
- return false if params[:tasks_to_be_done].blank? || params[:tasks_project_id].blank?
-
- # Only create task issues for existing users. Tasks for new users are created when they signup.
- member.member_task&.valid? && member.user.present?
- end
-
- def create_tasks_to_be_done
- return unless member_created_member_task_id # signal if there is any work to be done here
-
- TasksToBeDone::CreateWorker.perform_async(member_created_member_task_id,
- current_user.id,
- tasks_to_be_done_members.map(&:user_id))
- end
-
def user_limit
limit = params.fetch(:limit, DEFAULT_INVITE_LIMIT)
diff --git a/app/services/members/creator_service.rb b/app/services/members/creator_service.rb
index cc18aae7446..22d8b30db18 100644
--- a/app/services/members/creator_service.rb
+++ b/app/services/members/creator_service.rb
@@ -53,8 +53,7 @@ module Members
common_arguments = {
source: source,
access_level: access_level,
- existing_members: existing_members,
- tasks_to_be_done: args[:tasks_to_be_done] || []
+ existing_members: existing_members
}.merge(parsed_args(args))
members = emails.map do |email|
@@ -81,7 +80,6 @@ module Members
{
current_user: args[:current_user],
expires_at: args[:expires_at],
- tasks_project_id: args[:tasks_project_id],
ldap: args[:ldap]
}
end
@@ -212,22 +210,7 @@ module Members
end
def after_commit_tasks
- create_member_task
- end
-
- def create_member_task
- return unless member.persisted?
- return if member_task_attributes.value?(nil)
- return if member.member_task.present?
-
- member.create_member_task(member_task_attributes)
- end
-
- def member_task_attributes
- {
- tasks_to_be_done: args[:tasks_to_be_done],
- project_id: args[:tasks_project_id]
- }
+ # hook for overriding in other uses
end
def approve_request
diff --git a/app/services/merge_requests/approval_service.rb b/app/services/merge_requests/approval_service.rb
index dbe5567cbc5..f9857cdad39 100644
--- a/app/services/merge_requests/approval_service.rb
+++ b/app/services/merge_requests/approval_service.rb
@@ -7,7 +7,7 @@ module MergeRequests
approval = merge_request.approvals.new(
user: current_user,
- patch_id_sha: fetch_patch_id_sha(merge_request)
+ patch_id_sha: merge_request.current_patch_id_sha
)
return success unless save_approval(approval)
@@ -36,17 +36,6 @@ module MergeRequests
private
- def fetch_patch_id_sha(merge_request)
- diff_refs = merge_request.diff_refs
- base_sha = diff_refs&.base_sha
- head_sha = diff_refs&.head_sha
-
- return unless base_sha && head_sha
- return if base_sha == head_sha
-
- merge_request.project.repository.get_patch_id(base_sha, head_sha)
- end
-
def eligible_for_approval?(merge_request)
merge_request.eligible_for_approval_by?(current_user)
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 29aba3c8679..89e5920a4fb 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -181,3 +181,5 @@ module MergeRequests
end
end
end
+
+MergeRequests::MergeService.prepend_mod
diff --git a/app/services/merge_requests/mergeability/check_base_service.rb b/app/services/merge_requests/mergeability/check_base_service.rb
index e614a7c27fe..e1c4d751296 100644
--- a/app/services/merge_requests/mergeability/check_base_service.rb
+++ b/app/services/merge_requests/mergeability/check_base_service.rb
@@ -9,6 +9,10 @@ module MergeRequests
@params = params
end
+ def self.identifier
+ failure_reason
+ end
+
def skip?
raise NotImplementedError
end
@@ -24,12 +28,22 @@ module MergeRequests
private
+ def failure_reason
+ self.class.failure_reason
+ end
+
def success(**args)
- Gitlab::MergeRequests::Mergeability::CheckResult.success(payload: args)
+ Gitlab::MergeRequests::Mergeability::CheckResult
+ .success(payload: default_payload(args))
end
def failure(**args)
- Gitlab::MergeRequests::Mergeability::CheckResult.failed(payload: args)
+ Gitlab::MergeRequests::Mergeability::CheckResult
+ .failed(payload: default_payload(args))
+ end
+
+ def default_payload(args)
+ args.merge(identifier: self.class.identifier)
end
end
end
diff --git a/app/services/merge_requests/mergeability/check_broken_status_service.rb b/app/services/merge_requests/mergeability/check_broken_status_service.rb
index 6fe4eb4a57f..25293c53bb5 100644
--- a/app/services/merge_requests/mergeability/check_broken_status_service.rb
+++ b/app/services/merge_requests/mergeability/check_broken_status_service.rb
@@ -2,6 +2,10 @@
module MergeRequests
module Mergeability
class CheckBrokenStatusService < CheckBaseService
+ def self.failure_reason
+ :broken_status
+ end
+
def execute
if merge_request.broken?
failure(reason: failure_reason)
@@ -17,12 +21,6 @@ module MergeRequests
def cacheable?
false
end
-
- private
-
- def failure_reason
- :broken_status
- end
end
end
end
diff --git a/app/services/merge_requests/mergeability/check_ci_status_service.rb b/app/services/merge_requests/mergeability/check_ci_status_service.rb
index 9e09b513c57..f7fa3259d97 100644
--- a/app/services/merge_requests/mergeability/check_ci_status_service.rb
+++ b/app/services/merge_requests/mergeability/check_ci_status_service.rb
@@ -2,6 +2,10 @@
module MergeRequests
module Mergeability
class CheckCiStatusService < CheckBaseService
+ def self.failure_reason
+ :ci_must_pass
+ end
+
def execute
if merge_request.mergeable_ci_state?
success
@@ -17,12 +21,6 @@ module MergeRequests
def cacheable?
false
end
-
- private
-
- def failure_reason
- :ci_must_pass
- end
end
end
end
diff --git a/app/services/merge_requests/mergeability/check_conflict_status_service.rb b/app/services/merge_requests/mergeability/check_conflict_status_service.rb
new file mode 100644
index 00000000000..2bc253322c9
--- /dev/null
+++ b/app/services/merge_requests/mergeability/check_conflict_status_service.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ module Mergeability
+ class CheckConflictStatusService < CheckBaseService
+ def self.failure_reason
+ :conflict
+ end
+
+ def execute
+ if merge_request.can_be_merged?
+ success
+ else
+ failure(reason: failure_reason)
+ end
+ end
+
+ def skip?
+ false
+ end
+
+ def cacheable?
+ false
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/mergeability/check_discussions_status_service.rb b/app/services/merge_requests/mergeability/check_discussions_status_service.rb
index 3421d96e8ae..34db5f8a944 100644
--- a/app/services/merge_requests/mergeability/check_discussions_status_service.rb
+++ b/app/services/merge_requests/mergeability/check_discussions_status_service.rb
@@ -2,6 +2,10 @@
module MergeRequests
module Mergeability
class CheckDiscussionsStatusService < CheckBaseService
+ def self.failure_reason
+ :discussions_not_resolved
+ end
+
def execute
if merge_request.mergeable_discussions_state?
success
@@ -17,12 +21,6 @@ module MergeRequests
def cacheable?
false
end
-
- private
-
- def failure_reason
- :discussions_not_resolved
- end
end
end
end
diff --git a/app/services/merge_requests/mergeability/check_draft_status_service.rb b/app/services/merge_requests/mergeability/check_draft_status_service.rb
index a1524317155..85b67fdc629 100644
--- a/app/services/merge_requests/mergeability/check_draft_status_service.rb
+++ b/app/services/merge_requests/mergeability/check_draft_status_service.rb
@@ -3,6 +3,10 @@
module MergeRequests
module Mergeability
class CheckDraftStatusService < CheckBaseService
+ def self.failure_reason
+ :draft_status
+ end
+
def execute
if merge_request.draft?
failure(reason: failure_reason)
@@ -12,18 +16,12 @@ module MergeRequests
end
def skip?
- false
+ params[:skip_draft_check].present?
end
def cacheable?
false
end
-
- private
-
- def failure_reason
- :draft_status
- end
end
end
end
diff --git a/app/services/merge_requests/mergeability/check_open_status_service.rb b/app/services/merge_requests/mergeability/check_open_status_service.rb
index 29f3d0d3ccb..f5b70f18394 100644
--- a/app/services/merge_requests/mergeability/check_open_status_service.rb
+++ b/app/services/merge_requests/mergeability/check_open_status_service.rb
@@ -3,6 +3,10 @@
module MergeRequests
module Mergeability
class CheckOpenStatusService < CheckBaseService
+ def self.failure_reason
+ :not_open
+ end
+
def execute
if merge_request.open?
success
@@ -18,12 +22,6 @@ module MergeRequests
def cacheable?
false
end
-
- private
-
- def failure_reason
- :not_open
- end
end
end
end
diff --git a/app/services/merge_requests/mergeability/check_rebase_status_service.rb b/app/services/merge_requests/mergeability/check_rebase_status_service.rb
new file mode 100644
index 00000000000..2163fec8bd6
--- /dev/null
+++ b/app/services/merge_requests/mergeability/check_rebase_status_service.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ module Mergeability
+ class CheckRebaseStatusService < CheckBaseService
+ def self.failure_reason
+ :need_rebase
+ end
+
+ def execute
+ if merge_request.should_be_rebased?
+ failure(reason: failure_reason)
+ else
+ success
+ end
+ end
+
+ def skip?
+ params[:skip_rebase_check].present?
+ end
+
+ def cacheable?
+ false
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/mergeability/detailed_merge_status_service.rb b/app/services/merge_requests/mergeability/detailed_merge_status_service.rb
index 987d6ce8e9f..86c8122604c 100644
--- a/app/services/merge_requests/mergeability/detailed_merge_status_service.rb
+++ b/app/services/merge_requests/mergeability/detailed_merge_status_service.rb
@@ -24,7 +24,7 @@ module MergeRequests
ci_check_failure_reason
end
else
- check_results.failure_reason
+ check_results.payload[:failure_reason]
end
end
@@ -46,7 +46,11 @@ module MergeRequests
def check_results
strong_memoize(:check_results) do
- merge_request.execute_merge_checks(params: { skip_ci_check: true })
+ merge_request
+ .execute_merge_checks(
+ MergeRequest.mergeable_state_checks,
+ params: { skip_ci_check: true }
+ )
end
end
diff --git a/app/services/merge_requests/mergeability/run_checks_service.rb b/app/services/merge_requests/mergeability/run_checks_service.rb
index 740a6feac2c..5150c03d0a3 100644
--- a/app/services/merge_requests/mergeability/run_checks_service.rb
+++ b/app/services/merge_requests/mergeability/run_checks_service.rb
@@ -9,8 +9,8 @@ module MergeRequests
@params = params
end
- def execute
- @results = merge_request.mergeability_checks.each_with_object([]) do |check_class, result_hash|
+ def execute(checks, execute_all: false)
+ @results = checks.each_with_object([]) do |check_class, result_hash|
check = check_class.new(merge_request: merge_request, params: params)
next if check.skip?
@@ -21,24 +21,20 @@ module MergeRequests
result_hash << check_result
- break result_hash if check_result.failed?
+ break result_hash if check_result.failed? && !execute_all
end
logger.commit
- self
- end
-
- def success?
- raise 'Execute needs to be called before' if results.nil?
-
- results.all?(&:success?)
- end
-
- def failure_reason
- raise 'Execute needs to be called before' if results.nil?
+ return ServiceResponse.success(payload: { results: results }) if all_results_success?
- results.find(&:failed?)&.payload&.fetch(:reason)&.to_sym
+ ServiceResponse.error(
+ message: 'Checks failed.',
+ payload: {
+ results: results,
+ failure_reason: failure_reason
+ }
+ )
end
private
@@ -67,6 +63,14 @@ module MergeRequests
MergeRequests::Mergeability::Logger.new(merge_request: merge_request)
end
end
+
+ def all_results_success?
+ results.all?(&:success?)
+ end
+
+ def failure_reason
+ results.find(&:failed?)&.payload&.fetch(:reason)&.to_sym
+ end
end
end
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index c435048e343..37a829e3014 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -185,6 +185,7 @@ module MergeRequests
# email template itself, see `change_in_merge_request_draft_status_email` template.
notify_draft_status_changed(merge_request)
trigger_merge_request_status_updated(merge_request)
+ publish_draft_change_event(merge_request) if Feature.enabled?(:additional_merge_when_checks_ready, project)
end
if !old_title_draft && new_title_draft
@@ -196,6 +197,14 @@ module MergeRequests
end
end
+ def publish_draft_change_event(merge_request)
+ Gitlab::EventStore.publish(
+ MergeRequests::DraftStateChangeEvent.new(
+ data: { current_user_id: current_user.id, merge_request_id: merge_request.id }
+ )
+ )
+ end
+
def notify_draft_status_changed(merge_request)
notification_service.async.change_in_merge_request_draft_status(
merge_request,
diff --git a/app/services/ml/find_or_create_model_version_service.rb b/app/services/ml/find_or_create_model_version_service.rb
index 1316b2546b9..f4d3f3e72d3 100644
--- a/app/services/ml/find_or_create_model_version_service.rb
+++ b/app/services/ml/find_or_create_model_version_service.rb
@@ -11,7 +11,6 @@ module Ml
def execute
model = Ml::FindOrCreateModelService.new(project, name).execute
-
Ml::ModelVersion.find_or_create!(model, version, package)
end
diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb
index cba7398ebc0..1b852710677 100644
--- a/app/services/notes/quick_actions_service.rb
+++ b/app/services/notes/quick_actions_service.rb
@@ -61,7 +61,7 @@ module Notes
service_errors = if service_response.respond_to?(:errors)
service_response.errors.full_messages
elsif service_response.respond_to?(:[]) && service_response[:status] == :error
- service_response[:message]
+ Array.wrap(service_response[:message])
end
service_errors.blank? ? ServiceResponse.success : ServiceResponse.error(message: service_errors)
diff --git a/app/services/packages/create_dependency_service.rb b/app/services/packages/create_dependency_service.rb
index 10a86e44cb0..51f8a514c55 100644
--- a/app/services/packages/create_dependency_service.rb
+++ b/app/services/packages/create_dependency_service.rb
@@ -59,7 +59,7 @@ module Packages
# The bulk_insert statement above do not dirty the query cache. To make
# sure that the results are fresh from the database and not from a stalled
# and potentially wrong cache, this query has to be done with the query
- # chache disabled.
+ # cache disabled.
Packages::Dependency.ids_for_package_names_and_version_patterns(names_and_version_patterns)
end
end
diff --git a/app/services/packages/maven/find_or_create_package_service.rb b/app/services/packages/maven/find_or_create_package_service.rb
index ac0c77391d7..2ff3ebc3bb2 100644
--- a/app/services/packages/maven/find_or_create_package_service.rb
+++ b/app/services/packages/maven/find_or_create_package_service.rb
@@ -10,7 +10,7 @@ module Packages
package =
::Packages::Maven::PackageFinder.new(current_user, project, path: path)
- .execute
+ .execute&.last
unless Namespace::PackageSetting.duplicates_allowed?(package)
return ServiceResponse.error(message: 'Duplicate package is not allowed') if target_package_is_duplicate?(package)
diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb
index f6f2dbb8415..d599cecc8da 100644
--- a/app/services/packages/npm/create_package_service.rb
+++ b/app/services/packages/npm/create_package_service.rb
@@ -5,7 +5,7 @@ module Packages
include Gitlab::Utils::StrongMemoize
include ExclusiveLeaseGuard
- PACKAGE_JSON_NOT_ALLOWED_FIELDS = %w[readme readmeFilename licenseText].freeze
+ PACKAGE_JSON_NOT_ALLOWED_FIELDS = %w[readme readmeFilename licenseText contributors exports].freeze
DEFAULT_LEASE_TIMEOUT = 1.hour.to_i
def execute
diff --git a/app/services/packages/nuget/extract_metadata_file_service.rb b/app/services/packages/nuget/extract_metadata_file_service.rb
index cc040a45016..fd4f9b5d1c1 100644
--- a/app/services/packages/nuget/extract_metadata_file_service.rb
+++ b/app/services/packages/nuget/extract_metadata_file_service.rb
@@ -7,48 +7,30 @@ module Packages
MAX_FILE_SIZE = 4.megabytes.freeze
- def initialize(package_file)
- @package_file = package_file
+ def initialize(package_zip_file)
+ @package_zip_file = package_zip_file
end
def execute
- raise ExtractionError, 'invalid package file' unless valid_package_file?
-
ServiceResponse.success(payload: nuspec_file_content)
end
private
- attr_reader :package_file
-
- def valid_package_file?
- package_file &&
- package_file.package&.nuget? &&
- package_file.file.size > 0 # rubocop:disable Style/ZeroLengthPredicate
- end
+ attr_reader :package_zip_file
def nuspec_file_content
- with_zip_file do |zip_file|
- entry = zip_file.glob('*.nuspec').first
-
- raise ExtractionError, 'nuspec file not found' unless entry
- raise ExtractionError, 'nuspec file too big' if MAX_FILE_SIZE < entry.size
-
- Tempfile.open("nuget_extraction_package_file_#{package_file.id}") do |file|
- entry.extract(file.path) { true } # allow #extract to overwrite the file
- file.unlink
- file.read
- end
- rescue Zip::EntrySizeError => e
- raise ExtractionError, "nuspec file has the wrong entry size: #{e.message}"
- end
- end
+ entry = package_zip_file.glob('*.nuspec').first
+
+ raise ExtractionError, 'nuspec file not found' unless entry
+ raise ExtractionError, 'nuspec file too big' if MAX_FILE_SIZE < entry.size
- def with_zip_file
- package_file.file.use_open_file do |open_file|
- zip_file = Zip::File.new(open_file, false, true) # rubocop:disable Performance/Rubyzip
- yield(zip_file)
+ Tempfile.create('nuget_extraction_package_file') do |file|
+ entry.extract(file.path) { true } # allow #extract to overwrite the file
+ file.read
end
+ rescue Zip::EntrySizeError => e
+ raise ExtractionError, "nuspec file has the wrong entry size: #{e.message}"
end
end
end
diff --git a/app/services/packages/nuget/metadata_extraction_service.rb b/app/services/packages/nuget/metadata_extraction_service.rb
index 2c758a5ec20..53189063c85 100644
--- a/app/services/packages/nuget/metadata_extraction_service.rb
+++ b/app/services/packages/nuget/metadata_extraction_service.rb
@@ -23,10 +23,9 @@ module Packages
end
def nuspec_file_content
- ExtractMetadataFileService
+ ProcessPackageFileService
.new(package_file)
- .execute
- .payload
+ .execute[:nuspec_file_content]
end
end
end
diff --git a/app/services/packages/nuget/odata_package_entry_service.rb b/app/services/packages/nuget/odata_package_entry_service.rb
index 0cdcc38de16..679b01d6c48 100644
--- a/app/services/packages/nuget/odata_package_entry_service.rb
+++ b/app/services/packages/nuget/odata_package_entry_service.rb
@@ -5,9 +5,6 @@ module Packages
class OdataPackageEntryService
include API::Helpers::RelatedResourcesHelpers
- SEMVER_LATEST_VERSION_PLACEHOLDER = '0.0.0-latest-version'
- LATEST_VERSION_FOR_V2_DOWNLOAD_ENDPOINT = 'latest'
-
def initialize(project, params)
@project = project
@params = params
@@ -29,42 +26,40 @@ module Packages
<title type='text'>#{params[:package_name]}</title>
<content type='application/zip' src="#{download_url}"/>
<m:properties>
- <d:Version>#{package_version}</d:Version>
+ <d:Version>#{params[:package_version]}</d:Version>
</m:properties>
</entry>
XML
end
- def package_version
- params[:package_version] || SEMVER_LATEST_VERSION_PLACEHOLDER
- end
-
def id_url
expose_url "#{api_v4_projects_packages_nuget_v2_path(id: project.id)}" \
- "/Packages(Id='#{params[:package_name]}',Version='#{package_version}')"
+ "/Packages(Id='#{params[:package_name]}',Version='#{params[:package_version]}')"
end
- # TODO: use path helper when download endpoint is merged
def download_url
- expose_url "#{api_v4_projects_packages_nuget_v2_path(id: project.id)}" \
- "/download/#{params[:package_name]}/#{download_url_package_version}"
- end
-
- def download_url_package_version
- if latest_version?
- LATEST_VERSION_FOR_V2_DOWNLOAD_ENDPOINT
+ if params[:package_version].present?
+ expose_url api_v4_projects_packages_nuget_download_package_name_package_version_package_filename_path(
+ {
+ id: project.id,
+ package_name: params[:package_name],
+ package_version: params[:package_version],
+ package_filename: file_name
+ },
+ true
+ )
else
- params[:package_version]
+ xml_base
end
end
- def latest_version?
- params[:package_version].nil? || params[:package_version] == SEMVER_LATEST_VERSION_PLACEHOLDER
- end
-
def xml_base
expose_url api_v4_projects_packages_nuget_v2_path(id: project.id)
end
+
+ def file_name
+ "#{params[:package_name]}.#{params[:package_version]}.nupkg"
+ end
end
end
end
diff --git a/app/services/packages/nuget/process_package_file_service.rb b/app/services/packages/nuget/process_package_file_service.rb
new file mode 100644
index 00000000000..fa7a84ee3d6
--- /dev/null
+++ b/app/services/packages/nuget/process_package_file_service.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Packages
+ module Nuget
+ class ProcessPackageFileService
+ ExtractionError = Class.new(StandardError)
+ NUGET_SYMBOL_FILE_EXTENSION = '.snupkg'
+
+ def initialize(package_file)
+ @package_file = package_file
+ end
+
+ def execute
+ raise ExtractionError, 'invalid package file' unless valid_package_file?
+
+ nuspec_content = nil
+
+ with_zip_file do |zip_file|
+ nuspec_content = nuspec_file_content(zip_file)
+ create_symbol_files(zip_file) if symbol_package_file?
+ end
+
+ ServiceResponse.success(payload: { nuspec_file_content: nuspec_content })
+ end
+
+ private
+
+ attr_reader :package_file
+
+ def valid_package_file?
+ package_file &&
+ package_file.package&.nuget? &&
+ package_file.file.size > 0 # rubocop:disable Style/ZeroLengthPredicate
+ end
+
+ def with_zip_file(&block)
+ package_file.file.use_open_file(unlink_early: false) do |open_file|
+ Zip::File.open(open_file.file_path, &block) # rubocop: disable Performance/Rubyzip
+ end
+ end
+
+ def nuspec_file_content(zip_file)
+ ::Packages::Nuget::ExtractMetadataFileService
+ .new(zip_file)
+ .execute
+ .payload
+ end
+
+ def create_symbol_files(zip_file)
+ ::Packages::Nuget::Symbols::CreateSymbolFilesService
+ .new(package_file.package, zip_file)
+ .execute
+ end
+
+ def symbol_package_file?
+ package_file.file_name.end_with?(NUGET_SYMBOL_FILE_EXTENSION)
+ end
+ end
+ end
+end
diff --git a/app/services/packages/nuget/symbols/create_symbol_files_service.rb b/app/services/packages/nuget/symbols/create_symbol_files_service.rb
new file mode 100644
index 00000000000..03e14ba00e1
--- /dev/null
+++ b/app/services/packages/nuget/symbols/create_symbol_files_service.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module Packages
+ module Nuget
+ module Symbols
+ class CreateSymbolFilesService
+ ExtractionError = Class.new(StandardError)
+ SYMBOL_ENTRIES_LIMIT = 100
+ CONTENT_TYPE = 'application/octet-stream'
+
+ def initialize(package, package_zip_file)
+ @package = package
+ @symbol_entries = package_zip_file.glob('**/*.pdb')
+ end
+
+ def execute
+ return if symbol_entries.empty?
+
+ process_symbol_entries
+ rescue ExtractionError => e
+ Gitlab::ErrorTracking.log_exception(e, class: self.class.name, package_id: package.id)
+ end
+
+ private
+
+ attr_reader :package, :symbol_entries
+
+ def process_symbol_entries
+ Tempfile.create('nuget_extraction_symbol_file') do |tmp_file|
+ symbol_entries.each_with_index do |entry, index|
+ raise ExtractionError, 'too many symbol entries' if index >= SYMBOL_ENTRIES_LIMIT
+
+ entry.extract(tmp_file.path) { true }
+ File.open(tmp_file.path) do |file|
+ create_symbol(entry.name, file)
+ end
+ end
+ end
+ rescue Zip::EntrySizeError => e
+ raise ExtractionError, "symbol file has the wrong entry size: #{e.message}"
+ rescue Zip::EntryNameError => e
+ raise ExtractionError, "symbol file has the wrong entry name: #{e.message}"
+ end
+
+ def create_symbol(path, file)
+ signature = extract_signature(file.read(1.kilobyte))
+ return if signature.blank?
+
+ ::Packages::Nuget::Symbol.create!(
+ package: package,
+ file: { tempfile: file, filename: path.downcase, content_type: CONTENT_TYPE },
+ file_path: path,
+ signature: signature,
+ size: file.size
+ )
+ rescue StandardError => e
+ Gitlab::ErrorTracking.log_exception(e, class: self.class.name, package_id: package.id)
+ end
+
+ def extract_signature(content_fragment)
+ ExtractSymbolSignatureService
+ .new(content_fragment)
+ .execute
+ .payload
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/packages/nuget/symbols/extract_symbol_signature_service.rb b/app/services/packages/nuget/symbols/extract_symbol_signature_service.rb
new file mode 100644
index 00000000000..c2ccdb517b5
--- /dev/null
+++ b/app/services/packages/nuget/symbols/extract_symbol_signature_service.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Packages
+ module Nuget
+ module Symbols
+ class ExtractSymbolSignatureService
+ include Gitlab::Utils::StrongMemoize
+
+ # More information about the GUID format can be found here:
+ # https://github.com/dotnet/symstore/blob/main/docs/specs/SSQP_Key_Conventions.md#key-formatting-basic-rules
+ GUID_START_INDEX = 7
+ GUID_END_INDEX = 22
+ GUID_PARTS_LENGTHS = [4, 2, 2, 8].freeze
+ GUID_AGE_PART = 'FFFFFFFF'
+ TWO_CHARACTER_HEX_REGEX = /\h{2}/
+
+ # The extraction of the signature in this service is based on the following documentation:
+ # https://github.com/dotnet/symstore/blob/main/docs/specs/SSQP_Key_Conventions.md#portable-pdb-signature
+
+ def initialize(symbol_content)
+ @symbol_content = symbol_content
+ end
+
+ def execute
+ return error_response unless signature
+
+ ServiceResponse.success(payload: signature)
+ end
+
+ private
+
+ attr_reader :symbol_content
+
+ def signature
+ # Find the index of the first occurrence of 'Blob'
+ guid_index = symbol_content.index('Blob')
+ return if guid_index.nil?
+
+ # Extract the binary GUID from the symbol content
+ guid = symbol_content[(guid_index + GUID_START_INDEX)..(guid_index + GUID_END_INDEX)]
+ return if guid.nil?
+
+ # Convert the GUID into an array of two-character hex strings
+ guid = guid.unpack('H*').flat_map { |el| el.scan(TWO_CHARACTER_HEX_REGEX) }
+
+ # Reorder the GUID parts based on arbitrary lengths
+ guid = GUID_PARTS_LENGTHS.map { |length| guid.shift(length) }
+
+ # Concatenate the parts of the GUID back together
+ result = guid.first(3).map(&:reverse)
+ result << guid.last
+ result = result.join
+ result << GUID_AGE_PART
+ end
+ strong_memoize_attr :signature
+
+ def error_response
+ ServiceResponse.error(message: 'Could not find the signature in the symbol file')
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/packages/nuget/update_package_from_metadata_service.rb b/app/services/packages/nuget/update_package_from_metadata_service.rb
index 258f8c8f6aa..4cec4ed2fae 100644
--- a/app/services/packages/nuget/update_package_from_metadata_service.rb
+++ b/app/services/packages/nuget/update_package_from_metadata_service.rb
@@ -54,13 +54,16 @@ module Packages
update_linked_package
end
- update_package(target_package)
+ build_infos = package_to_destroy&.build_infos || []
+
+ update_package(target_package, build_infos)
+ update_symbol_files(target_package, package_to_destroy) if symbol_package?
::Packages::UpdatePackageFileService.new(@package_file, package_id: target_package.id, file_name: package_filename)
.execute
package_to_destroy&.destroy!
end
- def update_package(package)
+ def update_package(package, build_infos)
return if symbol_package?
::Packages::Nuget::SyncMetadatumService
@@ -71,28 +74,21 @@ module Packages
.new(package, package_tags)
.execute
+ package.build_infos << build_infos if build_infos.any?
rescue StandardError => e
raise InvalidMetadataError, e.message
end
+ def update_symbol_files(package, package_to_destroy)
+ package_to_destroy.nuget_symbols.update_all(package_id: package.id)
+ end
+
def valid_metadata?
fields = [package_name, package_version, package_description]
fields << package_authors unless symbol_package?
fields.all?(&:present?)
end
- def link_to_existing_package
- package_to_destroy = @package_file.package
- # Updating package_id updates the path where the file is stored.
- # We must pass the file again so that CarrierWave can handle the update
- @package_file.update!(
- package_id: existing_package.id,
- file: @package_file.file
- )
- package_to_destroy.destroy!
- existing_package
- end
-
def update_linked_package
@package_file.package.update!(
name: package_name,
@@ -106,12 +102,15 @@ module Packages
end
def existing_package
- @package_file.project.packages
- .nuget
- .with_name(package_name)
- .with_version(package_version)
- .not_pending_destruction
- .first
+ ::Packages::Nuget::PackageFinder
+ .new(
+ nil,
+ @package_file.project,
+ package_name: package_name,
+ package_version: package_version
+ )
+ .execute
+ .first
end
strong_memoize_attr :existing_package
diff --git a/app/services/packages/protection/create_rule_service.rb b/app/services/packages/protection/create_rule_service.rb
new file mode 100644
index 00000000000..e69eb8faf60
--- /dev/null
+++ b/app/services/packages/protection/create_rule_service.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Packages
+ module Protection
+ class CreateRuleService < BaseProjectService
+ ALLOWED_ATTRIBUTES = %i[
+ package_name_pattern
+ package_type
+ push_protected_up_to_access_level
+ ].freeze
+
+ def execute
+ unless can?(current_user, :admin_package, project)
+ error_message = _('Unauthorized to create a package protection rule')
+ return service_response_error(message: error_message)
+ end
+
+ package_protection_rule = project.package_protection_rules.create(params.slice(*ALLOWED_ATTRIBUTES))
+
+ unless package_protection_rule.persisted?
+ return service_response_error(message: package_protection_rule.errors.full_messages)
+ end
+
+ ServiceResponse.success(payload: { package_protection_rule: package_protection_rule })
+ rescue StandardError => e
+ service_response_error(message: e.message)
+ end
+
+ private
+
+ def service_response_error(message:)
+ ServiceResponse.error(
+ message: message,
+ payload: { package_protection_rule: nil }
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/pages/migrate_from_legacy_storage_service.rb b/app/services/pages/migrate_from_legacy_storage_service.rb
deleted file mode 100644
index d102f93e863..00000000000
--- a/app/services/pages/migrate_from_legacy_storage_service.rb
+++ /dev/null
@@ -1,90 +0,0 @@
-# frozen_string_literal: true
-
-module Pages
- class MigrateFromLegacyStorageService
- def initialize(logger, ignore_invalid_entries:, mark_projects_as_not_deployed:)
- @logger = logger
- @ignore_invalid_entries = ignore_invalid_entries
- @mark_projects_as_not_deployed = mark_projects_as_not_deployed
-
- @migrated = 0
- @errored = 0
- @counters_lock = Mutex.new
- end
-
- def execute_with_threads(threads:, batch_size:)
- @queue = SizedQueue.new(1)
-
- migration_threads = start_migration_threads(threads)
-
- ProjectPagesMetadatum.only_on_legacy_storage.each_batch(of: batch_size) do |batch|
- @queue.push(batch)
- end
-
- @queue.close
-
- @logger.info(message: "Pages legacy storage migration: Waiting for threads to finish...")
- migration_threads.each(&:join)
-
- { migrated: @migrated, errored: @errored }
- end
-
- def execute_for_batch(project_ids)
- batch = ProjectPagesMetadatum.only_on_legacy_storage.where(project_id: project_ids) # rubocop: disable CodeReuse/ActiveRecord
-
- process_batch(batch)
-
- { migrated: @migrated, errored: @errored }
- end
-
- private
-
- def start_migration_threads(count)
- Array.new(count) do
- Thread.new do
- while batch = @queue.pop
- Rails.application.executor.wrap do
- process_batch(batch)
- end
- end
- end
- end
- end
-
- def process_batch(batch)
- batch.with_project_route_and_deployment.each do |metadatum|
- project = metadatum.project
-
- migrate_project(project)
- end
-
- @logger.info(message: "Pages legacy storage migration: batch processed", migrated: @migrated, errored: @errored)
- rescue StandardError => e
- # This method should never raise exception otherwise all threads might be killed
- # and this will result in queue starving (and deadlock)
- Gitlab::ErrorTracking.track_exception(e)
- @logger.error(message: "Pages legacy storage migration: failed processing a batch: #{e.message}")
- end
-
- def migrate_project(project)
- result = nil
- time = Benchmark.realtime do
- result = ::Pages::MigrateLegacyStorageToDeploymentService.new(project,
- ignore_invalid_entries: @ignore_invalid_entries,
- mark_projects_as_not_deployed: @mark_projects_as_not_deployed).execute
- end
-
- if result[:status] == :success
- @logger.info(message: "Pages legacy storage migration: project migrated: #{result[:message]}", project_id: project.id, pages_path: project.pages_path, duration: time.round(2))
- @counters_lock.synchronize { @migrated += 1 }
- else
- @logger.error(message: "Pages legacy storage migration: project failed to be migrated: #{result[:message]}", project_id: project.id, pages_path: project.pages_path, duration: time.round(2))
- @counters_lock.synchronize { @errored += 1 }
- end
- rescue StandardError => e
- @counters_lock.synchronize { @errored += 1 }
- @logger.error(message: "Pages legacy storage migration: project failed to be migrated: #{result[:message]}", project_id: project&.id, pages_path: project&.pages_path)
- Gitlab::ErrorTracking.track_exception(e, project_id: project&.id)
- end
- end
-end
diff --git a/app/services/pages/migrate_legacy_storage_to_deployment_service.rb b/app/services/pages/migrate_legacy_storage_to_deployment_service.rb
deleted file mode 100644
index 9c1671fbc15..00000000000
--- a/app/services/pages/migrate_legacy_storage_to_deployment_service.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: true
-
-module Pages
- class MigrateLegacyStorageToDeploymentService
- include BaseServiceUtility
-
- attr_reader :project
-
- def initialize(project, ignore_invalid_entries: false, mark_projects_as_not_deployed: false)
- @project = project
- @ignore_invalid_entries = ignore_invalid_entries
- @mark_projects_as_not_deployed = mark_projects_as_not_deployed
- end
-
- def execute
- zip_result = ::Pages::ZipDirectoryService.new(project.pages_path, ignore_invalid_entries: @ignore_invalid_entries).execute
-
- if zip_result[:status] == :error
- return error("Can't create zip archive: #{zip_result[:message]}")
- end
-
- archive_path = zip_result[:archive_path]
-
- unless archive_path
- return error("Archive not created. Missing public directory in #{@project.pages_path}") unless @mark_projects_as_not_deployed
-
- project.set_first_pages_deployment!(nil)
-
- return success(
- message: "Archive not created. Missing public directory in #{project.pages_path}? Marked project as not deployed")
- end
-
- deployment = nil
- File.open(archive_path) do |file|
- deployment = project.pages_deployments.create!(
- file: file,
- file_count: zip_result[:entries_count],
- file_sha256: Digest::SHA256.file(archive_path).hexdigest
- )
- end
-
- project.set_first_pages_deployment!(deployment)
-
- success
- ensure
- FileUtils.rm_f(archive_path) if archive_path
- end
- end
-end
diff --git a/app/services/pages/zip_directory_service.rb b/app/services/pages/zip_directory_service.rb
deleted file mode 100644
index c9029b9666a..00000000000
--- a/app/services/pages/zip_directory_service.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-# frozen_string_literal: true
-
-module Pages
- class ZipDirectoryService
- include BaseServiceUtility
- include Gitlab::Utils::StrongMemoize
-
- # used only to track exceptions in Sentry
- InvalidEntryError = Class.new(StandardError)
-
- PUBLIC_DIR = 'public'
-
- attr_reader :public_dir, :real_dir
-
- def initialize(input_dir, ignore_invalid_entries: false)
- @input_dir = input_dir
- @ignore_invalid_entries = ignore_invalid_entries
- end
-
- def execute
- return success unless resolve_public_dir
-
- output_file = File.join(real_dir, "@migrated.zip") # '@' to avoid any name collision with groups or projects
-
- FileUtils.rm_f(output_file)
-
- entries_count = 0
- # Since we're writing not reading here, we can safely silence the cop.
- # It currently cannot discern between opening for reading or writing.
- ::Zip::File.open(output_file, ::Zip::File::CREATE) do |zipfile| # rubocop:disable Performance/Rubyzip
- write_entry(zipfile, PUBLIC_DIR)
- entries_count = zipfile.entries.count
- end
-
- success(archive_path: output_file, entries_count: entries_count)
- rescue StandardError => e
- FileUtils.rm_f(output_file) if output_file
- raise e
- end
-
- private
-
- def resolve_public_dir
- @real_dir = File.realpath(@input_dir)
- @public_dir = File.join(real_dir, PUBLIC_DIR)
-
- valid_path?(public_dir)
- rescue Errno::ENOENT
- false
- end
-
- def write_entry(zipfile, zipfile_path)
- disk_file_path = File.join(real_dir, zipfile_path)
-
- unless valid_path?(disk_file_path)
- # archive with invalid entry will just have this entry missing
- raise InvalidEntryError, "#{disk_file_path} is invalid, input_dir: #{@input_dir}"
- end
-
- ftype = File.lstat(disk_file_path).ftype
- case ftype
- when 'directory'
- recursively_zip_directory(zipfile, disk_file_path, zipfile_path)
- when 'file', 'link'
- zipfile.add(zipfile_path, disk_file_path)
- else
- raise InvalidEntryError, "#{disk_file_path} has invalid ftype: #{ftype}, input_dir: #{@input_dir}"
- end
- rescue Errno::ENOENT, Errno::ELOOP, InvalidEntryError => e
- Gitlab::ErrorTracking.track_exception(e, input_dir: @input_dir, disk_file_path: disk_file_path)
-
- raise e unless @ignore_invalid_entries
- end
-
- def recursively_zip_directory(zipfile, disk_file_path, zipfile_path)
- zipfile.mkdir(zipfile_path)
-
- entries = Dir.entries(disk_file_path) - %w[. ..]
- entries = entries.map { |entry| File.join(zipfile_path, entry) }
-
- write_entries(zipfile, entries)
- end
-
- def write_entries(zipfile, entries)
- entries.each do |zipfile_path|
- write_entry(zipfile, zipfile_path)
- end
- end
-
- # SafeZip was introduced only recently,
- # so we have invalid entries on disk
- def valid_path?(disk_file_path)
- realpath = File.realpath(disk_file_path)
- realpath == public_dir || realpath.start_with?(public_dir + "/")
- end
- end
-end
diff --git a/app/services/projects/after_rename_service.rb b/app/services/projects/after_rename_service.rb
index 9dc957b5be2..93d68eec3bc 100644
--- a/app/services/projects/after_rename_service.rb
+++ b/app/services/projects/after_rename_service.rb
@@ -62,13 +62,9 @@ module Projects
def rename_or_migrate_repository!
success =
- if migrate_to_hashed_storage?
- ::Projects::HashedStorage::MigrationService
- .new(project, full_path_before)
- .execute
- else
- project.storage.rename_repo(old_full_path: full_path_before, new_full_path: full_path_after)
- end
+ ::Projects::HashedStorage::MigrationService
+ .new(project, full_path_before)
+ .execute
rename_failed! unless success
end
@@ -105,11 +101,6 @@ module Projects
)
end
- def migrate_to_hashed_storage?
- Gitlab::CurrentSettings.hashed_storage_enabled? &&
- project.storage_upgradable?
- end
-
def send_move_instructions?
!project.import_started?
end
@@ -147,5 +138,3 @@ module Projects
end
end
end
-
-Projects::AfterRenameService.prepend_mod_with('Projects::AfterRenameService')
diff --git a/app/services/projects/container_repository/cleanup_tags_base_service.rb b/app/services/projects/container_repository/cleanup_tags_base_service.rb
index 61b09de1643..45557d03502 100644
--- a/app/services/projects/container_repository/cleanup_tags_base_service.rb
+++ b/app/services/projects/container_repository/cleanup_tags_base_service.rb
@@ -100,7 +100,7 @@ module Projects
def older_than_in_seconds
strong_memoize(:older_than_in_seconds) do
- ChronicDuration.parse(older_than, use_complete_matcher: true).seconds
+ ChronicDuration.parse(older_than).seconds
end
end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index e4987438c57..e0ee3683ac8 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -229,7 +229,7 @@ module Projects
%w[routes redirect_routes], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424281'
) do
ApplicationRecord.transaction do
- @project.create_or_update_import_data(data: @import_data[:data], credentials: @import_data[:credentials]) if @import_data
+ @project.build_or_assign_import_data(data: @import_data[:data], credentials: @import_data[:credentials]) if @import_data
# Avoid project callbacks being triggered multiple times by saving the parent first.
# See https://github.com/rails/rails/issues/41701.
diff --git a/app/services/projects/group_links/create_service.rb b/app/services/projects/group_links/create_service.rb
index f77bae71d63..c9642fb495a 100644
--- a/app/services/projects/group_links/create_service.rb
+++ b/app/services/projects/group_links/create_service.rb
@@ -16,7 +16,7 @@ module Projects
delegate :root_ancestor, to: :project
def valid_to_create?
- can?(current_user, :read_namespace, shared_with_group) && sharing_allowed?
+ can?(current_user, :read_namespace_via_membership, shared_with_group) && sharing_allowed?
end
def build_link
diff --git a/app/services/projects/hashed_storage/base_repository_service.rb b/app/services/projects/hashed_storage/base_repository_service.rb
deleted file mode 100644
index 6241a3e144f..00000000000
--- a/app/services/projects/hashed_storage/base_repository_service.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module HashedStorage
- # Returned when repository can't be made read-only because there is already a git transfer in progress
- RepositoryInUseError = Class.new(StandardError)
-
- class BaseRepositoryService < BaseService
- include Gitlab::ShellAdapter
-
- attr_reader :old_disk_path, :new_disk_path, :old_storage_version,
- :logger, :move_wiki, :move_design
-
- def initialize(project:, old_disk_path:, logger: nil)
- @project = project
- @logger = logger || Gitlab::AppLogger
- @old_disk_path = old_disk_path
- @move_wiki = has_wiki?
- @move_design = has_design?
- end
-
- protected
-
- def has_wiki?
- gitlab_shell.repository_exists?(project.repository_storage, "#{old_wiki_disk_path}.git")
- end
-
- def has_design?
- gitlab_shell.repository_exists?(project.repository_storage, "#{old_design_disk_path}.git")
- end
-
- def move_repository(from_name, to_name)
- from_exists = gitlab_shell.repository_exists?(project.repository_storage, "#{from_name}.git")
- to_exists = gitlab_shell.repository_exists?(project.repository_storage, "#{to_name}.git")
-
- # If we don't find the repository on either original or target we should log that as it could be an issue if the
- # project was not originally empty.
- if !from_exists && !to_exists
- logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..."
-
- # We return true so we still reflect the change in the database.
- # Next time the repository is (re)created it will be under the new storage layout
- return true
- elsif !from_exists
- # Repository have been moved already.
- return true
- end
-
- gitlab_shell.mv_repository(project.repository_storage, from_name, to_name).tap do |moved|
- if moved
- logger.info("Repository moved from '#{from_name}' to '#{to_name}' (PROJECT_ID=#{project.id})")
- else
- logger.error("Repository cannot be moved from '#{from_name}' to '#{to_name}' (PROJECT_ID=#{project.id})")
- end
- end
- end
-
- def move_repositories
- result = move_repository(old_disk_path, new_disk_path)
- project.reload_repository!
-
- if move_wiki
- result &&= move_repository(old_wiki_disk_path, new_wiki_disk_path)
- project.clear_memoization(:wiki)
- end
-
- if move_design
- result &&= move_repository(old_design_disk_path, new_design_disk_path)
- project.clear_memoization(:design_repository)
- end
-
- result
- end
-
- def rollback_folder_move
- move_repository(new_disk_path, old_disk_path)
- move_repository(new_wiki_disk_path, old_wiki_disk_path)
- move_repository(new_design_disk_path, old_design_disk_path) if move_design
- end
-
- def try_to_set_repository_read_only!
- project.set_repository_read_only!
- rescue Project::RepositoryReadOnlyError => err
- migration_error = "Target repository '#{old_disk_path}' cannot be made read-only: #{err.message}"
- logger.error migration_error
-
- raise RepositoryInUseError, migration_error
- end
-
- def wiki_path_suffix
- @wiki_path_suffix ||= Gitlab::GlRepository::WIKI.path_suffix
- end
-
- def old_wiki_disk_path
- @old_wiki_disk_path ||= "#{old_disk_path}#{wiki_path_suffix}"
- end
-
- def new_wiki_disk_path
- @new_wiki_disk_path ||= "#{new_disk_path}#{wiki_path_suffix}"
- end
-
- def design_path_suffix
- @design_path_suffix ||= ::Gitlab::GlRepository::DESIGN.path_suffix
- end
-
- def old_design_disk_path
- @old_design_disk_path ||= "#{old_disk_path}#{design_path_suffix}"
- end
-
- def new_design_disk_path
- @new_design_disk_path ||= "#{new_disk_path}#{design_path_suffix}"
- end
- end
- end
-end
diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb
deleted file mode 100644
index b65d0e63fd3..00000000000
--- a/app/services/projects/hashed_storage/migrate_repository_service.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module HashedStorage
- class MigrateRepositoryService < BaseRepositoryService
- def execute
- try_to_set_repository_read_only!
-
- @old_storage_version = project.storage_version
- project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
-
- @new_disk_path = project.disk_path
-
- result = move_repositories
-
- if result
- project.set_full_path
- project.track_project_repository
- else
- rollback_folder_move
- project.storage_version = nil
- end
-
- project.transaction do
- project.save!(validate: false)
- project.set_repository_writable!
- end
-
- result
- rescue Gitlab::Git::CommandError => e
- logger.error("Repository #{project.full_path} failed to upgrade (PROJECT_ID=#{project.id}). Git operation failed: #{e.inspect}")
-
- rollback_migration!
-
- false
- rescue OpenSSL::Cipher::CipherError => e
- logger.error("Repository #{project.full_path} failed to upgrade (PROJECT_ID=#{project.id}). There is a problem with encrypted attributes: #{e.inspect}")
-
- rollback_migration!
-
- false
- end
-
- private
-
- def rollback_migration!
- rollback_folder_move
- project.storage_version = nil
- project.set_repository_writable!
- end
- end
- end
-end
-
-Projects::HashedStorage::MigrateRepositoryService.prepend_mod_with('Projects::HashedStorage::MigrateRepositoryService')
diff --git a/app/services/projects/hashed_storage/migration_service.rb b/app/services/projects/hashed_storage/migration_service.rb
index 57a775a8f9e..e2015a4cca6 100644
--- a/app/services/projects/hashed_storage/migration_service.rb
+++ b/app/services/projects/hashed_storage/migration_service.rb
@@ -12,11 +12,6 @@ module Projects
end
def execute
- # Migrate repository from Legacy to Hashed Storage
- unless project.hashed_storage?(:repository)
- return false unless migrate_repository_service.execute
- end
-
# Migrate attachments from Legacy to Hashed Storage
unless project.hashed_storage?(:attachments)
return false unless migrate_attachments_service.execute
@@ -27,10 +22,6 @@ module Projects
private
- def migrate_repository_service
- HashedStorage::MigrateRepositoryService.new(project: project, old_disk_path: old_disk_path, logger: logger)
- end
-
def migrate_attachments_service
HashedStorage::MigrateAttachmentsService.new(project: project, old_disk_path: old_disk_path, logger: logger)
end
diff --git a/app/services/projects/hashed_storage/rollback_attachments_service.rb b/app/services/projects/hashed_storage/rollback_attachments_service.rb
deleted file mode 100644
index 4bb8cb605a3..00000000000
--- a/app/services/projects/hashed_storage/rollback_attachments_service.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module HashedStorage
- class RollbackAttachmentsService < BaseAttachmentService
- def execute
- origin = FileUploader.absolute_base_dir(project)
-
- project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
- target = FileUploader.absolute_base_dir(project)
-
- @new_disk_path = FileUploader.base_dir(project)
-
- result = move_folder!(origin, target)
-
- if result
- project.save!(validate: false)
-
- yield if block_given?
- else
- # Rollback changes
- project.rollback!
- end
-
- result
- end
- end
- end
-end
diff --git a/app/services/projects/hashed_storage/rollback_repository_service.rb b/app/services/projects/hashed_storage/rollback_repository_service.rb
deleted file mode 100644
index f4146ff9158..00000000000
--- a/app/services/projects/hashed_storage/rollback_repository_service.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module HashedStorage
- class RollbackRepositoryService < BaseRepositoryService
- def execute
- try_to_set_repository_read_only!
-
- @old_storage_version = project.storage_version
- project.storage_version = nil
-
- @new_disk_path = project.disk_path
-
- result = move_repositories
-
- if result
- project.set_full_path
- project.track_project_repository
- else
- rollback_folder_move
- project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
- end
-
- project.transaction do
- project.save!(validate: false)
- project.set_repository_writable!
- end
-
- result
- rescue Gitlab::Git::CommandError => e
- logger.error("Repository #{project.full_path} failed to rollback (PROJECT_ID=#{project.id}). Git operation failed: #{e.inspect}")
-
- rollback_migration!
-
- false
- rescue OpenSSL::Cipher::CipherError => e
- logger.error("Repository #{project.full_path} failed to rollback (PROJECT_ID=#{project.id}). There is a problem with encrypted attributes: #{e.inspect}")
-
- rollback_migration!
-
- false
- end
-
- private
-
- def rollback_migration!
- rollback_folder_move
- project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
- project.set_repository_writable!
- end
- end
- end
-end
diff --git a/app/services/projects/hashed_storage/rollback_service.rb b/app/services/projects/hashed_storage/rollback_service.rb
deleted file mode 100644
index 01b343a12d1..00000000000
--- a/app/services/projects/hashed_storage/rollback_service.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module HashedStorage
- class RollbackService < BaseService
- attr_reader :logger, :old_disk_path
-
- def initialize(project, old_disk_path, logger: nil)
- @project = project
- @old_disk_path = old_disk_path
- @logger = logger || Gitlab::AppLogger
- end
-
- def execute
- # Rollback attachments from Hashed Storage to Legacy
- if project.hashed_storage?(:attachments)
- return false unless rollback_attachments_service.execute
- end
-
- # Rollback repository from Hashed Storage to Legacy
- if project.hashed_storage?(:repository)
- rollback_repository_service.execute
- end
- end
-
- private
-
- def rollback_attachments_service
- HashedStorage::RollbackAttachmentsService.new(project: project, old_disk_path: old_disk_path, logger: logger)
- end
-
- def rollback_repository_service
- HashedStorage::RollbackRepositoryService.new(project: project, old_disk_path: old_disk_path, logger: logger)
- end
- end
- end
-end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index e22b728cea3..fde56d8429e 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -29,7 +29,7 @@ module Projects
after_execute_hook
success
- rescue Gitlab::UrlBlocker::BlockedUrlError, StandardError => e
+ rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError, StandardError => e
Gitlab::Import::ImportFailureService.track(
project_id: project.id,
error_source: self.class.name,
@@ -76,7 +76,7 @@ module Projects
if project.external_import? && !unknown_url?
begin
@resolved_address = get_resolved_address
- rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e
raise e, s_("ImportProjects|Blocked import URL: %{message}") % { message: e.message }
end
end
diff --git a/app/services/projects/in_product_marketing_campaign_emails_service.rb b/app/services/projects/in_product_marketing_campaign_emails_service.rb
deleted file mode 100644
index a549d8f594e..00000000000
--- a/app/services/projects/in_product_marketing_campaign_emails_service.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- class InProductMarketingCampaignEmailsService
- include Gitlab::Experiment::Dsl
-
- def initialize(project, campaign)
- @project = project
- @campaign = campaign
- @sent_email_records = ::Users::InProductMarketingEmailRecords.new
- end
-
- def execute
- send_emails
- end
-
- private
-
- attr_reader :project, :campaign, :sent_email_records
-
- def send_emails
- project_users.each do |user|
- send_email(user)
- end
-
- sent_email_records.save!
- end
-
- def project_users
- @project_users ||= project.users.merge(Users::InProductMarketingEmail.without_campaign(campaign))
- end
-
- def project_users_max_access_levels
- ids = project_users.map(&:id)
- @project_users_max_access_levels ||= project.team.max_member_access_for_user_ids(ids)
- end
-
- def send_email(user)
- return unless user.can?(:receive_notifications)
- return unless target_user?(user)
-
- Notify.build_ios_app_guide_email(user.notification_email_or_default).deliver_later
-
- sent_email_records.add(user, campaign: campaign)
- experiment(:build_ios_app_guide_email, project: project).track(:email_sent)
- end
-
- def target_user?(user)
- max_access_level = project_users_max_access_levels[user.id]
- max_access_level >= Gitlab::Access::DEVELOPER
- end
- end
-end
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index 458eaec4e2e..fe19d1f051d 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -11,8 +11,8 @@ module Projects
noteable_owner +
participants_in_noteable +
all_members +
- groups +
- project_members
+ project_members +
+ groups
render_participants_as_hash(participants.uniq)
end
diff --git a/app/services/projects/record_target_platforms_service.rb b/app/services/projects/record_target_platforms_service.rb
index 664e72e9785..d43b587154b 100644
--- a/app/services/projects/record_target_platforms_service.rb
+++ b/app/services/projects/record_target_platforms_service.rb
@@ -28,26 +28,11 @@ module Projects
project_setting.target_platforms = target_platforms
project_setting.save
-
- send_build_ios_app_guide_email
-
project_setting.target_platforms
end
def project_setting
@project_setting ||= ::ProjectSetting.find_or_initialize_by(project: project) # rubocop:disable CodeReuse/ActiveRecord
end
-
- def experiment_candidate?
- experiment(:build_ios_app_guide_email, project: project).run
- end
-
- def send_build_ios_app_guide_email
- return unless target_platforms.include? :ios
- return unless experiment_candidate?
-
- campaign = Users::InProductMarketingEmail::BUILD_IOS_APP_GUIDE
- Projects::InProductMarketingCampaignEmailsService.new(project, campaign).execute
- end
end
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 3d08039942b..30d9e1922cc 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -124,9 +124,6 @@ module Projects
# Notifications
project.send_move_instructions(@old_path)
- # Directories on disk
- move_project_folders(project)
-
transfer_missing_group_resources(@old_group)
# Move uploads
@@ -235,44 +232,15 @@ module Projects
end
def rollback_side_effects
- rollback_folder_move
project.reset
update_namespace_and_visibility(@old_namespace)
update_repository_configuration(@old_path)
end
- def rollback_folder_move
- return if project.hashed_storage?(:repository)
-
- move_repo_folder(@new_path, @old_path)
- move_repo_folder(new_wiki_repo_path, old_wiki_repo_path)
- move_repo_folder(new_design_repo_path, old_design_repo_path)
- end
-
- def move_repo_folder(from_name, to_name)
- gitlab_shell.mv_repository(project.repository_storage, from_name, to_name)
- end
-
def execute_system_hooks
system_hook_service.execute_hooks_for(project, :transfer)
end
- def move_project_folders(project)
- return if project.hashed_storage?(:repository)
-
- # Move main repository
- unless move_repo_folder(@old_path, @new_path)
- raise TransferError, s_("TransferProject|Cannot move project")
- end
-
- # Disk path is changed; we need to ensure we reload it
- project.reload_repository!
-
- # Move wiki and design repos also if present
- move_repo_folder(old_wiki_repo_path, new_wiki_repo_path)
- move_repo_folder(old_design_repo_path, new_design_repo_path)
- end
-
def move_project_uploads(project)
return if project.hashed_storage?(:attachments)
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index dc92c501b8c..ab38efff7c9 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -109,11 +109,6 @@ module Projects
PagesDeployment.deactivate_deployments_older_than(
deployment,
time: OLD_DEPLOYMENTS_DESTRUCTION_DELAY.from_now)
-
- DestroyPagesDeploymentsWorker.perform_in(
- OLD_DEPLOYMENTS_DESTRUCTION_DELAY,
- project.id,
- deployment.id)
end
def register_attempt
diff --git a/app/services/projects/update_repository_storage_service.rb b/app/services/projects/update_repository_storage_service.rb
index 799ae5677c3..85fb1890fcd 100644
--- a/app/services/projects/update_repository_storage_service.rb
+++ b/app/services/projects/update_repository_storage_service.rb
@@ -48,13 +48,8 @@ module Projects
pool_repository: pool_repository
)
- checksum, new_checksum = replicate_object_pool_repository(from: pool_repository, to: target_pool_repository)
-
- if checksum != new_checksum
- raise Error,
- format(s_('UpdateRepositoryStorage|Failed to verify %{type} repository checksum from %{old} to %{new}'),
- type: 'object_pool', old: checksum, new: new_checksum)
- end
+ Repositories::ReplicateService.new(pool_repository.object_pool.repository)
+ .execute(target_pool_repository.object_pool.repository, :object_pool)
end
def remove_old_paths
@@ -96,19 +91,6 @@ module Projects
)
end
- def replicate_object_pool_repository(from:, to:)
- old_object_pool = from.object_pool
- new_object_pool = to.object_pool
-
- checksum = old_object_pool.repository.checksum
-
- new_object_pool.repository.replicate(old_object_pool.repository)
-
- new_checksum = new_object_pool.repository.checksum
-
- [checksum, new_checksum]
- end
-
def replicate_object_pool_on_move_ff_enabled?
Feature.enabled?(:replicate_object_pool_on_move, project)
end
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
index f0243d844d9..95e0861a37a 100644
--- a/app/services/releases/create_service.rb
+++ b/app/services/releases/create_service.rb
@@ -19,7 +19,7 @@ module Releases
return tag unless tag.is_a?(Gitlab::Git::Tag)
if project.catalog_resource
- response = Ci::Catalog::ValidateResourceService.new(project, ref).execute
+ response = Ci::Catalog::Resources::ValidateService.new(project, ref).execute
return error(response.message) if response.error?
end
diff --git a/app/services/releases/destroy_service.rb b/app/services/releases/destroy_service.rb
index 41b421662ef..78613c05ff1 100644
--- a/app/services/releases/destroy_service.rb
+++ b/app/services/releases/destroy_service.rb
@@ -9,6 +9,8 @@ module Releases
if release.destroy
update_catalog_resource!
+ execute_hooks(release, 'delete')
+
success(tag: existing_tag, release: release)
else
error(release.errors.messages || '400 Bad request', 400)
diff --git a/app/services/repositories/base_service.rb b/app/services/repositories/base_service.rb
index bf7ac2e5fd8..371ff2fc499 100644
--- a/app/services/repositories/base_service.rb
+++ b/app/services/repositories/base_service.rb
@@ -15,10 +15,6 @@ class Repositories::BaseService < BaseService
gitlab_shell.repository_exists?(repository.shard, path + '.git')
end
- def mv_repository(from_path, to_path)
- gitlab_shell.mv_repository(repository.shard, from_path, to_path)
- end
-
# If we get a Gitaly error, the repository may be corrupted. We can
# ignore these errors since we're going to trash the repositories
# anyway.
diff --git a/app/services/repositories/replicate_service.rb b/app/services/repositories/replicate_service.rb
new file mode 100644
index 00000000000..0148223910f
--- /dev/null
+++ b/app/services/repositories/replicate_service.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Repositories
+ class ReplicateService < Repositories::BaseService
+ Error = Class.new(StandardError)
+
+ def execute(new_repository, type)
+ new_repository.replicate(repository)
+
+ new_checksum = new_repository.checksum
+ checksum = repository.checksum
+
+ return if new_checksum == checksum
+
+ raise Error, format(s_(
+ 'ReplicateService|Failed to verify %{type} repository checksum from %{old} to %{new}'
+ ), type: type, old: checksum, new: new_checksum)
+ rescue StandardError => e
+ new_repository.remove
+
+ raise e
+ end
+ end
+end
diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb
index 9efe51b43b8..2d4bebc8b2b 100644
--- a/app/services/spam/spam_verdict_service.rb
+++ b/app/services/spam/spam_verdict_service.rb
@@ -90,7 +90,7 @@ module Spam
end
def allow_possible_spam?
- target.allow_possible_spam?(user) || user.allow_possible_spam?
+ target.allow_possible_spam?(user) || user.trusted?
end
def spamcheck_client
diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb
index 04ae734a8fe..8442ff81d41 100644
--- a/app/services/system_notes/issuables_service.rb
+++ b/app/services/system_notes/issuables_service.rb
@@ -24,15 +24,16 @@ module SystemNotes
end
#
- # noteable_ref - Referenced noteable object
+ # noteable_ref - Referenced noteable object, or array of objects
#
# Example Note text:
#
# "marked this issue as related to gitlab-foss#9001"
+ # "marked this issue as related to gitlab-foss#9001, gitlab-foss#9002, and gitlab-foss#9003"
#
# Returns the created Note object
def relate_issuable(noteable_ref)
- body = "marked this #{noteable_name} as related to #{noteable_ref.to_reference(noteable.resource_parent)}"
+ body = "marked this #{noteable_name} as related to #{extract_issuable_reference(noteable_ref)}"
track_issue_event(:track_issue_related_action)
@@ -539,6 +540,14 @@ module SystemNotes
name.humanize(capitalize: false)
end
+
+ def extract_issuable_reference(reference)
+ if reference.is_a?(Array)
+ reference.map { |item| item.to_reference(noteable.resource_parent) }.to_sentence
+ else
+ reference.to_reference(noteable.resource_parent)
+ end
+ end
end
end
diff --git a/app/services/tasks_to_be_done/base_service.rb b/app/services/tasks_to_be_done/base_service.rb
deleted file mode 100644
index 1d50e5081ff..00000000000
--- a/app/services/tasks_to_be_done/base_service.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-module TasksToBeDone
- class BaseService < ::BaseContainerService
- LABEL_PREFIX = 'tasks to be done'
-
- def initialize(container:, current_user:, assignee_ids: [])
- params = {
- assignee_ids: assignee_ids,
- title: title,
- description: description,
- add_labels: label_name
- }
- super(container: container, current_user: current_user, params: params)
- end
-
- def execute
- if (issue = existing_task_issue)
- update_service = Issues::UpdateService.new(container: project, current_user: current_user, params: { add_assignee_ids: params[:assignee_ids] })
- update_service.execute(issue)
- else
- create_service = Issues::CreateService.new(container: project, current_user: current_user, params: params, perform_spam_check: false)
- create_service.execute
- end
- end
-
- private
-
- def existing_task_issue
- IssuesFinder.new(
- current_user,
- project_id: project.id,
- state: 'opened',
- non_archived: true,
- label_name: label_name
- ).execute.last
- end
-
- def title
- raise NotImplementedError
- end
-
- def description
- raise NotImplementedError
- end
-
- def label_suffix
- raise NotImplementedError
- end
-
- def label_name
- "#{LABEL_PREFIX}:#{label_suffix}"
- end
- end
-end
diff --git a/app/services/tasks_to_be_done/create_ci_task_service.rb b/app/services/tasks_to_be_done/create_ci_task_service.rb
deleted file mode 100644
index 025ca2feb8e..00000000000
--- a/app/services/tasks_to_be_done/create_ci_task_service.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-module TasksToBeDone
- class CreateCiTaskService < BaseService
- protected
-
- def title
- 'Set up CI/CD'
- end
-
- def description
- <<~DESCRIPTION
- GitLab CI/CD is a tool built into GitLab for software development through the [continuous methodologies](https://docs.gitlab.com/ee/ci/introduction/index.html#introduction-to-cicd-methodologies):
-
- * Continuous Integration (CI)
- * Continuous Delivery (CD)
- * Continuous Deployment (CD)
-
- Continuous Integration works by pushing small changes to your application’s codebase hosted in a Git repository, and, to every push, run a pipeline of scripts to build, test, and validate the code changes before merging them into the main branch.
-
- Continuous Delivery and Deployment consist of a step further CI, deploying your application to production at every push to the default branch of the repository.
-
- These methodologies allow you to catch bugs and errors early in the development cycle, ensuring that all the code deployed to production complies with the code standards you established for your app.
-
- * :book: [Read the documentation](https://docs.gitlab.com/ee/ci/introduction/index.html)
- * :clapper: [Watch a Demo](https://www.youtube.com/watch?v=1iXFbchozdY)
-
- ## Next steps
-
- * [ ] To start we recommend reviewing the following documentation:
- * [ ] [How GitLab CI/CD works.](https://docs.gitlab.com/ee/ci/introduction/index.html#how-gitlab-cicd-works)
- * [ ] [Fundamental pipeline architectures.](https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html)
- * [ ] [GitLab CI/CD basic workflow.](https://docs.gitlab.com/ee/ci/introduction/index.html#basic-cicd-workflow)
- * [ ] [Step-by-step guide for writing .gitlab-ci.yml for the first time.](https://docs.gitlab.com/ee/user/project/pages/getting_started_part_four.html)
- * [ ] When you're ready select **Projects** (in the top navigation bar) > **Your projects** > select the Project you've already created.
- * [ ] Select **CI / CD** in the left navigation to start setting up CI / CD in your project.
- DESCRIPTION
- end
-
- def label_suffix
- 'ci'
- end
- end
-end
diff --git a/app/services/tasks_to_be_done/create_code_task_service.rb b/app/services/tasks_to_be_done/create_code_task_service.rb
deleted file mode 100644
index dc3b9366a66..00000000000
--- a/app/services/tasks_to_be_done/create_code_task_service.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-module TasksToBeDone
- class CreateCodeTaskService < BaseService
- protected
-
- def title
- 'Create or import your code into your Project (Repository)'
- end
-
- def description
- <<~DESCRIPTION
- You've already created your Group and Project within GitLab; we'll quickly review this hierarchy below. Once you're within your project you can easily create or import repositories.
-
- **With GitLab Groups, you can:**
-
- * Create one or multiple Projects for hosting your codebase (repositories).
- * Assemble related projects together.
- * Grant members access to several projects at once.
-
- Groups can also be nested in subgroups.
-
- Read more about groups in our [documentation](https://docs.gitlab.com/ee/user/group/).
-
- **Within GitLab Projects, you can**
-
- * Use it as an issue tracker.
- * Collaborate on code.
- * Continuously build, test, and deploy your app with built-in GitLab CI/CD.
-
- You can also import an existing repository by providing the Git URL.
-
- * :book: [Read the documentation](https://docs.gitlab.com/ee/user/project/index.html).
-
- ## Next steps
-
- Create or import your first repository into the project you created:
-
- * [ ] Click **Projects** in the top navigation bar, then click **Your projects**.
- * [ ] Select the Project that you created, then select **Repository**.
- * [ ] Once on the Repository page you can select the **+** icon to add or import files.
- * [ ] You can review our full documentation on creating [repositories](https://docs.gitlab.com/ee/user/project/repository/) in GitLab.
-
- :tada: All done, you can close this issue!
- DESCRIPTION
- end
-
- def label_suffix
- 'code'
- end
- end
-end
diff --git a/app/services/tasks_to_be_done/create_issues_task_service.rb b/app/services/tasks_to_be_done/create_issues_task_service.rb
deleted file mode 100644
index a2de6852868..00000000000
--- a/app/services/tasks_to_be_done/create_issues_task_service.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-module TasksToBeDone
- class CreateIssuesTaskService < BaseService
- protected
-
- def title
- 'Create/import issues (tickets) to collaborate on ideas and plan work'
- end
-
- def description
- <<~DESCRIPTION
- Issues allow you and your team to discuss proposals before, and during, their implementation. They can be used for a variety of other purposes, customized to your needs and workflow.
-
- Issues are always associated with a specific project. If you have multiple projects in a group, you can view all the issues at the group level. [You can review our full Issue documentation here.](https://docs.gitlab.com/ee/user/project/issues/)
-
- If you have existing issues or equivalent tickets you can import them as long as they are formatted as a CSV file, [the import process is covered here](https://docs.gitlab.com/ee/user/project/issues/csv_import.html).
-
- **Common use cases include:**
-
- * Discussing the implementation of a new idea
- * Tracking tasks and work status
- * Accepting feature proposals, questions, support requests, or bug reports
- * Elaborating on new code implementations
-
- ## Next steps
-
- * [ ] Select **Projects** in the top navigation > **Your Projects** > select the Project you've already created.
- * [ ] Once you've selected that project, you can select **Issues** in the left navigation, then click **New issue**.
- * [ ] Fill in the title and description in the **New issue** page.
- * [ ] Click on **Create issue**.
-
- Pro tip: When you're in a group or project you can always utilize the **+** icon in the top navigation (located to the left of the search bar) to quickly create new issues.
-
- That's it! You can close this issue.
- DESCRIPTION
- end
-
- def label_suffix
- 'issues'
- end
- end
-end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 1f6cf2c83c9..be7405cc896 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -168,7 +168,7 @@ class TodoService
def mark_todo(target, current_user)
project = target.project
attributes = attributes_for_todo(project, target, current_user, Todo::MARKED)
- create_todos(current_user, attributes, project&.namespace, project)
+ create_todos(current_user, attributes, target_namespace(target), project)
end
def todo_exist?(issuable, current_user)
@@ -338,7 +338,7 @@ class TodoService
project = target.project
assignees = target.assignees - old_assignees
attributes = attributes_for_todo(project, target, author, Todo::ASSIGNED)
- create_todos(assignees, attributes, project.namespace, project)
+ create_todos(assignees, attributes, target_namespace(target), project)
end
end
@@ -386,6 +386,7 @@ class TodoService
attributes.merge!(target_id: nil, commit_id: target.id)
when Issue
attributes[:issue_type] = target.issue_type
+ attributes[:group] = target.namespace if target.project.blank?
when Discussion
attributes.merge!(target_type: nil, target_id: nil, discussion: target)
end
@@ -469,6 +470,11 @@ class TodoService
attributes
end
+
+ def target_namespace(target)
+ project = target.project
+ project&.namespace || target.try(:namespace)
+ end
end
TodoService.prepend_mod_with('TodoService')
diff --git a/app/services/todos/destroy/base_service.rb b/app/services/todos/destroy/base_service.rb
index ed5c4df85b1..c9c86330e1c 100644
--- a/app/services/todos/destroy/base_service.rb
+++ b/app/services/todos/destroy/base_service.rb
@@ -4,37 +4,6 @@ module Todos
module Destroy
class BaseService
def execute
- return unless todos_to_remove?
-
- ::Gitlab::Database.allow_cross_joins_across_databases(url:
- 'https://gitlab.com/gitlab-org/gitlab/-/issues/422045') do
- without_authorized(todos).delete_all
- end
- end
-
- private
-
- # rubocop: disable CodeReuse/ActiveRecord
- def without_authorized(items)
- items.where.not('todos.user_id' => authorized_users)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- # rubocop: disable CodeReuse/ActiveRecord
- def authorized_users
- ProjectAuthorization.select(:user_id).where(project_id: project_ids)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def todos
- raise NotImplementedError
- end
-
- def project_ids
- raise NotImplementedError
- end
-
- def todos_to_remove?
raise NotImplementedError
end
end
diff --git a/app/services/todos/destroy/confidential_issue_service.rb b/app/services/todos/destroy/confidential_issue_service.rb
index fadc76b1181..331c4a12681 100644
--- a/app/services/todos/destroy/confidential_issue_service.rb
+++ b/app/services/todos/destroy/confidential_issue_service.rb
@@ -9,58 +9,59 @@ module Todos
# When issue_id is passed it deletes matching todos for one confidential issue.
# When project_id is passed it deletes matching todos for all confidential issues of the project.
class ConfidentialIssueService < ::Todos::Destroy::BaseService
- extend ::Gitlab::Utils::Override
-
attr_reader :issues
- # rubocop: disable CodeReuse/ActiveRecord
def initialize(issue_id: nil, project_id: nil)
@issues =
if issue_id
- Issue.where(id: issue_id)
+ Issue.id_in(issue_id)
elsif project_id
project_confidential_issues(project_id)
end
end
- # rubocop: enable CodeReuse/ActiveRecord
+
+ def execute
+ return unless todos_to_remove?
+
+ ::Gitlab::Database.allow_cross_joins_across_databases(
+ url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/422045') do
+ delete_todos
+ end
+ end
private
+ def delete_todos
+ authorized_users = ProjectAuthorization.select(:user_id)
+ .for_project(project_ids)
+ .non_guests
+
+ todos.not_in_users(authorized_users).delete_all
+ end
+
def project_confidential_issues(project_id)
project = Project.find(project_id)
project.issues.confidential_only
end
- override :todos
# rubocop: disable CodeReuse/ActiveRecord
def todos
Todo.joins_issue_and_assignees
- .where(target: issues)
- .where(issues: { confidential: true })
+ .for_target(issues)
+ .merge(Issue.confidential_only)
.where('todos.user_id != issues.author_id')
.where('todos.user_id != issue_assignees.user_id')
end
# rubocop: enable CodeReuse/ActiveRecord
- override :todos_to_remove?
def todos_to_remove?
issues&.any?(&:confidential?)
end
- override :project_ids
def project_ids
issues&.distinct&.select(:project_id)
end
-
- override :authorized_users
- # rubocop: disable CodeReuse/ActiveRecord
- def authorized_users
- ProjectAuthorization.select(:user_id)
- .where(project_id: project_ids)
- .where('access_level >= ?', Gitlab::Access::REPORTER)
- end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/app/services/todos/destroy/group_private_service.rb b/app/services/todos/destroy/group_private_service.rb
index 60599ca9ca4..6962c204e0e 100644
--- a/app/services/todos/destroy/group_private_service.rb
+++ b/app/services/todos/destroy/group_private_service.rb
@@ -7,30 +7,34 @@ module Todos
attr_reader :group
- # rubocop: disable CodeReuse/ActiveRecord
def initialize(group_id)
- @group = Group.find_by(id: group_id)
+ @group = Group.find_by_id(group_id)
+ end
+
+ def execute
+ return unless todos_to_remove?
+
+ delete_todos
end
- # rubocop: enable CodeReuse/ActiveRecord
private
- override :todos
- # rubocop: disable CodeReuse/ActiveRecord
- def todos
- Todo.where(group_id: group.id)
+ def delete_todos
+ authorized_users = Member.from_union(
+ [
+ group.descendant_project_members_with_inactive.select(:user_id),
+ group.members_with_parents.select(:user_id)
+ ],
+ remove_duplicates: false
+ ).select(:user_id)
+
+ todos.not_in_users(authorized_users).delete_all
end
- # rubocop: enable CodeReuse/ActiveRecord
-
- override :authorized_users
- def authorized_users
- User.from_union([
- group.project_users_with_descendants.select(:id),
- group.members_with_parents.select(:user_id)
- ], remove_duplicates: false)
+
+ def todos
+ Todo.for_group(group.id)
end
- override :todos_to_remove?
def todos_to_remove?
group&.private?
end
diff --git a/app/services/todos/destroy/project_private_service.rb b/app/services/todos/destroy/project_private_service.rb
index e00d10c3780..a1ca0d8543c 100644
--- a/app/services/todos/destroy/project_private_service.rb
+++ b/app/services/todos/destroy/project_private_service.rb
@@ -7,27 +7,32 @@ module Todos
attr_reader :project
- # rubocop: disable CodeReuse/ActiveRecord
def initialize(project_id)
- @project = Project.find_by(id: project_id)
+ @project = Project.find_by_id(project_id)
+ end
+
+ def execute
+ return unless todos_to_remove?
+
+ delete_todos
end
- # rubocop: enable CodeReuse/ActiveRecord
private
- override :todos
- # rubocop: disable CodeReuse/ActiveRecord
+ def delete_todos
+ authorized_users = ProjectAuthorization.select(:user_id).for_project(project_ids)
+
+ todos.not_in_users(authorized_users).delete_all
+ end
+
def todos
- Todo.where(project_id: project.id)
+ Todo.for_project(project.id)
end
- # rubocop: enable CodeReuse/ActiveRecord
- override :project_ids
def project_ids
project.id
end
- override :todos_to_remove?
def todos_to_remove?
project&.private?
end
diff --git a/app/services/todos/destroy/unauthorized_features_service.rb b/app/services/todos/destroy/unauthorized_features_service.rb
index 513def10575..22f7a0b2a37 100644
--- a/app/services/todos/destroy/unauthorized_features_service.rb
+++ b/app/services/todos/destroy/unauthorized_features_service.rb
@@ -27,6 +27,14 @@ module Todos
private
+ def without_authorized(items)
+ items.not_in_users(authorized_users)
+ end
+
+ def authorized_users
+ ProjectAuthorization.select(:user_id).for_project(project_ids)
+ end
+
def related_todos
base_scope = Todo.for_project(project_id)
base_scope = base_scope.for_user(user_id) if user_id
diff --git a/app/services/update_container_registry_info_service.rb b/app/services/update_container_registry_info_service.rb
index 7d79b257687..b720f9b14a5 100644
--- a/app/services/update_container_registry_info_service.rb
+++ b/app/services/update_container_registry_info_service.rb
@@ -11,7 +11,7 @@ class UpdateContainerRegistryInfoService
# associated user when running this (e.g. from a rake task or a cron job),
# so we need to generate a valid JWT token with no access permissions to
# authenticate as a trusted client.
- token = Auth::ContainerRegistryAuthenticationService.access_token([], [])
+ token = Auth::ContainerRegistryAuthenticationService.access_token({})
client = ContainerRegistry::Client.new(registry_config.api_url, token: token)
info = client.registry_info
@@ -24,7 +24,8 @@ class UpdateContainerRegistryInfoService
Gitlab::CurrentSettings.update!(
container_registry_vendor: info[:vendor] || '',
container_registry_version: info[:version] || '',
- container_registry_features: info[:features] || []
+ container_registry_features: info[:features] || [],
+ container_registry_db_enabled: info[:db_enabled] || false
)
end
end
diff --git a/app/services/users/allow_possible_spam_service.rb b/app/services/users/allow_possible_spam_service.rb
deleted file mode 100644
index d9273fe0fc1..00000000000
--- a/app/services/users/allow_possible_spam_service.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module Users
- class AllowPossibleSpamService < BaseService
- def initialize(current_user)
- @current_user = current_user
- end
-
- def execute(user)
- custom_attribute = {
- user_id: user.id,
- key: UserCustomAttribute::ALLOW_POSSIBLE_SPAM,
- value: "#{current_user.username}/#{current_user.id}+#{Time.current}"
- }
- UserCustomAttribute.upsert_custom_attributes([custom_attribute])
- end
- end
-end
diff --git a/app/services/users/auto_ban_service.rb b/app/services/users/auto_ban_service.rb
new file mode 100644
index 00000000000..fa3b738b4cd
--- /dev/null
+++ b/app/services/users/auto_ban_service.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Users
+ class AutoBanService < BaseService
+ def initialize(user:, reason:)
+ @user = user
+ @reason = reason
+ end
+
+ def execute
+ if user.ban
+ record_custom_attribute
+ success
+ else
+ messages = user.errors.full_messages
+ error(messages.uniq.join('. '))
+ end
+ end
+
+ private
+
+ attr_reader :user, :reason
+
+ def record_custom_attribute
+ custom_attribute = {
+ user_id: user.id,
+ key: UserCustomAttribute::AUTO_BANNED_BY,
+ value: reason
+ }
+ UserCustomAttribute.upsert_custom_attributes([custom_attribute])
+ end
+ end
+end
diff --git a/app/services/users/in_product_marketing_email_records.rb b/app/services/users/in_product_marketing_email_records.rb
index 94dbd809496..fcb252536b3 100644
--- a/app/services/users/in_product_marketing_email_records.rb
+++ b/app/services/users/in_product_marketing_email_records.rb
@@ -13,10 +13,9 @@ module Users
@records = []
end
- def add(user, campaign: nil, track: nil, series: nil)
+ def add(user, track: nil, series: nil)
@records << Users::InProductMarketingEmail.new(
user: user,
- campaign: campaign,
track: track,
series: series,
created_at: Time.zone.now,
diff --git a/app/services/users/set_namespace_commit_email_service.rb b/app/services/users/set_namespace_commit_email_service.rb
index 30ee597120d..775db364625 100644
--- a/app/services/users/set_namespace_commit_email_service.rb
+++ b/app/services/users/set_namespace_commit_email_service.rb
@@ -20,7 +20,7 @@ module Users
return error(_("User doesn't exist or you don't have permission to change namespace commit emails."))
end
- unless can?(target_user, :read_namespace, namespace)
+ unless can?(target_user, :read_namespace_via_membership, namespace)
return error(_("Namespace doesn't exist or you don't have permission."))
end
diff --git a/app/services/users/signup_service.rb b/app/services/users/signup_service.rb
deleted file mode 100644
index 9eb1e75988c..00000000000
--- a/app/services/users/signup_service.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-module Users
- class SignupService < BaseService
- def initialize(current_user, params = {})
- @user = current_user
- @params = params.dup
- end
-
- def execute
- assign_attributes
- inject_validators
-
- if @user.save
- ServiceResponse.success
- else
- ServiceResponse.error(message: @user.errors.full_messages.join('. '))
- end
- end
-
- private
-
- def assign_attributes
- @user.assign_attributes(params) unless params.empty?
- end
-
- def inject_validators
- class << @user
- validates :role, presence: true
- validates :setup_for_company, inclusion: { in: [true, false], message: :blank } if Gitlab.com?
- end
- end
- end
-end
diff --git a/app/services/users/disallow_possible_spam_service.rb b/app/services/users/trust_service.rb
index e31ba7ddff0..faf0b9c40ea 100644
--- a/app/services/users/disallow_possible_spam_service.rb
+++ b/app/services/users/trust_service.rb
@@ -1,13 +1,14 @@
# frozen_string_literal: true
module Users
- class DisallowPossibleSpamService < BaseService
+ class TrustService < BaseService
def initialize(current_user)
@current_user = current_user
end
def execute(user)
- user.custom_attributes.by_key(UserCustomAttribute::ALLOW_POSSIBLE_SPAM).delete_all
+ UserCustomAttribute.set_trusted_by(user: user, trusted_by: @current_user)
+ success
end
end
end
diff --git a/app/services/users/untrust_service.rb b/app/services/users/untrust_service.rb
new file mode 100644
index 00000000000..aa5de71b97f
--- /dev/null
+++ b/app/services/users/untrust_service.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Users
+ class UntrustService < BaseService
+ def initialize(current_user)
+ @current_user = current_user
+ end
+
+ def execute(user)
+ user.trusted_with_spam_attribute.delete
+ success
+ end
+ end
+end
diff --git a/app/services/verify_pages_domain_service.rb b/app/services/verify_pages_domain_service.rb
index 408ee429a74..59c73aa929c 100644
--- a/app/services/verify_pages_domain_service.rb
+++ b/app/services/verify_pages_domain_service.rb
@@ -46,9 +46,15 @@ class VerifyPagesDomainService < BaseService
notify(:verification_succeeded)
end
+ after_successful_verification
+
success
end
+ def after_successful_verification
+ # method overridden in EE
+ end
+
def unverify_domain!
was_verified = domain.verified?
@@ -115,3 +121,5 @@ class VerifyPagesDomainService < BaseService
notification_service.public_send("pages_domain_#{type}", domain) # rubocop:disable GitlabSecurity/PublicSend
end
end
+
+VerifyPagesDomainService.prepend_mod
diff --git a/app/services/vs_code/settings/create_or_update_service.rb b/app/services/vs_code/settings/create_or_update_service.rb
new file mode 100644
index 00000000000..27688b911b7
--- /dev/null
+++ b/app/services/vs_code/settings/create_or_update_service.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module VsCode
+ module Settings
+ class CreateOrUpdateService
+ def initialize(current_user:, params: {})
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ # The GitLab VSCode settings API does not support creating or updating
+ # machines.
+ return ServiceResponse.success(payload: DEFAULT_MACHINE) if @params[:setting_type] == 'machines'
+
+ setting = VsCodeSetting.by_user(current_user).by_setting_type(params[:setting_type]).first
+
+ if setting.nil?
+ merged_params = params.merge(user: current_user, uuid: SecureRandom.uuid)
+ setting = VsCodeSetting.new(merged_params)
+ else
+ setting.content = params[:content]
+ setting.uuid = SecureRandom.uuid
+ end
+
+ if setting.save
+ ServiceResponse.success(payload: setting)
+ else
+ ServiceResponse.error(
+ message: setting.errors.full_messages.to_sentence,
+ payload: { setting: setting }
+ )
+ end
+ end
+
+ private
+
+ attr_reader :current_user, :params
+ end
+ end
+end
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 5bad2a1583c..27b29feed50 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -36,7 +36,9 @@ class WebHookService
attr_accessor :hook, :data, :hook_name, :request_options
attr_reader :uniqueness_token
- def self.hook_to_event(hook_name)
+ def self.hook_to_event(hook_name, hook = nil)
+ return hook.class.name.titleize if hook.is_a?(SystemHook)
+
hook_name.to_s.singularize.titleize
end
@@ -194,7 +196,7 @@ class WebHookService
headers = {
'Content-Type' => 'application/json',
'User-Agent' => "GitLab/#{Gitlab::VERSION}",
- Gitlab::WebHooks::GITLAB_EVENT_HEADER => self.class.hook_to_event(hook_name),
+ Gitlab::WebHooks::GITLAB_EVENT_HEADER => self.class.hook_to_event(hook_name, hook),
Gitlab::WebHooks::GITLAB_UUID_HEADER => SecureRandom.uuid,
Gitlab::WebHooks::GITLAB_INSTANCE_HEADER => Gitlab.config.gitlab.base_url
}
diff --git a/app/services/work_items/related_work_item_links/create_service.rb b/app/services/work_items/related_work_item_links/create_service.rb
index f313881470a..38e5ba3be7f 100644
--- a/app/services/work_items/related_work_item_links/create_service.rb
+++ b/app/services/work_items/related_work_item_links/create_service.rb
@@ -9,6 +9,7 @@ module WorkItems
return error(_('No matching work item found.'), 404) unless can?(current_user, :admin_work_item_link, issuable)
response = super
+ create_notes_async if new_links.any?
if response[:status] == :success
response[:message] = format(
@@ -30,6 +31,10 @@ module WorkItems
private
+ def create_notes(_issuable_link)
+ # no-op notes are created asynchronously
+ end
+
def link_class
WorkItems::RelatedWorkItemLink
end
@@ -49,9 +54,23 @@ module WorkItems
created_links.collect(&:target_id)
end
+ def create_notes_async
+ link_ids = new_links.collect(&:id)
+
+ worker_params = {
+ issuable_class: issuable.class.name,
+ issuable_id: issuable.id,
+ link_ids: link_ids,
+ link_type: params[:link_type] || 'relates_to',
+ user_id: current_user.id
+ }
+
+ Issuable::RelatedLinksCreateWorker.perform_async(worker_params)
+ end
+
override :issuables_already_assigned_message
def issuables_already_assigned_message
- _('Work items are already linked')
+ _('Items are already linked')
end
override :issuables_not_found_message
diff --git a/app/services/work_items/widgets/labels_service/update_service.rb b/app/services/work_items/widgets/labels_service/update_service.rb
index b880398677d..b0791571924 100644
--- a/app/services/work_items/widgets/labels_service/update_service.rb
+++ b/app/services/work_items/widgets/labels_service/update_service.rb
@@ -11,6 +11,7 @@ module WorkItems
end
return if params.blank?
+ return unless has_permission?(:set_work_item_metadata)
service_params.merge!(params.slice(:add_label_ids, :remove_label_ids))
end
diff --git a/app/services/work_items/widgets/start_and_due_date_service/update_service.rb b/app/services/work_items/widgets/start_and_due_date_service/update_service.rb
index 0dbf3aa31d9..5d47b3a1516 100644
--- a/app/services/work_items/widgets/start_and_due_date_service/update_service.rb
+++ b/app/services/work_items/widgets/start_and_due_date_service/update_service.rb
@@ -8,6 +8,7 @@ module WorkItems
return widget.work_item.assign_attributes({ start_date: nil, due_date: nil }) if new_type_excludes_widget?
return if params.blank?
+ return unless has_permission?(:set_work_item_metadata)
widget.work_item.assign_attributes(params.slice(:start_date, :due_date))
end
diff --git a/app/validators/addressable_url_validator.rb b/app/validators/addressable_url_validator.rb
index 6dcc089fa73..af7be326f51 100644
--- a/app/validators/addressable_url_validator.rb
+++ b/app/validators/addressable_url_validator.rb
@@ -84,7 +84,7 @@ class AddressableUrlValidator < ActiveModel::EachValidator
value = strip_value!(record, attribute, value)
Gitlab::UrlBlocker.validate!(value, **blocker_args)
- rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e
record.errors.add(attribute, options.fetch(:blocked_message) % { exception_message: e.message })
end
diff --git a/app/validators/duration_validator.rb b/app/validators/duration_validator.rb
index bcdcf665cba..defd28d7d3b 100644
--- a/app/validators/duration_validator.rb
+++ b/app/validators/duration_validator.rb
@@ -12,7 +12,7 @@
#
class DurationValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- ChronicDuration.parse(value, use_complete_matcher: true)
+ ChronicDuration.parse(value)
rescue ChronicDuration::DurationParseError
if options[:message]
record.errors.add(:base, options[:message])
diff --git a/app/validators/json_schemas/catalog_resource_component_inputs.json b/app/validators/json_schemas/catalog_resource_component_inputs.json
index 014a52d4f1b..830bf684838 100644
--- a/app/validators/json_schemas/catalog_resource_component_inputs.json
+++ b/app/validators/json_schemas/catalog_resource_component_inputs.json
@@ -15,6 +15,9 @@
"boolean"
]
},
+ "regex": {
+ "type": "string"
+ },
"^type$": {
"type": "string"
}
diff --git a/app/validators/json_schemas/vulnerability_cvss_vectors.json b/app/validators/json_schemas/vulnerability_cvss_vectors.json
new file mode 100644
index 00000000000..7ec1339e974
--- /dev/null
+++ b/app/validators/json_schemas/vulnerability_cvss_vectors.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Schema for cvss attribute of Vulnerability",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "default": "unknown"
+ },
+ "vector_string": {
+ "type": "string",
+ "example": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
+ }
+ },
+ "required": [
+ "vendor",
+ "vector_string"
+ ]
+ }
+}
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index b65649b5a07..4e55c99e445 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -17,7 +17,7 @@
.form-group
= f.label :receive_max_input_size, _('Maximum push size (MiB)'), class: 'label-light'
- = f.number_field :receive_max_input_size, class: 'form-control gl-form-input', title: _('Maximum size limit for a single commit.'), data: { toggle: 'tooltip', container: 'body', qa_selector: 'receive_max_input_size_field' }
+ = f.number_field :receive_max_input_size, class: 'form-control gl-form-input', title: _('Maximum size limit for a single commit.'), data: { toggle: 'tooltip', container: 'body', testid: 'receive-max-input-size-field' }
.form-group
= f.label :session_expire_delay, _('Session duration (minutes)'), class: 'label-light'
= f.number_field :session_expire_delay, class: 'form-control gl-form-input', title: _('Maximum duration of a session.'), data: { toggle: 'tooltip', container: 'body' }
@@ -70,4 +70,4 @@
= render_if_exists 'admin/application_settings/availability_on_namespace_setting', form: f
-# This is added for Jihu edition which should not be deleted without notifying Jihu
= render_if_exists 'admin/application_settings/password_expiration_setting', form: f
- = f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), pajamas_button: true, data: { testid: 'save-changes-button' }
diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml
index 65049fa5466..2d45391a839 100644
--- a/app/views/admin/application_settings/_email.html.haml
+++ b/app/views/admin/application_settings/_email.html.haml
@@ -18,11 +18,10 @@
.form-group
= f.gitlab_ui_checkbox_component :user_deactivation_emails_enabled, _('Enable user deactivation emails'), help_text: _('Send emails to users upon account deactivation.')
- - if Feature.enabled?(:deactivation_email_additional_text)
- .form-group
- = f.label :deactivation_email_additional_text, _('Additional text for deactivation email')
- = f.text_area :deactivation_email_additional_text, class: 'form-control gl-form-input', rows: 4
- .form-text.text-muted
- = _('Text added to the body of user deactivation email messages. 1000 character limit.')
+ .form-group
+ = f.label :deactivation_email_additional_text, _('Additional text for deactivation email')
+ = f.text_area :deactivation_email_additional_text, class: 'form-control gl-form-input', rows: 4
+ .form-text.text-muted
+ = _('Text added to the body of user deactivation email messages. 1000 character limit.')
- = f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), pajamas_button: true, data: { testid: 'save-changes-button' }
diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml
index 01d7bf0af67..cba37527606 100644
--- a/app/views/admin/application_settings/_ip_limits.html.haml
+++ b/app/views/admin/application_settings/_ip_limits.html.haml
@@ -8,7 +8,7 @@
.form-group
= f.gitlab_ui_checkbox_component :throttle_unauthenticated_api_enabled,
_("Enable unauthenticated API request rate limit"),
- checkbox_options: { data: { qa_selector: 'throttle_unauthenticated_api_checkbox' } },
+ checkbox_options: { data: { testid: 'throttle-unauthenticated-api-checkbox' } },
label_options: { class: 'label-bold' }
.form-group
= f.label :throttle_unauthenticated_api_requests_per_period, _('Maximum unauthenticated API requests per rate limit period per IP'), class: 'label-bold'
@@ -21,7 +21,7 @@
.form-group
= f.gitlab_ui_checkbox_component :throttle_unauthenticated_enabled,
_("Enable unauthenticated web request rate limit"),
- checkbox_options: { data: { qa_selector: 'throttle_unauthenticated_web_checkbox' } },
+ checkbox_options: { data: { testid: 'throttle-unauthenticated-web-checkbox' } },
label_options: { class: 'label-bold' }
.form-group
= f.label :throttle_unauthenticated_requests_per_period, _('Maximum unauthenticated web requests per rate limit period per IP'), class: 'label-bold'
@@ -34,7 +34,7 @@
.form-group
= f.gitlab_ui_checkbox_component :throttle_authenticated_api_enabled,
_("Enable authenticated API request rate limit"),
- checkbox_options: { data: { qa_selector: 'throttle_authenticated_api_checkbox' }},
+ checkbox_options: { data: { testid: 'throttle-authenticated-api-checkbox' }},
label_options: { class: 'label-bold' }
.form-group
= f.label :throttle_authenticated_api_requests_per_period, _('Maximum authenticated API requests per rate limit period per user'), class: 'label-bold'
@@ -47,7 +47,7 @@
.form-group
= f.gitlab_ui_checkbox_component :throttle_authenticated_web_enabled,
_("Enable authenticated web request rate limit"),
- checkbox_options: { data: { qa_selector: 'throttle_authenticated_web_checkbox' } },
+ checkbox_options: { data: { testid: 'throttle-authenticated-web-checkbox' } },
label_options: { class: 'label-bold' }
.form-group
= f.label :throttle_authenticated_web_requests_per_period, _('Maximum authenticated web requests per rate limit period per user'), class: 'label-bold'
@@ -57,6 +57,11 @@
= f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control gl-form-input'
%fieldset
+ .form-group
+ = f.label :project_jobs_api_rate_limit, safe_format('Maximum authenticated requests to %{open}project/:id/jobs%{close} per minute', tag_pair(tag.code, :open, :close)), class: 'label-bold'
+ = f.number_field :project_jobs_api_rate_limit, class: 'form-control gl-form-input'
+
+ %fieldset
%legend.h5.gl-border-none
= _('Response text')
.form-group
@@ -66,4 +71,4 @@
.form-text.text-muted
= html_escape(_("If blank, defaults to %{code_open}Retry later%{code_close}.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- = f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
+ = f.submit _('Save changes'), data: { testid: 'save-changes-button' }, pajamas_button: true
diff --git a/app/views/admin/application_settings/_jira_connect.html.haml b/app/views/admin/application_settings/_jira_connect.html.haml
index 23ad85334cb..0a96268a0a3 100644
--- a/app/views/admin/application_settings/_jira_connect.html.haml
+++ b/app/views/admin/application_settings/_jira_connect.html.haml
@@ -9,10 +9,10 @@
%p.gl-text-secondary
= s_('JiraConnect|Configure your Jira Connect Application ID.')
= link_to sprite_icon('question-o'),
- help_page_path('integration/jira/connect-app',
+ help_page_path('administration/settings/jira_cloud_app',
aria: { label: _('GitLab for Jira Cloud') },
class: 'has-tooltip',
- anchor: 'connect-the-gitlab-for-jira-cloud-app-for-self-managed-instances'),
+ anchor: 'connect-the-gitlab-for-jira-cloud-app'),
title: _('More information')
.settings-content
diff --git a/app/views/admin/application_settings/_localization.html.haml b/app/views/admin/application_settings/_localization.html.haml
index 4002aa076f7..25038e6f221 100644
--- a/app/views/admin/application_settings/_localization.html.haml
+++ b/app/views/admin/application_settings/_localization.html.haml
@@ -7,7 +7,7 @@
= f.select :first_day_of_week, first_day_of_week_choices, {}, class: 'form-control'
.form-text.text-muted
= _('Default first day of the week in calendars and date pickers.')
- = link_to _('Learn more.'), help_page_path('administration/settings/index.md', anchor: 'change-the-default-first-day-of-the-week'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/settings/localization.md', anchor: 'change-the-default-first-day-of-the-week'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.label :time_tracking, _('Time tracking'), class: 'label-bold'
diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml
index 8cb25627dfa..f36fbd8d68c 100644
--- a/app/views/admin/application_settings/_outbound.html.haml
+++ b/app/views/admin/application_settings/_outbound.html.haml
@@ -15,7 +15,7 @@
= s_('OutboundRequests|Webhooks and integrations might not work properly.')
= f.gitlab_ui_checkbox_component :allow_local_requests_from_web_hooks_and_services,
s_('OutboundRequests|Allow requests to the local network from webhooks and integrations'),
- checkbox_options: { disabled: deny_all_requests, class: 'js-allow-local-requests', data: { qa_selector: 'allow_requests_from_services_checkbox' } }
+ checkbox_options: { disabled: deny_all_requests, class: 'js-allow-local-requests', data: { testid: 'allow-requests-from-services-checkbox' } }
= f.gitlab_ui_checkbox_component :allow_local_requests_from_system_hooks,
s_('OutboundRequests|Allow requests to the local network from system hooks'),
checkbox_options: { disabled: deny_all_requests, class: 'js-allow-local-requests' }
@@ -33,4 +33,4 @@
s_('OutboundRequests|Enforce DNS-rebinding attack protection'),
help_text: s_('OutboundRequests|Resolve IP addresses for outbound requests to prevent DNS-rebinding attacks.')
- = f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), pajamas_button: true, data: { testid: 'save-changes-button' }
diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml
index c09ba01b7ed..017fce3be47 100644
--- a/app/views/admin/application_settings/_performance_bar.html.haml
+++ b/app/views/admin/application_settings/_performance_bar.html.haml
@@ -5,9 +5,9 @@
.form-group
= f.gitlab_ui_checkbox_component :performance_bar_enabled,
_("Allow non-administrators access to the performance bar"),
- checkbox_options: { data: { qa_selector: 'enable_performance_bar_checkbox' } }
+ checkbox_options: { data: { testid: 'enable-performance-bar-checkbox' } }
.form-group
= f.label :performance_bar_allowed_group_path, _('Allow access to members of the following group'), class: 'label-bold'
= f.text_field :performance_bar_allowed_group_path, class: 'form-control gl-form-input', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
- = f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), pajamas_button: true, data: { testid: 'save-changes-button' }
diff --git a/app/views/admin/application_settings/_sentry.html.haml b/app/views/admin/application_settings/_sentry.html.haml
index 9f2a40e4e54..7058a4b5cca 100644
--- a/app/views/admin/application_settings/_sentry.html.haml
+++ b/app/views/admin/application_settings/_sentry.html.haml
@@ -1,28 +1,31 @@
= gitlab_ui_form_for @application_setting, url: metrics_and_profiling_admin_application_settings_path(anchor: 'js-sentry-settings'), html: { class: 'fieldset-form', id: 'sentry-settings' } do |f|
= form_errors(@application_setting)
- %span.text-muted
- = _('Changing any setting here requires an application restart')
+ %fieldset.gl-text-secondary
+ = safe_format(s_('AdminSettings|GitLab uses the %{bold_start}Rails%{bold_end} and %{bold_start}Browser JavaScript%{bold_end} Sentry SDKs to send events to Sentry. For changes to Rails integration settings to take effect, restart GitLab.'), tag_pair(tag.b, :bold_start, :bold_end))
%fieldset
.form-group
- = f.gitlab_ui_checkbox_component :sentry_enabled, _('Enable Sentry error tracking')
+ = f.gitlab_ui_checkbox_component :sentry_enabled, s_('AdminSettings|Enable Sentry for Rails and Browser JavaScript')
+ .form-group
+ = f.label :sentry_environment, _('Environment'), class: 'label-light'
+ = f.text_field :sentry_environment, class: 'form-control gl-form-input', placeholder: Rails.env
+ .form-text.text-muted
+ = safe_format(s_('AdminSettings|%{setting_name} value used by both Rails and Browser JavaScript SDKs.'), setting_name: content_tag(:code, 'environment'))
.form-group
= f.label :sentry_dsn, _('DSN'), class: 'label-light'
= f.text_field :sentry_dsn, class: 'form-control gl-form-input', placeholder: 'https://public@sentry.example.com/1'
+ .form-text.text-muted
+ = safe_format(s_('AdminSettings|%{setting_name} value used by the Rails SDK.'), setting_name: content_tag(:code, 'dsn'))
.form-group
= f.label :sentry_clientside_dsn, _('Clientside DSN'), class: 'label-light'
= f.text_field :sentry_clientside_dsn, class: 'form-control gl-form-input', placeholder: 'https://public@sentry.example.com/2'
- .form-group
- = f.label :sentry_environment, _('Environment'), class: 'label-light'
- = f.text_field :sentry_environment, class: 'form-control gl-form-input', placeholder: Rails.env
-
- %p.text-muted
- = _("Changing any setting bellow doesn't require an application restart")
-
- %fieldset
+ .form-text.text-muted
+ = safe_format(s_('AdminSettings|%{setting_name} value used by the Browser JavaScript SDK.'), setting_name: content_tag(:code, 'dsn'))
.form-group
= f.label :sentry_clientside_traces_sample_rate, _('Clientside traces sample rate'), class: 'label-light'
= f.number_field :sentry_clientside_traces_sample_rate, class: 'form-control gl-form-input', placeholder: '0.5', min: 0, max: 1, step: 0.001
+ .form-text.text-muted
+ = safe_format(s_('AdminSettings|%{setting_name} value used by the Browser JavaScript SDK.'), setting_name: content_tag(:code, 'tracesSampleRate'))
= f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_snowplow.html.haml b/app/views/admin/application_settings/_snowplow.html.haml
index 1b90432e1f3..1049f42673d 100644
--- a/app/views/admin/application_settings/_snowplow.html.haml
+++ b/app/views/admin/application_settings/_snowplow.html.haml
@@ -1,12 +1,12 @@
- expanded = integration_expanded?('snowplow_')
-%section.settings.as-snowplow.no-animate#js-snowplow-settings{ class: ('expanded' if expanded), data: { qa_selector: 'snowplow_settings_content' } }
+%section.settings.as-snowplow.no-animate#js-snowplow-settings{ class: ('expanded' if expanded), data: { testid: 'snowplow-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Snowplow')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
- - help_link = link_to('', help_page_path('development/internal_analytics/snowplow/index'), target: '_blank', rel: 'noopener noreferrer')
+ - help_link = link_to('', help_page_path('development/internal_analytics/internal_event_instrumentation/index'), target: '_blank', rel: 'noopener noreferrer')
- snowplow_link = link_to('', 'https://snowplow.io/', target: '_blank', rel: 'noopener noreferrer')
= safe_format(_('Configure %{snowplow_link_start}Snowplow%{snowplow_link_end} to track events. %{help_link_start}Learn more.%{help_link_end}'), tag_pair(snowplow_link, :snowplow_link_start, :snowplow_link_end), tag_pair(help_link, :help_link_start, :help_link_end))
.settings-content
@@ -15,7 +15,7 @@
%fieldset
.form-group
- = f.gitlab_ui_checkbox_component :snowplow_enabled, _('Enable Snowplow tracking'), checkbox_options: { data: { qa_selector: 'snowplow_enabled_checkbox' } }
+ = f.gitlab_ui_checkbox_component :snowplow_enabled, _('Enable Snowplow tracking'), checkbox_options: { data: { testid: 'snowplow-enabled-checkbox' } }
.form-group
= f.label :snowplow_collector_hostname, _('Collector hostname'), class: 'label-light'
= f.text_field :snowplow_collector_hostname, class: 'form-control gl-form-input', placeholder: 'snowplow.example.com'
@@ -32,4 +32,4 @@
.form-text.text-muted
= _('The Snowplow cookie domain.')
- = f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
+ = f.submit _('Save changes'), data: { testid: 'save-changes-button' }, pajamas_button: true
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
index 5a3814ca83d..2d51dc2a6f2 100644
--- a/app/views/admin/application_settings/_usage.html.haml
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -18,7 +18,7 @@
- disabled_help_text = s_('AdminSettings|Service ping is disabled in your configuration file, and cannot be enabled through this form. For more information, see the documentation on %{link_start}deactivating service ping%{link_end}.').html_safe % { link_start: deactivating_service_ping_link_start, link_end: link_end }
= f.gitlab_ui_checkbox_component :usage_ping_enabled, s_('AdminSettings|Enable Service Ping'),
help_text: can_be_configured ? usage_ping_help_text : disabled_help_text,
- checkbox_options: { disabled: !can_be_configured, data: { qa_selector: 'enable_usage_data_checkbox' } }
+ checkbox_options: { disabled: !can_be_configured, data: { testid: 'enable-usage-data-checkbox' } }
.form-text.gl-pl-6
- if can_be_configured
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-preview-trigger', data: { payload_selector: ".#{payload_class}" } }) do
diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml
index 5aa2684f084..dad0bf08bb0 100644
--- a/app/views/admin/application_settings/general.html.haml
+++ b/app/views/admin/application_settings/general.html.haml
@@ -14,7 +14,7 @@
.settings-content
= render 'visibility_and_access'
-%section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'account_and_limit_settings_content', testid: 'account-limit' } }
+%section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded_by_default?), data: { testid: 'account-and-limit-settings-content'} }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Account and limit')
@@ -47,7 +47,7 @@
.settings-content
= render 'diff_limits'
-%section.settings.as-signup.no-animate#js-signup-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'sign_up_restrictions_settings_content' } }
+%section.settings.as-signup.no-animate#js-signup-settings{ class: ('expanded' if expanded_by_default?), data: { testid: 'sign-up-restrictions-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Sign-up restrictions')
diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml
index 4739a204147..188359158ef 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -29,7 +29,7 @@
.settings-content
= render 'grafana'
-%section.settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'performance_bar_settings_content' } }
+%section.settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded_by_default?), data: { testid: 'performance-bar-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Profiling - Performance bar')
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
index 9ccfc6cbc0a..849c5c749e0 100644
--- a/app/views/admin/application_settings/network.html.haml
+++ b/app/views/admin/application_settings/network.html.haml
@@ -14,7 +14,7 @@
.settings-content
= render 'performance'
-%section.settings.as-ip-limits.no-animate#js-ip-limits-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'ip_limits_content' } }
+%section.settings.as-ip-limits.no-animate#js-ip-limits-settings{ class: ('expanded' if expanded_by_default?), data: { testid: 'ip-limits-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('User and IP rate limits')
@@ -87,7 +87,7 @@
= render 'gitlab_shell_operation_limits'
-%section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'outbound_requests_content' } }
+%section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded_by_default?), data: { testid: 'outbound-requests-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= s_('OutboundRequests|Outbound requests')
diff --git a/app/views/admin/application_settings/preferences.html.haml b/app/views/admin/application_settings/preferences.html.haml
index bea399ee926..4590b6f4586 100644
--- a/app/views/admin/application_settings/preferences.html.haml
+++ b/app/views/admin/application_settings/preferences.html.haml
@@ -3,7 +3,7 @@
- add_page_specific_style 'page_bundles/settings'
- @force_desktop_expanded_sidebar = true
-%section.settings.as-email.no-animate#js-email-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'email_content' } }
+%section.settings.as-email.no-animate#js-email-settings{ class: ('expanded' if expanded_by_default?), data: { testid: 'email-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Email')
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index 27622dfa0bb..8d2e7366dcc 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -4,13 +4,13 @@
= content_tag :div, class: 'form-group row' do
.col-12
= f.label :name
- = f.text_field :name, class: 'form-control gl-form-input', data: { qa_selector: 'name_field' }
+ = f.text_field :name, class: 'form-control gl-form-input', data: { testid: 'name-field' }
= doorkeeper_errors_for application, :name
= content_tag :div, class: 'form-group row' do
.col-12
= f.label :redirect_uri
- = f.text_area :redirect_uri, class: 'form-control gl-form-input', data: { qa_selector: 'redirect_uri_field' }
+ = f.text_area :redirect_uri, class: 'form-control gl-form-input', data: { testid: 'redirect-uri-field' }
= doorkeeper_errors_for application, :redirect_uri
%span.form-text.text-muted
Use one line per URI
@@ -18,7 +18,7 @@
= content_tag :div, class: 'form-group row' do
.col-12
= f.label :trusted
- = f.gitlab_ui_checkbox_component :trusted, _('Trusted applications are automatically authorized on GitLab OAuth flow. It\'s highly recommended for the security of users that trusted applications have the confidential setting set to true.'), checkbox_options: { data: { qa_selector: 'trusted_checkbox' } }
+ = f.gitlab_ui_checkbox_component :trusted, _('Trusted applications are automatically authorized on GitLab OAuth flow. It\'s highly recommended for the security of users that trusted applications have the confidential setting set to true.'), checkbox_options: { data: { testid: 'trusted-checkbox' } }
= content_tag :div, class: 'form-group row' do
.col-12
@@ -31,5 +31,5 @@
= render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes, f: f
.gl-mt-5
- = f.submit _('Save application'), pajamas_button: true, data: { qa_selector: 'save_application_button' }
+ = f.submit _('Save application'), pajamas_button: true, data: { testid: 'save-application-button' }
= link_button_to _('Cancel'), admin_applications_path
diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml
index 6846fe8f4aa..07ccb8bb066 100644
--- a/app/views/admin/applications/index.html.haml
+++ b/app/views/admin/applications/index.html.haml
@@ -14,7 +14,7 @@
= s_('AdminArea|Manage applications for your instance that can use GitLab as an %{docs_link_start}OAuth provider%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe }
.gl-new-card-actions
- = render Pajamas::ButtonComponent.new(size: :small, href: new_admin_application_path, button_options: { data: { qa_selector: 'new_application_button' } }) do
+ = render Pajamas::ButtonComponent.new(size: :small, href: new_admin_application_path, button_options: { data: { testid: 'new-application-button' } }) do
= _('Add new application')
- c.with_body do
- if @applications.empty?
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index 728c748d01a..90859b5c170 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -2,12 +2,11 @@
- no_errors = @errors.blank?
%h1.page-title.gl-font-size-h-display= page_title
-.bs-callout.clearfix
- .float-left
- %p
- #{ s_('HealthCheck|Access token is') }
- %code#health-check-token= Gitlab::CurrentSettings.health_check_access_token
- .gl-mt-3
+= render Pajamas::AlertComponent.new(variant: :tip, dismissible: false, alert_options: { class: 'gl-mb-5' }) do |c|
+ - c.with_body do
+ #{ s_('HealthCheck|Access token is') }
+ %code#health-check-token= Gitlab::CurrentSettings.health_check_access_token
+ - c.with_actions do
= render Pajamas::ButtonComponent.new(href: reset_health_check_token_admin_application_settings_path, method: :put, button_options: { data: { confirm: _('Are you sure you want to reset the health check token?') } }) do
= _("Reset health check access token")
%p.light
diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml
index ad78c677da1..dec35e9cf15 100644
--- a/app/views/admin/identities/_form.html.haml
+++ b/app/views/admin/identities/_form.html.haml
@@ -7,6 +7,7 @@
.col-sm-10
- values = Gitlab::Auth::OAuth::Provider.providers.map { |name| ["#{Gitlab::Auth::OAuth::Provider.label_for(name)} (#{name})", name] }
= f.select :provider, values, { allow_blank: false }, class: 'form-control'
+ = render_if_exists partial: 'admin/identities/provider_id', locals: { f: f }
.form-group.row
.col-sm-2.col-form-label
= f.label :extern_uid, _("Identifier")
@@ -15,4 +16,3 @@
.form-actions
= f.submit _('Save changes'), pajamas_button: true
-
diff --git a/app/views/admin/jobs/index.html.haml b/app/views/admin/jobs/index.html.haml
index b8a9ad32259..9af5ffd7936 100644
--- a/app/views/admin/jobs/index.html.haml
+++ b/app/views/admin/jobs/index.html.haml
@@ -4,4 +4,4 @@
- page_title _("Jobs")
-#admin-jobs-app{ data: { job_statuses: job_statuses.to_json, empty_state_svg_path: image_path('jobs-empty-state.svg'), url: cancel_all_admin_jobs_path } }
+#admin-jobs-app{ data: { job_statuses: job_statuses.to_json, empty_state_svg_path: image_path('illustrations/empty-state/empty-pipeline-md.svg'), url: cancel_all_admin_jobs_path } }
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index 19460ddb0e5..c3857f4933d 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -1,4 +1,4 @@
-%li.label-list-item.gl-list-style-none.gl-py-3{ id: dom_id(label) }
+%li.js-label-list-item.gl-list-style-none.gl-py-3.gl-border-b.gl-last-of-type-border-b-0{ id: dom_id(label) }
.label-content.gl-px-3.gl-py-2.gl-rounded-base
= render "shared/label_row", label: label.present(issuable_subject: nil)
.label-actions-list.gl-display-inline-block
diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml
index 3d392a86566..b3d04e4f576 100644
--- a/app/views/admin/labels/index.html.haml
+++ b/app/views/admin/labels/index.html.haml
@@ -1,4 +1,5 @@
- page_title _("Labels")
+- add_page_specific_style 'page_bundles/labels'
= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card labels other-labels js-toggle-container js-admin-labels-container' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body gl-px-0' }) do |c|
- c.with_header do
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 31ec4935f64..412d8e64e89 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -1,5 +1,6 @@
- page_title _('Projects')
- add_page_specific_style 'page_bundles/search'
+- add_page_specific_style 'page_bundles/projects'
- params[:visibility_level] ||= []
.top-area.gl-flex-direction-column-reverse
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 85dce00752b..6be5aa003fc 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -1,4 +1,5 @@
- add_page_specific_style 'page_bundles/members'
+- add_page_specific_style 'page_bundles/projects'
- add_to_breadcrumbs _("Projects"), admin_projects_path
- breadcrumb_title @project.full_name
- page_title @project.full_name, _("Projects")
diff --git a/app/views/admin/sessions/_new_base.html.haml b/app/views/admin/sessions/_new_base.html.haml
index f880c2631ed..35b75e425f9 100644
--- a/app/views/admin/sessions/_new_base.html.haml
+++ b/app/views/admin/sessions/_new_base.html.haml
@@ -1,7 +1,7 @@
= gitlab_ui_form_for(:user, url: admin_session_path, html: { class: 'gl-p-5 gl-show-field-errors', aria: { live: 'assertive' } }) do |f|
.form-group
= f.label :password, _('Password')
- = f.password_field :password, class: 'form-control js-password', data: { id: 'user_password', name: 'user[password]', qa_selector: 'password_field', testid: 'password-field' }
+ = f.password_field :password, class: 'form-control js-password', data: { id: 'user_password', name: 'user[password]', testid: 'password-field' }
- = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { qa_selector: 'enter_admin_mode_button' } }) do
+ = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { testid: 'enter-admin-mode-button' } }) do
= _('Enter admin mode')
diff --git a/app/views/admin/users/_profile.html.haml b/app/views/admin/users/_profile.html.haml
index bb89b5baf28..df0a59ccfc3 100644
--- a/app/views/admin/users/_profile.html.haml
+++ b/app/views/admin/users/_profile.html.haml
@@ -1,4 +1,4 @@
-= render Pajamas::CardComponent.new(body_options: { class: 'gl-py-0' }) do |c|
+= render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-py-0' }) do |c|
- c.with_header do
= _('Profile')
- c.with_body do
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 4cc3e12a8ad..bee7e10906b 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -69,6 +69,7 @@
= @user.external? ? _('Yes') : _('No')
= render_if_exists 'admin/users/provisioned_by', user: @user
+ = render_if_exists 'admin/users/enterprise_group', user: @user
%li
%span.light= _('Can create top level groups:')
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index 5062599c261..fd4801b7941 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -1,8 +1,9 @@
- api_awards_path = local_assigns.fetch(:api_awards_path, nil)
+- new_custom_emoji_path = local_assigns.fetch(:new_custom_emoji_path, nil)
- if api_awards_path
.gl-display-flex.gl-flex-wrap.gl-justify-content-space-between.gl-pt-3
- #js-vue-awards-block{ data: { path: api_awards_path, can_award_emoji: can?(current_user, :award_emoji, awardable).to_s } }
+ #js-vue-awards-block{ data: { path: api_awards_path, can_award_emoji: can?(current_user, :award_emoji, awardable).to_s, new_custom_emoji_path: new_custom_emoji_path, show_default_award_emojis: @project&.show_default_award_emojis?.to_s } }
= yield
- else
- grouped_emojis = awardable.grouped_awards(with_thumbs: inline)
diff --git a/app/views/ci/status/_icon.html.haml b/app/views/ci/status/_icon.html.haml
index fdaacb732c7..9fa5734d6b6 100644
--- a/app/views/ci/status/_icon.html.haml
+++ b/app/views/ci/status/_icon.html.haml
@@ -1,14 +1,10 @@
- status = local_assigns.fetch(:status)
-- size = local_assigns.fetch(:size, 16)
- tooltip_placement = local_assigns.fetch(:tooltip_placement, "left")
- path = local_assigns.fetch(:path, status.has_details? ? status.details_path : nil)
- option_css_classes = local_assigns.fetch(:option_css_classes, '')
-- css_classes = "ci-status-link ci-status-icon ci-status-icon-#{status.group} has-tooltip #{option_css_classes}"
-- title = s_("PipelineStatusTooltip|Pipeline: %{ci_status}") % {ci_status: status.label}
+- css_classes = "gl-px-2 #{option_css_classes}"
+- ci_css_classes = "ci-status-link ci-status-icon ci-status-icon-#{status.group} gl-line-height-1"
+- title = s_("PipelineStatusTooltip|Pipeline: %{ci_status}") % {ci_status: status.label}
-- if path
- = link_to path, class: css_classes, title: title, data: { placement: tooltip_placement } do
- = sprite_icon(status.icon, size: size)
-- else
- %span{ class: css_classes, title: title, data: { placement: tooltip_placement } }
- = sprite_icon(status.icon, size: size)
+= gl_badge_tag(variant: badge_variant(status), size: :md, href: path, class: css_classes, title: title, data: { toggle: 'tooltip', placement: tooltip_placement, testid: "ci-status-badge" }) do
+ = content_tag :span, sprite_icon(status.icon, size: 16), class: ci_css_classes
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index b72b252a852..74dc2277f54 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -7,7 +7,7 @@
.page-title-controls.gl-display-flex.gl-align-items-center.gl-gap-5
= link_to _("Explore projects"), explore_projects_path
- if current_user.can_create_project?
- = render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm, button_options: { data: { qa_selector: 'new_project_button' } }) do
+ = render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm, button_options: { data: { testid: 'new-project-button' } }) do
= _("New project")
.top-area
diff --git a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
index 94f956896d6..1d2e6e1e332 100644
--- a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
@@ -4,7 +4,7 @@
- if has_start_trial?
= render_if_exists "dashboard/projects/blank_state_ee_trial"
- = link_to new_project_path, class: link_classes, data: { qa_selector: 'new_project_button' } do
+ = link_to new_project_path, class: link_classes, data: { testid: 'new-project-button' } do
.blank-state-icon
= custom_icon("add_new_project", size: 50)
.blank-state-body.gl-sm-pl-6
diff --git a/app/views/dashboard/projects/_blank_state_welcome.html.haml b/app/views/dashboard/projects/_blank_state_welcome.html.haml
index 08b914a218d..032c5206d99 100644
--- a/app/views/dashboard/projects/_blank_state_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_welcome.html.haml
@@ -2,7 +2,7 @@
.gl-display-flex.gl-flex-wrap.gl-justify-content-space-between
- if current_user.can_create_project?
- = link_to new_project_path, class: link_classes, data: { qa_selector: 'new_project_button' } do
+ = link_to new_project_path, class: link_classes, data: { testid: 'new-project-button' } do
.blank-state-icon
= custom_icon("add_new_project", size: 50)
.blank-state-body.gl-sm-pl-6
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index e72762f2ae5..da25dee1e88 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -1,6 +1,6 @@
.container
.gl-text-center.gl-pt-6.gl-pb-7
- %h2.gl-font-size-h1{ data: { qa_selector: 'welcome_title_content' } }
+ %h2.gl-font-size-h1{ data: { testid: 'welcome-title-content' } }
= _('Welcome to GitLab')
%p.gl-m-0
= _('Faster releases. Better code. Less pain.')
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 140bc6e06c3..c74a9f4cbe6 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -5,6 +5,7 @@
- page_title _("Projects")
- add_page_specific_style 'page_bundles/dashboard_projects'
+- add_page_specific_style 'page_bundles/projects'
= render "projects/last_push"
- if show_projects?(@projects, params)
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 3feb30085c0..ab97507b3c8 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -5,6 +5,9 @@
= render_if_exists 'dashboard/todos/saml_reauth_notice'
- add_page_specific_style 'page_bundles/todos'
- add_page_specific_style 'page_bundles/issuable'
+- filter_by_done = params[:state] == 'done'
+- open_todo_count = todos_has_filtered_results? && !filter_by_done ? @allowed_todos.count : todos_pending_count
+- done_todo_count = todos_has_filtered_results? && filter_by_done ? @allowed_todos.count : todos_done_count
.page-title-holder.d-flex.align-items-center
%h1.page-title.gl-font-size-h-display= _("To-Do List")
@@ -14,10 +17,10 @@
= gl_tabs_nav({ class: 'gl-flex-grow-1 gl-border-0' }) do
= gl_tab_link_to todos_filter_path(state: 'pending'), item_active: params[:state].blank? || params[:state] == 'pending', class: "js-todos-pending" do
= _("To Do")
- = gl_tab_counter_badge(number_with_delimiter(todos_pending_count), { class: 'js-todos-badge' })
- = gl_tab_link_to todos_filter_path(state: 'done'), item_active: params[:state] == 'done', class: "js-todos-done" do
+ = gl_tab_counter_badge(number_with_delimiter(open_todo_count), { class: 'js-todos-badge' })
+ = gl_tab_link_to todos_filter_path(state: 'done'), item_active: filter_by_done, class: "js-todos-done" do
= _("Done")
- = gl_tab_counter_badge(number_with_delimiter(todos_done_count), { class: 'js-todos-badge' })
+ = gl_tab_counter_badge(number_with_delimiter(done_todo_count), { class: 'js-todos-badge' })
.nav-controls
- if @allowed_todos.any?(&:pending?)
@@ -80,31 +83,38 @@
%ul.content-list.todos-list
= render @allowed_todos
= paginate @todos, theme: "gitlab"
- .js-nothing-here-container.empty-state.hidden
+ .js-nothing-here-container.gl-empty-state.gl-text-center.hidden
.svg-content.svg-150
= image_tag 'illustrations/empty-todos-all-done-md.svg'
.text-content.gl-text-center
- %h4
+ %h1.gl-font-size-h-display.gl-line-height-36.gl-mt-0
= s_("Todos|You're all done!")
- elsif current_user.todos.any?
- .col.todos-all-done.empty-state
+ .col.todos-all-done.gl-empty-state.gl-text-center
.svg-content.svg-150
- = image_tag 'illustrations/empty-todos-all-done-md.svg'
- .text-content.gl-text-center
- - if todos_filter_empty?
- %h4
+ = image_tag (!todos_filter_empty? && !todos_has_filtered_results?) ? 'illustrations/empty-todos-all-done-md.svg' : 'illustrations/empty-todos-md.svg'
+ .text-content.gl-text-center.gl-m-auto{ class: "gl-max-w-88!" }
+ %h1.gl-font-size-h-display.gl-line-height-36.gl-mt-0
+ - if todos_filter_empty?
= no_todos_messages.sample
+ - elsif todos_has_filtered_results?
+ = _("Sorry, your filter produced no results")
+ - else
+ = s_("Todos|Nothing is on your to-do list. Nice work!")
+
+ - if todos_filter_empty?
%p
= (s_("Todos|Are you looking for things to do? Take a look at %{strongStart}%{openIssuesLinkStart}open issues%{openIssuesLinkEnd}%{strongEnd}, contribute to %{strongStart}%{mergeRequestLinkStart}a merge request%{mergeRequestLinkEnd}%{mergeRequestLinkEnd}%{strongEnd}, or mention someone in a comment to automatically assign them a new to-do item.") % { strongStart: '<strong>', strongEnd: '</strong>', openIssuesLinkStart: "<a href=\"#{issues_dashboard_path}\">", openIssuesLinkEnd: '</a>', mergeRequestLinkStart: "<a href=\"#{merge_requests_dashboard_path}\">", mergeRequestLinkEnd: '</a>' }).html_safe
- - else
- %h4
- = s_("Todos|Nothing is on your to-do list. Nice work!")
+ - elsif todos_has_filtered_results?
+ %p
+ = link_to s_("Todos|Do you want to remove the filters?"), todos_filter_path(without: [:project_id, :author_id, :type, :action_id])
+
- else
- .col.empty-state
+ .col.gl-empty-state.gl-text-center
.svg-content.svg-150
= image_tag 'illustrations/empty-todos-md.svg'
- .text-content.gl-text-center
- %h4
+ .text-content.gl-text-center.gl-m-auto{ class: "gl-max-w-88!" }
+ %h1.gl-font-size-h-display.gl-line-height-36.gl-mt-0
= s_("Todos|Your To-Do List shows what to work on next")
%p
= (s_("Todos|When an issue or merge request is assigned to you, or when you receive a %{strongStart}@mention%{strongEnd} in a comment, this automatically triggers a new item in your To-Do List.") % { strongStart: '<strong>', strongEnd: '</strong>' }).html_safe
diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml
index 1760e6e0f84..8075697b758 100644
--- a/app/views/devise/confirmations/almost_there.haml
+++ b/app/views/devise/confirmations/almost_there.haml
@@ -3,10 +3,10 @@
- registration_link_start = '<a href="%{new_user_registration_path}">'.html_safe % { new_user_registration_path: new_user_registration_path }
- link_end = '</a>'.html_safe
- content_for :page_specific_javascripts do
- = render "layouts/google_tag_manager_head"
+ = render_if_exists "layouts/google_tag_manager_head"
= render "layouts/one_trust"
= render "layouts/bizible"
-= render "layouts/google_tag_manager_body"
+= render_if_exists "layouts/google_tag_manager_body"
= render_if_exists 'devise/shared/delete_unconfirmed_users_flash'
diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml
index 18856e04eb8..29f1a1f398b 100644
--- a/app/views/devise/registrations/new.html.haml
+++ b/app/views/devise/registrations/new.html.haml
@@ -2,10 +2,10 @@
- page_description _("Join GitLab today! You and your team can plan, build, and ship secure code all in one application. Get started here for free!")
- add_page_specific_style 'page_bundles/signup'
- content_for :page_specific_javascripts do
- = render "layouts/google_tag_manager_head"
+ = render_if_exists "layouts/google_tag_manager_head"
= render "layouts/one_trust"
= render "layouts/bizible"
-= render "layouts/google_tag_manager_body"
+= render_if_exists "layouts/google_tag_manager_body"
- content_for :omniauth_providers_bottom do
= render 'devise/shared/signup_omniauth_providers'
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index e6551adffde..88dd4fd1721 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -15,14 +15,12 @@
= link_to _('Forgot your password?'), new_password_path(:user)
.form-group
- - if Feature.enabled?(:arkose_labs_login_challenge)
- = render_if_exists 'devise/sessions/arkose_labs'
- - elsif captcha_enabled? || captcha_on_login_required?
+ - if captcha_enabled? || captcha_on_login_required?
= recaptcha_tags nonce: content_security_policy_nonce
- if remember_me_enabled?
.form-group
= f.gitlab_ui_checkbox_component :remember_me, _('Remember me'), checkbox_options: { autocomplete: 'off' }
- = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { class: "js-sign-in-button #{'js-no-auto-disable' if Feature.enabled?(:arkose_labs_login_challenge)}", data: { qa_selector: 'sign_in_button', testid: 'sign-in-button' } }) do
+ = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { class: 'js-sign-in-button', data: { qa_selector: 'sign_in_button', testid: 'sign-in-button' } }) do
= _('Sign in')
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index 0fd27f7f7e7..acfb16b64cd 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -1,13 +1,13 @@
- page_title _("Sign in")
- content_for :page_specific_javascripts do
- = render "layouts/google_tag_manager_head"
+ = render_if_exists "layouts/google_tag_manager_head"
= render "layouts/one_trust"
- content_for :sessions_broadcast do
= render "devise/sessions/broadcast"
-= render "layouts/google_tag_manager_body"
+= render_if_exists "layouts/google_tag_manager_body"
#signin-container
- if any_form_based_providers_enabled?
diff --git a/app/views/devise/shared/_signup_omniauth_providers.haml b/app/views/devise/shared/_signup_omniauth_providers.haml
index 399c23741a9..4e62c10b258 100644
--- a/app/views/devise/shared/_signup_omniauth_providers.haml
+++ b/app/views/devise/shared/_signup_omniauth_providers.haml
@@ -3,4 +3,4 @@
= _("or")
= render 'devise/shared/signup_omniauth_provider_list',
providers: enabled_button_based_providers,
- tracking_label: ::Onboarding::Status.tracking_label[:free]
+ tracking_label: oauth_tracking_label
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index ca7798257cb..544acd5ae56 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -35,7 +35,7 @@
- if can_create_projects
.gl-sm-w-auto.gl-w-full
- = render Pajamas::ButtonComponent.new(href: new_project_path(namespace_id: @group.id), variant: :confirm, button_options: { data: { qa_selector: 'new_project_button' }, class: 'gl-sm-w-auto gl-w-full' }) do
+ = render Pajamas::ButtonComponent.new(href: new_project_path(namespace_id: @group.id), variant: :confirm, button_options: { data: { testid: 'new-project-button' }, class: 'gl-sm-w-auto gl-w-full' }) do
= _('New project')
- if @group.description.present?
diff --git a/app/views/groups/custom_emoji/index.html.haml b/app/views/groups/custom_emoji/index.html.haml
new file mode 100644
index 00000000000..d8c28ac8959
--- /dev/null
+++ b/app/views/groups/custom_emoji/index.html.haml
@@ -0,0 +1,8 @@
+- page_title _('Custom emoji')
+
+#js-custom-emojis-root.row.gl-mt-5{ data: { base_path: group_custom_emoji_index_path(@group), group_path: @group.full_path } }
+ .col-12
+ %h4.gl-mt-0
+ = page_title
+ %p= _('Custom emoji will be available to use in every project in the group.')
+ = gl_loading_icon(size: 'lg')
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 8d6eebc27b0..b2ea15d0e47 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -49,7 +49,7 @@
= render_if_exists 'groups/templates_setting', expanded: expanded
= render_if_exists 'shared/groups/max_pages_size_setting'
-%section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded), data: { qa_selector: 'advanced_settings_content' } }
+%section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded), data: { testid: 'advanced-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' }
= _('Advanced')
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index 6b4832d81aa..e174d6318e9 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -3,6 +3,7 @@
- search = params[:search]
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || search.present? || subscribed.present?
+- add_page_specific_style 'page_bundles/labels'
- if labels_or_filters
#js-promote-label-modal
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index 7665da08582..ea36aae1c2f 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -6,7 +6,7 @@
.form-group
= f.label :title, _("Title")
- = f.text_field :title, maxlength: 255, class: "form-control", data: { qa_selector: "milestone_title_field" }, required: true, autofocus: true
+ = f.text_field :title, maxlength: 255, class: "form-control", data: { testid: "milestone-title-field" }, required: true, autofocus: true
= render "shared/milestones/form_dates", f: f
.form-group
= f.label :description, _("Description")
@@ -26,7 +26,7 @@
= f.hidden_field :lock_version
- if @milestone.new_record?
- = f.submit _('Create milestone'), data: { qa_selector: "create_milestone_button" }, class: 'gl-mr-2', pajamas_button: true
+ = f.submit _('Create milestone'), data: { testid: "create-milestone-button" }, class: 'gl-mr-2', pajamas_button: true
= render Pajamas::ButtonComponent.new(href: group_milestones_path(@group)) do
= _("Cancel")
- else
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index f49b69f821d..0235e252a9d 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -9,13 +9,13 @@
= render 'shared/milestones/search_form'
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @group)
- = render Pajamas::ButtonComponent.new(href: new_group_milestone_path(@group), variant: :confirm, button_options: { data: { qa_selector: "new_group_milestone_link" }, class: "gl-ml-3" }) do
+ = render Pajamas::ButtonComponent.new(href: new_group_milestone_path(@group), variant: :confirm, button_options: { data: { testid: "new-group-milestone-link" }, class: "gl-ml-3" }) do
= _('New milestone')
- if @milestones.blank?
= render 'shared/empty_states/milestones_tab', learn_more_path: help_page_path('user/project/milestones/index') do
- if can?(current_user, :admin_milestone, @group)
.text-center
- = render Pajamas::ButtonComponent.new(href: new_group_milestone_path(@group), variant: :confirm, button_options: { data: { qa_selector: "new_group_milestone_link" }}) do
+ = render Pajamas::ButtonComponent.new(href: new_group_milestone_path(@group), variant: :confirm, button_options: { data: { testid: "new-group-milestone-link" }}) do
= _('New milestone')
- else
.milestones
@@ -30,5 +30,5 @@
= render 'shared/empty_states/milestones', learn_more_path: help_page_path('user/project/milestones/index') do
- if can?(current_user, :admin_milestone, @group)
.text-center
- = render Pajamas::ButtonComponent.new(href: new_group_milestone_path(@group), variant: :confirm, button_options: { data: { qa_selector: "new_group_milestone_link" }}) do
+ = render Pajamas::ButtonComponent.new(href: new_group_milestone_path(@group), variant: :confirm, button_options: { data: { testid: "new-group-milestone-link" }}) do
= _('New milestone')
diff --git a/app/views/groups/observability/observability.html.haml b/app/views/groups/observability/observability.html.haml
deleted file mode 100644
index 834fa0e027c..00000000000
--- a/app/views/groups/observability/observability.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-- page_title observability_page_title
-
-#js-observability-app{ data: { observability_iframe_src: observability_iframe_src(@group) } }
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index 22e9f9f5071..76758769d01 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -1,5 +1,6 @@
- breadcrumb_title _("Projects")
- page_title _("Projects")
+- add_page_specific_style 'page_bundles/projects'
- @force_desktop_expanded_sidebar = true
= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card js-search-settings-section' }, header_options: { class: 'gl-new-card-header gl-display-flex' }, body_options: { class: 'gl-new-card-body' }) do |c|
diff --git a/app/views/groups/settings/_git_access_protocols.html.haml b/app/views/groups/settings/_git_access_protocols.html.haml
index db177da1d84..d23f72a3055 100644
--- a/app/views/groups/settings/_git_access_protocols.html.haml
+++ b/app/views/groups/settings/_git_access_protocols.html.haml
@@ -1,7 +1,7 @@
- if group.root?
.form-group
= f.label _('Enabled Git access protocols'), class: 'label-bold'
- = f.select :enabled_git_access_protocol, options_for_select(enabled_git_access_protocol_options_for_group, group.enabled_git_access_protocol), {}, class: 'form-control', data: { qa_selector: 'enabled_git_access_protocol_dropdown' }, disabled: !::Gitlab::CurrentSettings.enabled_git_access_protocol.blank?
+ = f.select :enabled_git_access_protocol, options_for_select(enabled_git_access_protocol_options_for_group(group), group.enabled_git_access_protocol), {}, class: 'form-control', data: { qa_selector: 'enabled_git_access_protocol_dropdown' }, disabled: !::Gitlab::CurrentSettings.enabled_git_access_protocol.blank?
- if !::Gitlab::CurrentSettings.enabled_git_access_protocol.blank?
.form-text.text-muted
= _("This setting has been configured at the instance level and cannot be overridden per group")
diff --git a/app/views/groups/settings/_transfer.html.haml b/app/views/groups/settings/_transfer.html.haml
index 368e4a981bc..aa533d54b1a 100644
--- a/app/views/groups/settings/_transfer.html.haml
+++ b/app/views/groups/settings/_transfer.html.haml
@@ -1,7 +1,7 @@
- form_id = "transfer-group-form"
- initial_data = { button_text: s_('GroupSettings|Transfer group'), group_full_path: @group.full_path, group_name: @group.name, group_id: @group.id, target_form_id: form_id, is_paid_group: group.paid?.to_s }
-= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card', data: { qa_selector: 'transfer_group_content' } }, header_options: { class: 'gl-new-card-header gl-flex-direction-column' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
+= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card', data: { testid: 'transfer-group-content' } }, header_options: { class: 'gl-new-card-header gl-flex-direction-column' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
- c.with_header do
.gl-new-card-title-wrapper
%h4.gl-new-card-title.warning-title= s_('GroupSettings|Transfer group')
diff --git a/app/views/groups/settings/access_tokens/index.html.haml b/app/views/groups/settings/access_tokens/index.html.haml
index ef85eab6778..832fd6e8ceb 100644
--- a/app/views/groups/settings/access_tokens/index.html.haml
+++ b/app/views/groups/settings/access_tokens/index.html.haml
@@ -50,6 +50,7 @@
access_levels: GroupMember.access_level_roles,
default_access_level: Gitlab::Access::GUEST,
prefix: :resource_access_token,
+ description_prefix: :group_access_token,
help_path: help_page_path('user/group/settings/group_access_tokens', anchor: 'scopes-for-a-group-access-token')
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json, no_active_tokens_message: _('This group has no active access tokens.'), show_role: true } }
diff --git a/app/views/groups/work_items/show.html.haml b/app/views/groups/work_items/show.html.haml
index eb962cc0b69..7def8b0d6e6 100644
--- a/app/views/groups/work_items/show.html.haml
+++ b/app/views/groups/work_items/show.html.haml
@@ -1 +1,7 @@
-.h1 Work Item
+- page_title "##{request.params['iid']}"
+- add_to_breadcrumbs _("Issues"), issues_group_path(@group)
+- add_page_specific_style 'page_bundles/work_items'
+- @gfm_form = true
+- @noteable_type = 'WorkItem'
+
+#js-work-items{ data: work_items_index_data(@group).merge(iid: request.params['iid']) }
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index b07374e5b5f..6f25bc75ca1 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -5,10 +5,8 @@
= sprite_icon('github', css_class: 'gl-mr-2')
= _('Import repositories from GitHub')
-- paginatable = Feature.enabled?(:remove_legacy_github_client)
-
= render 'import/githubish_status',
- provider: 'github', paginatable: paginatable,
+ provider: 'github', paginatable: true,
default_namespace: @namespace,
cancel_path: cancel_import_github_path,
details_path: details_import_github_path,
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index 5a558d42802..c08abfeb813 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -15,3 +15,4 @@
- elsif value
= render Pajamas::AlertComponent.new(variant: type_to_variant[key], dismissible: closable.include?(key), alert_options: {class: "flash-#{key}", data: { testid: "alert-#{type_to_variant[key]}" }}) do |c|
= c.with_body { value }
+ #js-global-alerts
diff --git a/app/views/layouts/_google_tag_manager_body.html.haml b/app/views/layouts/_google_tag_manager_body.html.haml
deleted file mode 100644
index 98d7bf5d138..00000000000
--- a/app/views/layouts/_google_tag_manager_body.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-- return unless google_tag_manager_enabled?
-
-<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=#{google_tag_manager_id}"
-height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
diff --git a/app/views/layouts/_google_tag_manager_head.html.haml b/app/views/layouts/_google_tag_manager_head.html.haml
deleted file mode 100644
index 711a3d66ff7..00000000000
--- a/app/views/layouts/_google_tag_manager_head.html.haml
+++ /dev/null
@@ -1,51 +0,0 @@
-- return unless google_tag_manager_enabled?
-
-- if Feature.enabled?(:gitlab_gtm_datalayer, type: :ops)
- = javascript_tag do
- :plain
- window.dataLayer = window.dataLayer || [];
- function gtag(){dataLayer.push(arguments);}
-
- gtag('consent', 'default', {
- 'analytics_storage': 'granted',
- 'ad_storage': 'granted',
- 'functionality_storage': 'granted',
- 'wait_for_update': 500
- });
-
- gtag('consent', 'default', {
- 'analytics_storage': 'denied',
- 'ad_storage': 'denied',
- 'functionality_storage': 'denied',
- 'region': ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'IS', 'LI', 'NO', 'GB', 'PE', 'RU'],
- 'wait_for_update': 500
- });
-
- window.geofeed = (options) => {
- dataLayer.push({
- 'event' : 'OneTrustCountryLoad',
- 'oneTrustCountryId': options.country.toString()
- })
- }
-
- const json = document.createElement('script');
- json.setAttribute('src', 'https://geolocation.onetrust.com/cookieconsentpub/v1/geo/location/geofeed');
- document.head.appendChild(json);
-
-- if Feature.enabled?(:gtm_nonce, type: :ops)
- = javascript_tag nonce: content_security_policy_nonce do
- :plain
- (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
- new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
- j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
- 'https://www.googletagmanager.com/gtm.js?id='+i+dl;j.setAttribute('nonce',
- '#{content_security_policy_nonce}');f.parentNode.insertBefore(j,f);
- })(window,document,'script','dataLayer','#{google_tag_manager_id}');
-- else
- = javascript_tag do
- :plain
- (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
- new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
- j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
- 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
- })(window,document,'script','dataLayer','#{google_tag_manager_id}');
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index bbde5f2843b..37d03bde72e 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -1,6 +1,11 @@
- page_description brand_title unless page_description
- site_name = _('GitLab')
- omit_og = sign_in_with_redirect?
+
+-# This is a temporary place for the page specific style migrations to be included on all pages like page_specific_files
+- if Feature.disabled?(:page_specific_styles, current_user)
+ - add_page_specific_style('page_bundles/projects')
+
%head{ omit_og ? { } : { prefix: "og: http://ogp.me/ns#" } }
%meta{ charset: "utf-8" }
%meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' }
@@ -38,6 +43,7 @@
= stylesheet_link_tag_defer "highlight/themes/#{user_color_scheme}"
= stylesheet_link_tag 'performance_bar' if performance_bar_enabled?
+
= render 'layouts/snowplow'
= render 'layouts/loading_hints'
diff --git a/app/views/layouts/_img_loader.html.haml b/app/views/layouts/_img_loader.html.haml
index c1fe3ae0924..ac00a18f0bc 100644
--- a/app/views/layouts/_img_loader.html.haml
+++ b/app/views/layouts/_img_loader.html.haml
@@ -13,6 +13,6 @@
img.removeAttribute('data-src');
img.classList.remove('lazy');
img.classList.add('js-lazy-loaded');
- img.dataset.testid = 'js_lazy_loaded_content';
+ img.dataset.testid = 'js-lazy-loaded-content';
});
}
diff --git a/app/views/layouts/_loading_hints.html.haml b/app/views/layouts/_loading_hints.html.haml
index 1e6f671aacb..bf0421e9624 100644
--- a/app/views/layouts/_loading_hints.html.haml
+++ b/app/views/layouts/_loading_hints.html.haml
@@ -17,6 +17,7 @@
-# Do not use preload_link_tag for fonts, to work around Firefox double-fetch bug.
-# See https://github.com/web-platform-tests/wpt/pull/36930
%link{ rel: 'preload', href: font_path('gitlab-sans/GitLabSans.woff2'), as: 'font', crossorigin: css_crossorigin }
+ %link{ rel: 'preload', href: font_path('gitlab-sans/GitLabSans-Italic.woff2'), as: 'font', crossorigin: css_crossorigin }
%link{ rel: 'preload', href: font_path('gitlab-mono/GitLabMono.woff2'), as: 'font', crossorigin: css_crossorigin }
%link{ rel: 'preload', href: font_path('gitlab-mono/GitLabMono-Italic.woff2'), as: 'font', crossorigin: css_crossorigin }
= preload_link_tag(path_to_stylesheet('fonts'), crossorigin: css_crossorigin)
diff --git a/app/views/layouts/_snowplow.html.haml b/app/views/layouts/_snowplow.html.haml
index 10b2002dfef..3582deea902 100644
--- a/app/views/layouts/_snowplow.html.haml
+++ b/app/views/layouts/_snowplow.html.haml
@@ -2,7 +2,7 @@
- namespace = @group || @project&.namespace || @namespace
= webpack_bundle_tag 'tracker'
-- if Gitlab.com? && Feature.enabled?(:browsersdk_tracking)
+- if Gitlab.com? && Feature.enabled?(:browsersdk_tracking) && Feature.enabled?(:gl_analytics_tracking, Feature.current_request)
= webpack_bundle_tag 'analytics'
= javascript_tag do
:plain
diff --git a/app/views/layouts/component_preview.html.haml b/app/views/layouts/component_preview.html.haml
index 8217ac13c52..82ad5e3e3bb 100644
--- a/app/views/layouts/component_preview.html.haml
+++ b/app/views/layouts/component_preview.html.haml
@@ -1,10 +1,12 @@
%head
+ = stylesheet_link_tag "fonts"
- if params[:lookbook][:display][:theme] == "light"
= stylesheet_link_tag "application"
= stylesheet_link_tag "application_utilities"
- else
= stylesheet_link_tag "application_dark"
= stylesheet_link_tag "application_utilities_dark"
+
%body
.gl-mt-6{ class: (params[:lookbook][:display][:layout] == "fluid" ? "container-fluid" : "container") }
- if params[:lookbook][:display][:bg_dark]
diff --git a/app/views/layouts/header/_super_sidebar_logged_out.haml b/app/views/layouts/header/_super_sidebar_logged_out.haml
index 31dfdfb2bb3..76c7ea03c2a 100644
--- a/app/views/layouts/header/_super_sidebar_logged_out.haml
+++ b/app/views/layouts/header/_super_sidebar_logged_out.haml
@@ -1,7 +1,7 @@
%header.navbar.navbar-gitlab.super-sidebar-logged-out{ data: { testid: 'navbar' } }
%a.gl-sr-only.gl-accessibility{ href: "#content-body" } Skip to content
.container-fluid
- .header-content.gl-displax-flex
+ %nav.header-content.gl-displax-flex{ 'aria-label': s_('LoggedOutMarketingHeader|Explore GitLab') }
.title-container.gl-display-flex.gl-align-items-stretch.gl-pt-0.gl-mr-3
= render 'layouts/header/title'
diff --git a/app/views/layouts/header/_title.html.haml b/app/views/layouts/header/_title.html.haml
index 0e57c6809c2..59141cfa2a3 100644
--- a/app/views/layouts/header/_title.html.haml
+++ b/app/views/layouts/header/_title.html.haml
@@ -1,6 +1,6 @@
.title
%span.gl-sr-only GitLab
- = link_to root_path, title: _('Homepage'), id: 'logo', class: 'has-tooltip', **tracking_attrs('main_navigation', 'click_gitlab_logo_link', 'navigation_top') do
+ = link_to root_path, title: _('Homepage'), id: 'logo', class: 'has-tooltip', aria: { label: _('Homepage') }, **tracking_attrs('main_navigation', 'click_gitlab_logo_link', 'navigation_top') do
= brand_header_logo
.gl-display-flex.gl-align-items-center
- if Gitlab.com_and_canary?
diff --git a/app/views/layouts/mailer/_user_deactivated_additional_text.html.haml b/app/views/layouts/mailer/_user_deactivated_additional_text.html.haml
index cabdd4b09ec..5104d4f6e11 100644
--- a/app/views/layouts/mailer/_user_deactivated_additional_text.html.haml
+++ b/app/views/layouts/mailer/_user_deactivated_additional_text.html.haml
@@ -1,3 +1,3 @@
-- return unless Feature.enabled?(:deactivation_email_additional_text) && Gitlab::CurrentSettings.deactivation_email_additional_text.present?
+- return unless Gitlab::CurrentSettings.deactivation_email_additional_text.present?
%p
= Gitlab::Utils.nlbr(Gitlab::CurrentSettings.deactivation_email_additional_text)
diff --git a/app/views/layouts/mailer/_user_deactivated_additional_text.text.erb b/app/views/layouts/mailer/_user_deactivated_additional_text.text.erb
index 5862c8059f9..0de59fe7a75 100644
--- a/app/views/layouts/mailer/_user_deactivated_additional_text.text.erb
+++ b/app/views/layouts/mailer/_user_deactivated_additional_text.text.erb
@@ -1,3 +1,3 @@
-<% return unless Feature.enabled?(:deactivation_email_additional_text) && Gitlab::CurrentSettings.deactivation_email_additional_text.present? %>
+<% return unless Gitlab::CurrentSettings.deactivation_email_additional_text.present? %>
<%= Gitlab::CurrentSettings.deactivation_email_additional_text %>
diff --git a/app/views/layouts/nav/_ask_duo_button.html.haml b/app/views/layouts/nav/_ask_duo_button.html.haml
index f17ccfc8afe..e37ce50352c 100644
--- a/app/views/layouts/nav/_ask_duo_button.html.haml
+++ b/app/views/layouts/nav/_ask_duo_button.html.haml
@@ -1,5 +1,5 @@
- if Gitlab.ee? && ::Gitlab::Llm::TanukiBot.show_breadcrumbs_entry_point_for?(user: current_user)
- - label = s_('TanukiBot|Ask GitLab Duo')
+ - label = s_('TanukiBot|GitLab Duo Chat')
= render Pajamas::ButtonComponent.new(variant: :confirm,
category: :secondary,
icon: 'tanuki-ai',
diff --git a/app/views/layouts/nav/_top_bar.html.haml b/app/views/layouts/nav/_top_bar.html.haml
index 73b253e18bd..ef783b688e0 100644
--- a/app/views/layouts/nav/_top_bar.html.haml
+++ b/app/views/layouts/nav/_top_bar.html.haml
@@ -8,8 +8,8 @@
%div{ class: top_bar_class, data: { testid: 'top-bar' } }
.top-bar-container.gl-display-flex.gl-align-items-center.gl-gap-2{ :class => top_bar_container_class }
- if show_super_sidebar?
- = render Pajamas::ButtonComponent.new(icon: 'sidebar', category: :tertiary, button_options: { class: 'js-super-sidebar-toggle-expand super-sidebar-toggle gl-ml-n3', title: _('Expand sidebar'), aria: { controls: 'super-sidebar', expanded: 'false', label: _('Navigation sidebar') } })
+ = render Pajamas::ButtonComponent.new(icon: 'sidebar', category: :tertiary, button_options: { class: 'js-super-sidebar-toggle-expand super-sidebar-toggle gl-ml-n3', aria: { controls: 'super-sidebar', expanded: 'false', label: _('Primary navigation sidebar') } })
- elsif defined?(@left_sidebar)
- = render Pajamas::ButtonComponent.new(icon: 'sidebar', category: :tertiary, button_options: { class: 'toggle-mobile-nav gl-ml-n3', data: { testid: 'toggle_mobile_nav_button' }, aria: { label: _('Open sidebar') } })
+ = render Pajamas::ButtonComponent.new(icon: 'sidebar', category: :tertiary, button_options: { class: 'toggle-mobile-nav gl-ml-n3', data: { testid: 'toggle-mobile-nav-button' }, aria: { label: _('Open sidebar') } })
= render "layouts/nav/breadcrumbs/breadcrumbs"
= render "layouts/nav/ask_duo_button"
diff --git a/app/views/layouts/nav/breadcrumbs/_breadcrumbs.html.haml b/app/views/layouts/nav/breadcrumbs/_breadcrumbs.html.haml
index b5f067cf42f..040793d616f 100644
--- a/app/views/layouts/nav/breadcrumbs/_breadcrumbs.html.haml
+++ b/app/views/layouts/nav/breadcrumbs/_breadcrumbs.html.haml
@@ -2,7 +2,7 @@
- unless @skip_current_level_breadcrumb
- push_to_schema_breadcrumb(@breadcrumb_title, breadcrumb_title_link)
-%nav.breadcrumbs{ 'aria-label': _('Breadcrumbs'), data: { testid: 'breadcrumb-links', qa_selector: 'breadcrumb_links_content' } }
+%nav.breadcrumbs{ 'aria-label': _('Breadcrumbs'), data: { testid: 'breadcrumb-links' } }
%ul.list-unstyled.breadcrumbs-list.js-breadcrumbs-list
- unless hide_top_links
= header_title
@@ -11,7 +11,7 @@
= breadcrumb_list_item link_to(extra[:text], extra[:link])
= render "layouts/nav/breadcrumbs/collapsed_inline_list", location: :after
- unless @skip_current_level_breadcrumb
- %li{ data: { testid: 'breadcrumb-current-link', qa_selector: 'breadcrumb_current_link' } }
+ %li{ data: { testid: 'breadcrumb-current-link' } }
= link_to @breadcrumb_title, breadcrumb_title_link
-# haml-lint:disable InlineJavaScript
%script{ type: 'application/ld+json' }
diff --git a/app/views/layouts/project_settings.html.haml b/app/views/layouts/project_settings.html.haml
index 29e30c4434f..7e5dd0d37c9 100644
--- a/app/views/layouts/project_settings.html.haml
+++ b/app/views/layouts/project_settings.html.haml
@@ -1,6 +1,7 @@
- page_title _("Settings")
- nav "project"
- add_page_specific_style 'page_bundles/settings'
+- add_page_specific_style 'page_bundles/projects'
- enable_search_settings locals: { container_class: 'gl-my-5' }
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
index 29a561ae1a9..32f00a4c0c6 100644
--- a/app/views/layouts/terms.html.haml
+++ b/app/views/layouts/terms.html.haml
@@ -20,14 +20,11 @@
= render Pajamas::CardComponent.new do |c|
- c.with_header do
= brand_header_logo({add_gitlab_black_text: true})
+ - if current_user
+ .gl-display-flex.gl-gap-2.gl-align-items-center
+ .gl-text-right.gl-line-height-normal
+ .gl-font-weight-bold= current_user.name
+ .gl-text-gray-700 @#{current_user.username}
+ = render Pajamas::AvatarComponent.new(current_user, size: 32, avatar_options: { data: { qa_selector: 'user_avatar_content' } })
- c.with_body do
- - if header_link?(:user_dropdown)
- .navbar-collapse
- %ul.nav.navbar-nav
- %li.header-user.dropdown
- = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
- = render Pajamas::AvatarComponent.new(current_user, size: 24, class: 'gl-mr-3', avatar_options: { data: { qa_selector: 'user_avatar_content' } })
- = sprite_icon('chevron-down')
- .dropdown-menu.dropdown-menu-right
- = render 'layouts/header/current_user_dropdown'
= yield
diff --git a/app/views/notify/build_ios_app_guide_email.html.haml b/app/views/notify/build_ios_app_guide_email.html.haml
deleted file mode 100644
index e9f23d3c0f9..00000000000
--- a/app/views/notify/build_ios_app_guide_email.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-%tr
- %td{ bgcolor: "#ffffff", height: "auto", style: "max-width: 600px; width: 100%; text-align: center; height: 200px; padding: 25px 15px; mso-line-height-rule: exactly; min-height: 40px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;", valign: "middle", width: "100%" }
- = inline_image_link(@message.logo_path, { width: '150', style: 'width: 150px;' })
- %h1{ style: "font-size: 40px; line-height: 46x; color: #000000; padding: 20px 0 0 0; font-weight: normal;" }
- = @message.title
-%tr
- %td{ style: "padding: 10px 20px 30px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; color:#000000; font-size: 18px; line-height: 24px;" }
- %p{ style: "margin: 0 0 20px 0;" }
- = @message.body_line1.html_safe
-%tr
- %td{ align: "center", style: "padding: 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" }
- .cta_link.cta_link_primary= @message.cta_link
- .cta_link.cta_link_secondary= @message.cta2_link
diff --git a/app/views/notify/build_ios_app_guide_email.text.erb b/app/views/notify/build_ios_app_guide_email.text.erb
deleted file mode 100644
index 59757b7c1b0..00000000000
--- a/app/views/notify/build_ios_app_guide_email.text.erb
+++ /dev/null
@@ -1,13 +0,0 @@
-<%= @message.title %>
-
-<%= @message.body_line1 %>
-
-<%= @message.cta_link %>
-
-<%= @message.cta2_link %>
-
-<%= @message.footer_links %>
-
-<%= @message.address %>
-
-<%= @message.unsubscribe %>
diff --git a/app/views/notify/resource_access_tokens_about_to_expire_email.html.haml b/app/views/notify/resource_access_tokens_about_to_expire_email.html.haml
index e4e34f6c8ee..35c2260f24f 100644
--- a/app/views/notify/resource_access_tokens_about_to_expire_email.html.haml
+++ b/app/views/notify/resource_access_tokens_about_to_expire_email.html.haml
@@ -11,3 +11,5 @@
%p
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: @target_url }
= html_escape(_('You can create a new one or check them in your %{link_start}access tokens%{link_end} settings.')) % { link_start: link_start, link_end: '</a>'.html_safe }
+%p
+ = @reason_text
diff --git a/app/views/notify/resource_access_tokens_about_to_expire_email.text.erb b/app/views/notify/resource_access_tokens_about_to_expire_email.text.erb
index bea74f09129..f57c3e7b0d0 100644
--- a/app/views/notify/resource_access_tokens_about_to_expire_email.text.erb
+++ b/app/views/notify/resource_access_tokens_about_to_expire_email.text.erb
@@ -9,3 +9,5 @@
<% end %>
<%= _('You can create a new one or check them in your access token settings: %{target_url}') % { target_url: @target_url } %>
+
+<%= @reason_text %>
diff --git a/app/views/organizations/organizations/index.html.haml b/app/views/organizations/organizations/index.html.haml
index 04a90b7589f..4e58cc7b9c2 100644
--- a/app/views/organizations/organizations/index.html.haml
+++ b/app/views/organizations/organizations/index.html.haml
@@ -1,2 +1,5 @@
+- add_page_specific_style 'page_bundles/organizations'
- page_title s_('Organization|Organizations')
- header_title _("Your work"), root_path
+
+#js-organizations-index{ data: organization_index_app_data }
diff --git a/app/views/organizations/organizations/new.html.haml b/app/views/organizations/organizations/new.html.haml
index 4d7f552c87b..1a6c5a79ff6 100644
--- a/app/views/organizations/organizations/new.html.haml
+++ b/app/views/organizations/organizations/new.html.haml
@@ -1,3 +1,6 @@
+- add_page_specific_style 'page_bundles/organizations'
- page_title s_('Organization|New organization')
- header_title _("Your work"), root_path
- add_to_breadcrumbs s_('Organization|Organizations'), organizations_path
+
+#js-organizations-new{ data: { app_data: organization_new_app_data } }
diff --git a/app/views/organizations/settings/general.html.haml b/app/views/organizations/settings/general.html.haml
new file mode 100644
index 00000000000..94892ef9fbb
--- /dev/null
+++ b/app/views/organizations/settings/general.html.haml
@@ -0,0 +1 @@
+- page_title _("General settings")
diff --git a/app/views/profiles/chat_names/index.html.haml b/app/views/profiles/chat_names/index.html.haml
index 7a63fc30d9c..99284abb73d 100644
--- a/app/views/profiles/chat_names/index.html.haml
+++ b/app/views/profiles/chat_names/index.html.haml
@@ -13,7 +13,7 @@
- if @chat_names.present?
.table-responsive
- %table.table
+ %table.table.gl-table
%thead
%tr
%th= _('Team domain')
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index 1307c388041..94671b69b5e 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -46,7 +46,7 @@
.gl-display-flex
%pre.well-pre.gl-pl-5.gl-mb-0.gl-border-0
= @key.key
- = deprecated_clipboard_button(title: s_('Profiles|Copy SSH key'), text: @key.key, class: 'gl-bg-gray-10 gl-px-3! gl-border-none! gl-rounded-top-left-none! gl-rounded-bottom-left-none!')
+ = clipboard_button(title: s_('Profiles|Copy SSH key'), text: @key.key, category: :tertiary, size: :medium)
= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body gl-px-0'}) do |c|
- c.with_header do
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 42297a0cf3d..ff0b31da022 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -18,24 +18,22 @@
- register_2fa_token = _('We recommend using cloud-based authenticator applications that can restore access if you lose your hardware device.')
= register_2fa_token.html_safe
= link_to _('What are some examples?'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'enable-one-time-password'), target: '_blank', rel: 'noopener noreferrer'
- .row.gl-mb-3
- .col-md-4.gl-min-w-fit-content
- .gl-p-2.gl-mb-3{ style: 'background: #fff' }
- = raw @qr_code
- .col-md-8
- = render Pajamas::CardComponent.new do |c|
- - c.with_body do
- %p.gl-mt-0.gl-mb-3.gl-font-weight-bold
- = _("Can't scan the code?")
- %p.gl-mt-0.gl-mb-3
- = _('To add the entry manually, provide the following details to the application on your phone.')
- %p.gl-mt-0.gl-mb-0
- = _('Account: %{account}') % { account: @account_string }
- %p.gl-mt-0.gl-mb-0{ data: { qa_selector: 'otp_secret_content' } }
- = _('Key:')
- %code.two-factor-secret= current_user.otp_secret.scan(/.{4}/).join(' ')
- %p.gl-mb-0.two-factor-new-manual-content
- = _('Time based: Yes')
+ .gl-p-2.gl-mb-3{ style: 'background: #fff' }
+ = raw @qr_code
+ .gl-mb-5
+ = render Pajamas::CardComponent.new do |c|
+ - c.with_body do
+ %p.gl-mt-0.gl-mb-3.gl-font-weight-bold
+ = _("Can't scan the code?")
+ %p.gl-mt-0.gl-mb-3
+ = _("To add the entry manually, provide the following details to the application on your phone.")
+ %p.gl-mt-0.gl-mb-0
+ = _('Account: %{account}') % { account: @account_string }
+ %p.gl-mt-0.gl-mb-0{ data: { qa_selector: 'otp_secret_content' } }
+ = _('Key:')
+ %code.two-factor-secret= current_user.otp_secret.scan(/.{4}/).join(' ')
+ %p.gl-mb-0.two-factor-new-manual-content
+ = _('Time based: Yes')
= form_tag profile_two_factor_auth_path, method: :post do |f|
- if @error
= render Pajamas::AlertComponent.new(title: @error[:message],
diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml
index 19943aa68a3..afb241dba7c 100644
--- a/app/views/projects/_commit_button.html.haml
+++ b/app/views/projects/_commit_button.html.haml
@@ -1,5 +1,5 @@
.gl-display-flex.gl-mt-7
- - submit_button_options = { type: :submit, variant: :confirm, button_options: { id: 'commit-changes', class: 'js-commit-button', data: { qa_selector: 'commit_button' } } }
+ - submit_button_options = { type: :submit, variant: :confirm, button_options: { id: 'commit-changes', class: 'js-commit-button', data: { testid: 'commit-button' } } }
= render Pajamas::ButtonComponent.new(**submit_button_options) do
= _('Commit changes')
= render Pajamas::ButtonComponent.new(loading: true, disabled: true, **submit_button_options.merge({ button_options: { class: 'js-commit-button-loading gl-display-none' } })) do
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index 58c760c54e8..cc11997d809 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -2,7 +2,7 @@
- project = local_assigns.fetch(:project)
-= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card', data: { qa_selector: 'export_project_content' } }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
+= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card', data: { testid: 'export-project-content' } }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
- c.with_header do
.gl-new-card-title-wrapper
%h4.gl-new-card-title= _('Export project')
@@ -27,10 +27,10 @@
- if project.export_status == :finished
= render Pajamas::ButtonComponent.new(href: download_export_project_path(project),
method: :get,
- button_options: { ref: 'nofollow', download: '', data: { qa_selector: 'download_export_link' } }) do
+ button_options: { ref: 'nofollow', download: '', data: { testid: 'download-export-link' } }) do
= _('Download export')
= render Pajamas::ButtonComponent.new(href: generate_new_export_project_path(project), method: :post) do
= _('Generate new export')
- else
- = render Pajamas::ButtonComponent.new(href: export_project_path(project), method: :post, button_options: { data: { qa_selector: 'export_project_link' } }) do
+ = render Pajamas::ButtonComponent.new(href: export_project_path(project), method: :post, button_options: { data: { testid: 'export-project-link' } }) do
= _('Export project')
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index cb341ede9de..7445a403865 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -21,7 +21,7 @@
#js-fork-info{ data: vue_fork_divergence_data(project, ref) }
- if is_project_overview && has_project_shortcut_buttons
- .project-buttons.gl-mb-5.js-show-on-project-root{ data: { qa_selector: 'project_buttons' } }
+ .project-buttons.gl-mb-5.js-show-on-project-root{ data: { testid: 'project-buttons' } }
= render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout), project_buttons: true
#js-tree-list{ data: vue_file_list_data(project, ref) }
diff --git a/app/views/projects/_find_file_link.html.haml b/app/views/projects/_find_file_link.html.haml
index 4ad2c339bcc..83f1370b88e 100644
--- a/app/views/projects/_find_file_link.html.haml
+++ b/app/views/projects/_find_file_link.html.haml
@@ -1,2 +1,2 @@
-= link_button_to project_find_file_path(@project, @ref), class: 'shortcuts-find-file', rel: 'nofollow' do
+= link_button_to project_find_file_path(@project, @ref, ref_type: @ref_type), class: 'shortcuts-find-file', rel: 'nofollow' do
= _('Find file')
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 759ec541af5..93f4fe62568 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -8,17 +8,17 @@
%div{ class: 'avatar-container rect-avatar s64 home-panel-avatar gl-flex-shrink-0 gl-w-11 gl-h-11 gl-mr-3! float-none' }
= project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64, itemprop: 'image')
%div
- %h1.home-panel-title.gl-font-size-h1.gl-mt-3.gl-mb-2.gl-display-flex.gl-word-break-word{ data: { qa_selector: 'project_name_content' }, itemprop: 'name' }
+ %h1.home-panel-title.gl-font-size-h1.gl-mt-3.gl-mb-2.gl-display-flex.gl-word-break-word{ data: { testid: 'project-name-content' }, itemprop: 'name' }
= @project.name
= visibility_level_content(@project, css_class: 'visibility-icon gl-text-secondary gl-ml-2', icon_css_class: 'icon')
= render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: @project, additional_classes: 'gl-align-self-center gl-ml-2'
- if @project.group
= render_if_exists 'shared/tier_badge', source: @project, source_type: 'Project'
- .home-panel-metadata.gl-font-sm.gl-text-secondary.gl-font-base.gl-font-weight-normal.gl-line-height-normal{ data: { qa_selector: 'project_id_content' }, itemprop: 'identifier' }
+ .home-panel-metadata.gl-font-sm.gl-text-secondary.gl-font-base.gl-font-weight-normal.gl-line-height-normal{ data: { testid: 'project-id-content' }, itemprop: 'identifier' }
- if can?(current_user, :read_project, @project)
%span.gl-display-inline-block.gl-vertical-align-middle
= s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id }
- = deprecated_clipboard_button(title: s_('ProjectPage|Copy project ID'), text: @project.id)
+ = clipboard_button(title: s_('ProjectPage|Copy project ID'), text: @project.id)
- if current_user
%span.gl-ml-3.gl-mb-3
= render 'shared/members/access_request_links', source: @project
@@ -52,13 +52,13 @@
= render_if_exists "projects/home_mirror"
- if @project.badges.present?
- .project-badges.mb-2{ data: { qa_selector: 'project_badges_content' } }
+ .project-badges.mb-2{ data: { testid: 'project-badges-content' } }
- @project.badges.each do |badge|
- badge_link_url = badge.rendered_link_url(@project)
%a.gl-mr-3{ href: badge_link_url,
target: '_blank',
rel: 'noopener noreferrer',
- data: { qa_selector: 'badge_image_link', qa_link_url: badge_link_url } }>
+ data: { testid: 'badge-image-link', qa_link_url: badge_link_url } }>
%img.project-badge{ src: badge.rendered_image_url(@project),
'aria-hidden': true,
alt: 'Project badge' }>
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 89c91887d19..240e9519975 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -16,5 +16,5 @@
- if create_mr_button_from_event?(event)
- c.with_actions do
- = render Pajamas::ButtonComponent.new(variant: :confirm, href: create_mr_path_from_push_event(event), button_options: { data: { qa_selector: 'create_merge_request_button' }}) do
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: create_mr_path_from_push_event(event), button_options: { data: { testid: 'create-merge-request-button' }}) do
= _('Create merge request')
diff --git a/app/views/projects/_transfer.html.haml b/app/views/projects/_transfer.html.haml
index cf0634ee411..43bb3627c4f 100644
--- a/app/views/projects/_transfer.html.haml
+++ b/app/views/projects/_transfer.html.haml
@@ -3,7 +3,7 @@
- hidden_input_id = "new_namespace_id"
- initial_data = { button_text: s_('ProjectSettings|Transfer project'), confirm_danger_message: transfer_project_message(@project), phrase: @project.name, target_form_id: form_id, target_hidden_input_id: hidden_input_id, project_id: @project.id }
-= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card', data: { qa_selector: 'transfer_project_content' } }, header_options: { class: 'gl-new-card-header gl-flex-direction-column' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
+= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card', data: { testid: 'transfer-project-content' } }, header_options: { class: 'gl-new-card-header gl-flex-direction-column' }, body_options: { class: 'gl-new-card-body gl-px-5 gl-py-4' }) do |c|
- c.with_header do
.gl-new-card-title-wrapper
%h4.gl-new-card-title.warning-title= _('Transfer project')
diff --git a/app/views/projects/artifacts/file.html.haml b/app/views/projects/artifacts/file.html.haml
index 5b9e5ad584f..d24df0d3472 100644
--- a/app/views/projects/artifacts/file.html.haml
+++ b/app/views/projects/artifacts/file.html.haml
@@ -12,8 +12,6 @@
= render 'projects/blob/header_content', blob: blob
.file-actions.d-none.d-sm-block
- = render 'projects/blob/viewer_switcher', blob: blob
-
.btn-group{ role: "group" }<
= copy_blob_source_button(blob)
= download_blob_button(blob)
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index e2cad2fb3d7..bd0f4577a32 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -1,5 +1,6 @@
- page_title _("Blame"), @blob.path, @ref
- add_page_specific_style 'page_bundles/tree'
+- add_page_specific_style 'page_bundles/projects'
- blame_streaming_url = blame_pages_streaming_url(@id, @project)
- if @blame_mode.streaming? && @blame_pagination.total_extra_pages > 0
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 543bdaf46df..2d9b7ada015 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -2,6 +2,7 @@
- project = @project.present(current_user: current_user)
- ref = local_assigns[:ref] || @ref
- expanded = params[:expanded].present?
+- add_page_specific_style 'page_bundles/projects'
-# If the blob has a RichViewer we preload the content except for GeoJSON since it is handled by Vue
- if blob.rich_viewer && blob.extension != 'geojson'
- add_page_startup_api_call local_assigns.fetch(:viewer_url) { url_for(safe_params.merge(viewer: blob.rich_viewer.type, format: :json)) }
diff --git a/app/views/projects/blob/_breadcrumb.html.haml b/app/views/projects/blob/_breadcrumb.html.haml
index 539453bf6af..c140eecd8c1 100644
--- a/app/views/projects/blob/_breadcrumb.html.haml
+++ b/app/views/projects/blob/_breadcrumb.html.haml
@@ -22,11 +22,11 @@
-# only show normal/blame view links for text files
- if blob.readable_text?
- if blame
- = link_button_to _('Normal view'), project_blob_path(@project, @id)
+ = link_button_to _('Normal view'), project_blob_path(@project, @id, ref_type: @ref_type)
- else
- = link_button_to _('Blame'), project_blame_path(@project, @id), class: 'js-blob-blame-link' unless blob.empty?
+ = link_button_to _('Blame'), project_blame_path(@project, @id, ref_type: @ref_type), class: 'js-blob-blame-link' unless blob.empty?
- = link_button_to _('History'), project_commits_path(@project, @id)
+ = link_button_to _('History'), project_commits_path(@project, @id, ref_type: @ref_type)
= link_button_to _('Permalink'), project_blob_path(@project, tree_join(@commit.sha, @path)),
class: 'js-data-file-blob-permalink-url'
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index 9c07713b9f8..a1d3bef2914 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -4,7 +4,6 @@
= render 'projects/blob/header_content', blob: blob
.file-actions.gl-display-flex.gl-align-items-center.gl-flex-wrap.gl-md-justify-content-end<
- = render 'projects/blob/viewer_switcher', blob: blob unless blame
= render 'shared/web_ide_button', blob: blob
.btn-group.gl-ml-3{ role: "group" }
= copy_blob_source_button(blob) unless blame
diff --git a/app/views/projects/blob/_viewer_switcher.html.haml b/app/views/projects/blob/_viewer_switcher.html.haml
deleted file mode 100644
index 043b8629289..00000000000
--- a/app/views/projects/blob/_viewer_switcher.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-- if blob.show_viewer_switcher?
- - simple_viewer = blob.simple_viewer
- - rich_viewer = blob.rich_viewer
-
- .btn-group.js-blob-viewer-switcher.gl-ml-3{ role: "group" }>
- - simple_label = "Display #{simple_viewer.switcher_title}"
- %button.btn.gl-button.btn-default.btn-icon.js-blob-viewer-switch-btn.has-tooltip{ 'aria-label' => simple_label, title: simple_label, data: { viewer: 'simple', container: 'body' } }>
- = sprite_icon(simple_viewer.switcher_icon)
-
- - rich_label = "Display #{rich_viewer.switcher_title}"
- %button.btn.gl-button.btn-default.btn-icon.js-blob-viewer-switch-btn.has-tooltip{ 'aria-label' => rich_label, title: rich_label, data: { viewer: 'rich', container: 'body' } }>
- = sprite_icon(rich_viewer.switcher_icon)
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 195dc03632a..74f1688a2db 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -12,7 +12,7 @@
- link_end = '</a>'.html_safe
- external_link_icon = content_tag 'span', { aria: { label: _('Opens new window') }} do
- sprite_icon('external-link', css_class: 'gl-icon').html_safe
- - if @different_project
+ - if commit_to_fork
= _("Error: Can't edit this file. The fork and upstream project have diverged. %{link_start}Edit the file on the fork %{icon}%{link_end}, and create a merge request.").html_safe % {link_start: blob_link_start % { url: project_blob_path(@project_to_commit_into, @id) } , link_end: link_end, icon: external_link_icon }
- else
- blob_url = project_blob_path(@project, @id)
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index 9ec824f64d4..82f517e8a84 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -4,7 +4,7 @@
- signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @last_commit, limit: 1)
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco', prefetch: true)
-- add_page_startup_graphql_call('repository/blob_info', { projectPath: @project.full_path, ref: current_ref, refType: @ref_type.to_s, filePath: @blob.path, shouldFetchRawText: @blob.rendered_as_text? && !@blob.rich_viewer })
+- add_page_startup_graphql_call('repository/blob_info', { projectPath: @project.full_path, ref: current_ref, refType: @ref_type.to_s.upcase, filePath: @blob.path, shouldFetchRawText: @blob.rendered_as_text? && !@blob.rich_viewer })
.js-signature-container{ data: { 'signatures-path': signatures_path } }
diff --git a/app/views/projects/blob/viewers/_loading.html.haml b/app/views/projects/blob/viewers/_loading.html.haml
index d8efaf9ad95..40b64cac1f7 100644
--- a/app/views/projects/blob/viewers/_loading.html.haml
+++ b/app/views/projects/blob/viewers/_loading.html.haml
@@ -1 +1 @@
-= gl_loading_icon(size: "md", css_class: "gl-my-4", data: { qa_selector: 'spinner_placeholder' })
+= gl_loading_icon(size: "md", css_class: "gl-my-4", data: { testid: 'spinner-placeholder' })
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 7c52350f101..61961172eb2 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -28,7 +28,7 @@
.pipeline-status.d-none.d-md-block<
- if commit_status
- = render 'ci/status/icon', size: 16, status: commit_status, option_css_classes: 'gl-display-inline-flex gl-vertical-align-middle gl-mr-3'
+ = render 'ci/status/icon', size: 16, status: commit_status
- elsif show_commit_status
.gl-display-inline-flex.gl-vertical-align-middle.gl-mr-3
%svg.s16
diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml
index b5679bc512c..0e645eda678 100644
--- a/app/views/projects/buttons/_clone.html.haml
+++ b/app/views/projects/buttons/_clone.html.haml
@@ -15,7 +15,7 @@
.input-group.btn-group
= text_field_tag :ssh_project_clone, ssh_clone_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'ssh_clone_url_content' }
.input-group-append
- = deprecated_clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default")
+ = clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), category: :primary, size: :medium)
= render_if_exists 'projects/buttons/geo'
- if http_enabled?
%li.pt-2{ class: 'gl-px-4!' }
@@ -24,7 +24,7 @@
.input-group.btn-group
= text_field_tag :http_project_clone, http_clone_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'http_clone_url_content' }
.input-group-append
- = deprecated_clipboard_button(target: '#http_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default")
+ = clipboard_button(target: '#http_project_clone', title: _("Copy URL"), category: :primary, size: :medium)
= render_if_exists 'projects/buttons/geo'
= render_if_exists 'projects/buttons/kerberos_clone_field'
%li.divider.mt-2
diff --git a/app/views/projects/buttons/_compare.html.haml b/app/views/projects/buttons/_compare.html.haml
new file mode 100644
index 00000000000..82b1b837fbb
--- /dev/null
+++ b/app/views/projects/buttons/_compare.html.haml
@@ -0,0 +1,8 @@
+- project = local_assigns.fetch(:project)
+- ref = local_assigns.fetch(:ref, nil)
+- root_ref = local_assigns.fetch(:root_ref, nil)
+- unless ref.blank? || root_ref == ref
+ - compare_path = project_compare_index_path(project, from: root_ref, to: ref)
+
+ = link_button_to compare_path, class: 'shortcuts-compare', rel: 'nofollow' do
+ = _('Compare')
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 23d18236738..b3282742407 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -6,7 +6,7 @@
- if !project.empty_repo? && can?(current_user, :download_code, project)
- archive_prefix = "#{project.path}-#{ref.tr('/', '-')}"
.project-action-button.dropdown.gl-dropdown.inline{ class: css_class }>
- = render Pajamas::ButtonComponent.new(button_options: { class: 'dropdown-toggle gl-dropdown-toggle dropdown-icon-only has-tooltip', title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download'), 'data-display' => 'static', data: { qa_selector: 'download_source_code_button' } }) do
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'dropdown-toggle gl-dropdown-toggle dropdown-icon-only has-tooltip', title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download'), 'data-display' => 'static', data: { testid: 'download-source-code-button' } }) do
= sprite_icon('download', css_class: 'gl-icon dropdown-icon')
%span.sr-only= _('Select Archive Format')
= sprite_icon('chevron-down', css_class: 'gl-icon dropdown-chevron')
@@ -15,7 +15,7 @@
%h5.m-0.dropdown-bold-header= _('Download source code')
.dropdown-menu-content
= render 'projects/buttons/download_links', project: project, ref: ref, archive_prefix: archive_prefix, path: nil
- #js-directory-downloads{ data: { links: directory_download_links(project, ref, archive_prefix).to_json } }
+ .js-directory-downloads{ data: { links: directory_download_links(project, ref, archive_prefix).to_json } }
- if pipeline && pipeline.latest_builds_with_artifacts.any?
%section.border-top.pt-1.mt-1
%h5.m-0.dropdown-bold-header= _('Download artifacts')
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 1034f06f722..be2bf43cbb9 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -1,6 +1,7 @@
- breadcrumb_title _("Commits")
- add_page_specific_style 'page_bundles/tree'
- add_page_specific_style 'page_bundles/merge_request'
+- add_page_specific_style 'page_bundles/projects'
- page_title _("Commits"), @ref
= content_for :meta_tags do
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 0158018ecc0..4e84a6ef7e7 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -18,7 +18,7 @@
%p.gl-text-secondary= _('Update your project name, topics, description, and avatar.')
.settings-content= render 'projects/settings/general'
-%section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded), data: { qa_selector: 'visibility_features_permissions_content' } }
+%section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded), data: { testid: 'visibility-features-permissions-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Visibility, project features, permissions')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
@@ -38,7 +38,7 @@
- c.with_body do
= _('On the left sidebar, select %{merge_requests_link} to view them.').html_safe % { merge_requests_link: link_to('Settings > Merge requests', project_settings_merge_requests_path(@project)).html_safe }
-%section.settings.no-animate{ class: ('expanded' if expanded), data: { qa_selector: 'badges_settings_content' } }
+%section.settings.no-animate{ class: ('expanded' if expanded), data: { testid: 'badges-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= s_('ProjectSettings|Badges')
@@ -56,7 +56,7 @@
= render 'projects/service_desk_settings'
-%section.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded), data: { qa_selector: 'advanced_settings_content' } }
+%section.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded), data: { testid: 'advanced-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Advanced')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
@@ -107,8 +107,8 @@
.input-group-prepend
.input-group-text
#{Gitlab::Utils.append_path(root_url, @project.namespace.full_path)}/
- = f.text_field :path, class: 'form-control gl-form-input-xl', data: { qa_selector: 'project_path_field' }
- = f.submit _('Change path'), class: "btn-danger", data: { qa_selector: 'change_path_button' }, pajamas_button: true
+ = f.text_field :path, class: 'form-control gl-form-input-xl', data: { testid: 'project-path-field' }
+ = f.submit _('Change path'), class: "btn-danger", data: { testid: 'change-path-button' }, pajamas_button: true
= render 'transfer', project: @project
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index deb3c33f733..902a5df9394 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -17,7 +17,7 @@
%p
= _('You can get started by cloning the repository or start adding files to it with one of the following options.')
-.project-buttons{ data: { qa_selector: 'quick_actions_container' } }
+.project-buttons{ data: { testid: 'quick-actions-container' } }
.project-clone-holder.d-block.d-md-none.gl-mt-3.gl-mr-3
= render "shared/mobile_clone_panel"
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
index 541b8c1147d..0c760ab82c9 100644
--- a/app/views/projects/find_file/show.html.haml
+++ b/app/views/projects/find_file/show.html.haml
@@ -1,13 +1,16 @@
- page_title _("Find File"), @ref
- add_page_specific_style 'page_bundles/tree'
+- add_page_specific_style 'page_bundles/projects'
-.file-finder-holder.tree-holder.clearfix.js-file-finder.gl-pt-4{ 'data-file-find-url': "#{escape_javascript(project_files_path(@project, @ref, format: :json))}", 'data-find-tree-url': escape_javascript(project_tree_path(@project, @ref)), 'data-blob-url-template': escape_javascript(project_blob_path(@project, @ref)) }
+- tree_path = project_tree_path(@project, @ref)
+- blob_path = project_blob_path(@project, @ref)
+.file-finder-holder.tree-holder.clearfix.js-file-finder.gl-pt-4{ data: { file_find_url: "#{escape_javascript(project_files_path(@project, @ref, ref_type: @ref_type, format: :json))}", find_tree_url: escape_javascript(tree_path), blob_url_template: escape_javascript(blob_path), ref_type: @ref_type } }
.nav-block.gl-xs-mr-0
.tree-ref-holder.gl-xs-mb-3.gl-xs-w-full.gl-max-w-26
- #js-blob-ref-switcher{ data: { project_id: @project.id, ref: @ref, namespace: "/-/find_file" } }
+ #js-blob-ref-switcher{ data: { project_id: @project.id, ref: @ref, ref_type: @ref_type, namespace: "/-/find_file" } }
%ul.breadcrumb.repo-breadcrumb.gl-flex-nowrap
%li.breadcrumb-item.gl-white-space-nowrap
- = link_to project_tree_path(@project, @ref) do
+ = link_to project_tree_path(@project, @ref, ref_type: @ref_type) do
= @project.path
%li.file-finder.breadcrumb-item
%input#file_find.form-control.file-finder-input{ type: "text", placeholder: _('Find by path'), autocomplete: 'off' }
diff --git a/app/views/projects/issues/_details_content.html.haml b/app/views/projects/issues/_details_content.html.haml
index 51ffb68f4e5..881e6863040 100644
--- a/app/views/projects/issues/_details_content.html.haml
+++ b/app/views/projects/issues/_details_content.html.haml
@@ -16,7 +16,7 @@
= edited_time_ago_with_tooltip(issuable, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
.js-issue-widgets
- = render 'projects/issues/emoji_block', issuable: issuable, api_awards_path: api_awards_path
+ = render 'projects/issues/emoji_block', issuable: issuable, api_awards_path: api_awards_path, new_custom_emoji_path: new_custom_emoji_path(@project.group)
.js-issue-widgets
= render 'projects/issues/sentry_stack_trace', issuable: issuable
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index c6e5102889a..5cb7fa8816e 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -13,4 +13,5 @@
current_user_data: UserSerializer.new.represent(current_user, {only_path: true}, CurrentUserEntity).to_json,
can_add_timeline_events: "#{can?(current_user, :admin_incident_management_timeline_event, @issue)}",
report_abuse_path: add_category_abuse_reports_path,
- new_comment_template_path: profile_comment_templates_path } }
+ new_comment_template_path: profile_comment_templates_path,
+ new_custom_emoji_path: new_custom_emoji_path(@project.group) } }
diff --git a/app/views/projects/issues/_emoji_block.html.haml b/app/views/projects/issues/_emoji_block.html.haml
index 7eb3c0f5c9f..f9eee9ec99e 100644
--- a/app/views/projects/issues/_emoji_block.html.haml
+++ b/app/views/projects/issues/_emoji_block.html.haml
@@ -1,8 +1,9 @@
- api_awards_path = local_assigns.fetch(:api_awards_path, nil)
+- new_custom_emoji_path = local_assigns.fetch(:new_custom_emoji_path, nil)
.emoji-block.emoji-block-sticky
.row.gl-m-0.gl-justify-content-space-between
.js-noteable-awards
- = render 'award_emoji/awards_block', awardable: issuable, inline: true, api_awards_path: api_awards_path
+ = render 'award_emoji/awards_block', awardable: issuable, inline: true, api_awards_path: api_awards_path, new_custom_emoji_path: new_custom_emoji_path
.new-branch-col.gl-font-size-0.gl-my-2
= render 'new_branch' if show_new_branch_button?
diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml
index 64143502b77..57f78152159 100644
--- a/app/views/projects/issues/new.html.haml
+++ b/app/views/projects/issues/new.html.haml
@@ -1,4 +1,5 @@
- add_page_specific_style 'page_bundles/merge_request'
+- add_page_specific_style 'page_bundles/labels'
- add_to_breadcrumbs _("Issues"), project_issues_path(@project)
- breadcrumb_title _("New")
- page_title _("New Issue")
diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml
index 0073c6b89cd..8f6efbf9c83 100644
--- a/app/views/projects/jobs/index.html.haml
+++ b/app/views/projects/jobs/index.html.haml
@@ -3,4 +3,4 @@
- add_page_specific_style 'page_bundles/merge_request'
- admin = local_assigns.fetch(:admin, false)
-#js-jobs-table{ data: { admin: admin, full_path: @project.full_path, job_statuses: job_statuses.to_json, pipeline_editor_path: project_ci_pipeline_editor_path(@project), empty_state_svg_path: image_path('jobs-empty-state.svg') } }
+#js-jobs-table{ data: { admin: admin, full_path: @project.full_path, job_statuses: job_statuses.to_json, pipeline_editor_path: project_ci_pipeline_editor_path(@project), empty_state_svg_path: image_path('illustrations/empty-state/empty-pipeline-md.svg') } }
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 4b27b344498..03086b19984 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -3,6 +3,7 @@
- search = params[:search]
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present?
+- add_page_specific_style 'page_bundles/labels'
- if labels_or_filters
#js-promote-label-modal
diff --git a/app/views/projects/merge_requests/_awards_block.html.haml b/app/views/projects/merge_requests/_awards_block.html.haml
index c1952793e72..f657f683a6d 100644
--- a/app/views/projects/merge_requests/_awards_block.html.haml
+++ b/app/views/projects/merge_requests/_awards_block.html.haml
@@ -1,2 +1,2 @@
.emoji-block.emoji-list-container.js-noteable-awards
- = render 'award_emoji/awards_block', awardable: @merge_request, inline: true, api_awards_path: award_emoji_merge_request_api_path(@merge_request)
+ = render 'award_emoji/awards_block', awardable: @merge_request, inline: true, api_awards_path: award_emoji_merge_request_api_path(@merge_request), new_custom_emoji_path: new_custom_emoji_path(@project.group)
diff --git a/app/views/projects/merge_requests/_description.html.haml b/app/views/projects/merge_requests/_description.html.haml
index 5590f9e6184..89eed0789e8 100644
--- a/app/views/projects/merge_requests/_description.html.haml
+++ b/app/views/projects/merge_requests/_description.html.haml
@@ -1,6 +1,6 @@
%div
- if @merge_request.description.present?
- .description{ class: ['gl-mt-4!', can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''], data: { qa_selector: 'description_content' } }
+ .description{ class: ['gl-mt-4!', can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''], data: { testid: 'description-content' } }
.md
= markdown_field(@merge_request, :description)
%textarea.hidden.js-task-list-field{ data: { value: @merge_request.description } }
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 4a7aa9a86ab..21a74d30ba5 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -43,7 +43,7 @@
= link_to_label(label, type: :merge_request, small: true)
.issuable-meta
- %ul.controls.d-flex.align-items-end
+ %ul.controls.d-flex.align-items-center
- if merge_request.merged?
- merged_at = merge_request.merged_at ? l(merge_request.merged_at.to_time) : _("Merge date & time could not be determined")
%li.d-none.d-sm-flex
diff --git a/app/views/projects/merge_requests/_mr_box.html.haml b/app/views/projects/merge_requests/_mr_box.html.haml
index 1774401ed78..08b3eb4e5b6 100644
--- a/app/views/projects/merge_requests/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/_mr_box.html.haml
@@ -1,3 +1,3 @@
.detail-page-description.gl-pt-2.gl-pb-4.gl-display-flex.gl-align-items-baseline.gl-flex-wrap{ class: "#{'is-merge-request' if moved_mr_sidebar_enabled? && !fluid_layout}" }
- = render 'shared/issuable/status_box', issuable: @merge_request
+ .js-mr-header{ data: { project_path: @merge_request.project.path_with_namespace, hidden: @merge_request.hidden?.to_s, iid: @merge_request.iid, state: @merge_request.state } }
= merge_request_header(@project, @merge_request)
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index f0e7df8a379..1b0aba8d496 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -2,7 +2,7 @@
- can_reopen_merge_request = can?(current_user, :reopen_merge_request, @merge_request)
- are_close_and_open_buttons_hidden = merge_request_button_hidden?(@merge_request, true) && merge_request_button_hidden?(@merge_request, false)
- hide_gutter_toggle = local_assigns.fetch(:hide_gutter_toggle, false)
-- cache_key = [@project, @merge_request, can_update_merge_request, can_reopen_merge_request, are_close_and_open_buttons_hidden, current_user&.preferred_language, "1.1-updated_header", moved_mr_sidebar_enabled?, hide_gutter_toggle, fluid_layout]
+- cache_key = [@project, @merge_request, can_update_merge_request, can_reopen_merge_request, are_close_and_open_buttons_hidden, current_user&.preferred_language, "1.1-updated_header", moved_mr_sidebar_enabled?, hide_gutter_toggle, fluid_layout, Gitlab::CurrentSettings.gitpod_enabled, current_user&.gitpod_enabled]
= cache(cache_key, expires_in: 1.day) do
- if @merge_request.closed_or_merged_without_fork?
@@ -14,10 +14,8 @@
.detail-page-header.border-bottom-0.gl-display-block.gl-pt-5{ class: "gl-md-display-flex! #{'is-merge-request' if moved_mr_sidebar_enabled? && !fluid_layout}" }
.detail-page-header-body
- .issuable-meta.gl-display-flex
- .js-header-metadata-root{ data: { hidden: @merge_request.hidden?.to_s } }
- %h1.title.page-title.gl-font-size-h-display.gl-my-0.gl-display-inline-block{ data: { qa_selector: 'title_content' } }
- = markdown_field(@merge_request, :title)
+ %h1.title.page-title.gl-font-size-h-display.gl-my-0.gl-display-inline-block.gl-flex-grow-1{ data: { testid: 'title-content' } }
+ = markdown_field(@merge_request, :title)
- unless hide_gutter_toggle
%div
@@ -26,7 +24,7 @@
.detail-page-header-actions.gl-align-self-start.is-merge-request.js-issuable-actions.gl-display-flex
- if can_update_merge_request
- = render Pajamas::ButtonComponent.new(href: edit_project_merge_request_path(@project, @merge_request), button_options: {class: "gl-display-none gl-md-display-block js-issuable-edit", data: { qa_selector: "edit_title_button" }}) do
+ = render Pajamas::ButtonComponent.new(href: edit_project_merge_request_path(@project, @merge_request), button_options: {class: "gl-display-none gl-md-display-block js-issuable-edit", data: { testid: "edit-title-button" }}) do
= _('Edit')
- if @merge_request.source_project
diff --git a/app/views/projects/merge_requests/_page.html.haml b/app/views/projects/merge_requests/_page.html.haml
index dfb18b52021..637980bd2f8 100644
--- a/app/views/projects/merge_requests/_page.html.haml
+++ b/app/views/projects/merge_requests/_page.html.haml
@@ -50,7 +50,11 @@
#js-vue-discussion-counter{ data: { blocks_merge: @project.only_allow_merge_if_all_discussions_are_resolved?.to_s } }
- if moved_mr_sidebar_enabled?
- if !!@issuable_sidebar.dig(:current_user, :id)
- .js-sidebar-todo-widget-root{ data: { project_path: @issuable_sidebar[:project_full_path], iid: @issuable_sidebar[:iid], id: @issuable_sidebar[:id] } }
+ .gl-display-flex.gl-gap-3
+ .js-sidebar-todo-widget-root{ data: { project_path: @issuable_sidebar[:project_full_path], iid: @issuable_sidebar[:iid], id: @issuable_sidebar[:id] } }
+ - if notifications_todos_buttons_enabled?
+ .js-sidebar-subscriptions-widget-root{ data: { full_path: @issuable_sidebar[:project_full_path], iid: @issuable_sidebar[:iid] } }
+
.gl-ml-auto.gl-align-items-center.gl-display-none.gl-md-display-flex.gl-ml-3.js-expand-sidebar.gl-absolute.gl-right-5{ class: "gl-lg-display-none!" }
= render Pajamas::ButtonComponent.new(icon: 'chevron-double-lg-left',
button_options: { class: 'js-sidebar-toggle' }) do
@@ -83,7 +87,8 @@
current_user_data: @current_user_data,
is_locked: @merge_request.discussion_locked.to_s,
report_abuse_path: add_category_abuse_reports_path,
- new_comment_template_path: profile_comment_templates_path } }
+ new_comment_template_path: profile_comment_templates_path,
+ new_custom_emoji_path: new_custom_emoji_path(@project.group) } }
- if moved_mr_sidebar_enabled?
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, reviewers: @merge_request.reviewers, source_branch: @merge_request.source_branch
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index abf2949938c..954bd48fb41 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -8,7 +8,7 @@
= f.hidden_field(:redirect_path, name: :redirect_path, id: :redirect_path, value: @redirect_path)
.form-group
= f.label :title, _('Title')
- = f.text_field :title, maxlength: 255, class: 'form-control gl-form-input', data: { qa_selector: 'milestone_title_field' }, required: true, autofocus: true
+ = f.text_field :title, maxlength: 255, class: 'form-control gl-form-input', data: { testid: 'milestone-title-field' }, required: true, autofocus: true
= render 'shared/milestones/form_dates', f: f
.form-group
= f.label :description, _('Description')
@@ -28,7 +28,7 @@
= f.hidden_field :lock_version
- if @milestone.new_record?
- = f.submit _('Create milestone'), data: { qa_selector: 'create_milestone_button' }, class: 'gl-mr-2', pajamas_button: true
+ = f.submit _('Create milestone'), data: { testid: 'create-milestone-button' }, class: 'gl-mr-2', pajamas_button: true
= link_button_to _('Cancel'), project_milestones_path(@project)
- else
= f.submit _('Save changes'), class: 'gl-mr-2', pajamas_button: true
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index a7a21ef0440..a6c49d8000d 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -9,14 +9,14 @@
= render 'shared/milestones/search_form'
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @project)
- = link_button_to new_project_milestone_path(@project), class: 'gl-ml-3', data: { qa_selector: "new_project_milestone_link" }, title: _('New milestone'), variant: :confirm do
+ = link_button_to new_project_milestone_path(@project), class: 'gl-ml-3', data: { testid: "new-project-milestone-link" }, title: _('New milestone'), variant: :confirm do
= _('New milestone')
- if @milestones.blank?
= render 'shared/empty_states/milestones_tab' do
- if can?(current_user, :admin_milestone, @project)
.text-center
- = link_button_to new_project_milestone_path(@project), data: { qa_selector: "new_project_milestone_link" }, title: _('New milestone'), variant: :confirm do
+ = link_button_to new_project_milestone_path(@project), data: { testid: "new-project-milestone-link" }, title: _('New milestone'), variant: :confirm do
= _('New milestone')
- else
@@ -32,5 +32,5 @@
= render 'shared/empty_states/milestones' do
- if can?(current_user, :admin_milestone, @project)
.text-center
- = link_button_to new_project_milestone_path(@project), data: { qa_selector: "new_project_milestone_link" }, title: _('New milestone'), variant: :confirm do
+ = link_button_to new_project_milestone_path(@project), data: { testid: "new-project-milestone-link" }, title: _('New milestone'), variant: :confirm do
= _('New milestone')
diff --git a/app/views/projects/ml/models/index.html.haml b/app/views/projects/ml/models/index.html.haml
index a1c6376e9b4..08f0db257ae 100644
--- a/app/views/projects/ml/models/index.html.haml
+++ b/app/views/projects/ml/models/index.html.haml
@@ -1,4 +1,4 @@
- breadcrumb_title s_('ModelRegistry|Model registry')
- page_title s_('ModelRegistry|Model registry')
-= render(Projects::Ml::ModelsIndexComponent.new(models: @models))
+= render(Projects::Ml::ModelsIndexComponent.new(paginator: @paginator))
diff --git a/app/views/projects/ml/models/show.html.haml b/app/views/projects/ml/models/show.html.haml
new file mode 100644
index 00000000000..be611e55304
--- /dev/null
+++ b/app/views/projects/ml/models/show.html.haml
@@ -0,0 +1,5 @@
+- add_to_breadcrumbs s_('ModelRegistry|Model registry'), project_ml_models_path(@model.project)
+- breadcrumb_title @model.name
+- page_title @model.name
+
+= render(Projects::Ml::ShowMlModelComponent.new(model: @model))
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index bf288d3601b..40acd123aaa 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -3,6 +3,7 @@
- page_title _('New Project')
- header_title _("Projects"), dashboard_projects_path
- add_page_specific_style 'page_bundles/new_namespace'
+- add_page_specific_style 'page_bundles/projects'
.project-edit-container
.project-edit-errors
diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml
index 6b875ff904c..68b7bcd5bb5 100644
--- a/app/views/projects/notes/_actions.html.haml
+++ b/app/views/projects/notes/_actions.html.haml
@@ -19,6 +19,6 @@
.note-actions-item.gl-ml-0
= render Pajamas::ButtonComponent.new(category: :tertiary,
icon: 'pencil',
- button_options: { class: 'note-action-button js-note-edit has-tooltip', data: { container: 'body', qa_selector: 'edit_comment_button' }, title: _('Edit comment'), 'aria-label': _('Edit comment') })
+ button_options: { class: 'note-action-button js-note-edit has-tooltip', data: { container: 'body', testid: 'edit-comment-button' }, title: _('Edit comment'), 'aria-label': _('Edit comment') })
= render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml
index 54d1bf012f3..b64824bf509 100644
--- a/app/views/projects/notes/_more_actions_dropdown.html.haml
+++ b/app/views/projects/notes/_more_actions_dropdown.html.haml
@@ -2,7 +2,7 @@
- if note_editable || !is_current_user
%div{ class: "dropdown more-actions note-actions-item gl-ml-0!" }
- = render Pajamas::ButtonComponent.new(icon: 'ellipsis_v', category: :tertiary, button_options: { class: 'note-action-button more-actions-toggle has-tooltip', data: { title: 'More actions', toggle: 'dropdown', container: 'body', qa_selector: 'more_actions_dropdown' }})
+ = render Pajamas::ButtonComponent.new(icon: 'ellipsis_v', category: :tertiary, button_options: { class: 'note-action-button more-actions-toggle has-tooltip', data: { title: 'More actions', toggle: 'dropdown', container: 'body', testid: 'more-actions-dropdown' }})
%ul.dropdown-menu.more-actions-dropdown.dropdown-open-left
%li
= deprecated_clipboard_button(text: noteable_note_url(note), title: _('Copy reference'), button_text: _('Copy link'), class: 'btn-clipboard', hide_tooltip: true, hide_button_icon: true)
@@ -11,6 +11,6 @@
.js-report-abuse-dropdown-item{ data: { report_abuse_path: add_category_abuse_reports_path, reported_user_id: note.author.id, reported_from_url: noteable_note_url(note) } }
- if note_editable
%li
- = link_to note_url(note), method: :delete, data: { confirm: _('Are you sure you want to delete this comment?'), confirm_btn_variant: 'danger', qa_selector: 'delete_comment_button' }, aria: { label: _('Delete comment') }, remote: true, class: 'js-note-delete' do
+ = link_to note_url(note), method: :delete, data: { confirm: _('Are you sure you want to delete this comment?'), confirm_btn_variant: 'danger', testid: 'delete-comment-button' }, aria: { label: _('Delete comment') }, remote: true, class: 'js-note-delete' do
%span.text-danger
= _('Delete comment')
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 210f9c35c79..d47de725603 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -4,7 +4,6 @@
%h1.page-title.gl-font-size-h-display
= s_('Pipeline|Run pipeline')
-%hr
#js-new-pipeline{ data: { project_id: @project.id,
pipelines_path: project_pipelines_path(@project),
diff --git a/app/views/projects/settings/_archive.html.haml b/app/views/projects/settings/_archive.html.haml
index e7da3177cde..dcb37541a04 100644
--- a/app/views/projects/settings/_archive.html.haml
+++ b/app/views/projects/settings/_archive.html.haml
@@ -13,10 +13,10 @@
- if @project.archived?
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'unarchive-a-project') }
%p= _("Unarchiving the project restores its members' ability to make commits, and create issues, comments, and other entities. %{strong_start}After you unarchive the project, it displays in the search and on the dashboard.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe }
- = render Pajamas::ButtonComponent.new(method: :post, href: unarchive_project_path(@project), variant: :confirm, button_options: { aria: { label: _('Unarchive project') }, data: { confirm: _("Are you sure that you want to unarchive this project?"), qa_selector: 'unarchive_project_link' } }) do
+ = render Pajamas::ButtonComponent.new(method: :post, href: unarchive_project_path(@project), variant: :confirm, button_options: { aria: { label: _('Unarchive project') }, data: { confirm: _("Are you sure that you want to unarchive this project?"), testid: 'unarchive-project-link' } }) do
= _('Unarchive project')
- else
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'archive-a-project') }
%p= _("Archiving the project makes it entirely read-only. It is hidden from the dashboard and doesn't display in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe }
- = render Pajamas::ButtonComponent.new(method: :post, href: archive_project_path(@project), variant: :confirm, button_options: { aria: { label: _('Archive project') }, data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link', 'confirm-btn-variant': 'confirm' } }) do
+ = render Pajamas::ButtonComponent.new(method: :post, href: archive_project_path(@project), variant: :confirm, button_options: { aria: { label: _('Archive project') }, data: { confirm: _("Are you sure that you want to archive this project?"), testid: 'archive-project-link', 'confirm-btn-variant': 'confirm' } }) do
= _('Archive project')
diff --git a/app/views/projects/settings/_general.html.haml b/app/views/projects/settings/_general.html.haml
index f5c275827fc..47ea2f1a544 100644
--- a/app/views/projects/settings/_general.html.haml
+++ b/app/views/projects/settings/_general.html.haml
@@ -7,7 +7,7 @@
.form-group.col-md-5
= f.label :name, class: 'label-bold', for: 'project_name_edit' do
= _('Project name')
- = f.text_field :name, class: 'form-control gl-form-input', id: "project_name_edit", data: { qa_selector: 'project_name_field' }
+ = f.text_field :name, class: 'form-control gl-form-input', id: "project_name_edit", data: { testid: 'project-name-field' }
.form-group.col-md-7
= f.label :id, class: 'label-bold' do
@@ -37,4 +37,4 @@
%hr
= link_button_to _('Remove avatar'), project_avatar_path(@project), aria: { label: _('Remove avatar') }, data: { confirm: _('Avatar will be removed. Are you sure?'), 'confirm-btn-variant': 'danger' }, method: :delete, variant: :danger, category: :secondary
- = f.submit _('Save changes'), pajamas_button: true, class: "gl-mt-6", data: { qa_selector: 'save_naming_topics_avatar_button' }
+ = f.submit _('Save changes'), pajamas_button: true, class: "gl-mt-6", data: { testid: 'save-naming-topics-avatar-button' }
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index 46cfcf20535..fd27b125602 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -20,7 +20,7 @@
%fieldset.builds-feature.js-auto-devops-settings
.form-group
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
- = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-3' }, footer_options: { class: auto_devops_enabled || 'hidden' }) do |c|
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-3' }, footer_options: { class: "js-extra-settings #{auto_devops_enabled || 'hidden'}", data: { testid: 'extra-auto-devops-settings' } }) do |c|
- c.with_body do
- autodevops_help_link = link_to _('Learn more.'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer'
- auto_devops_badge = auto_devops_enabled ? (gl_badge_tag badge_for_auto_devops_scope(@project), { variant: :info }, { class: 'js-instance-default-badge gl-ml-3 gl-mt-n1'}) : ''
diff --git a/app/views/projects/settings/integrations/_form.html.haml b/app/views/projects/settings/integrations/_form.html.haml
index d2df01c22bb..6f37eb1b32d 100644
--- a/app/views/projects/settings/integrations/_form.html.haml
+++ b/app/views/projects/settings/integrations/_form.html.haml
@@ -14,13 +14,14 @@
- if integration.to_param === 'slack'
= render 'shared/integrations/slack_notifications_deprecation_alert'
-%h2.gl-mb-0.gl-display-flex.gl-align-items-center.gl-gap-3
+.gl-display-flex.gl-align-items-center.gl-gap-3
= render Pajamas::AvatarComponent.new(integration, size: 64, alt: '')
- = integration.title
- - if integration.operating?
- = render Pajamas::BadgeComponent.new(_('Active'), variant: 'success', icon: 'status-success')
- - elsif integration.persisted?
- = render Pajamas::BadgeComponent.new(_('Inactive'), variant: 'neutral', icon: 'status-paused')
+ %h2.gl-m-0
+ = integration.title
+ - if integration.operating?
+ = render Pajamas::BadgeComponent.new(_('Active'), variant: 'success', icon: 'status-success')
+ - elsif integration.persisted?
+ = render Pajamas::BadgeComponent.new(_('Inactive'), variant: 'neutral', icon: 'status-paused')
= render 'shared/integration_settings', integration: integration
- if lookup_context.template_exists?('show', "shared/integrations/#{integration.to_param}", true)
diff --git a/app/views/projects/settings/merge_requests/show.html.haml b/app/views/projects/settings/merge_requests/show.html.haml
index 01af028f30c..e877be704a2 100644
--- a/app/views/projects/settings/merge_requests/show.html.haml
+++ b/app/views/projects/settings/merge_requests/show.html.haml
@@ -2,7 +2,7 @@
- page_title _('Merge requests')
- @force_desktop_expanded_sidebar = true
-%section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings.expanded{ class: [('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { qa_selector: 'merge_request_settings_content' } }
+%section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings.expanded{ class: [('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { testid: 'merge-request-settings-content' } }
.settings-header
%h4= _('Merge requests')
= render_if_exists 'projects/settings/merge_requests/merge_request_settings_description_text'
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 58e86ebffa0..7ff798d7324 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -6,6 +6,6 @@
#js-snippet-view{ data: { 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet), 'can-report-spam': @snippet.submittable_as_spam_by?(current_user).to_s } }
.gl-px-0.gl-py-2
- = render 'award_emoji/awards_block', awardable: @snippet, inline: true, api_awards_path: project_snippets_award_api_path(@snippet)
+ = render 'award_emoji/awards_block', awardable: @snippet, inline: true, api_awards_path: project_snippets_award_api_path(@snippet), new_custom_emoji_path: new_custom_emoji_path(@project.group)
#notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => true
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index a4ed19c2fc9..37f27aa7caf 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -1,5 +1,7 @@
+- add_page_specific_style 'page_bundles/projects'
+
.tree-ref-container.gl-display-flex.gl-flex-wrap.gl-gap-2.mb-2.mb-md-0
- .tree-ref-holder.gl-max-w-26{ data: { qa_selector: 'ref_dropdown_container' } }
+ .tree-ref-holder.gl-max-w-26{ data: { testid: 'ref-dropdown-container' } }
#js-tree-ref-switcher{ data: { project_id: @project.id, ref_type: @ref_type.to_s, project_root_path: project_path(@project) } }
#js-repo-breadcrumb{ data: breadcrumb_data_attributes }
@@ -10,6 +12,7 @@
= render_if_exists 'projects/tree/lock_link'
#js-tree-history-link{ data: { history_link: project_commits_path(@project, @ref) } }
+ = render 'projects/buttons/compare', project: @project, ref: @ref, root_ref: @repository&.root_ref
= render 'projects/find_file_link'
= render 'shared/web_ide_button', blob: nil
= render 'projects/buttons/download', project: @project, ref: @ref
diff --git a/app/views/projects/work_items/index.html.haml b/app/views/projects/work_items/show.html.haml
index 01b27eed267..7e0bddf1b5d 100644
--- a/app/views/projects/work_items/index.html.haml
+++ b/app/views/projects/work_items/show.html.haml
@@ -1,4 +1,4 @@
-- page_title "##{request.params['work_items_path']}"
+- page_title "##{request.params['iid']}"
- add_to_breadcrumbs _("Issues"), project_issues_path(@project)
- add_page_specific_style 'page_bundles/work_items'
- @gfm_form = true
diff --git a/app/views/protected_branches/shared/_dropdown.html.haml b/app/views/protected_branches/shared/_dropdown.html.haml
index c5dbf8991cd..678c2a1631d 100644
--- a/app/views/protected_branches/shared/_dropdown.html.haml
+++ b/app/views/protected_branches/shared/_dropdown.html.haml
@@ -6,13 +6,13 @@
options: { toggle_class: "js-protected-branch-select js-filter-submit wide monospace #{toggle_classes}",
filter: true,
dropdown_class: "dropdown-menu-selectable git-revision-dropdown",
- dropdown_qa_selector: "protected_branch_dropdown_content",
+ dropdown_testid: "protected-branch-dropdown-content",
placeholder: _("Search protected branches"),
footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_branch_name],
project_id: @project.try(:id),
- qa_selector: "protected_branch_dropdown" } }) do
+ testid: "protected-branch-dropdown" } }) do
%ul.dropdown-footer-list
%li
diff --git a/app/views/registrations/welcome/show.html.haml b/app/views/registrations/welcome/show.html.haml
deleted file mode 100644
index 0f161855cdb..00000000000
--- a/app/views/registrations/welcome/show.html.haml
+++ /dev/null
@@ -1,43 +0,0 @@
-- @html_class = "subscriptions-layout-html"
-- page_title _('Your profile')
-- add_page_specific_style 'page_bundles/signup'
-- add_page_specific_style 'page_bundles/login'
-- gitlab_experience_text = _('To personalize your GitLab experience, we\'d like to know a bit more about you')
-- content_for :page_specific_javascripts do
- = render "layouts/google_tag_manager_head"
- = render "layouts/one_trust"
- = render "layouts/bizible"
-= render "layouts/google_tag_manager_body"
-
-.row.gl-flex-grow-1
- .d-flex.gl-flex-direction-column.gl-align-items-center.gl-w-full.gl-px-5.gl-pb-5
- .edit-profile.login-page.d-flex.flex-column.gl-align-items-center
- %h2.gl-text-center= html_escape(_('Welcome to GitLab,%{br_tag}%{name}!')) % { name: html_escape(current_user.first_name), br_tag: '<br/>'.html_safe }
- - if Gitlab.com?
- %p.gl-text-center= html_escape(_('%{gitlab_experience_text}. We won\'t share this information with anyone.')) % { gitlab_experience_text: gitlab_experience_text }
- - else
- %p.gl-text-center= html_escape(_('%{gitlab_experience_text}. Don\'t worry, this information isn\'t shared outside of your self-managed GitLab instance.')) % { gitlab_experience_text: gitlab_experience_text }
- = gitlab_ui_form_for(current_user,
- url: users_sign_up_welcome_path(welcome_update_params),
- html: { class: 'gl-w-full! gl-p-5 js-users-signup-welcome',
- 'aria-live' => 'assertive',
- data: { testid: 'welcome-form' } }) do |f|
- = render Pajamas::CardComponent.new do |c|
- - c.with_body do
- .devise-errors
- = render 'devise/shared/error_messages', resource: current_user
- .row
- .form-group.col-sm-12
- = f.label :role, _('Role'), class: 'label-bold'
- = f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, { include_blank: _('Select a role') }, class: 'form-control js-user-role-dropdown', required: true, data: { qa_selector: 'role_dropdown' }
- = render_if_exists "registrations/welcome/jobs_to_be_done", f: f
- = render_if_exists "registrations/welcome/setup_for_company", f: f
- = render_if_exists "registrations/welcome/joining_project"
- = render_if_exists "registrations/welcome/opt_in_to_email"
- .row
- .form-group.col-sm-12.gl-mb-0
- - if partial_exists? "registrations/welcome/button"
- = render "registrations/welcome/button"
- - else
- = render Pajamas::ButtonComponent.new(block: true, type: :submit, variant: :confirm, button_options: { class: 'gl-mb-0', data: { qa_selector: 'get_started_button' }}) do
- = _('Get started!')
diff --git a/app/views/search/results/_error.html.haml b/app/views/search/results/_error.html.haml
index f0d9283c620..33e74b08dde 100644
--- a/app/views/search/results/_error.html.haml
+++ b/app/views/search/results/_error.html.haml
@@ -1,7 +1,7 @@
.gl-display-flex.gl-flex-direction-column.gl-align-items-center
%div
.svg-content.svg-150
- = image_tag 'illustrations/search-timeout-md.svg'
+ = image_tag 'illustrations/empty-state/empty-search-md.svg'
%div
%h4.gl-text-center.gl-font-weight-bold= s_('SearchError|A search query problem has occurred')
%p.gl-text-center= s_('SearchError|To resolve the problem, check the query syntax and try again.')
diff --git a/app/views/search/results/_timeout.html.haml b/app/views/search/results/_timeout.html.haml
index 740e2bedd54..530096ead43 100644
--- a/app/views/search/results/_timeout.html.haml
+++ b/app/views/search/results/_timeout.html.haml
@@ -1,7 +1,7 @@
.gl-display-flex.gl-flex-direction-column.gl-align-items-center
%div
.svg-content.svg-150
- = image_tag 'illustrations/search-timeout-md.svg'
+ = image_tag 'illustrations/empty-state/empty-search-md.svg'
%div
%h4.gl-text-center.gl-font-weight-bold= _('Your search timed out')
%p.gl-text-center= _('To resolve this, try to:')
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index 2fd6e4a5ca5..9c1f4c8643f 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -21,7 +21,7 @@
%h1.page-title.gl-font-size-h-display.gl-mr-5= _('Search')
= render_if_exists 'search/form_elasticsearch', attrs: { class: 'mb-2 mb-sm-0 align-self-center' }
-#js-search-topbar{ data: { "group-initial-json": group_attributes.to_json, "project-initial-json": project_attributes.to_json, "elasticsearch-enabled": @search_service_presenter.advanced_search_enabled?.to_s, "default-branch-name": @project&.default_branch } }
+#js-search-topbar{ data: { "group-initial-json": group_attributes.to_json, "project-initial-json": project_attributes.to_json, "default-branch-name": @project&.default_branch } }
.results.gl-lg-display-flex.gl-mt-0
#js-search-sidebar{ class: search_bar_classes, data: { navigation_json: search_navigation_json, search_type: search_service.search_type } }
- if @search_term
diff --git a/app/views/shared/_broadcast_message.html.haml b/app/views/shared/_broadcast_message.html.haml
index 2f470d5ef53..12571ef5b73 100644
--- a/app/views/shared/_broadcast_message.html.haml
+++ b/app/views/shared/_broadcast_message.html.haml
@@ -24,7 +24,7 @@
- else
- notification_class = "js-broadcast-notification-#{message.id}"
- notification_class << ' preview' if preview
- .gl-broadcast-message.broadcast-notification-message.gl-mt-3{ role: "alert", class: notification_class, data: { qa_selector: 'broadcast_notification_container' } }
+ .gl-broadcast-message.broadcast-notification-message.gl-mt-3{ role: "alert", class: notification_class, data: { testid: 'broadcast-notification-container' } }
.gl-broadcast-message-content
.gl-broadcast-message-icon
= sprite_icon(icon_name, css_class: 'vertical-align-text-top')
@@ -38,4 +38,4 @@
= render Pajamas::ButtonComponent.new(category: :tertiary,
icon: 'close',
size: :small,
- button_options: { class: 'js-dismiss-current-broadcast-notification', 'aria-label': _('Close'), data: { id: message.id, expire_date: message.ends_at.iso8601, qa_selector: 'close_button' } })
+ button_options: { class: 'js-dismiss-current-broadcast-notification', 'aria-label': _('Close'), data: { id: message.id, expire_date: message.ends_at.iso8601, testid: 'close-button' } })
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index dde4ec3cf52..4b39ec52837 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -5,18 +5,18 @@
%span.js-clone-dropdown-label
= enabled_protocol_button(container, enabled_protocol)
- else
- %a#clone-dropdown.input-group-text.gl-button.btn.btn-default.btn-icon.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown', qa_selector: 'clone_dropdown' } }
+ %a#clone-dropdown.input-group-text.gl-button.btn.btn-default.btn-icon.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown', testid: 'clone-dropdown' } }
%span.js-clone-dropdown-label
= default_clone_protocol.upcase
= sprite_icon('chevron-down', css_class: 'gl-icon')
- %ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown{ data: { qa_selector: 'clone_dropdown_content' } }
+ %ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown{ data: { testid: 'clone-dropdown-content' } }
%li
= ssh_clone_button(container)
%li
= http_clone_button(container)
= render_if_exists 'shared/kerberos_clone_button', container: container
- = text_field_tag :clone_url, default_url_to_repo(container), class: "js-select-on-focus btn gl-button", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'clone_url_content' }
+ = text_field_tag :clone_url, default_url_to_repo(container), class: "js-select-on-focus btn gl-button", readonly: true, aria: { label: _('Repository clone URL') }, data: { testid: 'clone-url-content' }
.input-group-append
= clipboard_button(target: '#clone_url', title: _("Copy URL"), variant: :default, category: :primary, size: :medium)
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index bb21c4a28fd..ceacd5c48cd 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -6,7 +6,7 @@
- toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user
- tooltip_title = label_status_tooltip(label, status) if status
-%li.label-list-item.gl-list-style-none{ id: label_css_id, data: { id: label.id } }
+%li.js-label-list-item.gl-list-style-none.gl-border-b.gl-last-of-type-border-b-0{ id: label_css_id, data: { id: label.id } }
.label-content.gl-pl-5.gl-pr-3.gl-py-4.gl-rounded-base{ class: "#{ 'gl-py-3' if force_priority }" }
= render "shared/label_row", label: label, force_priority: force_priority
%ul.label-actions-list
diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg
index dfc35856366..df65de87fb9 100644
--- a/app/views/shared/_logo.svg
+++ b/app/views/shared/_logo.svg
@@ -1,4 +1,4 @@
-<svg role="img" class="tanuki-logo" width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<svg aria-hidden="true" role="img" class="tanuki-logo" width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path class="tanuki-shape tanuki" d="m24.507 9.5-.034-.09L21.082.562a.896.896 0 0 0-1.694.091l-2.29 7.01H7.825L5.535.653a.898.898 0 0 0-1.694-.09L.451 9.411.416 9.5a6.297 6.297 0 0 0 2.09 7.278l.012.01.03.022 5.16 3.867 2.56 1.935 1.554 1.176a1.051 1.051 0 0 0 1.268 0l1.555-1.176 2.56-1.935 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5Z"
fill="#E24329"/>
<path class="tanuki-shape right-cheek" d="m24.507 9.5-.034-.09a11.44 11.44 0 0 0-4.56 2.051l-7.447 5.632 4.742 3.584 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5Z"
diff --git a/app/views/shared/_logo_with_black_text.svg b/app/views/shared/_logo_with_black_text.svg
index f5b0b70618b..bf59618bade 100644
--- a/app/views/shared/_logo_with_black_text.svg
+++ b/app/views/shared/_logo_with_black_text.svg
@@ -1,4 +1,4 @@
-<svg class="tanuki-logo" width="111" height="24" viewBox="0 0 111 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<svg aria-hidden="true" class="tanuki-logo" width="111" height="24" viewBox="0 0 111 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path class="logo-text" d="M44.814 9.042h3.645c-.608-3.875-3.963-6.574-8.33-6.574-5.166 0-9.043 3.798-9.043 10.16 0 6.248 3.703 10.123 9.15 10.123 4.887 0 8.386-3.144 8.386-8.234v-2.37h-8.01v2.794h4.55c-.058 2.816-1.938 4.599-4.908 4.599-3.305 0-5.57-2.477-5.57-6.95 0-4.445 2.303-6.913 5.494-6.913 2.38 0 4.01 1.272 4.636 3.365Zm6.218 13.438h3.49V7.68h-3.49v14.8Zm1.76-17.151c1.109 0 2.014-.85 2.014-1.89s-.905-1.9-2.014-1.9c-1.109 0-2.024.849-2.024 1.9s.9 1.89 2.017 1.89h.007ZM64.971 7.68H62.05V4.126h-3.49v3.556h-2.1v2.699h2.1v8.233c-.018 2.786 2.007 4.16 4.628 4.079a7.089 7.089 0 0 0 2.055-.348l-.59-2.73a4.247 4.247 0 0 1-1.02.137c-.878 0-1.582-.309-1.582-1.717v-7.662h2.921V7.68Zm2.701 14.8h12.272v-2.998H71.25V2.737h-3.578V22.48Zm18.957.3c2.323 0 3.71-1.09 4.347-2.333h.115v2.033h3.36v-9.91c0-3.913-3.19-5.09-6.016-5.09-3.113 0-5.504 1.388-6.275 4.087l3.26.464c.345-1.013 1.329-1.88 3.04-1.88 1.62 0 2.506.829 2.506 2.285v.057c0 1.002-1.05 1.051-3.664 1.33-2.872.309-5.619 1.166-5.619 4.502-.01 2.912 2.12 4.455 4.946 4.455Zm1.147-2.56c-1.456 0-2.498-.666-2.498-1.948 0-1.34 1.167-1.899 2.72-2.121.917-.125 2.75-.357 3.2-.722v1.744c.01 1.643-1.321 3.042-3.422 3.042v.005Zm9.244 2.26h3.433v-2.332h.201c.551 1.08 1.698 2.593 4.244 2.593 3.489 0 6.102-2.768 6.102-7.644 0-4.936-2.69-7.616-6.112-7.616-2.613 0-3.702 1.57-4.234 2.641h-.147V2.737h-3.486V22.48Zm3.423-7.403c0-2.88 1.234-4.734 3.48-4.734 2.323 0 3.52 1.976 3.52 4.734 0 2.759-1.214 4.8-3.52 4.8-2.227 0-3.48-1.928-3.48-4.8Z"
fill="#171321"/>
<path class="tanuki-shape tanuki" d="m24.507 9.5-.034-.09L21.082.562a.896.896 0 0 0-1.694.091l-2.29 7.01H7.825L5.535.653a.898.898 0 0 0-1.694-.09L.451 9.411.416 9.5a6.297 6.297 0 0 0 2.09 7.278l.012.01.03.022 5.16 3.867 2.56 1.935 1.554 1.176a1.051 1.051 0 0 0 1.268 0l1.555-1.176 2.56-1.935 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5Z"
diff --git a/app/views/shared/_logo_with_white_text.svg b/app/views/shared/_logo_with_white_text.svg
index d0067538058..b47c7c55d59 100644
--- a/app/views/shared/_logo_with_white_text.svg
+++ b/app/views/shared/_logo_with_white_text.svg
@@ -1,4 +1,4 @@
-<svg class="tanuki-logo" width="111" height="24" viewBox="0 0 111 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<svg aria-hidden="true" class="tanuki-logo" width="111" height="24" viewBox="0 0 111 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path class="logo-text" d="M44.814 9.042h3.645c-.608-3.875-3.963-6.574-8.33-6.574-5.166 0-9.043 3.798-9.043 10.16 0 6.248 3.703 10.123 9.15 10.123 4.887 0 8.386-3.144 8.386-8.234v-2.37h-8.01v2.794h4.55c-.058 2.816-1.938 4.599-4.908 4.599-3.305 0-5.57-2.477-5.57-6.95 0-4.445 2.303-6.913 5.494-6.913 2.38 0 4.01 1.272 4.636 3.365Zm6.218 13.438h3.49V7.68h-3.49v14.8Zm1.76-17.151c1.109 0 2.014-.85 2.014-1.89s-.905-1.9-2.014-1.9c-1.109 0-2.024.849-2.024 1.9s.9 1.89 2.017 1.89h.007ZM64.971 7.68H62.05V4.126h-3.49v3.556h-2.1v2.699h2.1v8.233c-.018 2.786 2.007 4.16 4.628 4.079a7.089 7.089 0 0 0 2.055-.348l-.59-2.73a4.247 4.247 0 0 1-1.02.137c-.878 0-1.582-.309-1.582-1.717v-7.662h2.921V7.68Zm2.701 14.8h12.272v-2.998H71.25V2.737h-3.578V22.48Zm18.957.3c2.323 0 3.71-1.09 4.347-2.333h.115v2.033h3.36v-9.91c0-3.913-3.19-5.09-6.016-5.09-3.113 0-5.504 1.388-6.275 4.087l3.26.464c.345-1.013 1.329-1.88 3.04-1.88 1.62 0 2.506.829 2.506 2.285v.057c0 1.002-1.05 1.051-3.664 1.33-2.872.309-5.619 1.166-5.619 4.502-.01 2.912 2.12 4.455 4.946 4.455Zm1.147-2.56c-1.456 0-2.498-.666-2.498-1.948 0-1.34 1.167-1.899 2.72-2.121.917-.125 2.75-.357 3.2-.722v1.744c.01 1.643-1.321 3.042-3.422 3.042v.005Zm9.244 2.26h3.433v-2.332h.201c.551 1.08 1.698 2.593 4.244 2.593 3.489 0 6.102-2.768 6.102-7.644 0-4.936-2.69-7.616-6.112-7.616-2.613 0-3.702 1.57-4.234 2.641h-.147V2.737h-3.486V22.48Zm3.423-7.403c0-2.88 1.234-4.734 3.48-4.734 2.323 0 3.52 1.976 3.52 4.734 0 2.759-1.214 4.8-3.52 4.8-2.227 0-3.48-1.928-3.48-4.8Z"
fill="#fff"/>
<path class="tanuki-shape tanuki" d="m24.507 9.5-.034-.09L21.082.562a.896.896 0 0 0-1.694.091l-2.29 7.01H7.825L5.535.653a.898.898 0 0 0-1.694-.09L.451 9.411.416 9.5a6.297 6.297 0 0 0 2.09 7.278l.012.01.03.022 5.16 3.867 2.56 1.935 1.554 1.176a1.051 1.051 0 0 0 1.268 0l1.555-1.176 2.56-1.935 5.197-3.89.014-.01A6.297 6.297 0 0 0 24.507 9.5Z"
diff --git a/app/views/shared/_zen.html.haml b/app/views/shared/_zen.html.haml
index 05bee9e4d42..039db4eec59 100644
--- a/app/views/shared/_zen.html.haml
+++ b/app/views/shared/_zen.html.haml
@@ -2,7 +2,7 @@
- current_text ||= nil
- supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true)
- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false)
-- qa_selector = local_assigns.fetch(:qa_selector, '')
+- testid = local_assigns.fetch(:testid, '')
- autofocus = local_assigns.fetch(:autofocus, false)
.zen-backdrop
@@ -14,9 +14,9 @@
dir: 'auto',
data: { supports_quick_actions: supports_quick_actions,
supports_autocomplete: supports_autocomplete,
- qa_selector: qa_selector,
+ testid: testid,
autofocus: autofocus }
- else
- = text_area_tag attr, current_text, data: { qa_selector: qa_selector }, class: classes, placeholder: placeholder
+ = text_area_tag attr, current_text, data: { testid: testid }, class: classes, placeholder: placeholder
%a.zen-control.zen-control-leave.js-zen-leave.gl-text-gray-500{ href: "#" }
= sprite_icon('minimize')
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index e5aa4c58da1..882730f536d 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -15,5 +15,6 @@
- page_title("#{board.name}", _("Boards"))
- add_page_specific_style 'page_bundles/boards'
+- add_page_specific_style 'page_bundles/labels'
#js-issuable-board-app{ data: board_data }
diff --git a/app/views/shared/doorkeeper/applications/_show.html.haml b/app/views/shared/doorkeeper/applications/_show.html.haml
index d309a335166..9bf29f3ce64 100644
--- a/app/views/shared/doorkeeper/applications/_show.html.haml
+++ b/app/views/shared/doorkeeper/applications/_show.html.haml
@@ -8,7 +8,7 @@
%td
.clipboard-group
.input-group
- %input.label.label-monospace.monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true, data: { qa_selector: 'application_id_field' } }
+ %input.label.label-monospace.monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true, data: { testid: 'application-id-field' } }
.input-group-append
= clipboard_button(target: "#application_id", title: _("Copy ID"), category: :primary, size: :medium)
%tr
@@ -48,4 +48,4 @@
= render 'shared/doorkeeper/applications/delete_form', path: delete_path
-# Create a hidden field to save the ID of application created
-= hidden_field_tag(:id_of_application, @application.id, data: { qa_selector: 'id_of_application_field' })
+= hidden_field_tag(:id_of_application, @application.id, data: { testid: 'id-of-application-field' })
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
index 4d2127c0161..9dd9cdcd051 100644
--- a/app/views/shared/empty_states/_labels.html.haml
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -1,6 +1,6 @@
.row.empty-state.labels
.col-12
- .svg-content.svg-150{ data: { qa_selector: 'label_svg_content' } }
+ .svg-content.svg-150{ data: { testid: 'label-svg-content' } }
= image_tag 'illustrations/empty-state/empty-labels-md.svg'
.col-12
.text-content
diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml
index 5b377818c6e..5a96b51be61 100644
--- a/app/views/shared/empty_states/_merge_requests.html.haml
+++ b/app/views/shared/empty_states/_merge_requests.html.haml
@@ -5,7 +5,7 @@
- is_closed_state = params[:state] == 'closed'
- can_create_merge_request = merge_request_source_project_for_project(@project)
-.row.empty-state.merge-requests
+.row.empty-state.merge-requests{ data: { testid: 'issuable-empty-state' } }
.col-12
.svg-content.svg-150
= image_tag 'illustrations/empty-state/empty-merge-requests-md.svg', { auto_dark: true }
@@ -37,4 +37,4 @@
= _("Interested parties can even contribute by pushing commits if they want to.")
- if button_path
.text-center
- = link_button_to _('New merge request'), button_path, title: _('New merge request'), id: 'new_merge_request_link', data: { qa_selector: "new_merge_request_button" }, variant: :confirm
+ = link_button_to _('New merge request'), button_path, title: _('New merge request'), id: 'new_merge_request_link', data: { testid: "new-merge-request-button" }, variant: :confirm
diff --git a/app/views/shared/empty_states/_priority_labels.html.haml b/app/views/shared/empty_states/_priority_labels.html.haml
index 688df1705aa..ad9dee10205 100644
--- a/app/views/shared/empty_states/_priority_labels.html.haml
+++ b/app/views/shared/empty_states/_priority_labels.html.haml
@@ -1,5 +1,5 @@
.text-center.gl-mt-1.gl-mb-5
- .svg-content{ data: { qa_selector: 'label_svg_content' } }
+ .svg-content{ data: { testid: 'label-svg-content' } }
= image_tag 'illustrations/empty-state/empty-labels-starred-md.svg'
- if can?(current_user, :admin_label, @project)
%h5.gl-my-0
diff --git a/app/views/shared/empty_states/_snippets.html.haml b/app/views/shared/empty_states/_snippets.html.haml
index 6fe36d75453..a2457fb0810 100644
--- a/app/views/shared/empty_states/_snippets.html.haml
+++ b/app/views/shared/empty_states/_snippets.html.haml
@@ -2,7 +2,7 @@
.row.empty-state
.col-12
- .svg-content.svg-150{ data: { qa_selector: 'svg_content' } }
+ .svg-content.svg-150{ data: { testid: 'svg-content' } }
= image_tag 'illustrations/empty-state/empty-snippets-md.svg'
.text-content.gl-text-center.gl-pt-0
- if current_user
@@ -12,7 +12,7 @@
= s_('SnippetsEmptyState|Store, share, and embed small pieces of code and text.')
.gl-mt-3<
- if button_path
- = link_button_to s_('SnippetsEmptyState|New snippet'), button_path, title: s_('SnippetsEmptyState|New snippet'), id: 'new_snippet_link', data: { qa_selector: 'create_first_snippet_link' }, variant: :confirm
+ = link_button_to s_('SnippetsEmptyState|New snippet'), button_path, title: s_('SnippetsEmptyState|New snippet'), id: 'new_snippet_link', data: { testid: 'create-first-snippet-link' }, variant: :confirm
= link_button_to s_('SnippetsEmptyState|Documentation'), help_page_path('user/snippets.md'), title: s_('SnippetsEmptyState|Documentation')
- else
%h4.gl-text-center= s_('SnippetsEmptyState|There are no snippets to show.')
diff --git a/app/views/shared/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml
index 567c4a2d444..e152390b0df 100644
--- a/app/views/shared/empty_states/_wikis.html.haml
+++ b/app/views/shared/empty_states/_wikis.html.haml
@@ -4,7 +4,7 @@
- if !hide_create && can?(current_user, :create_wiki, @wiki.container)
- create_path = wiki_page_path(@wiki, params[:id], view: 'create')
- - create_link = link_button_to s_('WikiEmpty|Create your first page'), create_path, title: s_('WikiEmpty|Create your first page'), data: { qa_selector: 'create_first_page_link' }, variant: :confirm
+ - create_link = link_button_to s_('WikiEmpty|Create your first page'), create_path, title: s_('WikiEmpty|Create your first page'), data: { testid: 'create-first-page-link' }, variant: :confirm
= render layout: layout_path, locals: { image_path: 'illustrations/empty-state/empty-wiki-md.svg' } do
%h4.text-left
diff --git a/app/views/shared/empty_states/_wikis_layout.html.haml b/app/views/shared/empty_states/_wikis_layout.html.haml
index 03054c959fd..831bdcac073 100644
--- a/app/views/shared/empty_states/_wikis_layout.html.haml
+++ b/app/views/shared/empty_states/_wikis_layout.html.haml
@@ -1,6 +1,6 @@
.row.empty-state.empty-state-wiki
.col-12
- .svg-content.svg-150{ data: { qa_selector: 'svg_content' } }
+ .svg-content.svg-150{ data: { testid: 'svg-content' } }
= image_tag image_path
.col-12
.text-content.text-center
diff --git a/app/views/shared/file_hooks/_index.html.haml b/app/views/shared/file_hooks/_index.html.haml
index 9f1b11d6ab5..478f047e9cc 100644
--- a/app/views/shared/file_hooks/_index.html.haml
+++ b/app/views/shared/file_hooks/_index.html.haml
@@ -23,7 +23,7 @@
- if file_hooks.any?
%ul.content-list{ class: 'gl-my-n3!' }
- file_hooks.each do |file|
- %li.label-list-item
+ %li.gl-border-b.gl-last-of-type-border-b-0
.monospace
= File.basename(file)
- else
diff --git a/app/views/shared/integrations/edit.html.haml b/app/views/shared/integrations/edit.html.haml
index 9d613d2ad94..3361dfba5d2 100644
--- a/app/views/shared/integrations/edit.html.haml
+++ b/app/views/shared/integrations/edit.html.haml
@@ -2,8 +2,10 @@
- breadcrumb_title @integration.title
- page_title @integration.title, _('Integrations')
-%h2.gl-mb-4
- = @integration.title
+.gl-display-flex.gl-align-items-center.gl-gap-3.gl-mt-5
+ = render Pajamas::AvatarComponent.new(@integration, size: 64, alt: '')
+ %h2.gl-m-0
+ = @integration.title
= render 'shared/integrations/tabs', integration: @integration, active_tab: 'edit' do
= render 'shared/integration_settings', integration: @integration
diff --git a/app/views/shared/integrations/mattermost_slash_commands/_detailed_help.html.haml b/app/views/shared/integrations/mattermost_slash_commands/_detailed_help.html.haml
index 5aaae5eb4ec..f1e2b8fff47 100644
--- a/app/views/shared/integrations/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/shared/integrations/mattermost_slash_commands/_detailed_help.html.haml
@@ -48,7 +48,7 @@
.form-group
= label_tag :request_url, s_('MattermostService|Request URL'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
- = text_field_tag :request_url, service_trigger_url(integration), class: 'form-control form-control-sm', readonly: 'readonly'
+ = text_field_tag :request_url, integration_trigger_url(integration), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#request_url', category: :primary, size: :medium)
diff --git a/app/views/shared/integrations/slack_slash_commands/_help.html.haml b/app/views/shared/integrations/slack_slash_commands/_help.html.haml
index defaf50efea..fd30c5b0da3 100644
--- a/app/views/shared/integrations/slack_slash_commands/_help.html.haml
+++ b/app/views/shared/integrations/slack_slash_commands/_help.html.haml
@@ -38,9 +38,9 @@
.form-group
= label_tag :url, 'URL', class: 'col-12 col-form-label label-bold'
.col-12.input-group
- = text_field_tag :url, service_trigger_url(integration), class: 'form-control form-control-sm', readonly: 'readonly'
+ = text_field_tag :url, integration_trigger_url(integration), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = deprecated_clipboard_button(target: '#url', class: 'input-group-text')
+ = clipboard_button(target: '#url', category: :primary, size: :medium)
.form-group
= label_tag nil, _('Method'), class: 'col-12 col-form-label label-bold'
@@ -51,7 +51,7 @@
.col-12.input-group
= text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = deprecated_clipboard_button(target: '#customize_name', class: 'input-group-text')
+ = clipboard_button(target: '#customize_name', category: :primary, size: :medium)
.form-group
= label_tag nil, _('Customize icon'), class: 'col-12 col-form-label label-bold'
@@ -68,21 +68,21 @@
.col-12.input-group
= text_field_tag :autocomplete_description, run_actions_text.html_safe, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = deprecated_clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
+ = clipboard_button(target: '#autocomplete_description', category: :primary, size: :medium)
.form-group
= label_tag :autocomplete_usage_hint, _('Autocomplete usage hint'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = deprecated_clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text')
+ = clipboard_button(target: '#autocomplete_usage_hint', category: :primary, size: :medium)
.form-group
= label_tag :descriptive_label, _('Descriptive label'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :descriptive_label, _('Perform common operations on GitLab project'), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = deprecated_clipboard_button(target: '#descriptive_label', class: 'input-group-text')
+ = clipboard_button(target: '#descriptive_label', category: :primary, size: :medium)
%hr
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index 3c4ee01d04f..286b3c7f66f 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -1,3 +1,4 @@
+- add_page_specific_style 'page_bundles/labels'
- project = @target_project || @project
- edit_context = local_assigns.fetch(:edit_context, nil) || project
- show_create = local_assigns.fetch(:show_create, true)
@@ -11,7 +12,7 @@
- dropdown_title = local_assigns.fetch(:dropdown_title, _('Filter by label'))
- dropdown_data = label_dropdown_data(edit_context, labels: labels_filter_path_with_defaults(only_group_labels: edit_context.is_a?(Group)), default_label: _('Labels'))
-- dropdown_data.merge!(data_options, qa_selector: "issuable_label_dropdown")
+- dropdown_data.merge!(data_options, testid: "issuable-label-dropdown")
- label_name = local_assigns.fetch(:label_name, _('Labels'))
- no_default_styles = local_assigns.fetch(:no_default_styles, false)
- classes << 'js-extra-options' if extra_options
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 93e1a53ccb4..1392c7ab89f 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -11,24 +11,28 @@
- is_merge_request = issuable_type === 'merge_request'
- moved_sidebar_enabled = moved_mr_sidebar_enabled?
- is_merge_request_with_flag = is_merge_request && moved_sidebar_enabled
+- add_page_specific_style 'page_bundles/labels'
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { always_show_toggle: true, signed: { in: signed_in }, issuable_type: issuable_type }, class: "#{sidebar_gutter_collapsed_class(is_merge_request_with_flag)} #{'right-sidebar-merge-requests' if is_merge_request_with_flag}", 'aria-live' => 'polite', 'aria-label': issuable_type }
.issuable-sidebar{ class: "#{'is-merge-request' if is_merge_request_with_flag}" }
- .issuable-sidebar-header{ class: "#{'gl-pb-2! gl-md-display-flex gl-justify-content-end gl-lg-display-none!' if is_merge_request_with_flag}" }
+ .issuable-sidebar-header{ class: "gl-pb-4! #{'gl-pb-2! gl-md-display-flex gl-justify-content-end gl-lg-display-none!' if is_merge_request_with_flag}" }
= render Pajamas::ButtonComponent.new(button_options: { class: "gutter-toggle float-right js-sidebar-toggle has-tooltip gl-shadow-none! #{'gl-display-block' if moved_sidebar_enabled}", type: 'button', 'aria-label' => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }) do
= sidebar_gutter_toggle_icon
- - if signed_in && !is_merge_request_with_flag
- .js-sidebar-todo-widget-root{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } }
+ - if signed_in
+ - if !is_merge_request_with_flag
+ .js-sidebar-todo-widget-root{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } }
+ - if notifications_todos_buttons_enabled?
+ .js-sidebar-subscriptions-widget-root
= form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
- .block.assignee{ class: "#{'gl-mt-3' if !signed_in && moved_sidebar_enabled}", data: { qa_selector: 'assignee_block_container', testid: 'assignee-block-container' } }
+ .block.assignee{ class: "#{'gl-mt-3' if !signed_in && moved_sidebar_enabled}", data: { testid: 'assignee-block-container' } }
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees, signed_in: signed_in
- if issuable_sidebar[:supports_severity]
.js-sidebar-severity-widget-root
- if reviewers
- .block.reviewer{ data: { qa_selector: 'reviewers_block_container' } }
+ .block.reviewer{ data: { testid: 'reviewers-block-container' } }
= render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers, signed_in: signed_in
- if issuable_sidebar[:supports_escalation]
@@ -42,7 +46,7 @@
.js-sidebar-labels-widget-root{ data: sidebar_labels_data(issuable_sidebar, @project) }
- if issuable_sidebar[:supports_milestone]
- .block.milestone{ data: { qa_selector: 'milestone_block', testid: 'sidebar-milestones' } }
+ .block.milestone{ data: { testid: 'sidebar-milestones' } }
.js-sidebar-milestone-widget-root{ data: { can_edit: can_edit_issuable.to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid] } }
- if in_group_context_with_iterations
@@ -88,11 +92,11 @@
- if is_merge_request && !moved_sidebar_enabled
.sub-block.js-sidebar-source-branch
.sidebar-collapsed-icon.js-dont-change-state
- = deprecated_clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
+ = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'js-source-branch-copy')
.gl-display-flex.gl-align-items-center.gl-justify-content-space-between.gl-mb-2.hide-collapsed
%span.gl-overflow-hidden.gl-text-overflow-ellipsis.gl-white-space-nowrap
= _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<span class='gl-font-monospace' data-testid='ref-name' title='#{html_escape(source_branch)}'>".html_safe, source_branch_close: "</span>".html_safe, source_branch: html_escape(source_branch) }
- = deprecated_clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
+ = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'js-source-branch-copy')
- if show_forwarding_email && !moved_sidebar_enabled
.block
diff --git a/app/views/shared/issuable/_status_box.html.haml b/app/views/shared/issuable/_status_box.html.haml
deleted file mode 100644
index f2e4e22788a..00000000000
--- a/app/views/shared/issuable/_status_box.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-- badge_text = state_name_with_icon(issuable)[0]
-- badge_icon = state_name_with_icon(issuable)[1]
-- badge_variant = issuable.open? ? :success : issuable.merged? ? :info : :danger
-- badge_classes = "js-mr-status-box gl-mr-3 gl-align-self-center"
-
-= gl_badge_tag({ variant: badge_variant, icon: badge_icon, icon_classes: 'gl-mr-0!' }, { class: badge_classes, data: { project_path: issuable.project.path_with_namespace, iid: issuable.iid, issuable_type: 'merge_request', state: issuable.state } }) do
- %span.gl-display-none.gl-sm-display-block.gl-ml-2
- = badge_text
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index 1da0b82b634..a0ec7ca20ff 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -3,6 +3,7 @@
- presenter = local_assigns.fetch(:presenter)
- has_due_date = issuable.has_attribute?(:due_date)
- form = local_assigns.fetch(:form)
+- add_page_specific_style 'page_bundles/labels'
- if @add_related_issue
.form-group
@@ -37,15 +38,7 @@
.issuable-form-select-holder
= render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]"
- - if Feature.enabled?(:visible_label_selection_on_metadata, project)
- .js-issuable-form-label-selector{ data: issuable_label_selector_data(project, issuable) }
- - else
- .form-group.row
- = form.label :label_ids, _('Labels'), class: "col-12"
- = form.hidden_field :label_ids, multiple: true, value: ''
- .col-12
- .issuable-form-select-holder
- = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label"
+ .js-issuable-form-label-selector{ data: issuable_label_selector_data(project, issuable) }
= render_if_exists "shared/issuable/form/merge_request_blocks", issuable: issuable, form: form
diff --git a/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
index d25ef3f4e83..1167d68534f 100644
--- a/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
+++ b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
@@ -8,4 +8,4 @@
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' }
= dropdown_tag(users_dropdown_label(issuable.assignees), options: assignees_dropdown_options(issuable.to_ability_name))
- = link_to _('Assign to me'), '#', class: "assign-to-me-link gl-white-space-nowrap gl-md-pl-3 #{'hide' if issuable.assignees.include?(current_user)}", data: { qa_selector: 'assign_to_me_link' }
+ = link_to _('Assign to me'), '#', class: "assign-to-me-link gl-white-space-nowrap gl-md-pl-3 #{'hide' if issuable.assignees.include?(current_user)}", data: { testid: 'assign-to-me-link' }
diff --git a/app/views/shared/issuable/form/_template_selector.html.haml b/app/views/shared/issuable/form/_template_selector.html.haml
index c870bb17a85..bad2f4fdfb0 100644
--- a/app/views/shared/issuable/form/_template_selector.html.haml
+++ b/app/views/shared/issuable/form/_template_selector.html.haml
@@ -3,7 +3,7 @@
- return unless issuable && issuable_templates(ref_project, issuable.to_ability_name).any?
.issuable-form-select-holder.selectbox.form-group
- .js-issuable-selector-wrap{ data: { issuable_type: issuable.to_ability_name.pluralize, qa_selector: 'template_dropdown' } }
+ .js-issuable-selector-wrap{ data: { issuable_type: issuable.to_ability_name.pluralize, testid: 'template-dropdown' } }
= template_dropdown_tag(issuable) do
%ul.dropdown-footer-list
%li
diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml
index 53fbe3dac03..5ac172315be 100644
--- a/app/views/shared/labels/_form.html.haml
+++ b/app/views/shared/labels/_form.html.haml
@@ -1,3 +1,4 @@
+- add_page_specific_style 'page_bundles/labels'
- show_lock_on_merge = local_assigns.fetch(:show_lock_on_merge, false)
= gitlab_ui_form_for @label, as: :label, url: url, html: { class: 'label-form js-quick-submit js-requires-input' } do |f|
@@ -6,20 +7,20 @@
.form-group.row
.col-12
= f.label :title
- = f.text_field :title, class: "gl-form-input form-control js-label-title", required: true, autofocus: true, data: { qa_selector: 'label_title_field' }
+ = f.text_field :title, class: "gl-form-input form-control js-label-title", required: true, autofocus: true, data: { testid: 'label-title-field' }
= render_if_exists 'shared/labels/create_label_help_text'
.form-group.row
.col-12
= f.label :description, _("Description (optional)")
- = f.text_area :description, class: "gl-form-input form-control js-quick-submit", rows: 4, data: { qa_selector: 'label_description_field' }
+ = f.text_area :description, class: "gl-form-input form-control js-quick-submit", rows: 4, data: { testid: 'label-description-field' }
.form-group.row
.col-12
= f.label :color, _("Background color")
.input-group
.input-group-prepend
%input.label-color-preview.gl-w-7.gl-h-full.gl-border-1.gl-border-solid.gl-border-gray-500.gl-border-r-0.gl-rounded-top-right-none.gl-rounded-bottom-right-none{ type: "color", placeholder: _('Select color') }
- = f.text_field :color, class: "gl-form-input form-control", data: { qa_selector: 'label_color_field' }
+ = f.text_field :color, class: "gl-form-input form-control", data: { testid: 'label-color-field' }
.form-text.text-muted
= _('Select a color from the color picker or from the presets below.')
= render_suggested_colors
@@ -35,7 +36,7 @@
- if @label.persisted?
= f.submit _('Save changes'), class: 'js-save-button gl-mr-2', pajamas_button: true
- else
- = f.submit _('Create label'), class: 'js-save-button gl-mr-2', data: { qa_selector: 'label_create_button' }, pajamas_button: true
+ = f.submit _('Create label'), class: 'js-save-button gl-mr-2', data: { testid: 'label-create-button' }, pajamas_button: true
= render Pajamas::ButtonComponent.new(href: back_path) do
= _('Cancel')
diff --git a/app/views/shared/labels/_nav.html.haml b/app/views/shared/labels/_nav.html.haml
index c82a22c73b8..21255e655ea 100644
--- a/app/views/shared/labels/_nav.html.haml
+++ b/app/views/shared/labels/_nav.html.haml
@@ -14,8 +14,8 @@
= render Pajamas::ButtonComponent.new(icon: 'search', button_options: { type: "submit", "aria-label" => _('Submit search') })
= render 'shared/labels/sort_dropdown'
- if labels_or_filters && can_admin_label && @project
- = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_project_label_path(@project), button_options: { data: { qa_selector: 'create_new_label_button' } }) do
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_project_label_path(@project), button_options: { data: { testid: 'create-new-label-button' } }) do
= _('New label')
- if labels_or_filters && can_admin_label && @group
- = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_group_label_path(@group), button_options: { data: { qa_selector: 'create_new_label_button' } }) do
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_group_label_path(@group), button_options: { data: { testid: 'create-new-label-button' } }) do
= _('New label')
diff --git a/app/views/shared/milestones/_description.html.haml b/app/views/shared/milestones/_description.html.haml
index 3774fb0869f..ff1224f89d0 100644
--- a/app/views/shared/milestones/_description.html.haml
+++ b/app/views/shared/milestones/_description.html.haml
@@ -1,14 +1,14 @@
.detail-page-description.milestone-detail.gl-py-4
- %h2.gl-m-0{ data: { qa_selector: "milestone_title_content" } }
+ %h2.gl-m-0{ data: { testid: "milestone-title-content" } }
= markdown_field(milestone, :title)
- .gl-font-sm.gl-text-secondary.gl-font-base.gl-font-weight-normal.gl-line-height-normal{ data: { qa_selector: 'milestone_id_content' }, itemprop: 'identifier' }
+ .gl-font-sm.gl-text-secondary.gl-font-base.gl-font-weight-normal.gl-line-height-normal{ itemprop: 'identifier' }
- if can?(current_user, :read_milestone, @milestone)
%span.gl-display-inline-block.gl-vertical-align-middle
= s_('MilestonePage|Milestone ID: %{milestone_id}') % { milestone_id: @milestone.id }
= clipboard_button(title: s_('MilestonePage|Copy milestone ID'), text: @milestone.id)
- if milestone.try(:description).present?
- %div{ data: { qa_selector: "milestone_description_content" } }
+ %div{ data: { testid: "milestone-description-content" } }
.description.md.gl-px-0.gl-pt-4{ class: ('js-task-list-container' if can?(current_user, :admin_milestone, milestone)), data: { lock_version: @milestone.lock_version } }
= markdown_field(milestone, :description)
-# This textarea is necessary for `task_list.js` to work.
diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml
index 3e75775bf73..22bdcb1f80f 100644
--- a/app/views/shared/milestones/_form_dates.html.haml
+++ b/app/views/shared/milestones/_form_dates.html.haml
@@ -3,12 +3,12 @@
= f.label :start_date, _('Start Date')
%div
.issuable-form-select-holder
- = f.gitlab_ui_datepicker :start_date, data: { qa_selector: "start_date_field" }, placeholder: _('Select start date'), autocomplete: 'off'
+ = f.gitlab_ui_datepicker :start_date, data: { testid: "start-date-field" }, placeholder: _('Select start date'), autocomplete: 'off'
%a.gl-white-space-nowrap.gl-pl-4.js-clear-start-date{ href: "#" }= _('Clear start date')
.gl-form-group
%div
= f.label :due_date, _('Due Date')
%div
.issuable-form-select-holder
- = f.gitlab_ui_datepicker :due_date, data: { qa_selector: "due_date_field" }, placeholder: _('Select due date'), autocomplete: 'off'
+ = f.gitlab_ui_datepicker :due_date, data: { testid: "due-date-field" }, placeholder: _('Select due date'), autocomplete: 'off'
%a.gl-white-space-nowrap.gl-pl-4.js-clear-due-date{ href: "#" }= _('Clear due date')
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
index 1e856bf4355..1abf4b46d09 100644
--- a/app/views/shared/milestones/_labels_tab.html.haml
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -1,3 +1,5 @@
+- add_page_specific_style 'page_bundles/labels'
+
%ul.bordered-list.manage-labels-list
- labels.each do |label|
- options = { milestone_title: @milestone.title, label_name: label.title }
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index c36d3a8b92b..6554f557c89 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -6,7 +6,7 @@
.row
.col-md-6
.gl-mb-2
- %strong{ data: { qa_selector: "milestone_link", qa_milestone_title: milestone.title } }
+ %strong{ data: { testid: "milestone-link", qa_milestone_title: milestone.title } }
= link_to truncate(milestone.title, length: 100), milestone_path(milestone)
- if @group || dashboard
= " - #{milestone_type}"
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 9387d8d3ad1..7d1e9c06966 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -24,7 +24,7 @@
- if @project && can?(current_user, :admin_milestone, @project)
= link_to s_('MilestoneSidebar|Edit'), edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value
- %span.value-content{ data: { qa_selector: 'start_date_content' } }
+ %span.value-content{ data: { testid: 'start-date-content' } }
- if milestone.start_date
%span.bold= milestone.start_date.to_fs(:medium)
- else
@@ -61,7 +61,7 @@
- if @project && can?(current_user, :admin_milestone, @project)
= link_to s_('MilestoneSidebar|Edit'), edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed
- %span.value-content{ data: { qa_selector: 'due_date_content' } }
+ %span.value-content{ data: { testid: 'due-date-content' } }
- if milestone.due_date
%span.bold= milestone.due_date.to_fs(:medium)
- else
diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml
index bbcd072c762..53205301ec7 100644
--- a/app/views/shared/notes/_comment_button.html.haml
+++ b/app/views/shared/notes/_comment_button.html.haml
@@ -1,5 +1,5 @@
- noteable_name = @note.noteable.human_class_name
.js-comment-type-dropdown.float-left.gl-sm-mr-3{ data: { noteable_name: noteable_name } }
- = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { class: 'js-comment-button js-comment-submit-button', value: _('Comment'), data: { qa_selector: 'comment_button' }}) do
+ = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { class: 'js-comment-button js-comment-submit-button', value: _('Comment'), data: { testid: 'comment-button' }}) do
= _('Comment')
diff --git a/app/views/shared/notes/_edit_form.html.haml b/app/views/shared/notes/_edit_form.html.haml
index 40a71aa53dc..d4dec49c367 100644
--- a/app/views/shared/notes/_edit_form.html.haml
+++ b/app/views/shared/notes/_edit_form.html.haml
@@ -4,13 +4,13 @@
= hidden_field_tag :target_type, '', class: 'js-form-target-type'
.flash-container
= render layout: 'shared/md_preview', locals: { url: preview_markdown_path(project), referenced_users: true } do
- = render 'shared/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', qa_selector: 'edit_note_field', placeholder: _("Write a comment or drag your files here…")
+ = render 'shared/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', testid: 'edit-note-field', placeholder: _("Write a comment or drag your files here…")
= render 'shared/notes/hints'
.note-form-actions.clearfix
.settings-message.note-edit-warning.js-finish-edit-warning
= _("Finish editing this message first!")
- = render Pajamas::ButtonComponent.new(type: 'submit', variant: :confirm, button_options: { class: 'js-comment-save-button', data: { qa_selector: 'save_comment_button' } }) do
+ = render Pajamas::ButtonComponent.new(type: 'submit', variant: :confirm, button_options: { class: 'js-comment-save-button', data: { testid: 'save-comment-button' } }) do
= _("Save comment")
= render Pajamas::ButtonComponent.new(button_options: { class: 'note-edit-cancel' }) do
= _("Cancel")
diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml
index 9a5e9b2179f..d37d707e935 100644
--- a/app/views/shared/notes/_form.html.haml
+++ b/app/views/shared/notes/_form.html.haml
@@ -26,7 +26,7 @@
.discussion-form-container.discussion-with-resolve-btn.flex-column.p-0
= render layout: 'shared/md_preview', locals: { url: preview_url, referenced_users: true, supports_quick_actions: supports_quick_actions } do
- = render 'shared/zen', f: f, qa_selector: 'note_field',
+ = render 'shared/zen', f: f, testid: 'note-field',
attr: :note,
classes: 'note-textarea js-note-text',
placeholder: _("Write a comment or drag your files here…"),
@@ -38,5 +38,5 @@
.note-form-actions.clearfix.gl-display-flex.gl-flex-wrap
= render partial: 'shared/notes/comment_button'
- %a.btn.gl-button.btn-cancel.js-close-discussion-note-form.hide{ role: "button", data: { cancel_text: _("Cancel") } }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-close-discussion-note-form hide' }) do
= _('Cancel')
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 95e0beee5e0..3e72a66561b 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -35,7 +35,7 @@
%span.note-header-author-name.bold
= note.author.name
= user_status(note.author)
- %spannote-headline-light{ data: { qa_selector: 'note_author_content' } }
+ %spannote-headline-light{ data: { testid: 'note-author-content' } }
= note.author.to_reference
%span.note-headline-ligh.note-headline-meta
- if note.system
@@ -52,7 +52,7 @@
- else
= render 'projects/notes/actions', note: note, note_editable: note_editable
.note-body{ class: note_editable ? 'js-task-list-container' : '' }
- .note-text.md{ data: { qa_selector: 'note_content' } }
+ .note-text.md{ data: { testid: 'note-content' } }
= markdown_field(note, :note)
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago')
.original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index 0fed59aaff3..336fdedf89b 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -1,7 +1,7 @@
- issuable = @issue || @merge_request
- discussion_locked = issuable&.discussion_locked?
-%ul#notes-list.notes.main-notes-list.timeline{ data: { 'qa_selector': 'notes_list' } }
+%ul#notes-list.notes.main-notes-list.timeline{ data: { 'testid': 'notes-list' } }
= render "shared/notes/notes"
= render 'shared/notes/edit_form', project: @project
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index a2c831bfd1c..14785870dc0 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -1,3 +1,4 @@
+- add_page_specific_style 'page_bundles/projects'
- projects_limit = 20 unless local_assigns[:projects_limit]
- avatar = true unless local_assigns[:avatar] == false
- use_creator_avatar = false unless local_assigns[:use_creator_avatar] == true
@@ -27,7 +28,7 @@
- explore_groups_button_label = _('Explore groups')
- explore_groups_button_link = explore_groups_path
-.js-projects-list-holder{ data: { qa_selector: 'projects_list' } }
+.js-projects-list-holder{ data: { testid: 'projects-list' } }
- if any_projects?(projects)
- load_pipeline_status(projects) if pipeline_status
- load_max_project_member_accesses(projects) # Prime cache used in shared/projects/project view rendered below
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index ac5e65747d5..2de4a9d7780 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -24,7 +24,7 @@
- else
= render Pajamas::AvatarComponent.new(project, size: 48, alt: '', class: 'gl-mr-5')
.project-cell{ class: css_class }
- .project-details.gl-pr-9.gl-sm-pr-0.gl-w-full.gl-display-flex.gl-flex-direction-column{ data: { qa_selector: 'project_content', qa_project_name: project.name } }
+ .project-details.gl-pr-9.gl-sm-pr-0.gl-w-full.gl-display-flex.gl-flex-direction-column{ data: { testid: 'project-content', qa_project_name: project.name } }
.gl-display-flex.gl-align-items-center.gl-flex-wrap
%h2.gl-font-base.gl-line-height-20.gl-my-0.gl-overflow-wrap-anywhere
= link_to project_path(project), class: 'text-plain gl-mr-3 js-prefetch-document', title: project.name do
@@ -45,7 +45,7 @@
- if !explore_projects_tab? && access&.nonzero?
-# haml-lint:disable UnnecessaryStringOutput
= ' ' # prevent haml from eating the space between elements
- %span.user-access-role.gl-display-block.gl-m-0{ data: { qa_selector: 'user_role_content' } }= Gitlab::Access.human_access(access)
+ %span.user-access-role.gl-display-block.gl-m-0{ data: { testid: 'user-role-content' } }= localized_project_human_access(access)
- if !explore_projects_tab?
= render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: project, additional_classes: 'gl-ml-3!'
diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml
index 2388bf2f0be..de54cc2810b 100644
--- a/app/views/shared/projects/_search_form.html.haml
+++ b/app/views/shared/projects/_search_form.html.haml
@@ -2,7 +2,7 @@
- admin_view ||= false
- top_padding = admin_view ? 'gl-lg-pt-3' : ''
-= form_tag filter_projects_path, method: :get, class: "project-filter-form gl-display-flex! gl-flex-wrap gl-w-full gl-gap-3 #{top_padding}", data: { qa_selector: 'project_filter_form_container' }, id: 'project-filter-form' do |f|
+= form_tag filter_projects_path, method: :get, class: "project-filter-form gl-display-flex! gl-flex-wrap gl-w-full gl-gap-3 #{top_padding}", data: { testid: 'project-filter-form-container' }, id: 'project-filter-form' do |f|
= search_field_tag :name, params[:name],
placeholder: placeholder,
class: "project-filter-form-field form-control input-short js-projects-list-filter gl-m-0!",
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 6caadeb0ba4..9767f7929d0 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,13 +1,13 @@
- link_project = local_assigns.fetch(:link_project, false)
- notes_count = @noteable_meta_data[snippet.id].user_notes_count
-%li.snippet-row.py-3{ data: { qa_selector: 'snippet_link', qa_snippet_title: snippet.title } }
+%li.snippet-row.py-3{ data: { testid: 'snippet-link', qa_snippet_title: snippet.title } }
= render Pajamas::AvatarComponent.new(snippet.author, size: 48, alt: "", class: 'gl-display-none gl-sm-display-block gl-float-left gl-mr-3')
= link_to gitlab_snippet_path(snippet), class: "title" do
= snippet.title
- %ul.controls{ data: { qa_selector: 'snippet_file_count_content', qa_snippet_files: snippet.statistics&.file_count } }
+ %ul.controls{ data: { testid: 'snippet-file-count-content', qa_snippet_files: snippet.statistics&.file_count } }
%li
= snippet_file_count(snippet)
%li
@@ -15,7 +15,7 @@
= sprite_icon('comments', css_class: 'gl-vertical-align-text-bottom')
= notes_count
%li
- %span.sr-only{ data: { qa_selector: 'snippet_visibility_content', qa_snippet_visibility: visibility_level_label(snippet.visibility_level) } }
+ %span.sr-only{ data: { testid: 'snippet-visibility-content', qa_snippet_visibility: visibility_level_label(snippet.visibility_level) } }
= visibility_level_label(snippet.visibility_level)
= visibility_level_icon(snippet.visibility_level)
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
index 1c63ce490ed..8e363f6c86a 100644
--- a/app/views/shared/tokens/_scopes_form.html.haml
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -9,7 +9,7 @@
- help_text = t scope, scope: scope_description(description_prefix)
= f.gitlab_ui_checkbox_component :scopes, scope,
help_text: help_text,
- checkbox_options: { checked: token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", multiple: true, data: { qa_selector: "#{scope}_checkbox" } },
+ checkbox_options: { checked: token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", multiple: true, data: { testid: "#{scope}-checkbox" } },
checked_value: scope,
unchecked_value: nil,
- label_options: { data: { qa_selector: "#{scope}_label" } }
+ label_options: { data: { testid: "#{scope}-label" } }
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index f040ea8e542..7c713e63cd7 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -63,7 +63,7 @@
%li.gl-pb-3
= form.gitlab_ui_checkbox_component :releases_events,
integration_webhook_event_human_name(:releases_events),
- help_text: s_('Webhooks|A release is created or updated.')
+ help_text: s_('Webhooks|A release is created, updated, or deleted.')
- if Feature.enabled?(:emoji_webhooks, hook.parent)
%li.gl-pb-5
- emoji_help_link = link_to s_('Which emoji events trigger webhooks'), help_page_path('user/project/integrations/webhook_events.md', anchor: 'emoji-events')
diff --git a/app/views/shared/web_hooks/_hook.html.haml b/app/views/shared/web_hooks/_hook.html.haml
index 9b84222e920..a332fd9cce7 100644
--- a/app/views/shared/web_hooks/_hook.html.haml
+++ b/app/views/shared/web_hooks/_hook.html.haml
@@ -1,7 +1,7 @@
- sslStatus = hook.enable_ssl_verification ? _('enabled') : _('disabled')
- sslBadgeText = _('SSL Verification:') + ' ' + sslStatus
-%li.label-list-item
+%li.gl-border-b.gl-last-of-type-border-b-0
.gl-display-flex.lgl-align-items-center.row.gl-mx-0
.col-md-8.col-lg-7.gl-px-5
.light-header.gl-mb-2
diff --git a/app/views/shared/wikis/_form.html.haml b/app/views/shared/wikis/_form.html.haml
index 34bedbd928a..cdf4b50a99d 100644
--- a/app/views/shared/wikis/_form.html.haml
+++ b/app/views/shared/wikis/_form.html.haml
@@ -1,4 +1,4 @@
-- page_info = { last_commit_sha: @page.last_commit_sha, persisted: @page.persisted?, title: @page.title, content: @page.content || '', format: @page.format.to_s, uploads_path: uploads_path, path: wiki_page_path(@wiki, @page), wiki_path: wiki_path(@wiki), help_path: help_page_path('user/project/wiki/index'), markdown_help_path: help_page_path('user/markdown'), markdown_preview_path: wiki_page_path(@wiki, @page, action: :preview_markdown), create_path: wiki_path(@wiki, action: :create) }
+- page_info = { last_commit_sha: @page.last_commit_sha, persisted: @page.persisted?, title: @page.title, content: @page.raw_content || '', format: @page.format.to_s, uploads_path: uploads_path, path: wiki_page_path(@wiki, @page), wiki_path: wiki_path(@wiki), help_path: help_page_path('user/project/wiki/index'), markdown_help_path: help_page_path('user/markdown'), markdown_preview_path: wiki_page_path(@wiki, @page, action: :preview_markdown), create_path: wiki_path(@wiki, action: :create) }
.gl-mt-3
= form_errors(@page, truncate: :title)
diff --git a/app/views/shared/wikis/_main_links.html.haml b/app/views/shared/wikis/_main_links.html.haml
index 41831c95198..9a76069e8f6 100644
--- a/app/views/shared/wikis/_main_links.html.haml
+++ b/app/views/shared/wikis/_main_links.html.haml
@@ -1,6 +1,6 @@
- if @page&.persisted?
- = link_button_to wiki_page_path(@wiki, @page, action: :history), role: "button", data: { qa_selector: 'page_history_button' } do
+ = link_button_to wiki_page_path(@wiki, @page, action: :history), role: "button", data: { testid: 'page-history-button' } do
= s_("Wiki|Page history")
- if can?(current_user, :create_wiki, @wiki.container)
- = link_button_to wiki_path(@wiki, action: :new), role: "button", data: { qa_selector: 'new_page_button' }, variant: :confirm, category: :secondary do
+ = link_button_to wiki_path(@wiki, action: :new), role: "button", data: { testid: 'new-page-button' }, variant: :confirm, category: :secondary do
= s_("Wiki|New page")
diff --git a/app/views/shared/wikis/_pages_wiki_page.html.haml b/app/views/shared/wikis/_pages_wiki_page.html.haml
index fb6f58d044d..23931bbbc32 100644
--- a/app/views/shared/wikis/_pages_wiki_page.html.haml
+++ b/app/views/shared/wikis/_pages_wiki_page.html.haml
@@ -1,5 +1,5 @@
%li
- = link_to wiki_page.human_title, wiki_page_path(@wiki, wiki_page), data: { qa_selector: 'wiki_page_link', qa_page_name: wiki_page.slug }
+ = link_to wiki_page.human_title, wiki_page_path(@wiki, wiki_page), data: { testid: 'wiki-page-link', qa_page_name: wiki_page.slug }
%small (#{wiki_page.format})
.float-right
- if wiki_page.last_version
diff --git a/app/views/shared/wikis/_sidebar.html.haml b/app/views/shared/wikis/_sidebar.html.haml
index a34827602ab..cd752d31643 100644
--- a/app/views/shared/wikis/_sidebar.html.haml
+++ b/app/views/shared/wikis/_sidebar.html.haml
@@ -8,7 +8,7 @@
.gl-display-flex.gl-flex-wrap
- git_access_url = wiki_path(@wiki, action: :git_access)
- = link_to git_access_url, class: 'gl-mr-5' + (active_nav_link?(path: 'wikis#git_access') ? ' active' : ''), data: { qa_selector: 'clone_repository_link' } do
+ = link_to git_access_url, class: 'gl-mr-5' + (active_nav_link?(path: 'wikis#git_access') ? ' active' : ''), data: { testid: 'clone-repository-link' } do
= sprite_icon('download', css_class: 'gl-mr-2')
%span= _("Clone repository")
@@ -32,5 +32,5 @@
= render partial: entry.to_partial_path, object: entry, locals: { context: 'sidebar' }
.block.w-100
- if @sidebar_limited
- = link_button_to wiki_path(@wiki, action: :pages), data: { qa_selector: 'view_all_pages_button' }, block: true do
+ = link_button_to wiki_path(@wiki, action: :pages), data: { testid: 'view-all-pages-button' }, block: true do
= s_("Wiki|View All Pages")
diff --git a/app/views/shared/wikis/_sidebar_wiki_page.html.haml b/app/views/shared/wikis/_sidebar_wiki_page.html.haml
index 2c5c3aa68a3..710ecf6196e 100644
--- a/app/views/shared/wikis/_sidebar_wiki_page.html.haml
+++ b/app/views/shared/wikis/_sidebar_wiki_page.html.haml
@@ -3,5 +3,5 @@
%li{ class: active_when(params[:id] == wiki_page.slug) }
.gl-relative.gl-display-flex.gl-align-items-center.js-wiki-list-toggle.wiki-list{ data: { testid: 'wiki-list' } }
= render Pajamas::ButtonComponent.new(icon: 'plus', href: "#{wiki_path}/{new_page_title}", button_options: { class: 'wiki-list-create-child-button gl-bg-transparent! gl-hover-bg-gray-50! gl-focus-bg-gray-50! gl-absolute gl-top-half gl-translate-y-n50 gl-cursor-pointer gl-right-3' })
- = link_to wiki_path, data: { qa_selector: 'wiki_page_link', qa_page_name: wiki_page.human_title } do
+ = link_to wiki_path, data: { testid: 'wiki-page-link', qa_page_name: wiki_page.human_title } do
= wiki_page.human_title
diff --git a/app/views/shared/wikis/_wiki_content.html.haml b/app/views/shared/wikis/_wiki_content.html.haml
index 780e4c4746d..b5210b340f3 100644
--- a/app/views/shared/wikis/_wiki_content.html.haml
+++ b/app/views/shared/wikis/_wiki_content.html.haml
@@ -1,2 +1,2 @@
-.js-wiki-page-content.md.gl-pt-2{ data: { qa_selector: 'wiki_page_content', testid: 'wiki-page-content', tracking_context: wiki_page_tracking_context(@page).to_json } }
+.js-wiki-page-content.md.gl-pt-2{ data: { testid: 'wiki-page-content', tracking_context: wiki_page_tracking_context(@page).to_json } }
= render_wiki_content(@page)
diff --git a/app/views/shared/wikis/_wiki_directory.html.haml b/app/views/shared/wikis/_wiki_directory.html.haml
index 6a066e0a838..cce81257691 100644
--- a/app/views/shared/wikis/_wiki_directory.html.haml
+++ b/app/views/shared/wikis/_wiki_directory.html.haml
@@ -1,11 +1,11 @@
- wiki_path = wiki_page_path(@wiki, wiki_directory)
-%li{ class: active_when(params[:id] == wiki_directory.slug), data: { qa_selector: 'wiki_directory_content' } }
+%li{ class: active_when(params[:id] == wiki_directory.slug), data: { testid: 'wiki-directory-content' } }
.gl-relative.gl-display-flex.gl-align-items-center.js-wiki-list-toggle.wiki-list{ data: { testid: 'wiki-list' } }<
= sprite_icon('chevron-right', css_class: 'js-wiki-list-expand-button wiki-list-expand-button gl-mr-2 gl-cursor-pointer')
= sprite_icon('chevron-down', css_class: 'js-wiki-list-collapse-button wiki-list-collapse-button gl-mr-2 gl-cursor-pointer')
= render Pajamas::ButtonComponent.new(icon: 'plus', href: "#{wiki_path}/{new_page_title}", button_options: { class: 'wiki-list-create-child-button gl-bg-transparent! gl-hover-bg-gray-50! gl-focus-bg-gray-50! gl-absolute gl-top-half gl-translate-y-n50 gl-cursor-pointer gl-right-3' })
- = link_to wiki_path, data: { qa_selector: 'wiki_dir_page_link', qa_page_name: wiki_directory.title } do
+ = link_to wiki_path, data: { testid: 'wiki-dir-page-link', qa_page_name: wiki_directory.title } do
= wiki_directory.title
%ul
- wiki_directory.entries.each do |entry|
diff --git a/app/views/shared/wikis/show.html.haml b/app/views/shared/wikis/show.html.haml
index be1f43f44de..9537d6fec15 100644
--- a/app/views/shared/wikis/show.html.haml
+++ b/app/views/shared/wikis/show.html.haml
@@ -29,10 +29,10 @@
.gl-mt-5.gl-mb-3
.gl-display-flex.gl-justify-content-space-between
- %h2.gl-mt-0.gl-mb-5{ data: { qa_selector: 'wiki_page_title', testid: 'wiki_page_title' } }= @page.human_title
+ %h2.gl-mt-0.gl-mb-5{ data: { testid: 'wiki-page-title' } }= @page.human_title
%div
- if can?(current_user, :create_wiki, @wiki.container) && @page.latest? && @valid_encoding
- = render Pajamas::ButtonComponent.new(href: wiki_page_path(@wiki, @page, action: :edit), icon: 'pencil', button_options: { class: 'js-wiki-edit', title: "Edit", data: { qa_selector: 'edit_page_button', testid: 'wiki_edit_button' }})
+ = render Pajamas::ButtonComponent.new(href: wiki_page_path(@wiki, @page, action: :edit), icon: 'pencil', button_options: { class: 'js-wiki-edit', title: "Edit", data: { testid: 'wiki-edit-button' }})
.js-async-wiki-page-content.md.gl-pt-2{ data: { qa_selector: 'wiki_page_content', testid: 'wiki-page-content', tracking_context: wiki_page_tracking_context(@page).to_json, get_wiki_content_url: wiki_page_render_api_endpoint(@page) } }
diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml
index e3a14b0454e..07640f579a2 100644
--- a/app/views/snippets/notes/_actions.html.haml
+++ b/app/views/snippets/notes/_actions.html.haml
@@ -8,6 +8,6 @@
- if note_editable
.note-actions-item.gl-ml-0
- = render Pajamas::ButtonComponent.new(category: :tertiary, icon: 'pencil', button_options: { title: _('Edit comment'), class: 'note-action-button js-note-edit has-tooltip', data: { container: 'body', qa_selector: 'edit_comment_button' } })
+ = render Pajamas::ButtonComponent.new(category: :tertiary, icon: 'pencil', button_options: { title: _('Edit comment'), class: 'note-action-button js-note-edit has-tooltip', data: { container: 'body', testid: 'edit-comment-button' } })
= render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
diff --git a/app/views/users/_cover_controls.html.haml b/app/views/users/_cover_controls.html.haml
index 43278e9d232..899a08c8a17 100644
--- a/app/views/users/_cover_controls.html.haml
+++ b/app/views/users/_cover_controls.html.haml
@@ -1,2 +1,2 @@
-.cover-controls.d-flex.px-2.pb-4.d-sm-block.p-sm-0
+.cover-controls.gl-display-flex.gl-gap-3.gl-pb-4
= yield
diff --git a/app/views/users/_follow_user.html.haml b/app/views/users/_follow_user.html.haml
index 3ee8c81496c..71f8a462cbf 100644
--- a/app/views/users/_follow_user.html.haml
+++ b/app/views/users/_follow_user.html.haml
@@ -7,5 +7,5 @@
= _('Unfollow')
- else
= form_tag user_follow_path(@user, :json), class: link_classes do
- = render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit, button_options: { class: 'gl-w-full', data: { qa_selector: 'follow_user_link', track_action: 'click_button', track_label: 'follow_from_profile' } }) do
+ = render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit, button_options: { class: 'gl-w-full', data: { testid: 'follow-user-link', track_action: 'click_button', track_label: 'follow_from_profile' } }) do
= _('Follow')
diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml
index 0b76ed6c086..3649f72c956 100644
--- a/app/views/users/_overview.html.haml
+++ b/app/views/users/_overview.html.haml
@@ -33,7 +33,7 @@
%h4.gl-flex-grow-1
= Feature.enabled?(:security_auto_fix) && @user.bot? ? s_('UserProfile|Bot activity') : s_('UserProfile|Activity')
= link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
- .overview-content-list{ data: { href: user_activity_path, qa_selector: 'user_activity_content' } }
+ .overview-content-list{ data: { href: user_activity_path, testid: 'user-activity-content' } }
= gl_loading_icon(size: 'md', css_class: 'loading')
- unless Feature.enabled?(:security_auto_fix) && @user.bot?
diff --git a/app/views/users/_profile_basic_info.html.haml b/app/views/users/_profile_basic_info.html.haml
index fb9721028d5..6de9e80008e 100644
--- a/app/views/users/_profile_basic_info.html.haml
+++ b/app/views/users/_profile_basic_info.html.haml
@@ -5,6 +5,6 @@
- unless Feature.enabled?(:user_profile_overflow_menu_vue)
= render 'middle_dot_divider', stacking: true do
= s_('UserProfile|User ID: %{id}') % { id: @user.id }
- = deprecated_clipboard_button(title: s_('UserProfile|Copy user ID'), text: @user.id)
+ = clipboard_button(title: s_('UserProfile|Copy user ID'), text: @user.id)
= render 'middle_dot_divider', stacking: true do
= s_('Member since %{date}') % { date: l(@user.created_at.to_date, format: :long) }
diff --git a/app/views/users/_view_user_in_admin_area.html.haml b/app/views/users/_view_user_in_admin_area.html.haml
index b13f22956f6..36b3c33d8ab 100644
--- a/app/views/users/_view_user_in_admin_area.html.haml
+++ b/app/views/users/_view_user_in_admin_area.html.haml
@@ -1,4 +1,4 @@
- if current_user && current_user.admin?
= render Pajamas::ButtonComponent.new(href: [:admin, @user],
icon: 'user',
- button_options: { class: 'gl-flex-grow-1 gl-mx-1 has-tooltip', title: s_('UserProfile|View user in admin area'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } })
+ button_options: { class: 'gl-flex-grow-1 has-tooltip', title: s_('UserProfile|View user in admin area'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } })
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index a2f6b3da746..0881c5bba54 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -5,6 +5,7 @@
- page_description @user.bio unless @user.blocked? || !@user.confirmed?
- page_itemtype 'http://schema.org/Person'
- add_page_specific_style 'page_bundles/profile'
+- add_page_specific_style 'page_bundles/projects'
- if show_super_sidebar?
- @left_sidebar = true
- @force_desktop_expanded_sidebar = true
@@ -17,7 +18,7 @@
.cover-block.user-cover-block.gl-border-t.gl-border-b.gl-mt-n1
%div{ class: container_class }
- if Feature.enabled?(:user_profile_overflow_menu_vue)
- .cover-controls.d-flex.px-2.pb-4.d-sm-block.p-sm-0
+ .cover-controls.gl-display-flex.gl-gap-3.gl-pb-4
= render 'users/follow_user'
-# The following edit button is mutually exclusive to the follow user button, they won't be shown together
- if @user == current_user
@@ -32,14 +33,14 @@
- if @user == current_user
= render Pajamas::ButtonComponent.new(href: profile_path,
icon: 'pencil',
- button_options: { class: 'gl-flex-grow-1 gl-mx-1 has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }})
+ button_options: { class: 'gl-flex-grow-1 has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }})
- elsif current_user
#js-report-abuse{ data: { report_abuse_path: add_category_abuse_reports_path, reported_user_id: @user.id, reported_from_url: user_url(@user) } }
= render 'users/view_gpg_keys'
- if can?(current_user, :read_user_profile, @user)
= render Pajamas::ButtonComponent.new(href: user_path(@user, rss_url_options),
icon: 'rss',
- button_options: { class: 'gl-flex-grow-1 gl-mx-1 has-tooltip', title: s_('UserProfile|Subscribe'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }})
+ button_options: { class: 'gl-flex-grow-1 has-tooltip', title: s_('UserProfile|Subscribe'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }})
= render 'users/view_user_in_admin_area'
= render 'users/follow_user'
diff --git a/app/views/users/terms/index.html.haml b/app/views/users/terms/index.html.haml
index afe257c2fc2..9f7c17dad9a 100644
--- a/app/views/users/terms/index.html.haml
+++ b/app/views/users/terms/index.html.haml
@@ -1,7 +1,7 @@
- content_for :page_specific_javascripts do
- = render "layouts/google_tag_manager_head"
+ = render_if_exists "layouts/google_tag_manager_head"
= render "layouts/one_trust"
= render "layouts/bizible"
-= render "layouts/google_tag_manager_body"
+= render_if_exists "layouts/google_tag_manager_body"
#js-terms-of-service{ data: { terms_data: terms_data(@term, @redirect) } }
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 6ef7447b9da..e5b860ba525 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -174,15 +174,6 @@
:weight: 1
:idempotent: true
:tags: []
-- :name: container_repository:delete_container_repository
- :worker_name: DeleteContainerRepositoryWorker
- :feature_category: :container_registry
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: false
- :tags: []
- :name: container_repository_delete:container_registry_delete_container_repository
:worker_name: ContainerRegistry::DeleteContainerRepositoryWorker
:feature_category: :container_registry
@@ -300,6 +291,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: cronjob:ci_schedule_unlock_pipelines_in_queue_cron
+ :worker_name: Ci::ScheduleUnlockPipelinesInQueueCronWorker
+ :feature_category: :build_artifacts
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:ci_stuck_builds_drop_running
:worker_name: Ci::StuckBuilds::DropRunningWorker
:feature_category: :continuous_integration
@@ -327,6 +327,15 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: cronjob:click_house_events_sync
+ :worker_name: ClickHouse::EventsSyncWorker
+ :feature_category: :value_stream_management
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:container_expiration_policy
:worker_name: ContainerExpirationPolicyWorker
:feature_category: :container_registry
@@ -642,6 +651,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: cronjob:pages_deactivated_deployments_delete_cron
+ :worker_name: Pages::DeactivatedDeploymentsDeleteCronWorker
+ :feature_category: :pages
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:pages_domain_removal_cron
:worker_name: PagesDomainRemovalCronWorker
:feature_category: :pages
@@ -1920,6 +1938,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: pipeline_background:ci_refs_unlock_previous_pipelines
+ :worker_name: Ci::Refs::UnlockPreviousPipelinesWorker
+ :feature_category: :continuous_integration
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: pipeline_background:ci_test_failure_history
:worker_name: Ci::TestFailureHistoryWorker
:feature_category: :continuous_integration
@@ -2352,6 +2379,33 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: bitbucket_import_import_issue
+ :worker_name: Gitlab::BitbucketImport::ImportIssueWorker
+ :feature_category: :importers
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
+- :name: bitbucket_import_import_issue_notes
+ :worker_name: Gitlab::BitbucketImport::ImportIssueNotesWorker
+ :feature_category: :importers
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
+- :name: bitbucket_import_import_lfs_object
+ :worker_name: Gitlab::BitbucketImport::ImportLfsObjectWorker
+ :feature_category: :importers
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
- :name: bitbucket_import_import_pull_request
:worker_name: Gitlab::BitbucketImport::ImportPullRequestWorker
:feature_category: :importers
@@ -2361,6 +2415,15 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: bitbucket_import_import_pull_request_notes
+ :worker_name: Gitlab::BitbucketImport::ImportPullRequestNotesWorker
+ :feature_category: :importers
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
- :name: bitbucket_import_stage_finish_import
:worker_name: Gitlab::BitbucketImport::Stage::FinishImportWorker
:feature_category: :importers
@@ -2370,6 +2433,33 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: bitbucket_import_stage_import_issues
+ :worker_name: Gitlab::BitbucketImport::Stage::ImportIssuesWorker
+ :feature_category: :importers
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
+- :name: bitbucket_import_stage_import_issues_notes
+ :worker_name: Gitlab::BitbucketImport::Stage::ImportIssuesNotesWorker
+ :feature_category: :importers
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
+- :name: bitbucket_import_stage_import_lfs_objects
+ :worker_name: Gitlab::BitbucketImport::Stage::ImportLfsObjectsWorker
+ :feature_category: :importers
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
- :name: bitbucket_import_stage_import_pull_requests
:worker_name: Gitlab::BitbucketImport::Stage::ImportPullRequestsWorker
:feature_category: :importers
@@ -2379,6 +2469,15 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: bitbucket_import_stage_import_pull_requests_notes
+ :worker_name: Gitlab::BitbucketImport::Stage::ImportPullRequestsNotesWorker
+ :feature_category: :importers
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
- :name: bitbucket_import_stage_import_repository
:worker_name: Gitlab::BitbucketImport::Stage::ImportRepositoryWorker
:feature_category: :importers
@@ -2631,10 +2730,10 @@
:weight: 1
:idempotent: true
:tags: []
-- :name: click_house_events_sync
- :worker_name: ClickHouse::EventsSyncWorker
- :feature_category: :value_stream_management
- :has_external_dependencies: true
+- :name: ci_unlock_pipelines_in_queue
+ :worker_name: Ci::UnlockPipelinesInQueueWorker
+ :feature_category: :build_artifacts
+ :has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -2811,6 +2910,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: environments_stop_job_failed
+ :worker_name: Environments::StopJobFailedWorker
+ :feature_category: :continuous_delivery
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: environments_stop_job_success
:worker_name: Environments::StopJobSuccessWorker
:feature_category: :continuous_delivery
@@ -2883,15 +2991,6 @@
:weight: 1
:idempotent: true
:tags: []
-- :name: gitlab_shell
- :worker_name: GitlabShellWorker
- :feature_category: :source_code_management
- :has_external_dependencies: false
- :urgency: :high
- :resource_boundary: :unknown
- :weight: 2
- :idempotent: false
- :tags: []
- :name: google_cloud_create_cloudsql_instance
:worker_name: GoogleCloud::CreateCloudsqlInstanceWorker
:feature_category: :not_owned
@@ -3045,6 +3144,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: issuable_related_links_create
+ :worker_name: Issuable::RelatedLinksCreateWorker
+ :feature_category: :portfolio_management
+ :has_external_dependencies: false
+ :urgency: :high
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: issuables_clear_groups_issue_counter
:worker_name: Issuables::ClearGroupsIssueCounterWorker
:feature_category: :team_planning
@@ -3524,7 +3632,7 @@
:tags: []
- :name: projects_record_target_platforms
:worker_name: Projects::RecordTargetPlatformsWorker
- :feature_category: :experimentation_activation
+ :feature_category: :activation
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
diff --git a/app/workers/bulk_import_worker.rb b/app/workers/bulk_import_worker.rb
index 83b881ee525..5b9b46081cc 100644
--- a/app/workers/bulk_import_worker.rb
+++ b/app/workers/bulk_import_worker.rb
@@ -3,124 +3,14 @@
class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- PERFORM_DELAY = 5.seconds
- DEFAULT_BATCH_SIZE = 5
-
data_consistency :always
feature_category :importers
sidekiq_options retry: false, dead: false
def perform(bulk_import_id)
- @bulk_import = BulkImport.find_by_id(bulk_import_id)
-
- return unless @bulk_import
- return if @bulk_import.finished? || @bulk_import.failed?
- return @bulk_import.fail_op! if all_entities_failed?
- return @bulk_import.finish! if all_entities_processed? && @bulk_import.started?
- return re_enqueue if max_batch_size_exceeded? # Do not start more jobs if max allowed are already running
-
- @bulk_import.start! if @bulk_import.created?
-
- created_entities.first(next_batch_size).each do |entity|
- create_tracker(entity)
-
- entity.start!
-
- BulkImports::ExportRequestWorker.perform_async(entity.id)
- end
-
- re_enqueue
- rescue StandardError => e
- Gitlab::ErrorTracking.track_exception(e, bulk_import_id: @bulk_import&.id)
-
- @bulk_import&.fail_op
- end
-
- private
-
- def entities
- @entities ||= @bulk_import.entities
- end
-
- def created_entities
- entities.with_status(:created)
- end
-
- def all_entities_processed?
- entities.all? { |entity| entity.finished? || entity.failed? }
- end
-
- def all_entities_failed?
- entities.all?(&:failed?)
- end
-
- # A new BulkImportWorker job is enqueued to either
- # - Process the new BulkImports::Entity created during import (e.g. for the subgroups)
- # - Or to mark the `bulk_import` as finished
- def re_enqueue
- BulkImportWorker.perform_in(PERFORM_DELAY, @bulk_import.id)
- end
-
- def started_entities
- entities.with_status(:started)
- end
-
- def max_batch_size_exceeded?
- started_entities.count >= DEFAULT_BATCH_SIZE
- end
-
- def next_batch_size
- [DEFAULT_BATCH_SIZE - started_entities.count, 0].max
- end
-
- def create_tracker(entity)
- entity.class.transaction do
- entity.pipelines.each do |pipeline|
- status = skip_pipeline?(pipeline, entity) ? :skipped : :created
-
- entity.trackers.create!(
- stage: pipeline[:stage],
- pipeline_name: pipeline[:pipeline],
- status: BulkImports::Tracker.state_machine.states[status].value
- )
- end
- end
- end
-
- def skip_pipeline?(pipeline, entity)
- return false unless entity.source_version.valid?
-
- minimum_version, maximum_version = pipeline.values_at(:minimum_source_version, :maximum_source_version)
-
- if source_version_out_of_range?(minimum_version, maximum_version, entity.source_version.without_patch)
- log_skipped_pipeline(pipeline, entity, minimum_version, maximum_version)
- return true
- end
-
- false
- end
-
- def source_version_out_of_range?(minimum_version, maximum_version, non_patch_source_version)
- (minimum_version && non_patch_source_version < Gitlab::VersionInfo.parse(minimum_version)) ||
- (maximum_version && non_patch_source_version > Gitlab::VersionInfo.parse(maximum_version))
- end
-
- def log_skipped_pipeline(pipeline, entity, minimum_version, maximum_version)
- logger.info(
- message: 'Pipeline skipped as source instance version not compatible with pipeline',
- bulk_import_entity_id: entity.id,
- bulk_import_id: entity.bulk_import_id,
- bulk_import_entity_type: entity.source_type,
- source_full_path: entity.source_full_path,
- pipeline_name: pipeline[:pipeline],
- minimum_source_version: minimum_version,
- maximum_source_version: maximum_version,
- source_version: entity.source_version.to_s,
- importer: 'gitlab_migration'
- )
- end
+ bulk_import = BulkImport.find_by_id(bulk_import_id)
+ return unless bulk_import
- def logger
- @logger ||= Gitlab::Import::Logger.build
+ BulkImports::ProcessService.new(bulk_import).execute
end
end
diff --git a/app/workers/bulk_imports/entity_worker.rb b/app/workers/bulk_imports/entity_worker.rb
index fb99d63d06e..9b60dcdeb8a 100644
--- a/app/workers/bulk_imports/entity_worker.rb
+++ b/app/workers/bulk_imports/entity_worker.rb
@@ -1,97 +1,68 @@
# frozen_string_literal: true
module BulkImports
- class EntityWorker # rubocop:disable Scalability/IdempotentWorker
+ class EntityWorker
include ApplicationWorker
idempotent!
- deduplicate :until_executing
+ deduplicate :until_executed
data_consistency :always
feature_category :importers
sidekiq_options retry: false, dead: false
worker_has_external_dependencies!
- def perform(entity_id, current_stage = nil)
+ PERFORM_DELAY = 5.seconds
+
+ # Keep `_current_stage` parameter for backwards compatibility.
+ # The parameter will be remove in https://gitlab.com/gitlab-org/gitlab/-/issues/426311
+ def perform(entity_id, _current_stage = nil)
@entity = ::BulkImports::Entity.find(entity_id)
- if stage_running?(entity_id, current_stage)
- logger.info(
- structured_payload(
- bulk_import_entity_id: entity_id,
- bulk_import_id: entity.bulk_import_id,
- bulk_import_entity_type: entity.source_type,
- source_full_path: entity.source_full_path,
- current_stage: current_stage,
- message: 'Stage running',
- source_version: source_version,
- importer: 'gitlab_migration'
- )
- )
+ return unless @entity.started?
- return
+ if running_tracker.present?
+ log_info(message: 'Stage running', entity_stage: running_tracker.stage)
+ else
+ start_next_stage
end
- logger.info(
- structured_payload(
- bulk_import_entity_id: entity_id,
- bulk_import_id: entity.bulk_import_id,
- bulk_import_entity_type: entity.source_type,
- source_full_path: entity.source_full_path,
- current_stage: current_stage,
- message: 'Stage starting',
- source_version: source_version,
- importer: 'gitlab_migration'
- )
- )
-
- next_pipeline_trackers_for(entity_id).each do |pipeline_tracker|
- BulkImports::PipelineWorker.perform_async(
- pipeline_tracker.id,
- pipeline_tracker.stage,
- entity_id
- )
- end
+ re_enqueue
rescue StandardError => e
- log_exception(e,
- {
- bulk_import_entity_id: entity_id,
- bulk_import_id: entity.bulk_import_id,
- bulk_import_entity_type: entity.source_type,
- source_full_path: entity.source_full_path,
- current_stage: current_stage,
- message: 'Entity failed',
- source_version: source_version,
- importer: 'gitlab_migration'
- }
- )
-
- Gitlab::ErrorTracking.track_exception(
- e,
- bulk_import_entity_id: entity_id,
- bulk_import_id: entity.bulk_import_id,
- bulk_import_entity_type: entity.source_type,
- source_full_path: entity.source_full_path,
- source_version: source_version,
- importer: 'gitlab_migration'
- )
+ Gitlab::ErrorTracking.track_exception(e, log_params(message: 'Entity failed'))
- entity.fail_op!
+ @entity.fail_op!
end
private
attr_reader :entity
- def stage_running?(entity_id, stage)
- return unless stage
+ def re_enqueue
+ BulkImports::EntityWorker.perform_in(PERFORM_DELAY, entity.id)
+ end
- BulkImports::Tracker.stage_running?(entity_id, stage)
+ def running_tracker
+ @running_tracker ||= BulkImports::Tracker.running_trackers(entity.id).first
end
def next_pipeline_trackers_for(entity_id)
BulkImports::Tracker.next_pipeline_trackers_for(entity_id).update(status_event: 'enqueue')
end
+ def start_next_stage
+ next_pipeline_trackers = next_pipeline_trackers_for(entity.id)
+
+ next_pipeline_trackers.each_with_index do |pipeline_tracker, index|
+ log_info(message: 'Stage starting', entity_stage: pipeline_tracker.stage) if index == 0
+
+ BulkImports::PipelineWorker.perform_async(
+ pipeline_tracker.id,
+ pipeline_tracker.stage,
+ entity.id
+ )
+ end
+ end
+
def source_version
entity.bulk_import.source_version_info.to_s
end
@@ -105,5 +76,22 @@ module BulkImports
logger.error(structured_payload(payload))
end
+
+ def log_info(payload)
+ logger.info(structured_payload(log_params(payload)))
+ end
+
+ def log_params(extra)
+ defaults = {
+ bulk_import_entity_id: entity.id,
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
+ source_version: source_version,
+ importer: 'gitlab_migration'
+ }
+
+ defaults.merge(extra)
+ end
end
end
diff --git a/app/workers/bulk_imports/finish_batched_pipeline_worker.rb b/app/workers/bulk_imports/finish_batched_pipeline_worker.rb
index 4200d0e4a0f..b1f3757e058 100644
--- a/app/workers/bulk_imports/finish_batched_pipeline_worker.rb
+++ b/app/workers/bulk_imports/finish_batched_pipeline_worker.rb
@@ -12,6 +12,8 @@ module BulkImports
data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency
feature_category :importers
+ version 2
+
def perform(pipeline_tracker_id)
@tracker = Tracker.find(pipeline_tracker_id)
@@ -27,7 +29,9 @@ module BulkImports
end
ensure
- ::BulkImports::EntityWorker.perform_async(tracker.entity.id, tracker.stage)
+ # This is needed for in-flight migrations.
+ # It will be remove in https://gitlab.com/gitlab-org/gitlab/-/issues/426299
+ ::BulkImports::EntityWorker.perform_async(tracker.entity.id) if job_version.nil?
end
private
@@ -39,7 +43,7 @@ module BulkImports
end
def import_in_progress?
- tracker.batches.any?(&:started?)
+ tracker.batches.any? { |b| b.started? || b.created? }
end
end
end
diff --git a/app/workers/bulk_imports/pipeline_batch_worker.rb b/app/workers/bulk_imports/pipeline_batch_worker.rb
index 634d7ed3c87..6230d517641 100644
--- a/app/workers/bulk_imports/pipeline_batch_worker.rb
+++ b/app/workers/bulk_imports/pipeline_batch_worker.rb
@@ -14,15 +14,16 @@ module BulkImports
def perform(batch_id)
@batch = ::BulkImports::BatchTracker.find(batch_id)
@tracker = @batch.tracker
+ @pending_retry = false
try_obtain_lease { run }
ensure
- ::BulkImports::FinishBatchedPipelineWorker.perform_async(tracker.id)
+ ::BulkImports::FinishBatchedPipelineWorker.perform_async(tracker.id) unless pending_retry
end
private
- attr_reader :batch, :tracker
+ attr_reader :batch, :tracker, :pending_retry
def run
return batch.skip! if tracker.failed? || tracker.finished?
@@ -31,6 +32,7 @@ module BulkImports
tracker.pipeline_class.new(context).run
batch.finish!
rescue BulkImports::RetryPipelineError => e
+ @pending_retry = true
retry_batch(e)
rescue StandardError => e
fail_batch(e)
diff --git a/app/workers/bulk_imports/pipeline_worker.rb b/app/workers/bulk_imports/pipeline_worker.rb
index 098e167ac29..24185f43795 100644
--- a/app/workers/bulk_imports/pipeline_worker.rb
+++ b/app/workers/bulk_imports/pipeline_worker.rb
@@ -14,7 +14,10 @@ module BulkImports
deduplicate :until_executing
worker_resource_boundary :memory
- def perform(pipeline_tracker_id, stage, entity_id)
+ version 2
+
+ # Keep _stage parameter for backwards compatibility.
+ def perform(pipeline_tracker_id, _stage, entity_id)
@entity = ::BulkImports::Entity.find(entity_id)
@pipeline_tracker = ::BulkImports::Tracker.find(pipeline_tracker_id)
@@ -32,7 +35,9 @@ module BulkImports
end
end
ensure
- ::BulkImports::EntityWorker.perform_async(entity_id, stage)
+ # This is needed for in-flight migrations.
+ # It will be remove in https://gitlab.com/gitlab-org/gitlab/-/issues/426299
+ ::BulkImports::EntityWorker.perform_async(entity_id) if job_version.nil?
end
private
diff --git a/app/workers/ci/initial_pipeline_process_worker.rb b/app/workers/ci/initial_pipeline_process_worker.rb
index 067dbb7492f..703cae8bf88 100644
--- a/app/workers/ci/initial_pipeline_process_worker.rb
+++ b/app/workers/ci/initial_pipeline_process_worker.rb
@@ -28,6 +28,8 @@ module Ci
private
def create_deployments!(pipeline)
+ return if Feature.enabled?(:create_deployment_only_for_processable_jobs, pipeline.project)
+
pipeline.stages.flat_map(&:statuses).each { |build| create_deployment(build) }
end
diff --git a/app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb b/app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb
index 98bb259db0a..8bcbe9d6c9f 100644
--- a/app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb
+++ b/app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module Ci
module MergeRequests
class AddTodoWhenBuildFailsWorker
diff --git a/app/workers/ci/ref_delete_unlock_artifacts_worker.rb b/app/workers/ci/ref_delete_unlock_artifacts_worker.rb
index aeadf111bfb..e343c0aedd4 100644
--- a/app/workers/ci/ref_delete_unlock_artifacts_worker.rb
+++ b/app/workers/ci/ref_delete_unlock_artifacts_worker.rb
@@ -13,17 +13,21 @@ module Ci
def perform(project_id, user_id, ref_path)
::Project.find_by_id(project_id).try do |project|
- ::User.find_by_id(user_id).try do |user|
+ ::User.find_by_id(user_id).try do |_|
project.ci_refs.find_by_ref_path(ref_path).try do |ci_ref|
- results = ::Ci::UnlockArtifactsService
- .new(project, user)
- .execute(ci_ref)
-
- log_extra_metadata_on_done(:unlocked_pipelines, results[:unlocked_pipelines])
- log_extra_metadata_on_done(:unlocked_job_artifacts, results[:unlocked_job_artifacts])
+ enqueue_pipelines_to_unlock(ci_ref)
end
end
end
end
+
+ private
+
+ def enqueue_pipelines_to_unlock(ci_ref)
+ result = ::Ci::Refs::EnqueuePipelinesToUnlockService.new.execute(ci_ref)
+
+ log_extra_metadata_on_done(:total_pending_entries, result[:total_pending_entries])
+ log_extra_metadata_on_done(:total_new_entries, result[:total_new_entries])
+ end
end
end
diff --git a/app/workers/ci/refs/unlock_previous_pipelines_worker.rb b/app/workers/ci/refs/unlock_previous_pipelines_worker.rb
new file mode 100644
index 00000000000..bf595590cb1
--- /dev/null
+++ b/app/workers/ci/refs/unlock_previous_pipelines_worker.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Ci
+ module Refs
+ class UnlockPreviousPipelinesWorker
+ include ApplicationWorker
+
+ data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency
+
+ sidekiq_options retry: 3
+ include PipelineBackgroundQueue
+
+ idempotent!
+
+ def perform(ref_id)
+ ::Ci::Ref.find_by_id(ref_id).try do |ref|
+ pipeline = ref.last_finished_pipeline
+ result = ::Ci::Refs::EnqueuePipelinesToUnlockService.new.execute(ref, before_pipeline: pipeline)
+
+ log_extra_metadata_on_done(:total_pending_entries, result[:total_pending_entries])
+ log_extra_metadata_on_done(:total_new_entries, result[:total_new_entries])
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/ci/schedule_unlock_pipelines_in_queue_cron_worker.rb b/app/workers/ci/schedule_unlock_pipelines_in_queue_cron_worker.rb
new file mode 100644
index 00000000000..1a593326120
--- /dev/null
+++ b/app/workers/ci/schedule_unlock_pipelines_in_queue_cron_worker.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Ci
+ class ScheduleUnlockPipelinesInQueueCronWorker
+ include ApplicationWorker
+
+ data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency
+
+ # rubocop:disable Scalability/CronWorkerContext
+ # This worker does not perform work scoped to a context
+ include CronjobQueue
+ # rubocop:enable Scalability/CronWorkerContext
+
+ feature_category :build_artifacts
+ idempotent!
+
+ def perform(...)
+ Ci::UnlockPipelinesInQueueWorker.perform_with_capacity(...)
+ end
+ end
+end
diff --git a/app/workers/ci/unlock_pipelines_in_queue_worker.rb b/app/workers/ci/unlock_pipelines_in_queue_worker.rb
new file mode 100644
index 00000000000..de579504711
--- /dev/null
+++ b/app/workers/ci/unlock_pipelines_in_queue_worker.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Ci
+ class UnlockPipelinesInQueueWorker
+ include ApplicationWorker
+
+ data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency
+
+ include LimitedCapacity::Worker
+
+ feature_category :build_artifacts
+ idempotent!
+
+ MAX_RUNNING_LOW = 50
+ MAX_RUNNING_MEDIUM = 500
+ MAX_RUNNING_HIGH = 1500
+
+ def perform_work(*_)
+ pipeline_id, enqueue_timestamp = Ci::UnlockPipelineRequest.next!
+ return log_extra_metadata_on_done(:remaining_pending, 0) unless pipeline_id
+
+ Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
+ log_extra_metadata_on_done(:pipeline_id, pipeline.id)
+ log_extra_metadata_on_done(:project, pipeline.project.full_path)
+
+ result = Ci::UnlockPipelineService.new(pipeline).execute
+
+ log_extra_metadata_on_done(:unlock_wait_time, Time.current.utc.to_i - enqueue_timestamp)
+ log_extra_metadata_on_done(:remaining_pending, Ci::UnlockPipelineRequest.total_pending)
+ log_extra_metadata_on_done(:skipped_already_leased, result[:skipped_already_leased])
+ log_extra_metadata_on_done(:skipped_already_unlocked, result[:skipped_already_unlocked])
+ log_extra_metadata_on_done(:exec_timeout, result[:exec_timeout])
+ log_extra_metadata_on_done(:unlocked_job_artifacts, result[:unlocked_job_artifacts])
+ log_extra_metadata_on_done(:unlocked_pipeline_artifacts, result[:unlocked_pipeline_artifacts])
+ end
+ end
+
+ def remaining_work_count(*_)
+ Ci::UnlockPipelineRequest.total_pending
+ end
+
+ def max_running_jobs
+ if ::Feature.enabled?(:ci_unlock_pipelines_high, type: :ops)
+ MAX_RUNNING_HIGH
+ elsif ::Feature.enabled?(:ci_unlock_pipelines_medium, type: :ops)
+ MAX_RUNNING_MEDIUM
+ elsif ::Feature.enabled?(:ci_unlock_pipelines, type: :ops)
+ # This is the default enabled flag
+ MAX_RUNNING_LOW
+ else
+ 0
+ end
+ end
+ end
+end
diff --git a/app/workers/click_house/events_sync_worker.rb b/app/workers/click_house/events_sync_worker.rb
index 5b7398cb071..e884a43b1e3 100644
--- a/app/workers/click_house/events_sync_worker.rb
+++ b/app/workers/click_house/events_sync_worker.rb
@@ -6,6 +6,7 @@ module ClickHouse
include Gitlab::ExclusiveLeaseHelpers
idempotent!
+ queue_namespace :cronjob
data_consistency :delayed
worker_has_external_dependencies! # the worker interacts with a ClickHouse database
feature_category :value_stream_management
diff --git a/app/workers/concerns/auto_devops_queue.rb b/app/workers/concerns/auto_devops_queue.rb
index 61e3c1544bd..cdf429a8be5 100644
--- a/app/workers/concerns/auto_devops_queue.rb
+++ b/app/workers/concerns/auto_devops_queue.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-#
+
module AutoDevopsQueue
extend ActiveSupport::Concern
diff --git a/app/workers/concerns/chaos_queue.rb b/app/workers/concerns/chaos_queue.rb
index 23e58b5182b..9a3d518dda8 100644
--- a/app/workers/concerns/chaos_queue.rb
+++ b/app/workers/concerns/chaos_queue.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-#
+
module ChaosQueue
extend ActiveSupport::Concern
diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb
index e190ced5073..fcc7a96fa2b 100644
--- a/app/workers/concerns/gitlab/github_import/object_importer.rb
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -10,7 +10,6 @@ module Gitlab
included do
include ApplicationWorker
- sidekiq_options retry: 3
include GithubImport::Queue
include ReschedulingMethods
@@ -19,11 +18,8 @@ module Gitlab
sidekiq_retries_exhausted do |msg|
args = msg['args']
- correlation_id = msg['correlation_id']
jid = msg['jid']
- new.perform_failure(args[0], args[1], correlation_id)
-
# If a job is being exhausted we still want to notify the
# Gitlab::Import::AdvanceStageWorker to prevent the entire import from getting stuck
if args.length == 3 && (key = args.last) && key.is_a?(String)
@@ -64,29 +60,15 @@ module Gitlab
rescue NoMethodError => e
# This exception will be more useful in development when a new
# Representation is created but the developer forgot to add a
- # `:github_identifiers` field.
+ # `#github_identifiers` method.
track_and_raise_exception(project, e, fail_import: true)
rescue ActiveRecord::RecordInvalid, NotRetriableError => e
# We do not raise exception to prevent job retry
- failure = track_exception(project, e)
- add_identifiers_to_failure(failure, object.github_identifiers)
+ track_exception(project, e)
rescue StandardError => e
track_and_raise_exception(project, e)
end
- # hash - A Hash containing the details of the object to import.
- def perform_failure(project_id, hash, correlation_id)
- project = Project.find_by_id(project_id)
- return unless project
-
- failure = project.import_failures.failures_by_correlation_id(correlation_id).first
- return unless failure
-
- object = representation_class.from_json_hash(hash)
-
- add_identifiers_to_failure(failure, object.github_identifiers)
- end
-
def increment_object_counter?(_object)
true
end
@@ -118,16 +100,20 @@ module Gitlab
extra.merge(
project_id: project_id,
importer: importer_class.name,
- github_identifiers: github_identifiers
+ external_identifiers: github_identifiers
)
end
def track_exception(project, exception, fail_import: false)
+ external_identifiers = github_identifiers || {}
+ external_identifiers[:object_type] ||= object_type&.to_s
+
Gitlab::Import::ImportFailureService.track(
project_id: project.id,
error_source: importer_class.name,
exception: exception,
- fail_import: fail_import
+ fail_import: fail_import,
+ external_identifiers: external_identifiers
)
end
@@ -136,12 +122,6 @@ module Gitlab
raise(exception)
end
-
- def add_identifiers_to_failure(failure, external_identifiers)
- external_identifiers[:object_type] = object_type
-
- failure.update_column(:external_identifiers, external_identifiers)
- end
end
end
end
diff --git a/app/workers/concerns/gitlab/github_import/queue.rb b/app/workers/concerns/gitlab/github_import/queue.rb
index e7156ac12f8..7cc23dd7c0b 100644
--- a/app/workers/concerns/gitlab/github_import/queue.rb
+++ b/app/workers/concerns/gitlab/github_import/queue.rb
@@ -15,14 +15,6 @@ module Gitlab
# this is better than a project being stuck in the "import" state
# forever.
sidekiq_options dead: false, retry: 5
-
- sidekiq_retries_exhausted do |msg, e|
- Gitlab::Import::ImportFailureService.track(
- project_id: msg['args'][0],
- exception: e,
- fail_import: true
- )
- end
end
end
end
diff --git a/app/workers/concerns/gitlab/github_import/stage_methods.rb b/app/workers/concerns/gitlab/github_import/stage_methods.rb
index 75db5589415..80013ff3cd9 100644
--- a/app/workers/concerns/gitlab/github_import/stage_methods.rb
+++ b/app/workers/concerns/gitlab/github_import/stage_methods.rb
@@ -3,6 +3,21 @@
module Gitlab
module GithubImport
module StageMethods
+ extend ActiveSupport::Concern
+
+ included do
+ include ApplicationWorker
+
+ sidekiq_retries_exhausted do |msg, e|
+ Gitlab::Import::ImportFailureService.track(
+ project_id: msg['args'][0],
+ exception: e,
+ error_source: self.class.name,
+ fail_import: true
+ )
+ end
+ end
+
# project_id - The ID of the GitLab project to import the data into.
def perform(project_id)
info(project_id, message: 'starting stage')
@@ -29,7 +44,8 @@ module Gitlab
project_id: project_id,
exception: e,
error_source: self.class.name,
- fail_import: abort_on_failure
+ fail_import: false,
+ metrics: true
)
raise(e)
@@ -51,10 +67,6 @@ module Gitlab
# rubocop: enable CodeReuse/ActiveRecord
end
- def abort_on_failure
- false
- end
-
private
def info(project_id, extra = {})
diff --git a/app/workers/concerns/limited_capacity/job_tracker.rb b/app/workers/concerns/limited_capacity/job_tracker.rb
index 4b5ce8a01f6..b4d884f914d 100644
--- a/app/workers/concerns/limited_capacity/job_tracker.rb
+++ b/app/workers/concerns/limited_capacity/job_tracker.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module LimitedCapacity
class JobTracker # rubocop:disable Scalability/IdempotentWorker
include Gitlab::Utils::StrongMemoize
diff --git a/app/workers/concerns/limited_capacity/worker.rb b/app/workers/concerns/limited_capacity/worker.rb
index af66d80b3e9..0a79c5c46d5 100644
--- a/app/workers/concerns/limited_capacity/worker.rb
+++ b/app/workers/concerns/limited_capacity/worker.rb
@@ -1,41 +1,5 @@
# frozen_string_literal: true
-# Usage:
-#
-# Worker that performs the tasks:
-#
-# class DummyWorker
-# include ApplicationWorker
-# include LimitedCapacity::Worker
-#
-# # For each job that raises any error, a worker instance will be disabled
-# # until the next schedule-run.
-# # If you wish to get around this, exceptions must by handled by the implementer.
-# #
-# def perform_work(*args)
-# end
-#
-# def remaining_work_count(*args)
-# 5
-# end
-#
-# def max_running_jobs
-# 25
-# end
-# end
-#
-# Cron worker to fill the pool of regular workers:
-#
-# class ScheduleDummyCronWorker
-# include ApplicationWorker
-# include CronjobQueue
-#
-# def perform(*args)
-# DummyWorker.perform_with_capacity(*args)
-# end
-# end
-#
-
module LimitedCapacity
module Worker
extend ActiveSupport::Concern
diff --git a/app/workers/concerns/worker_attributes.rb b/app/workers/concerns/worker_attributes.rb
index 02eda924b71..cb09aaf1a6a 100644
--- a/app/workers/concerns/worker_attributes.rb
+++ b/app/workers/concerns/worker_attributes.rb
@@ -155,6 +155,10 @@ module WorkerAttributes
::Gitlab::SidekiqMiddleware::PauseControl::WorkersMap.set_strategy_for(strategy: value, worker: self)
end
+ def get_pause_control
+ ::Gitlab::SidekiqMiddleware::PauseControl::WorkersMap.strategy_for(worker: self)
+ end
+
def get_weight
get_class_attribute(:weight) ||
NAMESPACE_WEIGHTS[queue_namespace] ||
diff --git a/app/workers/database/batched_background_migration/ci_database_worker.rb b/app/workers/database/batched_background_migration/ci_database_worker.rb
index 58b0f5496f4..417af4c7172 100644
--- a/app/workers/database/batched_background_migration/ci_database_worker.rb
+++ b/app/workers/database/batched_background_migration/ci_database_worker.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module Database
module BatchedBackgroundMigration
class CiDatabaseWorker # rubocop:disable Scalability/IdempotentWorker
diff --git a/app/workers/delete_container_repository_worker.rb b/app/workers/delete_container_repository_worker.rb
deleted file mode 100644
index d0552dce9fc..00000000000
--- a/app/workers/delete_container_repository_worker.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-class DeleteContainerRepositoryWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
- include ExclusiveLeaseGuard
-
- data_consistency :always
-
- sidekiq_options retry: 3
-
- queue_namespace :container_repository
- feature_category :container_registry
-
- def perform(current_user_id, container_repository_id); end
-end
diff --git a/app/workers/environments/stop_job_failed_worker.rb b/app/workers/environments/stop_job_failed_worker.rb
new file mode 100644
index 00000000000..c04601e0428
--- /dev/null
+++ b/app/workers/environments/stop_job_failed_worker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Environments
+ class StopJobFailedWorker
+ include ApplicationWorker
+
+ data_consistency :delayed
+ idempotent!
+ feature_category :continuous_delivery
+
+ def perform(job_id, _params = {})
+ Ci::Processable.find_by_id(job_id).try do |job|
+ revert_environment(job) if job.stops_environment? && job.failed?
+ end
+ end
+
+ private
+
+ def revert_environment(job)
+ return if job.persisted_environment.nil?
+
+ job.persisted_environment.fire_state_event(:recover_stuck_stopping)
+ end
+ end
+end
diff --git a/app/workers/gitlab/bitbucket_import/advance_stage_worker.rb b/app/workers/gitlab/bitbucket_import/advance_stage_worker.rb
index 7f281352a1b..ed89f332652 100644
--- a/app/workers/gitlab/bitbucket_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/bitbucket_import/advance_stage_worker.rb
@@ -20,13 +20,23 @@ module Gitlab
# The known importer stages and their corresponding Sidekiq workers.
STAGES = {
+ repository: Stage::ImportRepositoryWorker,
+ pull_requests: Stage::ImportPullRequestsWorker,
+ pull_requests_notes: Stage::ImportPullRequestsNotesWorker,
+ issues: Stage::ImportIssuesWorker,
+ issues_notes: Stage::ImportIssuesNotesWorker,
+ lfs_objects: Stage::ImportLfsObjectsWorker,
finish: Stage::FinishImportWorker
}.freeze
- def find_import_state(project_id)
+ def find_import_state_jid(project_id)
ProjectImportState.jid_by(project_id: project_id, status: :started)
end
+ def find_import_state(id)
+ ProjectImportState.find(id)
+ end
+
private
def next_stage_worker(next_stage)
diff --git a/app/workers/gitlab/bitbucket_import/import_issue_notes_worker.rb b/app/workers/gitlab/bitbucket_import/import_issue_notes_worker.rb
new file mode 100644
index 00000000000..de8239f30d9
--- /dev/null
+++ b/app/workers/gitlab/bitbucket_import/import_issue_notes_worker.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BitbucketImport
+ class ImportIssueNotesWorker # rubocop:disable Scalability/IdempotentWorker
+ include ObjectImporter
+
+ def importer_class
+ Importers::IssueNotesImporter
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/bitbucket_import/import_issue_worker.rb b/app/workers/gitlab/bitbucket_import/import_issue_worker.rb
new file mode 100644
index 00000000000..7df3f6d4a62
--- /dev/null
+++ b/app/workers/gitlab/bitbucket_import/import_issue_worker.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BitbucketImport
+ class ImportIssueWorker # rubocop:disable Scalability/IdempotentWorker
+ include ObjectImporter
+
+ def importer_class
+ Importers::IssueImporter
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/bitbucket_import/import_lfs_object_worker.rb b/app/workers/gitlab/bitbucket_import/import_lfs_object_worker.rb
new file mode 100644
index 00000000000..39b66684026
--- /dev/null
+++ b/app/workers/gitlab/bitbucket_import/import_lfs_object_worker.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BitbucketImport
+ class ImportLfsObjectWorker # rubocop:disable Scalability/IdempotentWorker
+ include ObjectImporter
+
+ def importer_class
+ Importers::LfsObjectImporter
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/bitbucket_import/import_pull_request_notes_worker.rb b/app/workers/gitlab/bitbucket_import/import_pull_request_notes_worker.rb
new file mode 100644
index 00000000000..8c9f84c97a5
--- /dev/null
+++ b/app/workers/gitlab/bitbucket_import/import_pull_request_notes_worker.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BitbucketImport
+ class ImportPullRequestNotesWorker # rubocop:disable Scalability/IdempotentWorker
+ include ObjectImporter
+
+ def importer_class
+ Importers::PullRequestNotesImporter
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/bitbucket_import/stage/import_issues_notes_worker.rb b/app/workers/gitlab/bitbucket_import/stage/import_issues_notes_worker.rb
new file mode 100644
index 00000000000..cbd67099086
--- /dev/null
+++ b/app/workers/gitlab/bitbucket_import/stage/import_issues_notes_worker.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BitbucketImport
+ module Stage
+ class ImportIssuesNotesWorker # rubocop:disable Scalability/IdempotentWorker
+ include StageMethods
+
+ private
+
+ # project - An instance of Project.
+ def import(project)
+ waiter = importer_class.new(project).execute
+
+ project.import_state.refresh_jid_expiration
+
+ AdvanceStageWorker.perform_async(
+ project.id,
+ { waiter.key => waiter.jobs_remaining },
+ :lfs_objects
+ )
+ end
+
+ def importer_class
+ Importers::IssuesNotesImporter
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/bitbucket_import/stage/import_issues_worker.rb b/app/workers/gitlab/bitbucket_import/stage/import_issues_worker.rb
new file mode 100644
index 00000000000..31a11d802c7
--- /dev/null
+++ b/app/workers/gitlab/bitbucket_import/stage/import_issues_worker.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BitbucketImport
+ module Stage
+ class ImportIssuesWorker # rubocop:disable Scalability/IdempotentWorker
+ include StageMethods
+
+ private
+
+ # project - An instance of Project.
+ def import(project)
+ waiter = importer_class.new(project).execute
+
+ project.import_state.refresh_jid_expiration
+
+ AdvanceStageWorker.perform_async(
+ project.id,
+ { waiter.key => waiter.jobs_remaining },
+ :issues_notes
+ )
+ end
+
+ def importer_class
+ Importers::IssuesImporter
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/bitbucket_import/stage/import_lfs_objects_worker.rb b/app/workers/gitlab/bitbucket_import/stage/import_lfs_objects_worker.rb
new file mode 100644
index 00000000000..c88a1be3446
--- /dev/null
+++ b/app/workers/gitlab/bitbucket_import/stage/import_lfs_objects_worker.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BitbucketImport
+ module Stage
+ class ImportLfsObjectsWorker # rubocop:disable Scalability/IdempotentWorker
+ include StageMethods
+
+ private
+
+ # project - An instance of Project.
+ def import(project)
+ waiter = importer_class.new(project).execute
+
+ project.import_state.refresh_jid_expiration
+
+ AdvanceStageWorker.perform_async(
+ project.id,
+ { waiter.key => waiter.jobs_remaining },
+ :finish
+ )
+ end
+
+ def importer_class
+ Importers::LfsObjectsImporter
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/bitbucket_import/stage/import_pull_requests_notes_worker.rb b/app/workers/gitlab/bitbucket_import/stage/import_pull_requests_notes_worker.rb
new file mode 100644
index 00000000000..36d60c7246c
--- /dev/null
+++ b/app/workers/gitlab/bitbucket_import/stage/import_pull_requests_notes_worker.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BitbucketImport
+ module Stage
+ class ImportPullRequestsNotesWorker # rubocop:disable Scalability/IdempotentWorker
+ include StageMethods
+
+ private
+
+ # project - An instance of Project.
+ def import(project)
+ waiter = importer_class.new(project).execute
+
+ project.import_state.refresh_jid_expiration
+
+ AdvanceStageWorker.perform_async(
+ project.id,
+ { waiter.key => waiter.jobs_remaining },
+ :issues
+ )
+ end
+
+ def importer_class
+ Importers::PullRequestsNotesImporter
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/bitbucket_import/stage/import_pull_requests_worker.rb b/app/workers/gitlab/bitbucket_import/stage/import_pull_requests_worker.rb
index e1f3b5ab79a..3f85c832d50 100644
--- a/app/workers/gitlab/bitbucket_import/stage/import_pull_requests_worker.rb
+++ b/app/workers/gitlab/bitbucket_import/stage/import_pull_requests_worker.rb
@@ -17,7 +17,7 @@ module Gitlab
AdvanceStageWorker.perform_async(
project.id,
{ waiter.key => waiter.jobs_remaining },
- :finish
+ :pull_requests_notes
)
end
diff --git a/app/workers/gitlab/bitbucket_server_import/advance_stage_worker.rb b/app/workers/gitlab/bitbucket_server_import/advance_stage_worker.rb
index 2c8db639725..1fc35725c9f 100644
--- a/app/workers/gitlab/bitbucket_server_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/bitbucket_server_import/advance_stage_worker.rb
@@ -25,10 +25,14 @@ module Gitlab
finish: Stage::FinishImportWorker
}.freeze
- def find_import_state(project_id)
+ def find_import_state_jid(project_id)
ProjectImportState.jid_by(project_id: project_id, status: :started)
end
+ def find_import_state(id)
+ ProjectImportState.find(id)
+ end
+
private
def next_stage_worker(next_stage)
diff --git a/app/workers/gitlab/github_gists_import/import_gist_worker.rb b/app/workers/gitlab/github_gists_import/import_gist_worker.rb
index 60e4c8fdad6..151788150dd 100644
--- a/app/workers/gitlab/github_gists_import/import_gist_worker.rb
+++ b/app/workers/gitlab/github_gists_import/import_gist_worker.rb
@@ -106,9 +106,9 @@ module Gitlab
def error(user_id, error_message, github_identifiers)
attributes = {
user_id: user_id,
- github_identifiers: github_identifiers,
+ external_identifiers: github_identifiers,
message: 'importer failed',
- 'error.message': error_message
+ 'exception.message': error_message
}
Gitlab::GithubImport::Logger.error(structured_payload(attributes))
@@ -120,7 +120,7 @@ module Gitlab
attributes = {
user_id: user_id,
message: message,
- github_identifiers: gist_id
+ external_identifiers: gist_id
}
Gitlab::GithubImport::Logger.info(structured_payload(attributes))
diff --git a/app/workers/gitlab/github_gists_import/start_import_worker.rb b/app/workers/gitlab/github_gists_import/start_import_worker.rb
index 33c91611719..f7d3eb1d759 100644
--- a/app/workers/gitlab/github_gists_import/start_import_worker.rb
+++ b/app/workers/gitlab/github_gists_import/start_import_worker.rb
@@ -51,7 +51,7 @@ module Gitlab
end
def log_error_and_raise!(user_id, error)
- logger.error(structured_payload(user_id: user_id, message: 'import failed', 'error.message': error.message))
+ logger.error(structured_payload(user_id: user_id, message: 'import failed', 'exception.message': error.message))
raise(error)
end
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index 45f4bf486d7..a012241e90c 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -33,10 +33,14 @@ module Gitlab
finish: Stage::FinishImportWorker
}.freeze
- def find_import_state(project_id)
+ def find_import_state_jid(project_id)
ProjectImportState.jid_by(project_id: project_id, status: :started)
end
+ def find_import_state(id)
+ ProjectImportState.find(id)
+ end
+
private
def next_stage_worker(next_stage)
diff --git a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
index 2b9fb26d53a..3de4bef053f 100644
--- a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
+++ b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
@@ -7,7 +7,6 @@ module Gitlab
data_consistency :always
- sidekiq_options retry: 3
include GithubImport::Queue
# The interval to schedule new instances of this job at.
diff --git a/app/workers/gitlab/github_import/stage/finish_import_worker.rb b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
index e716eda5c99..90445a6d46c 100644
--- a/app/workers/gitlab/github_import/stage/finish_import_worker.rb
+++ b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
@@ -8,7 +8,6 @@ module Gitlab
data_consistency :always
- sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_attachments_worker.rb b/app/workers/gitlab/github_import/stage/import_attachments_worker.rb
index 4045852e3f0..f9952f04e99 100644
--- a/app/workers/gitlab/github_import/stage/import_attachments_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_attachments_worker.rb
@@ -8,7 +8,6 @@ module Gitlab
data_consistency :always
- sidekiq_options retry: 5
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_base_data_worker.rb b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
index cc6a2255160..94cb3cb6c71 100644
--- a/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
@@ -8,7 +8,6 @@ module Gitlab
data_consistency :always
- sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
@@ -31,22 +30,6 @@ module Gitlab
project.import_state.refresh_jid_expiration
ImportPullRequestsWorker.perform_async(project.id)
- rescue StandardError => e
- Gitlab::Import::ImportFailureService.track(
- project_id: project.id,
- error_source: self.class.name,
- exception: e,
- fail_import: abort_on_failure,
- metrics: true
- )
-
- raise(e)
- end
-
- private
-
- def abort_on_failure
- true
end
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_collaborators_worker.rb b/app/workers/gitlab/github_import/stage/import_collaborators_worker.rb
index 8f72cc051b3..751ca92388a 100644
--- a/app/workers/gitlab/github_import/stage/import_collaborators_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_collaborators_worker.rb
@@ -8,7 +8,6 @@ module Gitlab
data_consistency :always
- sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
@@ -24,16 +23,6 @@ module Gitlab
project.import_state.refresh_jid_expiration
move_to_next_stage(project, { waiter.key => waiter.jobs_remaining })
- rescue StandardError => e
- Gitlab::Import::ImportFailureService.track(
- project_id: project.id,
- error_source: self.class.name,
- exception: e,
- fail_import: abort_on_failure,
- metrics: true
- )
-
- raise(e)
end
private
@@ -58,10 +47,6 @@ module Gitlab
project.id, waiters, :pull_requests_merged_by
)
end
-
- def abort_on_failure
- true
- end
end
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb b/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb
index 54ed4c47e78..c80412d941b 100644
--- a/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb
@@ -8,7 +8,6 @@ module Gitlab
data_consistency :always
- sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
index 3d1a8437da2..592b789cc94 100644
--- a/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
@@ -8,7 +8,6 @@ module Gitlab
data_consistency :always
- sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb b/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb
index f6f5687130f..e89a850c991 100644
--- a/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb
@@ -8,7 +8,6 @@ module Gitlab
data_consistency :always
- sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
index 40ca12b130f..c1fdb76d03e 100644
--- a/app/workers/gitlab/github_import/stage/import_notes_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
@@ -8,7 +8,6 @@ module Gitlab
data_consistency :always
- sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_protected_branches_worker.rb b/app/workers/gitlab/github_import/stage/import_protected_branches_worker.rb
index 73f4ea580c4..f8448094c28 100644
--- a/app/workers/gitlab/github_import/stage/import_protected_branches_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_protected_branches_worker.rb
@@ -8,7 +8,6 @@ module Gitlab
data_consistency :always
- sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
@@ -27,15 +26,6 @@ module Gitlab
{ waiter.key => waiter.jobs_remaining },
:lfs_objects
)
- rescue StandardError => e
- Gitlab::Import::ImportFailureService.track(
- project_id: project.id,
- error_source: self.class.name,
- exception: e,
- metrics: true
- )
-
- raise(e)
end
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb
index 329bf8f84b1..2e7cd28578f 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb
@@ -8,7 +8,6 @@ module Gitlab
data_consistency :always
- sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker.rb
index bcbf5dd471a..2f860349e25 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker.rb
@@ -8,7 +8,6 @@ module Gitlab
data_consistency :always
- sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb
index 33dee47bd03..51730033133 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb
@@ -8,7 +8,6 @@ module Gitlab
data_consistency :always
- sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
index b2dfded0280..029d38d8b93 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
@@ -8,7 +8,6 @@ module Gitlab
data_consistency :always
- sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
@@ -33,16 +32,6 @@ module Gitlab
{ waiter.key => waiter.jobs_remaining },
:collaborators
)
- rescue StandardError => e
- Gitlab::Import::ImportFailureService.track(
- project_id: project.id,
- error_source: self.class.name,
- exception: e,
- fail_import: abort_on_failure,
- metrics: true
- )
-
- raise(e)
end
private
@@ -57,10 +46,6 @@ module Gitlab
MergeRequest.track_target_project_iid!(project, last_github_pull_request[:number])
end
-
- def abort_on_failure
- true
- end
end
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_repository_worker.rb b/app/workers/gitlab/github_import/stage/import_repository_worker.rb
index d998771b328..2a62930b5ea 100644
--- a/app/workers/gitlab/github_import/stage/import_repository_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_repository_worker.rb
@@ -8,7 +8,6 @@ module Gitlab
data_consistency :always
- sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
@@ -34,17 +33,6 @@ module Gitlab
counter.increment
ImportBaseDataWorker.perform_async(project.id)
-
- rescue StandardError => e
- Gitlab::Import::ImportFailureService.track(
- project_id: project.id,
- error_source: self.class.name,
- exception: e,
- fail_import: abort_on_failure,
- metrics: true
- )
-
- raise(e)
end
def counter
@@ -54,10 +42,6 @@ module Gitlab
)
end
- def abort_on_failure
- true
- end
-
private
def allocate_issues_internal_id!(project, client)
diff --git a/app/workers/gitlab/import/advance_stage.rb b/app/workers/gitlab/import/advance_stage.rb
index 5d5abc88388..180c08905ff 100644
--- a/app/workers/gitlab/import/advance_stage.rb
+++ b/app/workers/gitlab/import/advance_stage.rb
@@ -4,6 +4,9 @@ module Gitlab
module Import
module AdvanceStage
INTERVAL = 30.seconds.to_i
+ TIMEOUT_DURATION = 2.hours
+
+ AdvanceStageTimeoutError = Class.new(StandardError)
# The number of seconds to wait (while blocking the thread) before
# continuing to the next waiter.
@@ -14,30 +17,35 @@ module Gitlab
# remaining jobs.
# next_stage - The name of the next stage to start when all jobs have been
# completed.
- def perform(project_id, waiters, next_stage)
- import_state = find_import_state(project_id)
+ # timeout_timer - Time the sidekiq worker was first initiated with the current job_count
+ # previous_job_count - Number of jobs remaining on last invocation of this worker
+ def perform(project_id, waiters, next_stage, timeout_timer = Time.zone.now, previous_job_count = nil)
+ import_state_jid = find_import_state_jid(project_id)
# If the import state is nil the project may have been deleted or the import
# may have failed or been canceled. In this case we tidy up the cache data and no
# longer attempt to advance to the next stage.
- if import_state.nil?
+ if import_state_jid.nil?
clear_waiter_caches(waiters)
return
end
new_waiters = wait_for_jobs(waiters)
+ new_job_count = new_waiters.values.sum
+
+ # Reset the timeout timer as some jobs finished processing
+ if new_job_count != previous_job_count
+ timeout_timer = Time.zone.now
+ previous_job_count = new_job_count
+ end
if new_waiters.empty?
- # We refresh the import JID here so workers importing individual
- # resources (e.g. notes) don't have to do this all the time, reducing
- # the pressure on Redis. We _only_ do this once all jobs are done so
- # we don't get stuck forever if one or more jobs failed to notify the
- # JobWaiter.
- import_state.refresh_jid_expiration
-
- next_stage_worker(next_stage).perform_async(project_id)
+ proceed_to_next_stage(import_state_jid, next_stage, project_id)
+ elsif timeout_reached?(timeout_timer) && new_job_count == previous_job_count
+
+ handle_timeout(import_state_jid, next_stage, project_id, new_waiters, new_job_count)
else
- self.class.perform_in(INTERVAL, project_id, new_waiters, next_stage)
+ self.class.perform_in(INTERVAL, project_id, new_waiters, next_stage, timeout_timer, previous_job_count)
end
end
@@ -55,12 +63,66 @@ module Gitlab
end
end
- def find_import_state(project_id)
+ def find_import_state_jid(project_id)
+ raise NotImplementedError
+ end
+
+ def find_import_state(id)
raise NotImplementedError
end
private
+ def proceed_to_next_stage(import_state_jid, next_stage, project_id)
+ # We refresh the import JID here so workers importing individual
+ # resources (e.g. notes) don't have to do this all the time, reducing
+ # the pressure on Redis. We _only_ do this once all jobs are done so
+ # we don't get stuck forever if one or more jobs failed to notify the
+ # JobWaiter.
+ import_state_jid.refresh_jid_expiration
+
+ next_stage_worker(next_stage).perform_async(project_id)
+ end
+
+ def handle_timeout(import_state_jid, next_stage, project_id, new_waiters, new_job_count)
+ project = Project.find_by_id(project_id)
+ strategy = project.import_data&.data&.dig("timeout_strategy") || ProjectImportData::PESSIMISTIC_TIMEOUT
+
+ Gitlab::Import::Logger.info(
+ message: 'Timeout reached, no longer retrying',
+ project_id: project_id,
+ jobs_remaining: new_job_count,
+ waiters: new_waiters,
+ timeout_strategy: strategy
+ )
+
+ clear_waiter_caches(new_waiters)
+
+ case strategy
+ when ProjectImportData::OPTIMISTIC_TIMEOUT
+ proceed_to_next_stage(import_state_jid, next_stage, project_id)
+ when ProjectImportData::PESSIMISTIC_TIMEOUT
+ import_state = find_import_state(import_state_jid.id)
+ fail_import_and_log_status(import_state)
+ end
+ end
+
+ def fail_import_and_log_status(import_state)
+ raise AdvanceStageTimeoutError, "Failing advance stage, timeout reached with pessimistic strategy"
+ rescue AdvanceStageTimeoutError => e
+ Gitlab::Import::ImportFailureService.track(
+ import_state: import_state,
+ exception: e,
+ error_source: self.class.name,
+ fail_import: true
+ )
+ end
+
+ def timeout_reached?(timeout_timer)
+ timeout_timer = Time.zone.parse(timeout_timer) if timeout_timer.is_a?(String)
+ Time.zone.now > timeout_timer + TIMEOUT_DURATION
+ end
+
def next_stage_worker(next_stage)
raise NotImplementedError
end
diff --git a/app/workers/gitlab/import/stuck_project_import_jobs_worker.rb b/app/workers/gitlab/import/stuck_project_import_jobs_worker.rb
index 01979b2029f..93d670e1b8b 100644
--- a/app/workers/gitlab/import/stuck_project_import_jobs_worker.rb
+++ b/app/workers/gitlab/import/stuck_project_import_jobs_worker.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module Gitlab
module Import
class StuckProjectImportJobsWorker # rubocop:disable Scalability/IdempotentWorker
diff --git a/app/workers/gitlab/jira_import/advance_stage_worker.rb b/app/workers/gitlab/jira_import/advance_stage_worker.rb
index 5fae7caf791..9641b55a584 100644
--- a/app/workers/gitlab/jira_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/jira_import/advance_stage_worker.rb
@@ -20,10 +20,14 @@ module Gitlab
finish: Gitlab::JiraImport::Stage::FinishImportWorker
}.freeze
- def find_import_state(project_id)
+ def find_import_state_jid(project_id)
JiraImportState.jid_by(project_id: project_id, status: :started)
end
+ def find_import_state(id)
+ JiraImportState.find(id)
+ end
+
private
def next_stage_worker(next_stage)
diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb
deleted file mode 100644
index b3c0fa79658..00000000000
--- a/app/workers/gitlab_shell_worker.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-class GitlabShellWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
- include Gitlab::ShellAdapter
-
- feature_category :source_code_management
- urgency :high
- weight 2
- loggable_arguments 0
-
- def perform(action, *arg)
- if Gitlab::Shell::PERMITTED_ACTIONS.exclude?(action)
- raise(ArgumentError, "#{action} not allowed for #{self.class.name}")
- end
-
- Gitlab::GitalyClient::NamespaceService.allow do
- gitlab_shell.public_send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
- end
- end
-end
diff --git a/app/workers/hashed_storage/migrator_worker.rb b/app/workers/hashed_storage/migrator_worker.rb
index 5f90b8f1009..a7e7a505681 100644
--- a/app/workers/hashed_storage/migrator_worker.rb
+++ b/app/workers/hashed_storage/migrator_worker.rb
@@ -13,9 +13,6 @@ module HashedStorage
# @param [Integer] start initial ID of the batch
# @param [Integer] finish last ID of the batch
- def perform(start, finish)
- migrator = Gitlab::HashedStorage::Migrator.new
- migrator.bulk_migrate(start: start, finish: finish)
- end
+ def perform(start, finish); end
end
end
diff --git a/app/workers/hashed_storage/project_migrate_worker.rb b/app/workers/hashed_storage/project_migrate_worker.rb
index 01e2d6307de..e1bf71de179 100644
--- a/app/workers/hashed_storage/project_migrate_worker.rb
+++ b/app/workers/hashed_storage/project_migrate_worker.rb
@@ -13,17 +13,6 @@ module HashedStorage
attr_reader :project_id
- def perform(project_id, old_disk_path = nil)
- @project_id = project_id # we need to set this in order to create the lease_key
-
- try_obtain_lease do
- project = Project.without_deleted.find_by_id(project_id)
- break unless project && project.storage_upgradable?
-
- old_disk_path ||= Storage::LegacyProject.new(project).disk_path
-
- ::Projects::HashedStorage::MigrationService.new(project, old_disk_path, logger: logger).execute
- end
- end
+ def perform(project_id, old_disk_path = nil); end
end
end
diff --git a/app/workers/hashed_storage/project_rollback_worker.rb b/app/workers/hashed_storage/project_rollback_worker.rb
index 2ec323248ab..af4223ff354 100644
--- a/app/workers/hashed_storage/project_rollback_worker.rb
+++ b/app/workers/hashed_storage/project_rollback_worker.rb
@@ -13,17 +13,6 @@ module HashedStorage
attr_reader :project_id
- def perform(project_id, old_disk_path = nil)
- @project_id = project_id # we need to set this in order to create the lease_key
-
- try_obtain_lease do
- project = Project.without_deleted.find_by_id(project_id)
- break unless project
-
- old_disk_path ||= project.disk_path
-
- ::Projects::HashedStorage::RollbackService.new(project, old_disk_path, logger: logger).execute
- end
- end
+ def perform(project_id, old_disk_path = nil); end
end
end
diff --git a/app/workers/hashed_storage/rollbacker_worker.rb b/app/workers/hashed_storage/rollbacker_worker.rb
index c6c4990d799..e659e65a370 100644
--- a/app/workers/hashed_storage/rollbacker_worker.rb
+++ b/app/workers/hashed_storage/rollbacker_worker.rb
@@ -13,9 +13,6 @@ module HashedStorage
# @param [Integer] start initial ID of the batch
# @param [Integer] finish last ID of the batch
- def perform(start, finish)
- migrator = Gitlab::HashedStorage::Migrator.new
- migrator.bulk_rollback(start: start, finish: finish)
- end
+ def perform(start, finish); end
end
end
diff --git a/app/workers/integrations/irker_worker.rb b/app/workers/integrations/irker_worker.rb
index 3152d68b372..4c1f0df0fc7 100644
--- a/app/workers/integrations/irker_worker.rb
+++ b/app/workers/integrations/irker_worker.rb
@@ -58,7 +58,7 @@ module Integrations
allow_local_network: allow_local_requests?,
schemes: ['irc'])
@socket = TCPSocket.new ip_address, irker_port
- rescue Errno::ECONNREFUSED, Gitlab::UrlBlocker::BlockedUrlError => e
+ rescue Errno::ECONNREFUSED, Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e
logger.fatal "Can't connect to Irker daemon: #{e}"
return false
end
diff --git a/app/workers/issuable/related_links_create_worker.rb b/app/workers/issuable/related_links_create_worker.rb
new file mode 100644
index 00000000000..7cbf70fd5ab
--- /dev/null
+++ b/app/workers/issuable/related_links_create_worker.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Issuable
+ class RelatedLinksCreateWorker
+ include ApplicationWorker
+
+ data_consistency :delayed
+
+ sidekiq_options retry: 3
+
+ feature_category :portfolio_management
+ worker_resource_boundary :unknown
+ urgency :high
+ idempotent!
+
+ def perform(args)
+ @params = args.with_indifferent_access
+ @user = User.find_by_id(params[:user_id])
+ @issuable = issuable_class.find_by_id(params[:issuable_id])
+ @links = issuable_class.related_link_class&.where(id: params[:link_ids])
+ return unless user && issuable && links.present?
+
+ create_issuable_notes!
+ rescue ArgumentError => error
+ logger.error(
+ worker: self.class.name,
+ message: "Failed to complete job (user_id:#{params[:user_id]}, issuable_id:#{params[:issuable_id]}, " \
+ "issuable_class:#{params[:issuable_class]}): #{error.message}"
+ )
+ end
+
+ private
+
+ attr_reader :params, :user, :issuable, :links
+
+ def issuable_class
+ params[:issuable_class].constantize
+ rescue NameError
+ raise ArgumentError, "Unknown class '#{params[:issuable_class]}'"
+ end
+
+ def create_issuable_notes!
+ errors = create_notes.compact
+ return unless errors.any?
+
+ raise ArgumentError, "Could not create notes: #{errors.join(', ')}"
+ end
+
+ def create_notes
+ linked_item_notes_errors = links.filter_map { |link| create_system_note(link.target, issuable) }
+ issuable_note_error = create_system_note(issuable, links.collect(&:target))
+
+ linked_item_notes_errors << issuable_note_error
+ end
+
+ def create_system_note(noteable, references, method_name = :relate_issuable)
+ note = ::SystemNoteService.try(method_name, noteable, references, user)
+ return if note.present?
+
+ "{noteable_id: #{noteable.id}, reference_ids: #{[references].flatten.collect(&:id)}}"
+ end
+ end
+end
+
+Issuable::RelatedLinksCreateWorker.prepend_mod_with('Issuable::RelatedLinksCreateWorker')
diff --git a/app/workers/jira_connect/sync_project_worker.rb b/app/workers/jira_connect/sync_project_worker.rb
index 40f225ab756..09aa5edc73b 100644
--- a/app/workers/jira_connect/sync_project_worker.rb
+++ b/app/workers/jira_connect/sync_project_worker.rb
@@ -33,7 +33,10 @@ module JiraConnect
# rubocop: disable CodeReuse/ActiveRecord
def merge_requests_to_sync(project)
- project.merge_requests.with_jira_issue_keys.preload(:author).limit(MAX_RECORDS_LIMIT).order(id: :desc)
+ project.merge_requests.with_jira_issue_keys
+ .preload(:author, :approvals, merge_request_reviewers: :reviewer)
+ .limit(MAX_RECORDS_LIMIT)
+ .order(id: :desc)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
index a0594b15e31..29f0c0bbbf4 100644
--- a/app/workers/merge_worker.rb
+++ b/app/workers/merge_worker.rb
@@ -16,8 +16,6 @@ class MergeWorker # rubocop:disable Scalability/IdempotentWorker
deduplicate :until_executed, including_scheduled: true
def perform(merge_request_id, current_user_id, params)
- params = params.with_indifferent_access
-
begin
current_user = User.find(current_user_id)
merge_request = MergeRequest.find(merge_request_id)
@@ -25,6 +23,9 @@ class MergeWorker # rubocop:disable Scalability/IdempotentWorker
return
end
+ params = params.with_indifferent_access
+ params[:check_mergeability_retry_lease] = true unless params.has_key?(:check_mergeability_retry_lease)
+
MergeRequests::MergeService.new(project: merge_request.target_project, current_user: current_user, params: params)
.execute(merge_request)
end
diff --git a/app/workers/pages/deactivated_deployments_delete_cron_worker.rb b/app/workers/pages/deactivated_deployments_delete_cron_worker.rb
new file mode 100644
index 00000000000..7ee6327cea7
--- /dev/null
+++ b/app/workers/pages/deactivated_deployments_delete_cron_worker.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Pages
+ class DeactivatedDeploymentsDeleteCronWorker
+ include ApplicationWorker
+ include CronjobQueue # rubocop: disable Scalability/CronWorkerContext
+
+ idempotent!
+ data_consistency :always # rubocop: disable SidekiqLoadBalancing/WorkerDataConsistency
+
+ feature_category :pages
+
+ def perform
+ PagesDeployment.deactivated.each_batch do |deployments| # rubocop: disable Style/SymbolProc
+ deployments.delete_all
+ end
+ end
+ end
+end
diff --git a/app/workers/projects/after_import_worker.rb b/app/workers/projects/after_import_worker.rb
index 06211b2d991..47bd07d0850 100644
--- a/app/workers/projects/after_import_worker.rb
+++ b/app/workers/projects/after_import_worker.rb
@@ -31,7 +31,7 @@ module Projects
message: 'Project housekeeping failed',
project_full_path: @project.full_path,
project_id: @project.id,
- 'error.message' => e.message
+ 'exception.message' => e.message
)
end
diff --git a/app/workers/projects/record_target_platforms_worker.rb b/app/workers/projects/record_target_platforms_worker.rb
index bbe0c63cfd1..d458c9563d0 100644
--- a/app/workers/projects/record_target_platforms_worker.rb
+++ b/app/workers/projects/record_target_platforms_worker.rb
@@ -8,7 +8,7 @@ module Projects
LEASE_TIMEOUT = 1.hour.to_i
APPLE_PLATFORM_LANGUAGES = %w[swift objective-c].freeze
- feature_category :experimentation_activation
+ feature_category :activation
data_consistency :always
deduplicate :until_executed
urgency :low
diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb
index dab92e16ee3..61ef7494d38 100644
--- a/app/workers/run_pipeline_schedule_worker.rb
+++ b/app/workers/run_pipeline_schedule_worker.rb
@@ -22,7 +22,7 @@ class RunPipelineScheduleWorker # rubocop:disable Scalability/IdempotentWorker
options.symbolize_keys!
if options[:scheduling]
- return if schedule.next_run_at > Time.current
+ return if schedule.next_run_at.future?
update_next_run_at_for(schedule)
end
diff --git a/app/workers/tasks_to_be_done/create_worker.rb b/app/workers/tasks_to_be_done/create_worker.rb
index d3824ceb4ae..91046e3cfed 100644
--- a/app/workers/tasks_to_be_done/create_worker.rb
+++ b/app/workers/tasks_to_be_done/create_worker.rb
@@ -11,21 +11,8 @@ module TasksToBeDone
worker_resource_boundary :cpu
def perform(member_task_id, current_user_id, assignee_ids = [])
- member_task = MemberTask.find(member_task_id)
- current_user = User.find(current_user_id)
- project = member_task.project
-
- member_task.tasks_to_be_done.each do |task|
- service_class(task)
- .new(container: project, current_user: current_user, assignee_ids: assignee_ids)
- .execute
- end
- end
-
- private
-
- def service_class(task)
- "TasksToBeDone::Create#{task.to_s.camelize}TaskService".constantize
+ # no-op removing
+ # https://docs.gitlab.com/ee/development/sidekiq/compatibility_across_updates.html#removing-worker-classes
end
end
end