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:
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/auth_buttons/salesforce_64.pngbin2012 -> 0 bytes
-rw-r--r--app/assets/javascripts/access_tokens/components/access_token_table_app.vue17
-rw-r--r--app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue3
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue46
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue19
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/activity_history_item.vue42
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql13
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql11
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql10
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/history_items.vue51
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/labels_select.vue235
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/report_actions.vue8
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/report_details.vue49
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/report_header.vue16
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/reported_content.vue13
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/user_details.vue40
-rw-r--r--app/assets/javascripts/admin/abuse_report/constants.js4
-rw-r--r--app/assets/javascripts/admin/abuse_report/index.js18
-rw-r--r--app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue21
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/message_form.vue3
-rw-r--r--app/assets/javascripts/admin/users/components/actions/approve.vue4
-rw-r--r--app/assets/javascripts/admin/users/components/user_actions.vue3
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue2
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_form.vue8
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue11
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue3
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/formatted_stage_count.vue3
-rw-r--r--app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue3
-rw-r--r--app/assets/javascripts/api.js14
-rw-r--r--app/assets/javascripts/api/application_settings_api.js14
-rw-r--r--app/assets/javascripts/authentication/webauthn/error.js9
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js3
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/keybindings.js1
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue26
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.js8
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js16
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue61
-rw-r--r--app/assets/javascripts/blob/line_highlighter.js19
-rw-r--r--app/assets/javascripts/blob/openapi/index.js9
-rw-r--r--app/assets/javascripts/blob/queries/application_info.query.graphql (renamed from app/assets/javascripts/repository/queries/application_info.query.graphql)0
-rw-r--r--app/assets/javascripts/blob/queries/user_info.query.graphql (renamed from app/assets/javascripts/repository/queries/user_info.query.graphql)0
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue9
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue8
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue14
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_top_bar.vue9
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue10
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue5
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue9
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue5
-rw-r--r--app/assets/javascripts/boards/graphql/cache_updates.js4
-rw-r--r--app/assets/javascripts/boards/graphql/group_board_members.query.graphql15
-rw-r--r--app/assets/javascripts/boards/graphql/project_board_members.query.graphql15
-rw-r--r--app/assets/javascripts/boards/index.js1
-rw-r--r--app/assets/javascripts/boards/issue_board_filters.js40
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue (renamed from app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue)80
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs.vue (renamed from app/assets/javascripts/pages/admin/jobs/components/cancel_jobs.vue)2
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs_modal.vue (renamed from app/assets/javascripts/pages/admin/jobs/components/cancel_jobs_modal.vue)2
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue (renamed from app/assets/javascripts/pages/admin/jobs/components/table/cell/project_cell.vue)0
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue (renamed from app/assets/javascripts/pages/admin/jobs/components/table/cells/runner_cell.vue)2
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/jobs_skeleton_loader.vue (renamed from app/assets/javascripts/pages/admin/jobs/components/jobs_skeleton_loader.vue)0
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/constants.js (renamed from app/assets/javascripts/pages/admin/jobs/components/constants.js)2
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/graphql/cache_config.js (renamed from app/assets/javascripts/pages/admin/jobs/components/table/graphql/cache_config.js)0
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql (renamed from app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql)9
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs_count.query.graphql5
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_cancelable_jobs_count.query.graphql (renamed from app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_cancelable_jobs_count.query.graphql)2
-rw-r--r--app/assets/javascripts/ci/artifacts/components/feedback_banner.vue41
-rw-r--r--app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue3
-rw-r--r--app/assets/javascripts/ci/artifacts/constants.js7
-rw-r--r--app/assets/javascripts/ci/artifacts/index.js8
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/ci_variable_list.js262
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue195
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue5
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue3
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue49
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/constants.js1
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/native_form_variable_list.js25
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/utils.js10
-rw-r--r--app/assets/javascripts/ci/common/pipelines_table.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue)26
-rw-r--r--app/assets/javascripts/ci/common/private/job_action_component.vue (renamed from app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue)2
-rw-r--r--app/assets/javascripts/ci/common/private/job_links_layer.vue (renamed from app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue)6
-rw-r--r--app/assets/javascripts/ci/common/private/job_name_component.vue (renamed from app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue)0
-rw-r--r--app/assets/javascripts/ci/common/private/jobs_filtered_search/app.vue99
-rw-r--r--app/assets/javascripts/ci/common/private/jobs_filtered_search/constants.js23
-rw-r--r--app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_runner_type_token.vue79
-rw-r--r--app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_status_token.vue (renamed from app/assets/javascripts/jobs/components/filtered_search/tokens/job_status_token.vue)0
-rw-r--r--app/assets/javascripts/ci/common/private/jobs_filtered_search/utils.js22
-rw-r--r--app/assets/javascripts/ci/constants.js51
-rw-r--r--app/assets/javascripts/ci/event_hub.js (renamed from app/assets/javascripts/jobs/components/table/event_hub.js)0
-rw-r--r--app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/components/empty_state.vue (renamed from app/assets/javascripts/jobs/components/job/empty_state.vue)2
-rw-r--r--app/assets/javascripts/ci/job_details/components/environments_block.vue (renamed from app/assets/javascripts/jobs/components/job/environments_block.vue)0
-rw-r--r--app/assets/javascripts/ci/job_details/components/erased_block.vue (renamed from app/assets/javascripts/jobs/components/job/erased_block.vue)0
-rw-r--r--app/assets/javascripts/ci/job_details/components/job_header.vue (renamed from app/assets/javascripts/vue_shared/components/header_ci_component.vue)48
-rw-r--r--app/assets/javascripts/ci/job_details/components/job_log_controllers.vue (renamed from app/assets/javascripts/jobs/components/job/job_log_controllers.vue)17
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/collapsible_section.vue (renamed from app/assets/javascripts/jobs/components/log/collapsible_section.vue)16
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/duration_badge.vue (renamed from app/assets/javascripts/jobs/components/log/duration_badge.vue)0
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line.vue (renamed from app/assets/javascripts/jobs/components/log/line.vue)26
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_header.vue (renamed from app/assets/javascripts/jobs/components/log/line_header.vue)28
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_number.vue (renamed from app/assets/javascripts/jobs/components/log/line_number.vue)0
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/log.vue (renamed from app/assets/javascripts/jobs/components/log/log.vue)10
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/utils.js12
-rw-r--r--app/assets/javascripts/ci/job_details/components/manual_variables_form.vue (renamed from app/assets/javascripts/jobs/components/job/manual_variables_form.vue)12
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/artifacts_block.vue)7
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue54
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/external_links_block.vue34
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/job_container_item.vue)10
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/job_retry_forward_deployment_modal.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue)12
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/job_sidebar_retry_button.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue)7
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/jobs_container.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/jobs_container.vue)3
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue)75
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/sidebar_detail_row.vue)29
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue)56
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue)12
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/stages_dropdown.vue)44
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/trigger_block.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue)2
-rw-r--r--app/assets/javascripts/ci/job_details/components/stuck_block.vue (renamed from app/assets/javascripts/jobs/components/job/stuck_block.vue)3
-rw-r--r--app/assets/javascripts/ci/job_details/components/unmet_prerequisites_block.vue (renamed from app/assets/javascripts/jobs/components/job/unmet_prerequisites_block.vue)0
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql (renamed from app/assets/javascripts/jobs/components/job/graphql/fragments/ci_job.fragment.graphql)2
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql (renamed from app/assets/javascripts/jobs/components/job/graphql/fragments/ci_variable.fragment.graphql)0
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/mutations/job_play_with_variables.mutation.graphql (renamed from app/assets/javascripts/jobs/components/job/graphql/mutations/job_play_with_variables.mutation.graphql)2
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql (renamed from app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql)2
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/queries/get_job.query.graphql (renamed from app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql)2
-rw-r--r--app/assets/javascripts/ci/job_details/index.js (renamed from app/assets/javascripts/jobs/index.js)2
-rw-r--r--app/assets/javascripts/ci/job_details/job_app.vue (renamed from app/assets/javascripts/jobs/components/job/job_app.vue)33
-rw-r--r--app/assets/javascripts/ci/job_details/store/actions.js (renamed from app/assets/javascripts/jobs/store/actions.js)2
-rw-r--r--app/assets/javascripts/ci/job_details/store/getters.js (renamed from app/assets/javascripts/jobs/store/getters.js)0
-rw-r--r--app/assets/javascripts/ci/job_details/store/index.js (renamed from app/assets/javascripts/jobs/store/index.js)0
-rw-r--r--app/assets/javascripts/ci/job_details/store/mutation_types.js (renamed from app/assets/javascripts/jobs/store/mutation_types.js)0
-rw-r--r--app/assets/javascripts/ci/job_details/store/mutations.js (renamed from app/assets/javascripts/jobs/store/mutations.js)0
-rw-r--r--app/assets/javascripts/ci/job_details/store/state.js (renamed from app/assets/javascripts/jobs/store/state.js)0
-rw-r--r--app/assets/javascripts/ci/job_details/store/utils.js (renamed from app/assets/javascripts/jobs/store/utils.js)0
-rw-r--r--app/assets/javascripts/ci/job_details/utils.js29
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue (renamed from app/assets/javascripts/jobs/components/table/cells/actions_cell.vue)14
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue (renamed from app/assets/javascripts/jobs/components/table/cells/duration_cell.vue)9
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue (renamed from app/assets/javascripts/jobs/components/table/cells/job_cell.vue)6
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue (renamed from app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue)10
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/jobs_table.vue (renamed from app/assets/javascripts/jobs/components/table/jobs_table.vue)16
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue (renamed from app/assets/javascripts/jobs/components/table/jobs_table_empty_state.vue)1
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/jobs_table_tabs.vue (renamed from app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue)4
-rw-r--r--app/assets/javascripts/ci/jobs_page/constants.js (renamed from app/assets/javascripts/jobs/components/table/constants.js)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/event_hub.js (renamed from app/assets/javascripts/pipelines/event_hub.js)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/cache_config.js (renamed from app/assets/javascripts/jobs/components/table/graphql/cache_config.js)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/fragments/job.fragment.graphql (renamed from app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/mutations/job_cancel.mutation.graphql (renamed from app/assets/javascripts/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/mutations/job_play.mutation.graphql (renamed from app/assets/javascripts/jobs/components/table/graphql/mutations/job_play.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql (renamed from app/assets/javascripts/jobs/components/table/graphql/mutations/job_retry.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/mutations/job_unschedule.mutation.graphql (renamed from app/assets/javascripts/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql (renamed from app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs_count.query.graphql (renamed from app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs_count.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/index.js (renamed from app/assets/javascripts/jobs/components/table/index.js)2
-rw-r--r--app/assets/javascripts/ci/jobs_page/jobs_page_app.vue (renamed from app/assets/javascripts/jobs/components/table/jobs_table_app.vue)12
-rw-r--r--app/assets/javascripts/ci/merge_requests/components/pipelines_table_wrapper.vue60
-rw-r--r--app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql (renamed from app/assets/javascripts/pipelines/graphql/mutations/retry_mr_failed_job.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/merge_requests/graphql/queries/get_merge_request_pipelines.query.graphql16
-rw-r--r--app/assets/javascripts/ci/mixins/delayed_job_mixin.js (renamed from app/assets/javascripts/jobs/mixins/delayed_job_mixin.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/constants.js (renamed from app/assets/javascripts/pipelines/constants.js)45
-rw-r--r--app/assets/javascripts/ci/pipeline_details/dag/components/dag_annotations.vue (renamed from app/assets/javascripts/pipelines/components/dag/dag_annotations.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/dag/components/dag_graph.vue (renamed from app/assets/javascripts/pipelines/components/dag/dag_graph.vue)8
-rw-r--r--app/assets/javascripts/ci/pipeline_details/dag/constants.js (renamed from app/assets/javascripts/pipelines/components/dag/constants.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/dag/dag.vue (renamed from app/assets/javascripts/pipelines/components/dag/dag.vue)15
-rw-r--r--app/assets/javascripts/ci/pipeline_details/dag/graphql/queries/get_dag_vis_data.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/dag/utils/drawing_utils.js (renamed from app/assets/javascripts/pipelines/components/dag/drawing_utils.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/dag/utils/interactions.js (renamed from app/assets/javascripts/pipelines/components/dag/interactions.js)2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/api_utils.js (renamed from app/assets/javascripts/pipelines/components/graph_shared/api.js)2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue (renamed from app/assets/javascripts/pipelines/components/graph/graph_component.vue)12
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue (renamed from app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue (renamed from app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue)4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue (renamed from app/assets/javascripts/pipelines/components/graph/job_item.vue)10
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue (renamed from app/assets/javascripts/pipelines/components/graph_shared/linked_graph_wrapper.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue (renamed from app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue)14
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue (renamed from app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue)8
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/links_inner.vue (renamed from app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue)7
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue (renamed from app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue (renamed from app/assets/javascripts/pipelines/components/graph/stage_column_component.vue)12
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/constants.js (renamed from app/assets/javascripts/pipelines/components/graph/constants.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue (renamed from app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue)12
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/graphql/mutations/dismiss_pipeline_notification.graphql (renamed from app/assets/javascripts/pipelines/graphql/mutations/dismiss_pipeline_notification.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/perf_utils.js (renamed from app/assets/javascripts/pipelines/components/graph/perf_utils.js)2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/utils.js (renamed from app/assets/javascripts/pipelines/components/graph/utils.js)7
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graphql/fragments/pipeline_stages_connection.fragment.graphql (renamed from app/assets/javascripts/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql (renamed from app/assets/javascripts/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graphql/mutations/delete_pipeline.mutation.graphql (renamed from app/assets/javascripts/pipelines/graphql/mutations/delete_pipeline.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql (renamed from app/assets/javascripts/pipelines/graphql/mutations/retry_pipeline.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graphql/provider.js (renamed from app/assets/javascripts/pipelines/graphql/provider.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graphql/queries/get_linked_pipelines.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_linked_pipelines.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_details_header.vue)18
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue (renamed from app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue)5
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/failed_jobs_app.vue (renamed from app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue)4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql (renamed from app/assets/javascripts/pipelines/graphql/mutations/retry_failed_job.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/graphql/queries/get_failed_jobs.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/graphql/queries/get_pipeline_jobs.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/jobs_app.vue (renamed from app/assets/javascripts/pipelines/components/jobs/jobs_app.vue)8
-rw-r--r--app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js (renamed from app/assets/javascripts/pipelines/mixins/pipelines_mixin.js)4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipeline_details_bundle.js (renamed from app/assets/javascripts/pipelines/pipeline_details_bundle.js)10
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js (renamed from app/assets/javascripts/pipelines/pipeline_details_header.js)2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js (renamed from app/assets/javascripts/pipelines/pipeline_shared_client.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipeline_tabs.js (renamed from app/assets/javascripts/pipelines/pipeline_tabs.js)5
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipelines_index.js (renamed from app/assets/javascripts/pipelines/pipelines_index.js)2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/routes.js (renamed from app/assets/javascripts/pipelines/routes.js)10
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/pipelines_store.js (renamed from app/assets/javascripts/pipelines/stores/pipelines_store.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/actions.js (renamed from app/assets/javascripts/pipelines/stores/test_reports/actions.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/constants.js (renamed from app/assets/javascripts/pipelines/stores/test_reports/constants.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/getters.js (renamed from app/assets/javascripts/pipelines/stores/test_reports/getters.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/index.js (renamed from app/assets/javascripts/pipelines/stores/test_reports/index.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/mutation_types.js (renamed from app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/mutations.js (renamed from app/assets/javascripts/pipelines/stores/test_reports/mutations.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/state.js (renamed from app/assets/javascripts/pipelines/stores/test_reports/state.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js (renamed from app/assets/javascripts/pipelines/stores/test_reports/utils.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/tabs/pipeline_tabs.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_tabs.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/test_reports/empty_state.vue (renamed from app/assets/javascripts/pipelines/components/test_reports/empty_state.vue)1
-rw-r--r--app/assets/javascripts/ci/pipeline_details/test_reports/test_case_details.vue (renamed from app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue)7
-rw-r--r--app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue (renamed from app/assets/javascripts/pipelines/components/test_reports/test_reports.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/test_reports/test_suite_table.vue (renamed from app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/test_reports/test_summary.vue (renamed from app/assets/javascripts/pipelines/components/test_reports/test_summary.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/test_reports/test_summary_table.vue (renamed from app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/utils/drawing_utils.js (renamed from app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/utils/index.js (renamed from app/assets/javascripts/pipelines/utils.js)27
-rw-r--r--app/assets/javascripts/ci/pipeline_details/utils/parsing_utils.js (renamed from app/assets/javascripts/pipelines/components/parsing_utils.js)4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/utils/unwrapping_utils.js (renamed from app/assets/javascripts/pipelines/components/unwrapping_utils.js)2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue7
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/graph/job_pill.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/graph/pipeline_graph.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue)11
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/graph/stage_name.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_header.vue50
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue6
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue28
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js6
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/popovers/file_tree_popover.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue3
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.query.graphql2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/accessors/linked_pipelines_accessors.js (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/accessors/linked_pipelines_accessors.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stage.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_pipeline_stage.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stages.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_pipeline_stages.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/job_item.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_job_item.vue)10
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/pipeline_mini_graph.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue)13
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/pipeline_stage.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue)9
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/pipeline_stages.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue38
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue7
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue50
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/ci_templates.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue)2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue)4
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue)4
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_jobs_list.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue)7
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue)2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/failure_widget/utils.js (renamed from app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/utils.js)4
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipeline_labels.vue)4
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_multi_actions.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue)107
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue)2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue)7
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue)2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipelines_filtered_search.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue)10
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue)2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipelines_status_badge.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue)3
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/time_ago.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/constants.js2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_actions.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_pipeline_actions.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_pipeline_failed_jobs.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs_count.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_pipeline_failed_jobs_count.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/pipelines.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue)33
-rw-r--r--app/assets/javascripts/ci/pipelines_page/services/pipelines_service.js (renamed from app/assets/javascripts/pipelines/services/pipelines_service.js)2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/tokens/constants.js (renamed from app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/tokens/pipeline_branch_name_token.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue)5
-rw-r--r--app/assets/javascripts/ci/pipelines_page/tokens/pipeline_source_token.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue)2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/tokens/pipeline_status_token.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/tokens/pipeline_tag_name_token.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue)5
-rw-r--r--app/assets/javascripts/ci/pipelines_page/tokens/pipeline_trigger_author_token.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue)9
-rw-r--r--app/assets/javascripts/ci/reports/components/issue_status_icon.vue5
-rw-r--r--app/assets/javascripts/ci/reports/components/report_section.vue3
-rw-r--r--app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue28
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_owner_cell.vue7
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue16
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_create_form.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_filtered_search_bar.vue1
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_form_fields.vue15
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_managers_table.vue12
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue4
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_type_tabs.vue7
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_update_form.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/stat/runner_count.vue6
-rw-r--r--app/assets/javascripts/ci/runner/components/stat/runner_stats.vue1
-rw-r--r--app/assets/javascripts/ci/runner/constants.js3
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue34
-rw-r--r--app/assets/javascripts/ci/runner/project_runners/index.js23
-rw-r--r--app/assets/javascripts/ci/runner/project_runners/project_runners_app.vue19
-rw-r--r--app/assets/javascripts/ci/runner/runner_search_utils.js3
-rw-r--r--app/assets/javascripts/ci/utils.js (renamed from app/assets/javascripts/jobs/utils.js)13
-rw-r--r--app/assets/javascripts/ci_secure_files/components/metadata/modal.vue6
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_table.vue6
-rw-r--r--app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue3
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters.vue12
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_actions.vue3
-rw-r--r--app/assets/javascripts/commit/constants.js6
-rw-r--r--app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue (renamed from app/assets/javascripts/commit/pipelines/pipelines_table.vue)12
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js3
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table_wrapper.vue0
-rw-r--r--app/assets/javascripts/commons/gitlab_ui.js10
-rw-r--r--app/assets/javascripts/commons/index.js1
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_created.vue2
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_destroyed.vue28
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_pushed.vue3
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_updated.vue25
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_events.vue10
-rw-r--r--app/assets/javascripts/contribution_events/components/target_link.vue2
-rw-r--r--app/assets/javascripts/contribution_events/constants.js26
-rw-r--r--app/assets/javascripts/create_item_dropdown.js4
-rw-r--r--app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue10
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js12
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js2
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js3
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/index.js5
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/render.js3
-rw-r--r--app/assets/javascripts/deprecated_notes.js3
-rw-r--r--app/assets/javascripts/diffs/components/app.vue9
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_inline_findings_item.vue32
-rw-r--r--app/assets/javascripts/diffs/components/diff_row.vue14
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue3
-rw-r--r--app/assets/javascripts/diffs/store/actions.js3
-rw-r--r--app/assets/javascripts/diffs/store/getters.js3
-rw-r--r--app/assets/javascripts/drawio/drawio_editor.js3
-rw-r--r--app/assets/javascripts/editor/schema/ci.json7
-rw-r--r--app/assets/javascripts/entrypoints/analytics.js17
-rw-r--r--app/assets/javascripts/entrypoints/jira_connect_app.js1
-rw-r--r--app/assets/javascripts/entrypoints/main.js6
-rw-r--r--app/assets/javascripts/entrypoints/main_ee.js5
-rw-r--r--app/assets/javascripts/entrypoints/main_jh.js5
-rw-r--r--app/assets/javascripts/entrypoints/performance_bar.js1
-rw-r--r--app/assets/javascripts/entrypoints/redirect_listbox.js1
-rw-r--r--app/assets/javascripts/entrypoints/sandboxed_mermaid.js1
-rw-r--r--app/assets/javascripts/entrypoints/sentry.js1
-rw-r--r--app/assets/javascripts/environments/components/deployment.vue2
-rw-r--r--app/assets/javascripts/environments/components/edit_environment.vue7
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_form.vue7
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue17
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_status_bar.vue15
-rw-r--r--app/assets/javascripts/environments/components/new_environment_item.vue8
-rw-r--r--app/assets/javascripts/environments/environment_details/deployments_table.vue2
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment.query.graphql1
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql1
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_flux_resource.query.graphql21
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_with_flux_resource.query.graphql16
-rw-r--r--app/assets/javascripts/environments/mixins/environments_pagination_api_mixin.js2
-rw-r--r--app/assets/javascripts/environments/mount_show.js3
-rw-r--r--app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue147
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js2
-rw-r--r--app/assets/javascripts/frequent_items/utils.js3
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js26
-rw-r--r--app/assets/javascripts/gitlab_version_check/components/gitlab_version_check_badge.vue6
-rw-r--r--app/assets/javascripts/google_cloud/deployments/service_table.vue2
-rw-r--r--app/assets/javascripts/google_cloud/service_accounts/list.vue1
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json5
-rw-r--r--app/assets/javascripts/graphql_shared/queries/project_autocomplete_users.query.graphql12
-rw-r--r--app/assets/javascripts/graphql_shared/queries/project_autocomplete_users_with_mr_permissions.query.graphql19
-rw-r--r--app/assets/javascripts/groups/components/empty_states/groups_dashboard_empty_state.vue24
-rw-r--r--app/assets/javascripts/groups/components/empty_states/groups_explore_empty_state.vue17
-rw-r--r--app/assets/javascripts/groups/index.js13
-rw-r--r--app/assets/javascripts/helpers/avatar_helper.js38
-rw-r--r--app/assets/javascripts/ide/commit_icon.js3
-rw-r--r--app/assets/javascripts/ide/components/file_templates/dropdown.vue103
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue6
-rw-r--r--app/assets/javascripts/ide/components/terminal/terminal.vue3
-rw-r--r--app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue6
-rw-r--r--app/assets/javascripts/ide/init_gitlab_web_ide.js3
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js6
-rw-r--r--app/assets/javascripts/ide/lib/errors.js6
-rw-r--r--app/assets/javascripts/ide/lib/files.js3
-rw-r--r--app/assets/javascripts/ide/lib/mirror.js3
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js3
-rw-r--r--app/assets/javascripts/ide/stores/getters.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/messages.js3
-rw-r--r--app/assets/javascripts/ide/stores/utils.js9
-rw-r--r--app/assets/javascripts/import_entities/components/import_status.vue6
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue10
-rw-r--r--app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue2
-rw-r--r--app/assets/javascripts/integrations/index/components/integrations_table.vue66
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue126
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue2
-rw-r--r--app/assets/javascripts/invite_members/constants.js20
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js3
-rw-r--r--app/assets/javascripts/invite_members/utils/member_utils.js6
-rw-r--r--app/assets/javascripts/issuable/components/csv_export_modal.vue4
-rw-r--r--app/assets/javascripts/issuable/components/csv_import_export_buttons.vue2
-rw-r--r--app/assets/javascripts/issuable/components/issue_assignees.vue4
-rw-r--r--app/assets/javascripts/issuable/components/issue_milestone.vue3
-rw-r--r--app/assets/javascripts/issuable/components/status_badge.vue98
-rw-r--r--app/assets/javascripts/issuable/components/status_box.vue146
-rw-r--r--app/assets/javascripts/issuable/index.js19
-rw-r--r--app/assets/javascripts/issuable/issuable_context.js23
-rw-r--r--app/assets/javascripts/issuable/popover/components/issue_popover.vue13
-rw-r--r--app/assets/javascripts/issues/constants.js1
-rw-r--r--app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue1
-rw-r--r--app/assets/javascripts/issues/index.js38
-rw-r--r--app/assets/javascripts/issues/issue.js5
-rw-r--r--app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue2
-rw-r--r--app/assets/javascripts/issues/list/components/issue_card_time_info.vue40
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue49
-rw-r--r--app/assets/javascripts/issues/list/constants.js1
-rw-r--r--app/assets/javascripts/issues/list/queries/issue.fragment.graphql1
-rw-r--r--app/assets/javascripts/issues/list/queries/search_milestones.query.graphql11
-rw-r--r--app/assets/javascripts/issues/list/queries/search_users.query.graphql29
-rw-r--r--app/assets/javascripts/issues/list/queries/user.fragment.graphql6
-rw-r--r--app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue9
-rw-r--r--app/assets/javascripts/issues/related_merge_requests/index.js10
-rw-r--r--app/assets/javascripts/issues/service_desk/components/empty_state_with_any_issues.vue (renamed from app/assets/javascripts/service_desk/components/empty_state_with_any_issues.vue)3
-rw-r--r--app/assets/javascripts/issues/service_desk/components/empty_state_without_any_issues.vue (renamed from app/assets/javascripts/service_desk/components/empty_state_without_any_issues.vue)0
-rw-r--r--app/assets/javascripts/issues/service_desk/components/info_banner.vue (renamed from app/assets/javascripts/service_desk/components/info_banner.vue)0
-rw-r--r--app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue (renamed from app/assets/javascripts/service_desk/components/service_desk_list_app.vue)188
-rw-r--r--app/assets/javascripts/issues/service_desk/constants.js (renamed from app/assets/javascripts/service_desk/constants.js)5
-rw-r--r--app/assets/javascripts/issues/service_desk/graphql.js (renamed from app/assets/javascripts/service_desk/graphql.js)0
-rw-r--r--app/assets/javascripts/issues/service_desk/index.js (renamed from app/assets/javascripts/service_desk/index.js)6
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/get_service_desk_issues.query.graphql (renamed from app/assets/javascripts/service_desk/queries/get_service_desk_issues.query.graphql)7
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/get_service_desk_issues_counts.query.graphql (renamed from app/assets/javascripts/service_desk/queries/get_service_desk_issues_counts.query.graphql)11
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/issue.fragment.graphql (renamed from app/assets/javascripts/service_desk/queries/issue.fragment.graphql)1
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/label.fragment.graphql (renamed from app/assets/javascripts/service_desk/queries/label.fragment.graphql)0
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/milestone.fragment.graphql (renamed from app/assets/javascripts/service_desk/queries/milestone.fragment.graphql)0
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/reorder_service_desk_issues.mutation.graphql13
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/search_project_labels.query.graphql (renamed from app/assets/javascripts/service_desk/queries/search_project_labels.query.graphql)0
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/search_project_milestones.query.graphql (renamed from app/assets/javascripts/service_desk/queries/search_project_milestones.query.graphql)0
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/set_sorting_preference.mutation.graphql5
-rw-r--r--app/assets/javascripts/issues/service_desk/search_tokens.js (renamed from app/assets/javascripts/service_desk/search_tokens.js)0
-rw-r--r--app/assets/javascripts/issues/service_desk/utils.js (renamed from app/assets/javascripts/service_desk/utils.js)0
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue202
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue3
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue79
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue5
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/sticky_header.vue130
-rw-r--r--app/assets/javascripts/issues/show/components/task_list_item_actions.vue17
-rw-r--r--app/assets/javascripts/issues/show/index.js190
-rw-r--r--app/assets/javascripts/issues/show/stores/index.js46
-rw-r--r--app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue1
-rw-r--r--app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue2
-rw-r--r--app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue55
-rw-r--r--app/assets/javascripts/jira_connect/branches/index.js1
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_form.vue4
-rw-r--r--app/assets/javascripts/jobs/components/filtered_search/constants.js13
-rw-r--r--app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue64
-rw-r--r--app/assets/javascripts/jobs/components/filtered_search/utils.js27
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue47
-rw-r--r--app/assets/javascripts/jobs/constants.js40
-rw-r--r--app/assets/javascripts/labels/labels_select.js6
-rw-r--r--app/assets/javascripts/lib/swagger.js2
-rw-r--r--app/assets/javascripts/lib/utils/array_utility.js15
-rw-r--r--app/assets/javascripts/lib/utils/breadcrumbs.js28
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js10
-rw-r--r--app/assets/javascripts/lib/utils/constants.js9
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js9
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_format_utility.js27
-rw-r--r--app/assets/javascripts/lib/utils/datetime_range.js311
-rw-r--r--app/assets/javascripts/lib/utils/grammar.js6
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js6
-rw-r--r--app/assets/javascripts/lib/utils/secret_detection.js4
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js3
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js18
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js8
-rw-r--r--app/assets/javascripts/lib/utils/webpack.js5
-rw-r--r--app/assets/javascripts/locale/ensure_single_line.cjs10
-rw-r--r--app/assets/javascripts/locale/index.js64
-rw-r--r--app/assets/javascripts/locale/sprintf.js22
-rw-r--r--app/assets/javascripts/main.js3
-rw-r--r--app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue2
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_member_button.vue2
-rw-r--r--app/assets/javascripts/members/components/modals/remove_member_modal.vue3
-rw-r--r--app/assets/javascripts/members/components/table/members_table_cell.vue6
-rw-r--r--app/assets/javascripts/merge_request_tabs.js10
-rw-r--r--app/assets/javascripts/merge_requests/components/compare_app.vue20
-rw-r--r--app/assets/javascripts/merge_requests/components/compare_dropdown.vue13
-rw-r--r--app/assets/javascripts/merge_requests/components/header_metadata.vue (renamed from app/assets/javascripts/issuable/components/issuable_header_warnings.vue)32
-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.vue12
-rw-r--r--app/assets/javascripts/merge_requests/index.js19
-rw-r--r--app/assets/javascripts/notebook/cells/output/index.vue45
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue13
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue3
-rw-r--r--app/assets/javascripts/notes/components/mr_discussion_filter.vue3
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue14
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue2
-rw-r--r--app/assets/javascripts/notes/components/sidebar_subscription.vue2
-rw-r--r--app/assets/javascripts/notes/stores/actions.js115
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/observability/client.js115
-rw-r--r--app/assets/javascripts/observability/mock_traces.json443
-rw-r--r--app/assets/javascripts/organizations/constants.js4
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/components/app.vue21
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue43
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue46
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/constants.js2
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/index.js21
-rw-r--r--app/assets/javascripts/organizations/mock_data.js258
-rw-r--r--app/assets/javascripts/organizations/shared/components/groups_view.vue82
-rw-r--r--app/assets/javascripts/organizations/shared/components/projects_view.vue86
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/queries/groups.query.graphql (renamed from app/assets/javascripts/organizations/groups_and_projects/graphql/queries/groups.query.graphql)0
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql (renamed from app/assets/javascripts/organizations/groups_and_projects/graphql/queries/projects.query.graphql)0
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/resolvers.js (renamed from app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js)6
-rw-r--r--app/assets/javascripts/organizations/shared/utils.js (renamed from app/assets/javascripts/organizations/groups_and_projects/utils.js)9
-rw-r--r--app/assets/javascripts/organizations/show/components/app.vue37
-rw-r--r--app/assets/javascripts/organizations/show/components/association_count_card.vue54
-rw-r--r--app/assets/javascripts/organizations/show/components/association_counts.vue71
-rw-r--r--app/assets/javascripts/organizations/show/components/groups_and_projects.vue110
-rw-r--r--app/assets/javascripts/organizations/show/components/organization_avatar.vue71
-rw-r--r--app/assets/javascripts/organizations/show/constants.js1
-rw-r--r--app/assets/javascripts/organizations/show/index.js63
-rw-r--r--app/assets/javascripts/organizations/show/utils.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue58
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/index.js14
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/router.js14
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/utils.js24
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/index.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue77
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue7
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/index.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue41
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/utils.js24
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/cli_commands.vue17
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/utils.js26
-rw-r--r--app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js38
-rw-r--r--app/assets/javascripts/pages/admin/abuse_reports/index.js7
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs_count.query.graphql5
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/index.js45
-rw-r--r--app/assets/javascripts/pages/dashboard/groups/index/index.js3
-rw-r--r--app/assets/javascripts/pages/explore/groups/index.js3
-rw-r--r--app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue11
-rw-r--r--app/assets/javascripts/pages/organizations/organizations/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/incidents/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/show/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/jobs/index/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/branch_finder.js1
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js37
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js18
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/page.js4
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js74
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue62
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js94
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/tracing/index/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/tracing/show/index.js4
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue3
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js2
-rw-r--r--app/assets/javascripts/pipelines/mixins/stage_column_mixin.js14
-rw-r--r--app/assets/javascripts/profile/profile.js1
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue15
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue5
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue156
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/constants.js2
-rw-r--r--app/assets/javascripts/projects/project_star_button.js46
-rw-r--r--app/assets/javascripts/projects/settings/access_dropdown.js611
-rw-r--r--app/assets/javascripts/projects/settings/api/access_dropdown_api.js16
-rw-r--r--app/assets/javascripts/projects/settings/components/access_dropdown.vue126
-rw-r--r--app/assets/javascripts/projects/settings/constants.js7
-rw-r--r--app/assets/javascripts/projects/settings/init_access_dropdown.js25
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue33
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue1
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js2
-rw-r--r--app/assets/javascripts/projects/star.js43
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js101
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js141
-rw-r--r--app/assets/javascripts/protected_tags/constants.js4
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js71
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js115
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.vue113
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit_list.js46
-rw-r--r--app/assets/javascripts/related_issues/components/add_issuable_form.vue3
-rw-r--r--app/assets/javascripts/related_issues/index.js5
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue141
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue22
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue7
-rw-r--r--app/assets/javascripts/repository/constants.js8
-rw-r--r--app/assets/javascripts/rest_api.js1
-rw-r--r--app/assets/javascripts/right_sidebar.js15
-rw-r--r--app/assets/javascripts/run_modules.js9
-rw-r--r--app/assets/javascripts/search/index.js3
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue81
-rw-r--r--app/assets/javascripts/search/sidebar/components/archived_filter/data.js5
-rw-r--r--app/assets/javascripts/search/sidebar/components/archived_filter/index.vue6
-rw-r--r--app/assets/javascripts/search/sidebar/components/blobs_filters.vue22
-rw-r--r--app/assets/javascripts/search/sidebar/components/commits_filters.vue18
-rw-r--r--app/assets/javascripts/search/sidebar/components/filters_template.vue8
-rw-r--r--app/assets/javascripts/search/sidebar/components/issues_filters.vue21
-rw-r--r--app/assets/javascripts/search/sidebar/components/language_filter/index.vue2
-rw-r--r--app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue33
-rw-r--r--app/assets/javascripts/search/sidebar/components/notes_filters.vue18
-rw-r--r--app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue3
-rw-r--r--app/assets/javascripts/search/sidebar/components/small_screen_drawer_navigation.vue61
-rw-r--r--app/assets/javascripts/search/sidebar/constants/index.js6
-rw-r--r--app/assets/javascripts/search/sidebar/index.js6
-rw-r--r--app/assets/javascripts/search/store/actions.js2
-rw-r--r--app/assets/javascripts/search/store/mutations.js2
-rw-r--r--app/assets/javascripts/search/store/state.js3
-rw-r--r--app/assets/javascripts/search/store/utils.js3
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue13
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js4
-rw-r--r--app/assets/javascripts/security_configuration/components/continuous_vulnerability_scan.vue127
-rw-r--r--app/assets/javascripts/security_configuration/components/feature_card.vue7
-rw-r--r--app/assets/javascripts/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql8
-rw-r--r--app/assets/javascripts/security_configuration/index.js2
-rw-r--r--app/assets/javascripts/security_configuration/utils.js5
-rw-r--r--app/assets/javascripts/sentry/index.js35
-rw-r--r--app/assets/javascripts/sentry/init_sentry.js77
-rw-r--r--app/assets/javascripts/sentry/sentry_browser_wrapper.js4
-rw-r--r--app/assets/javascripts/sentry/sentry_config.js31
-rw-r--r--app/assets/javascripts/settings_panels.js12
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue13
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue27
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/copy/sidebar_reference_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/getters.js3
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue24
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue13
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_footer.vue15
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue11
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue24
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/report.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue3
-rw-r--r--app/assets/javascripts/sidebar/constants.js268
-rw-r--r--app/assets/javascripts/sidebar/queries/constants.js291
-rw-r--r--app/assets/javascripts/sidebar/queries/test_case_confidential.query.graphql9
-rw-r--r--app/assets/javascripts/sidebar/queries/update_test_case_confidential.mutation.graphql9
-rw-r--r--app/assets/javascripts/silent_mode_settings/components/app.vue70
-rw-r--r--app/assets/javascripts/silent_mode_settings/index.js27
-rw-r--r--app/assets/javascripts/snippets/components/embed_dropdown.vue39
-rw-r--r--app/assets/javascripts/snippets/utils/blob.js3
-rw-r--r--app/assets/javascripts/super_sidebar/components/brand_logo.vue14
-rw-r--r--app/assets/javascripts/super_sidebar/components/context_header.vue56
-rw-r--r--app/assets/javascripts/super_sidebar/components/context_switcher.vue209
-rw-r--r--app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue43
-rw-r--r--app/assets/javascripts/super_sidebar/components/create_menu.vue78
-rw-r--r--app/assets/javascripts/super_sidebar/components/flyout_menu.vue129
-rw-r--r--app/assets/javascripts/super_sidebar/components/frequent_items_list.vue106
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue15
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js10
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js35
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue8
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue17
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue20
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/store/getters.js13
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/utils.js13
-rw-r--r--app/assets/javascripts/super_sidebar/components/groups_list.vue81
-rw-r--r--app/assets/javascripts/super_sidebar/components/items_list.vue44
-rw-r--r--app/assets/javascripts/super_sidebar/components/menu_section.vue16
-rw-r--r--app/assets/javascripts/super_sidebar/components/nav_item.vue148
-rw-r--r--app/assets/javascripts/super_sidebar/components/pinned_section.vue30
-rw-r--r--app/assets/javascripts/super_sidebar/components/projects_list.vue82
-rw-r--r--app/assets/javascripts/super_sidebar/components/search_results.vue99
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_hover_peek_behavior.vue126
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_menu.vue16
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue14
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar.vue95
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue26
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue57
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu.vue14
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_name_group.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/constants.js10
-rw-r--r--app/assets/javascripts/super_sidebar/graphql/queries/search_user_groups_and_projects.query.graphql24
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js9
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js3
-rw-r--r--app/assets/javascripts/super_sidebar/utils.js64
-rw-r--r--app/assets/javascripts/time_tracking/components/queries/get_timelogs.query.graphql8
-rw-r--r--app/assets/javascripts/time_tracking/components/timelogs_app.vue28
-rw-r--r--app/assets/javascripts/token_access/components/outbound_token_access.vue10
-rw-r--r--app/assets/javascripts/tracing/components/tracing_details.vue90
-rw-r--r--app/assets/javascripts/tracing/components/tracing_empty_state.vue35
-rw-r--r--app/assets/javascripts/tracing/components/tracing_list.vue125
-rw-r--r--app/assets/javascripts/tracing/components/tracing_list_filtered_search.vue87
-rw-r--r--app/assets/javascripts/tracing/components/tracing_table_list.vue101
-rw-r--r--app/assets/javascripts/tracing/details_index.vue49
-rw-r--r--app/assets/javascripts/tracing/filters.js104
-rw-r--r--app/assets/javascripts/tracing/list_index.vue37
-rw-r--r--app/assets/javascripts/tracking/constants.js8
-rw-r--r--app/assets/javascripts/tracking/dispatch_snowplow_event.js8
-rw-r--r--app/assets/javascripts/tracking/index.js1
-rw-r--r--app/assets/javascripts/tracking/internal_events.js48
-rw-r--r--app/assets/javascripts/tracking/tracker.js8
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/project_storage_app.stories.js64
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue80
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/usage_graph.vue136
-rw-r--r--app/assets/javascripts/user_lists/components/user_list.vue2
-rw-r--r--app/assets/javascripts/users_select/index.js1047
-rw-r--r--app/assets/javascripts/visibility_level/constants.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue55
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue36
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js189
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue313
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js11
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/constants.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue85
-rw-r--r--app/assets/javascripts/vue_shared/components/changed_file_icon.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/confidentiality_badge.vue28
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue283
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue77
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js91
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/empty_file.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/group_select.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/utils.js19
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/index.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue203
-rw-r--r--app/assets/javascripts/vue_shared/components/incidents/utils.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/list_actions/constants.js16
-rw-r--r--app/assets/javascripts/vue_shared/components/list_actions/list_actions.stories.js44
-rw-r--r--app/assets/javascripts/vue_shared/components/list_actions/list_actions.vue52
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field_view.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/projects_list/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue49
-rw-r--r--app/assets/javascripts/vue_shared/components/source_editor.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue66
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/split_button.vue85
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/user_select/user_select.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue6
-rw-r--r--app/assets/javascripts/vue_shared/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue56
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue8
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue11
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue8
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue3
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue24
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue44
-rw-r--r--app/assets/javascripts/vuex_shared/bindings.js3
-rw-r--r--app/assets/javascripts/webpack.js3
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue35
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue6
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue150
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_discussion.vue6
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue4
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue4
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue11
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue3
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue25
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_token_input.vue145
-rw-r--r--app/assets/javascripts/work_items/components/widget_wrapper.vue19
-rw-r--r--app/assets/javascripts/work_items/components/work_item_actions.vue80
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_created_updated.vue8
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue23
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue5
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue7
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue3
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue94
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue7
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue61
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue185
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state_badge.vue12
-rw-r--r--app/assets/javascripts/work_items/components/work_item_type_icon.vue2
-rw-r--r--app/assets/javascripts/work_items/constants.js27
-rw-r--r--app/assets/javascripts/work_items/graphql/group_work_item_types.query.graphql11
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql27
-rw-r--r--app/assets/javascripts/work_items/list/components/work_items_list_app.vue25
-rw-r--r--app/assets/javascripts/work_items/list/index.js15
-rw-r--r--app/assets/javascripts/work_items/list/queries/base_work_item_widgets.fragment.graphql38
-rw-r--r--app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql27
-rw-r--r--app/assets/javascripts/work_items/list/queries/work_item_widgets.fragment.graphql5
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue11
-rw-r--r--app/assets/javascripts/work_items/utils.js23
-rw-r--r--app/assets/javascripts/work_items_hierarchy/hierarchy_util.js3
-rw-r--r--app/assets/stylesheets/disable_animations.scss5
-rw-r--r--app/assets/stylesheets/framework/common.scss5
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss17
-rw-r--r--app/assets/stylesheets/framework/filters.scss16
-rw-r--r--app/assets/stylesheets/framework/header.scss8
-rw-r--r--app/assets/stylesheets/framework/job_log.scss2
-rw-r--r--app/assets/stylesheets/framework/layout.scss12
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss2
-rw-r--r--app/assets/stylesheets/framework/mixins.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss35
-rw-r--r--app/assets/stylesheets/framework/super_sidebar.scss103
-rw-r--r--app/assets/stylesheets/framework/variables.scss148
-rw-r--r--app/assets/stylesheets/page_bundles/admin/jobs_index.scss5
-rw-r--r--app/assets/stylesheets/page_bundles/build.scss77
-rw-r--r--app/assets/stylesheets/page_bundles/incidents.scss3
-rw-r--r--app/assets/stylesheets/page_bundles/issuable.scss12
-rw-r--r--app/assets/stylesheets/page_bundles/merge_request.scss7
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss15
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline_schedules.scss82
-rw-r--r--app/assets/stylesheets/page_bundles/profiles/preferences.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/projects_usage_quotas.scss19
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss10
-rw-r--r--app/assets/stylesheets/pages/note_form.scss2
-rw-r--r--app/assets/stylesheets/pages/projects.scss2
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_helper.scss9
-rw-r--r--app/assets/stylesheets/themes/theme_light_gray.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_green.scss2
-rw-r--r--app/assets/stylesheets/utilities.scss13
-rw-r--r--app/channels/noteable/notes_channel.rb1
-rw-r--r--app/components/pajamas/banner_component.html.haml3
-rw-r--r--app/components/pajamas/banner_component.rb8
-rw-r--r--app/controllers/activity_pub/application_controller.rb27
-rw-r--r--app/controllers/activity_pub/projects/application_controller.rb28
-rw-r--r--app/controllers/activity_pub/projects/releases_controller.rb29
-rw-r--r--app/controllers/admin/abuse_reports_controller.rb14
-rw-r--r--app/controllers/admin/jobs_controller.rb12
-rw-r--r--app/controllers/admin/users_controller.rb9
-rw-r--r--app/controllers/application_controller.rb35
-rw-r--r--app/controllers/clusters/agents/dashboard_controller.rb34
-rw-r--r--app/controllers/concerns/access_tokens_actions.rb1
-rw-r--r--app/controllers/concerns/harbor/access.rb8
-rw-r--r--app/controllers/concerns/issuable_actions.rb13
-rw-r--r--app/controllers/concerns/issuable_collections.rb18
-rw-r--r--app/controllers/concerns/notes_actions.rb3
-rw-r--r--app/controllers/concerns/onboarding/status.rb6
-rw-r--r--app/controllers/concerns/preferred_language_switcher.rb31
-rw-r--r--app/controllers/concerns/search_rate_limitable.rb3
-rw-r--r--app/controllers/concerns/verifies_with_email.rb1
-rw-r--r--app/controllers/concerns/web_hooks/hook_log_actions.rb9
-rw-r--r--app/controllers/confirmations_controller.rb1
-rw-r--r--app/controllers/groups/email_campaigns_controller.rb69
-rw-r--r--app/controllers/groups/labels_controller.rb5
-rw-r--r--app/controllers/groups/runners_controller.rb2
-rw-r--r--app/controllers/groups/work_items_controller.rb4
-rw-r--r--app/controllers/groups_controller.rb1
-rw-r--r--app/controllers/help_controller.rb2
-rw-r--r--app/controllers/import/bitbucket_server_controller.rb4
-rw-r--r--app/controllers/invites_controller.rb2
-rw-r--r--app/controllers/oauth/authorizations_controller.rb2
-rw-r--r--app/controllers/organizations/application_controller.rb15
-rw-r--r--app/controllers/organizations/organizations_controller.rb16
-rw-r--r--app/controllers/passwords_controller.rb4
-rw-r--r--app/controllers/profiles/notifications_controller.rb2
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb1
-rw-r--r--app/controllers/profiles/preferences_controller.rb1
-rw-r--r--app/controllers/projects/alerting/notifications_controller.rb6
-rw-r--r--app/controllers/projects/commits_controller.rb2
-rw-r--r--app/controllers/projects/environments/sample_metrics_controller.rb16
-rw-r--r--app/controllers/projects/environments_controller.rb14
-rw-r--r--app/controllers/projects/graphs_controller.rb2
-rw-r--r--app/controllers/projects/incidents_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb12
-rw-r--r--app/controllers/projects/jobs_controller.rb20
-rw-r--r--app/controllers/projects/labels_controller.rb5
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb12
-rw-r--r--app/controllers/projects/merge_requests/conflicts_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb20
-rw-r--r--app/controllers/projects/mirrors_controller.rb1
-rw-r--r--app/controllers/projects/notes_controller.rb3
-rw-r--r--app/controllers/projects/pages_controller.rb10
-rw-r--r--app/controllers/projects/pipeline_schedules_controller.rb5
-rw-r--r--app/controllers/projects/pipelines/tests_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_controller.rb7
-rw-r--r--app/controllers/projects/prometheus/alerts_controller.rb43
-rw-r--r--app/controllers/projects/service_desk_controller.rb2
-rw-r--r--app/controllers/projects/tracing_controller.rb23
-rw-r--r--app/controllers/projects/work_items_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/controllers/pwa_controller.rb2
-rw-r--r--app/controllers/registrations/welcome_controller.rb10
-rw-r--r--app/controllers/registrations_controller.rb10
-rw-r--r--app/controllers/search_controller.rb4
-rw-r--r--app/controllers/sent_notifications_controller.rb2
-rw-r--r--app/controllers/sessions_controller.rb11
-rw-r--r--app/controllers/users/namespace_visits_controller.rb15
-rw-r--r--app/experiments/ios_specific_templates_experiment.rb2
-rw-r--r--app/finders/abuse_reports_finder.rb47
-rw-r--r--app/finders/ci/jobs_finder.rb33
-rw-r--r--app/finders/ci/runners_finder.rb3
-rw-r--r--app/finders/ci/triggers_finder.rb16
-rw-r--r--app/finders/group_members_finder.rb16
-rw-r--r--app/finders/groups/accepting_group_transfers_finder.rb4
-rw-r--r--app/finders/groups/base.rb4
-rw-r--r--app/finders/issuable_finder.rb2
-rw-r--r--app/finders/organizations/groups_finder.rb59
-rw-r--r--app/finders/organizations/organization_users_finder.rb33
-rw-r--r--app/finders/packages/npm/packages_for_user_finder.rb18
-rw-r--r--app/finders/packages/nuget/package_finder.rb3
-rw-r--r--app/finders/repositories/changelog_commits_finder.rb2
-rw-r--r--app/finders/work_items/namespace_work_items_finder.rb27
-rw-r--r--app/graphql/gitlab_schema.rb6
-rw-r--r--app/graphql/mutations/admin/abuse_report_labels/create.rb36
-rw-r--r--app/graphql/mutations/ci/project_ci_cd_settings_update.rb13
-rw-r--r--app/graphql/mutations/issues/bulk_update.rb2
-rw-r--r--app/graphql/mutations/issues/update.rb8
-rw-r--r--app/graphql/mutations/merge_requests/update.rb8
-rw-r--r--app/graphql/mutations/metrics/dashboard/annotations/base.rb18
-rw-r--r--app/graphql/mutations/metrics/dashboard/annotations/create.rb24
-rw-r--r--app/graphql/mutations/metrics/dashboard/annotations/delete.rb4
-rw-r--r--app/graphql/mutations/work_items/linked_items/add.rb3
-rw-r--r--app/graphql/mutations/work_items/linked_items/base.rb7
-rw-r--r--app/graphql/mutations/work_items/linked_items/remove.rb28
-rw-r--r--app/graphql/resolvers/blame_resolver.rb62
-rw-r--r--app/graphql/resolvers/ci/all_jobs_resolver.rb28
-rw-r--r--app/graphql/resolvers/ci/pipeline_triggers_resolver.rb3
-rw-r--r--app/graphql/resolvers/ci/runner_jobs_resolver.rb16
-rw-r--r--app/graphql/resolvers/ci/runners_resolver.rb24
-rw-r--r--app/graphql/resolvers/ci/test_suite_resolver.rb2
-rw-r--r--app/graphql/resolvers/codequality_reports_comparer_resolver.rb19
-rw-r--r--app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb2
-rw-r--r--app/graphql/resolvers/namespaces/work_item_resolver.rb31
-rw-r--r--app/graphql/resolvers/namespaces/work_items_resolver.rb38
-rw-r--r--app/graphql/resolvers/organizations/groups_resolver.rb37
-rw-r--r--app/graphql/resolvers/organizations/organization_resolver.rb22
-rw-r--r--app/graphql/resolvers/organizations/organization_users_resolver.rb36
-rw-r--r--app/graphql/resolvers/work_items/linked_items_resolver.rb14
-rw-r--r--app/graphql/resolvers/work_items_resolver.rb11
-rw-r--r--app/graphql/types/blame/blame_type.rb20
-rw-r--r--app/graphql/types/blame/commit_data_type.rb20
-rw-r--r--app/graphql/types/blame/groups_type.rb20
-rw-r--r--app/graphql/types/ci/ci_cd_setting_type.rb3
-rw-r--r--app/graphql/types/ci/job_base_field.rb35
-rw-r--r--app/graphql/types/ci/job_failure_reason_enum.rb15
-rw-r--r--app/graphql/types/ci/job_trace_type.rb19
-rw-r--r--app/graphql/types/ci/job_type.rb28
-rw-r--r--app/graphql/types/ci/pipeline_schedule_type.rb5
-rw-r--r--app/graphql/types/ci/runner_job_execution_status_enum.rb4
-rw-r--r--app/graphql/types/ci/runner_membership_filter_enum.rb2
-rw-r--r--app/graphql/types/group_type.rb6
-rw-r--r--app/graphql/types/issue_type.rb2
-rw-r--r--app/graphql/types/label_type.rb3
-rw-r--r--app/graphql/types/merge_request_type.rb12
-rw-r--r--app/graphql/types/mutation_type.rb2
-rw-r--r--app/graphql/types/organizations/group_sort_enum.rb24
-rw-r--r--app/graphql/types/organizations/organization_type.rb38
-rw-r--r--app/graphql/types/organizations/organization_user_type.rb36
-rw-r--r--app/graphql/types/permission_types/work_item.rb2
-rw-r--r--app/graphql/types/query_type.rb9
-rw-r--r--app/graphql/types/repository/blob_type.rb3
-rw-r--r--app/graphql/types/security/codequality_reports_comparer/degradation_type.rb45
-rw-r--r--app/graphql/types/security/codequality_reports_comparer/report_type.rb40
-rw-r--r--app/graphql/types/security/codequality_reports_comparer/status_enum.rb16
-rw-r--r--app/graphql/types/security/codequality_reports_comparer/summary_type.rb30
-rw-r--r--app/graphql/types/security/codequality_reports_comparer_type.rb19
-rw-r--r--app/graphql/types/user_type.rb2
-rw-r--r--app/graphql/types/work_items/award_emoji_update_action_enum.rb1
-rw-r--r--app/graphql/types/work_items/widgets/linked_items_type.rb2
-rw-r--r--app/helpers/admin/abuse_reports_helper.rb3
-rw-r--r--app/helpers/application_helper.rb12
-rw-r--r--app/helpers/application_settings_helper.rb6
-rw-r--r--app/helpers/artifacts_helper.rb3
-rw-r--r--app/helpers/auth_helper.rb9
-rw-r--r--app/helpers/button_helper.rb68
-rw-r--r--app/helpers/ci/status_helper.rb72
-rw-r--r--app/helpers/ci/variables_helper.rb4
-rw-r--r--app/helpers/clusters_helper.rb2
-rw-r--r--app/helpers/colors_helper.rb2
-rw-r--r--app/helpers/diff_helper.rb2
-rw-r--r--app/helpers/emails_helper.rb2
-rw-r--r--app/helpers/environment_helper.rb2
-rw-r--r--app/helpers/environments_helper.rb7
-rw-r--r--app/helpers/external_link_helper.rb2
-rw-r--r--app/helpers/icons_helper.rb74
-rw-r--r--app/helpers/integrations_helper.rb23
-rw-r--r--app/helpers/invite_members_helper.rb25
-rw-r--r--app/helpers/issuables_helper.rb237
-rw-r--r--app/helpers/issues_helper.rb30
-rw-r--r--app/helpers/labels_helper.rb10
-rw-r--r--app/helpers/markup_helper.rb6
-rw-r--r--app/helpers/members_helper.rb8
-rw-r--r--app/helpers/merge_requests_helper.rb9
-rw-r--r--app/helpers/nav/new_dropdown_helper.rb2
-rw-r--r--app/helpers/nav_helper.rb4
-rw-r--r--app/helpers/organizations/organization_helper.rb34
-rw-r--r--app/helpers/profiles_helper.rb2
-rw-r--r--app/helpers/projects/observability_helper.rb23
-rw-r--r--app/helpers/projects/pipeline_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb8
-rw-r--r--app/helpers/registrations_helper.rb2
-rw-r--r--app/helpers/routing/projects_helper.rb13
-rw-r--r--app/helpers/safe_format_helper.rb2
-rw-r--r--app/helpers/search_helper.rb8
-rw-r--r--app/helpers/sidebars_helper.rb99
-rw-r--r--app/helpers/sidekiq_helper.rb2
-rw-r--r--app/helpers/stat_anchors_helper.rb2
-rw-r--r--app/helpers/todos_helper.rb4
-rw-r--r--app/helpers/users/callouts_helper.rb7
-rw-r--r--app/helpers/users_helper.rb4
-rw-r--r--app/helpers/version_check_helper.rb3
-rw-r--r--app/helpers/vite_helper.rb29
-rw-r--r--app/helpers/webpack_helper.rb10
-rw-r--r--app/helpers/work_items_helper.rb6
-rw-r--r--app/mailers/emails/in_product_marketing.rb9
-rw-r--r--app/mailers/emails/profile.rb26
-rw-r--r--app/mailers/emails/service_desk.rb22
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/mailers/previews/notify_preview.rb4
-rw-r--r--app/models/ability.rb1
-rw-r--r--app/models/abuse_report.rb15
-rw-r--r--app/models/active_session.rb11
-rw-r--r--app/models/alerting/project_alerting_setting.rb28
-rw-r--r--app/models/analytics/cycle_analytics/runtime_limiter.rb30
-rw-r--r--app/models/analytics/cycle_analytics/stage_event_hash.rb2
-rw-r--r--app/models/application_setting.rb71
-rw-r--r--app/models/application_setting_implementation.rb24
-rw-r--r--app/models/approval.rb3
-rw-r--r--app/models/award_emoji.rb2
-rw-r--r--app/models/badge.rb2
-rw-r--r--app/models/blob_viewer/binary_stl.rb2
-rw-r--r--app/models/blob_viewer/cargo_toml.rb2
-rw-r--r--app/models/blob_viewer/cartfile.rb2
-rw-r--r--app/models/blob_viewer/changelog.rb2
-rw-r--r--app/models/blob_viewer/composer_json.rb2
-rw-r--r--app/models/blob_viewer/contributing.rb2
-rw-r--r--app/models/blob_viewer/csv.rb2
-rw-r--r--app/models/blob_viewer/gemfile.rb2
-rw-r--r--app/models/blob_viewer/gemspec.rb2
-rw-r--r--app/models/blob_viewer/gitlab_ci_yml.rb2
-rw-r--r--app/models/blob_viewer/go_mod.rb4
-rw-r--r--app/models/blob_viewer/godeps_json.rb2
-rw-r--r--app/models/blob_viewer/license.rb2
-rw-r--r--app/models/blob_viewer/markup.rb2
-rw-r--r--app/models/blob_viewer/notebook.rb2
-rw-r--r--app/models/blob_viewer/open_api.rb2
-rw-r--r--app/models/blob_viewer/package_json.rb2
-rw-r--r--app/models/blob_viewer/pdf.rb2
-rw-r--r--app/models/blob_viewer/podfile.rb2
-rw-r--r--app/models/blob_viewer/podspec.rb2
-rw-r--r--app/models/blob_viewer/podspec_json.rb2
-rw-r--r--app/models/blob_viewer/readme.rb2
-rw-r--r--app/models/blob_viewer/requirements_txt.rb2
-rw-r--r--app/models/blob_viewer/route_map.rb2
-rw-r--r--app/models/blob_viewer/sketch.rb2
-rw-r--r--app/models/blob_viewer/svg.rb2
-rw-r--r--app/models/blob_viewer/yarn_lock.rb2
-rw-r--r--app/models/bulk_imports/batch_tracker.rb2
-rw-r--r--app/models/bulk_imports/entity.rb8
-rw-r--r--app/models/bulk_imports/file_transfer/group_config.rb2
-rw-r--r--app/models/bulk_imports/file_transfer/project_config.rb4
-rw-r--r--app/models/chat_name.rb3
-rw-r--r--app/models/ci/build.rb24
-rw-r--r--app/models/ci/build_need.rb5
-rw-r--r--app/models/ci/build_runner_session.rb2
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/models/ci/runner.rb10
-rw-r--r--app/models/clusters/agent.rb2
-rw-r--r--app/models/clusters/agent_token.rb12
-rw-r--r--app/models/clusters/platforms/kubernetes.rb2
-rw-r--r--app/models/commit.rb20
-rw-r--r--app/models/commit_range.rb6
-rw-r--r--app/models/commit_signatures/gpg_signature.rb1
-rw-r--r--app/models/commit_status.rb17
-rw-r--r--app/models/concerns/avatarable.rb6
-rw-r--r--app/models/concerns/chronic_duration_attribute.rb7
-rw-r--r--app/models/concerns/ci/deployable.rb6
-rw-r--r--app/models/concerns/ci/has_runner_executor.rb4
-rw-r--r--app/models/concerns/ci/maskable.rb4
-rw-r--r--app/models/concerns/ci/partitionable.rb5
-rw-r--r--app/models/concerns/clusters/agents/authorizations/ci_access/config_scopes.rb2
-rw-r--r--app/models/concerns/cross_database_ignored_tables.rb11
-rw-r--r--app/models/concerns/diff_positionable_note.rb2
-rw-r--r--app/models/concerns/each_batch.rb4
-rw-r--r--app/models/concerns/editable.rb2
-rw-r--r--app/models/concerns/enums/prometheus_metric.rb14
-rw-r--r--app/models/concerns/has_unique_internal_users.rb51
-rw-r--r--app/models/concerns/has_user_type.rb17
-rw-r--r--app/models/concerns/integrations/enable_ssl_verification.rb17
-rw-r--r--app/models/concerns/integrations/reset_secret_fields.rb4
-rw-r--r--app/models/concerns/integrations/slack_mattermost_fields.rb43
-rw-r--r--app/models/concerns/issuable.rb17
-rw-r--r--app/models/concerns/issue_available_features.rb8
-rw-r--r--app/models/concerns/linkable_item.rb1
-rw-r--r--app/models/concerns/mentionable/reference_regexes.rb2
-rw-r--r--app/models/concerns/noteable.rb27
-rw-r--r--app/models/concerns/packages/nuget/version_normalizable.rb2
-rw-r--r--app/models/concerns/pg_full_text_searchable.rb6
-rw-r--r--app/models/concerns/protected_ref.rb5
-rw-r--r--app/models/concerns/redactable.rb2
-rw-r--r--app/models/concerns/require_email_verification.rb5
-rw-r--r--app/models/concerns/resolvable_discussion.rb2
-rw-r--r--app/models/concerns/resolvable_note.rb2
-rw-r--r--app/models/concerns/restricted_signup.rb2
-rw-r--r--app/models/concerns/routable.rb71
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb2
-rw-r--r--app/models/concerns/taskable.rb6
-rw-r--r--app/models/concerns/transitionable.rb21
-rw-r--r--app/models/concerns/users/visitable.rb18
-rw-r--r--app/models/concerns/with_uploads.rb2
-rw-r--r--app/models/container_expiration_policy.rb4
-rw-r--r--app/models/container_registry/event.rb12
-rw-r--r--app/models/custom_emoji.rb2
-rw-r--r--app/models/deploy_key.rb2
-rw-r--r--app/models/deploy_token.rb4
-rw-r--r--app/models/description_version.rb2
-rw-r--r--app/models/design_management.rb2
-rw-r--r--app/models/diff_note.rb2
-rw-r--r--app/models/discussion_note.rb2
-rw-r--r--app/models/draft_note.rb4
-rw-r--r--app/models/environment.rb7
-rw-r--r--app/models/environment_status.rb3
-rw-r--r--app/models/error_tracking/project_error_tracking_setting.rb2
-rw-r--r--app/models/event.rb5
-rw-r--r--app/models/gpg_key.rb2
-rw-r--r--app/models/group.rb25
-rw-r--r--app/models/hooks/web_hook.rb2
-rw-r--r--app/models/hooks/web_hook_log.rb11
-rw-r--r--app/models/instance_configuration.rb2
-rw-r--r--app/models/integration.rb6
-rw-r--r--app/models/integrations/apple_app_store.rb4
-rw-r--r--app/models/integrations/asana.rb5
-rw-r--r--app/models/integrations/assembla.rb2
-rw-r--r--app/models/integrations/base_chat_notification.rb89
-rw-r--r--app/models/integrations/base_issue_tracker.rb2
-rw-r--r--app/models/integrations/base_monitoring.rb2
-rw-r--r--app/models/integrations/base_slack_notification.rb14
-rw-r--r--app/models/integrations/base_slash_commands.rb2
-rw-r--r--app/models/integrations/base_third_party_wiki.rb2
-rw-r--r--app/models/integrations/buildkite.rb2
-rw-r--r--app/models/integrations/campfire.rb30
-rw-r--r--app/models/integrations/chat_message/base_message.rb2
-rw-r--r--app/models/integrations/chat_message/deployment_message.rb8
-rw-r--r--app/models/integrations/confluence.rb10
-rw-r--r--app/models/integrations/datadog.rb4
-rw-r--r--app/models/integrations/discord.rb28
-rw-r--r--app/models/integrations/drone_ci.rb2
-rw-r--r--app/models/integrations/emails_on_push.rb2
-rw-r--r--app/models/integrations/external_wiki.rb2
-rw-r--r--app/models/integrations/gitlab_slack_application.rb17
-rw-r--r--app/models/integrations/hangouts_chat.rb6
-rw-r--r--app/models/integrations/jenkins.rb2
-rw-r--r--app/models/integrations/jira.rb2
-rw-r--r--app/models/integrations/mattermost.rb3
-rw-r--r--app/models/integrations/microsoft_teams.rb26
-rw-r--r--app/models/integrations/packagist.rb2
-rw-r--r--app/models/integrations/pivotaltracker.rb2
-rw-r--r--app/models/integrations/prometheus.rb12
-rw-r--r--app/models/integrations/pumble.rb6
-rw-r--r--app/models/integrations/pushover.rb24
-rw-r--r--app/models/integrations/shimo.rb4
-rw-r--r--app/models/integrations/slack.rb4
-rw-r--r--app/models/integrations/teamcity.rb4
-rw-r--r--app/models/integrations/telegram.rb29
-rw-r--r--app/models/integrations/unify_circuit.rb6
-rw-r--r--app/models/integrations/webex_teams.rb6
-rw-r--r--app/models/integrations/zentao.rb6
-rw-r--r--app/models/issuable_severity.rb10
-rw-r--r--app/models/issue.rb54
-rw-r--r--app/models/label_link.rb2
-rw-r--r--app/models/lfs_download_object.rb2
-rw-r--r--app/models/license_template.rb6
-rw-r--r--app/models/loose_foreign_keys/modification_tracker.rb22
-rw-r--r--app/models/loose_foreign_keys/turbo_modification_tracker.rb25
-rw-r--r--app/models/members/group_member.rb13
-rw-r--r--app/models/members/project_member.rb2
-rw-r--r--app/models/merge_request.rb55
-rw-r--r--app/models/metrics/dashboard/annotation.rb34
-rw-r--r--app/models/metrics/users_starred_dashboard.rb18
-rw-r--r--app/models/milestone.rb4
-rw-r--r--app/models/ml/model_version.rb2
-rw-r--r--app/models/namespace.rb29
-rw-r--r--app/models/namespace/detail.rb1
-rw-r--r--app/models/namespace/root_storage_statistics.rb6
-rw-r--r--app/models/namespace/traversal_hierarchy.rb13
-rw-r--r--app/models/namespaces/randomized_suffix_path.rb2
-rw-r--r--app/models/note.rb24
-rw-r--r--app/models/notification_setting.rb2
-rw-r--r--app/models/operations/feature_flag.rb2
-rw-r--r--app/models/organizations/organization.rb3
-rw-r--r--app/models/packages/debian.rb8
-rw-r--r--app/models/packages/debian/file_entry.rb2
-rw-r--r--app/models/packages/dependency_link.rb28
-rw-r--r--app/models/packages/ml_model/package.rb19
-rw-r--r--app/models/packages/nuget/metadatum.rb3
-rw-r--r--app/models/packages/nuget/symbol.rb32
-rw-r--r--app/models/packages/package.rb14
-rw-r--r--app/models/packages/protection.rb9
-rw-r--r--app/models/packages/protection/rule.rb21
-rw-r--r--app/models/pages/lookup_path.rb2
-rw-r--r--app/models/pages/virtual_domain.rb27
-rw-r--r--app/models/pages_deployment.rb13
-rw-r--r--app/models/pages_domain.rb13
-rw-r--r--app/models/performance_monitoring/prometheus_metric.rb33
-rw-r--r--app/models/performance_monitoring/prometheus_panel.rb42
-rw-r--r--app/models/performance_monitoring/prometheus_panel_group.rb36
-rw-r--r--app/models/personal_access_token.rb2
-rw-r--r--app/models/plan.rb2
-rw-r--r--app/models/pool_repository.rb10
-rw-r--r--app/models/project.rb80
-rw-r--r--app/models/project_authorization.rb11
-rw-r--r--app/models/project_authorizations/changes.rb2
-rw-r--r--app/models/project_ci_cd_setting.rb1
-rw-r--r--app/models/project_feature.rb8
-rw-r--r--app/models/project_import_state.rb7
-rw-r--r--app/models/project_metrics_setting.rb16
-rw-r--r--app/models/project_setting.rb27
-rw-r--r--app/models/project_team.rb1
-rw-r--r--app/models/releases/link.rb4
-rw-r--r--app/models/repository.rb30
-rw-r--r--app/models/resource_label_event.rb9
-rw-r--r--app/models/resource_state_event.rb2
-rw-r--r--app/models/resource_timebox_event.rb2
-rw-r--r--app/models/review.rb4
-rw-r--r--app/models/self_managed_prometheus_alert_event.rb18
-rw-r--r--app/models/sent_notification.rb4
-rw-r--r--app/models/snippet_repository.rb2
-rw-r--r--app/models/terraform/state.rb2
-rw-r--r--app/models/user.rb149
-rw-r--r--app/models/user_custom_attribute.rb9
-rw-r--r--app/models/user_interacted_project.rb2
-rw-r--r--app/models/user_preference.rb1
-rw-r--r--app/models/users/callout.rb8
-rw-r--r--app/models/users/credit_card_validation.rb27
-rw-r--r--app/models/users/group_visit.rb17
-rw-r--r--app/models/users/project_callout.rb6
-rw-r--r--app/models/users/project_visit.rb17
-rw-r--r--app/models/work_item.rb30
-rw-r--r--app/models/work_items/parent_link.rb3
-rw-r--r--app/models/work_items/related_work_item_link.rb14
-rw-r--r--app/models/work_items/type.rb2
-rw-r--r--app/models/work_items/widgets/linked_items.rb2
-rw-r--r--app/models/x509_certificate.rb2
-rw-r--r--app/models/x509_issuer.rb7
-rw-r--r--app/policies/base_policy.rb6
-rw-r--r--app/policies/ci/bridge_policy.rb8
-rw-r--r--app/policies/ci/pipeline_policy.rb8
-rw-r--r--app/policies/design_management/repository_policy.rb7
-rw-r--r--app/policies/global_policy.rb5
-rw-r--r--app/policies/group_policy.rb1
-rw-r--r--app/policies/organizations/organization_policy.rb2
-rw-r--r--app/policies/organizations/organization_user_policy.rb7
-rw-r--r--app/policies/project_member_policy.rb2
-rw-r--r--app/policies/project_policy.rb6
-rw-r--r--app/presenters/blob_presenter.rb2
-rw-r--r--app/presenters/ci/build_runner_presenter.rb10
-rw-r--r--app/presenters/dev_ops_report/metric_presenter.rb20
-rw-r--r--app/presenters/event_presenter.rb25
-rw-r--r--app/presenters/gitlab/blame_presenter.rb16
-rw-r--r--app/presenters/issue_presenter.rb3
-rw-r--r--app/presenters/projects/security/configuration_presenter.rb8
-rw-r--r--app/presenters/search_service_presenter.rb2
-rw-r--r--app/serializers/activity_pub/activity_streams_serializer.rb90
-rw-r--r--app/serializers/activity_pub/project_entity.rb23
-rw-r--r--app/serializers/activity_pub/release_entity.rb40
-rw-r--r--app/serializers/activity_pub/releases_actor_entity.rb29
-rw-r--r--app/serializers/activity_pub/releases_actor_serializer.rb7
-rw-r--r--app/serializers/activity_pub/releases_outbox_serializer.rb9
-rw-r--r--app/serializers/activity_pub/user_entity.rb22
-rw-r--r--app/serializers/admin/abuse_report_details_entity.rb50
-rw-r--r--app/serializers/admin/abuse_report_entity.rb1
-rw-r--r--app/serializers/admin/reported_content_entity.rb38
-rw-r--r--app/serializers/build_details_entity.rb4
-rw-r--r--app/serializers/ci/job_annotation_entity.rb8
-rw-r--r--app/serializers/codequality_degradation_entity.rb3
-rw-r--r--app/serializers/issue_serializer.rb27
-rw-r--r--app/serializers/pipeline_serializer.rb2
-rw-r--r--app/serializers/profile/event_entity.rb31
-rw-r--r--app/services/admin/abuse_report_labels/create_service.rb17
-rw-r--r--app/services/admin/abuse_reports/moderate_user_service.rb7
-rw-r--r--app/services/admin/abuse_reports/update_service.rb32
-rw-r--r--app/services/application_settings/update_service.rb2
-rw-r--r--app/services/auth/container_registry_authentication_service.rb4
-rw-r--r--app/services/auto_merge/base_service.rb21
-rw-r--r--app/services/boards/update_service.rb2
-rw-r--r--app/services/bulk_imports/create_pipeline_trackers_service.rb72
-rw-r--r--app/services/bulk_imports/create_service.rb5
-rw-r--r--app/services/bulk_imports/file_download_service.rb10
-rw-r--r--app/services/ci/create_commit_status_service.rb158
-rw-r--r--app/services/ci/create_pipeline_service.rb7
-rw-r--r--app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb6
-rw-r--r--app/services/ci/register_job_service.rb22
-rw-r--r--app/services/ci/update_instance_variables_service.rb2
-rw-r--r--app/services/clusters/agent_tokens/track_usage_service.rb2
-rw-r--r--app/services/clusters/kubernetes/create_or_update_service_account_service.rb12
-rw-r--r--app/services/commits/create_service.rb7
-rw-r--r--app/services/compare_service.rb5
-rw-r--r--app/services/concerns/alert_management/alert_processing.rb2
-rw-r--r--app/services/concerns/rate_limited_service.rb8
-rw-r--r--app/services/concerns/service_desk/custom_emails/logger.rb41
-rw-r--r--app/services/concerns/update_repository_storage_methods.rb12
-rw-r--r--app/services/design_management/copy_design_collection/copy_service.rb4
-rw-r--r--app/services/design_management/delete_designs_service.rb6
-rw-r--r--app/services/design_management/runs_design_actions.rb10
-rw-r--r--app/services/design_management/save_designs_service.rb8
-rw-r--r--app/services/error_tracking/base_service.rb6
-rw-r--r--app/services/event_create_service.rb12
-rw-r--r--app/services/feature_flags/base_service.rb2
-rw-r--r--app/services/files/multi_service.rb2
-rw-r--r--app/services/files/update_service.rb22
-rw-r--r--app/services/google_cloud/create_cloudsql_instance_service.rb38
-rw-r--r--app/services/google_cloud/fetch_google_ip_list_service.rb8
-rw-r--r--app/services/google_cloud/generate_pipeline_service.rb8
-rw-r--r--app/services/gpg_keys/destroy_service.rb15
-rw-r--r--app/services/groups/create_service.rb12
-rw-r--r--app/services/groups/destroy_service.rb7
-rw-r--r--app/services/groups/ssh_certificates/create_service.rb51
-rw-r--r--app/services/groups/ssh_certificates/destroy_service.rb35
-rw-r--r--app/services/groups/transfer_service.rb18
-rw-r--r--app/services/groups/update_service.rb19
-rw-r--r--app/services/import/bitbucket_server_service.rb2
-rw-r--r--app/services/import/fogbugz_service.rb2
-rw-r--r--app/services/import/github/cancel_project_import_service.rb6
-rw-r--r--app/services/import/github_service.rb2
-rw-r--r--app/services/import/gitlab_projects/file_acquisition_strategies/remote_file.rb2
-rw-r--r--app/services/import/gitlab_projects/file_acquisition_strategies/remote_file_s3.rb2
-rw-r--r--app/services/import/validate_remote_git_endpoint_service.rb2
-rw-r--r--app/services/import_export_clean_up_service.rb2
-rw-r--r--app/services/incident_management/pager_duty/create_incident_issue_service.rb2
-rw-r--r--app/services/incident_management/pager_duty/process_webhook_service.rb2
-rw-r--r--app/services/issuable/bulk_update_service.rb2
-rw-r--r--app/services/issuable_base_service.rb51
-rw-r--r--app/services/issuable_links/create_service.rb6
-rw-r--r--app/services/issues/base_service.rb5
-rw-r--r--app/services/issues/close_service.rb12
-rw-r--r--app/services/issues/create_service.rb11
-rw-r--r--app/services/issues/move_service.rb20
-rw-r--r--app/services/issues/relative_position_rebalancing_service.rb2
-rw-r--r--app/services/labels/available_labels_service.rb15
-rw-r--r--app/services/labels/create_service.rb4
-rw-r--r--app/services/labels/update_service.rb6
-rw-r--r--app/services/loose_foreign_keys/process_deleted_records_service.rb6
-rw-r--r--app/services/members/creator_service.rb51
-rw-r--r--app/services/members/destroy_service.rb4
-rw-r--r--app/services/merge_requests/approval_service.rb18
-rw-r--r--app/services/merge_requests/base_service.rb18
-rw-r--r--app/services/merge_requests/build_service.rb8
-rw-r--r--app/services/merge_requests/create_ref_service.rb94
-rw-r--r--app/services/merge_requests/ff_merge_service.rb31
-rw-r--r--app/services/merge_requests/merge_base_service.rb36
-rw-r--r--app/services/merge_requests/merge_service.rb97
-rw-r--r--app/services/merge_requests/merge_strategies/from_source_branch.rb4
-rw-r--r--app/services/merge_requests/merge_to_ref_service.rb8
-rw-r--r--app/services/merge_requests/refresh_service.rb4
-rw-r--r--app/services/merge_requests/update_service.rb2
-rw-r--r--app/services/metrics/global_metrics_update_service.rb24
-rw-r--r--app/services/metrics/sample_metrics_service.rb36
-rw-r--r--app/services/namespaces/in_product_marketing_emails_service.rb102
-rw-r--r--app/services/notes/create_service.rb4
-rw-r--r--app/services/notes/post_process_service.rb2
-rw-r--r--app/services/notification_recipients/builder/base.rb1
-rw-r--r--app/services/notification_service.rb20
-rw-r--r--app/services/packages/debian/generate_distribution_service.rb4
-rw-r--r--app/services/packages/npm/generate_metadata_service.rb52
-rw-r--r--app/services/packages/nuget/check_duplicates_service.rb88
-rw-r--r--app/services/packages/nuget/extract_metadata_file_service.rb15
-rw-r--r--app/services/packages/nuget/extract_remote_metadata_file_service.rb82
-rw-r--r--app/services/packages/nuget/metadata_extraction_service.rb18
-rw-r--r--app/services/packages/nuget/odata_package_entry_service.rb70
-rw-r--r--app/services/packages/nuget/update_package_from_metadata_service.rb2
-rw-r--r--app/services/preview_markdown_service.rb2
-rw-r--r--app/services/projects/apple_target_platform_detector_service.rb2
-rw-r--r--app/services/projects/container_repository/cleanup_tags_base_service.rb2
-rw-r--r--app/services/projects/container_repository/gitlab/delete_tags_service.rb2
-rw-r--r--app/services/projects/create_service.rb28
-rw-r--r--app/services/projects/destroy_service.rb6
-rw-r--r--app/services/projects/download_service.rb2
-rw-r--r--app/services/projects/hashed_storage/migrate_attachments_service.rb2
-rw-r--r--app/services/projects/import_error_filter.rb2
-rw-r--r--app/services/projects/in_product_marketing_campaign_emails_service.rb6
-rw-r--r--app/services/projects/lfs_pointers/lfs_object_download_list_service.rb4
-rw-r--r--app/services/projects/transfer_service.rb42
-rw-r--r--app/services/projects/update_pages_service.rb79
-rw-r--r--app/services/projects/update_repository_storage_service.rb4
-rw-r--r--app/services/projects/update_service.rb13
-rw-r--r--app/services/releases/destroy_service.rb10
-rw-r--r--app/services/repositories/base_service.rb2
-rw-r--r--app/services/repository_archive_clean_up_service.rb4
-rw-r--r--app/services/resource_access_tokens/create_service.rb2
-rw-r--r--app/services/resource_access_tokens/revoke_service.rb2
-rw-r--r--app/services/resource_events/base_change_timebox_service.rb2
-rw-r--r--app/services/resource_events/change_labels_service.rb2
-rw-r--r--app/services/resource_events/change_state_service.rb2
-rw-r--r--app/services/search/global_service.rb2
-rw-r--r--app/services/search/project_service.rb2
-rw-r--r--app/services/service_desk/custom_email_verifications/base_service.rb8
-rw-r--r--app/services/service_desk/custom_email_verifications/create_service.rb5
-rw-r--r--app/services/service_desk/custom_email_verifications/update_service.rb7
-rw-r--r--app/services/service_desk/custom_emails/base_service.rb3
-rw-r--r--app/services/service_desk/custom_emails/create_service.rb1
-rw-r--r--app/services/service_desk/custom_emails/destroy_service.rb1
-rw-r--r--app/services/service_desk_settings/update_service.rb7
-rw-r--r--app/services/service_response.rb22
-rw-r--r--app/services/snippets/update_service.rb2
-rw-r--r--app/services/spam/akismet_service.rb6
-rw-r--r--app/services/spam/spam_action_service.rb22
-rw-r--r--app/services/spam/spam_verdict_service.rb21
-rw-r--r--app/services/submodules/update_service.rb12
-rw-r--r--app/services/suggestions/create_service.rb26
-rw-r--r--app/services/system_notes/alert_management_service.rb4
-rw-r--r--app/services/system_notes/issuables_service.rb13
-rw-r--r--app/services/todos/destroy/destroyed_issuable_service.rb2
-rw-r--r--app/services/todos/destroy/entity_leave_service.rb2
-rw-r--r--app/services/users/activity_service.rb4
-rw-r--r--app/services/users/authorized_build_service.rb2
-rw-r--r--app/services/users/build_service.rb4
-rw-r--r--app/services/users/destroy_service.rb11
-rw-r--r--app/services/users/migrate_records_to_ghost_user_in_batches_service.rb8
-rw-r--r--app/services/users/migrate_records_to_ghost_user_service.rb2
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb20
-rw-r--r--app/services/users/upsert_credit_card_validation_service.rb8
-rw-r--r--app/services/webauthn/authenticate_service.rb11
-rw-r--r--app/services/work_items/callbacks/award_emoji.rb3
-rw-r--r--app/services/work_items/create_service.rb14
-rw-r--r--app/services/work_items/related_work_item_links/create_service.rb2
-rw-r--r--app/services/work_items/related_work_item_links/destroy_service.rb85
-rw-r--r--app/uploaders/design_management/design_v432x230_uploader.rb2
-rw-r--r--app/uploaders/file_uploader.rb4
-rw-r--r--app/uploaders/gitlab_uploader.rb2
-rw-r--r--app/uploaders/packages/nuget/symbol_uploader.rb25
-rw-r--r--app/validators/addressable_url_validator.rb2
-rw-r--r--app/validators/certificate_fingerprint_validator.rb2
-rw-r--r--app/validators/duration_validator.rb2
-rw-r--r--app/validators/gitlab/zoom_url_validator.rb2
-rw-r--r--app/validators/json_schema_validator.rb4
-rw-r--r--app/validators/json_schemas/pinned_nav_items.json7
-rw-r--r--app/validators/json_schemas/scan_result_policy_project_approval_settings.json22
-rw-r--r--app/validators/line_code_validator.rb2
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml39
-rw-r--r--app/views/admin/abuse_reports/index.html.haml34
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml21
-rw-r--r--app/views/admin/application_settings/_email.html.haml3
-rw-r--r--app/views/admin/application_settings/_gitpod.html.haml2
-rw-r--r--app/views/admin/application_settings/_import_and_export.html.haml43
-rw-r--r--app/views/admin/application_settings/_pages.html.haml19
-rw-r--r--app/views/admin/application_settings/_protected_paths.html.haml13
-rw-r--r--app/views/admin/application_settings/_search_limits.html.haml6
-rw-r--r--app/views/admin/application_settings/_sentry.html.haml8
-rw-r--r--app/views/admin/application_settings/_silent_mode_settings_form.html.haml11
-rw-r--r--app/views/admin/application_settings/_snowplow.html.haml2
-rw-r--r--app/views/admin/application_settings/_usage.html.haml21
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml20
-rw-r--r--app/views/admin/application_settings/general.html.haml14
-rw-r--r--app/views/admin/dev_ops_report/_score.html.haml2
-rw-r--r--app/views/admin/groups/_form.html.haml2
-rw-r--r--app/views/admin/groups/_group.html.haml4
-rw-r--r--app/views/admin/groups/index.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml2
-rw-r--r--app/views/admin/hook_logs/show.html.haml2
-rw-r--r--app/views/admin/identities/_identity.html.haml2
-rw-r--r--app/views/admin/identities/index.html.haml3
-rw-r--r--app/views/admin/jobs/index.html.haml25
-rw-r--r--app/views/admin/topics/_form.html.haml2
-rw-r--r--app/views/admin/users/_access_levels.html.haml2
-rw-r--r--app/views/admin/users/_head.html.haml4
-rw-r--r--app/views/admin/users/_users.html.haml4
-rw-r--r--app/views/admin/users/show.html.haml4
-rw-r--r--app/views/ci/runner/_how_to_setup_runner.html.haml4
-rw-r--r--app/views/ci/variables/_variable_row.html.haml36
-rw-r--r--app/views/clusters/clusters/_provider_details_form.html.haml6
-rw-r--r--app/views/dashboard/groups/_groups.html.haml2
-rw-r--r--app/views/dashboard/groups/index.html.haml6
-rw-r--r--app/views/dashboard/todos/_todo.html.haml6
-rw-r--r--app/views/dashboard/todos/index.html.haml2
-rw-r--r--app/views/devise/sessions/_new_base.html.haml2
-rw-r--r--app/views/devise/shared/_email_opted_in.html.haml6
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml10
-rw-r--r--app/views/doorkeeper/authorized_applications/_delete_form.html.haml3
-rw-r--r--app/views/events/_event.atom.builder2
-rw-r--r--app/views/events/event/_push.html.haml8
-rw-r--r--app/views/explore/groups/_groups.html.haml2
-rw-r--r--app/views/explore/groups/index.html.haml5
-rw-r--r--app/views/groups/_import_group_from_another_instance_panel.html.haml16
-rw-r--r--app/views/groups/dependency_proxies/show.html.haml1
-rw-r--r--app/views/groups/edit.html.haml4
-rw-r--r--app/views/groups/group_members/index.html.haml2
-rw-r--r--app/views/groups/labels/edit.html.haml3
-rw-r--r--app/views/groups/milestones/_form.html.haml2
-rw-r--r--app/views/groups/settings/_permissions.html.haml2
-rw-r--r--app/views/groups/settings/applications/index.html.haml2
-rw-r--r--app/views/groups/work_items/index.html.haml2
-rw-r--r--app/views/groups/work_items/show.html.haml1
-rw-r--r--app/views/help/instance_configuration/_size_limits.html.haml4
-rw-r--r--app/views/import/bitbucket_server/new.html.haml3
-rw-r--r--app/views/layouts/_head.html.haml12
-rw-r--r--app/views/layouts/_page.html.haml2
-rw-r--r--app/views/layouts/_snowplow.html.haml2
-rw-r--r--app/views/layouts/application.html.haml9
-rw-r--r--app/views/layouts/errors.html.haml2
-rw-r--r--app/views/layouts/group.html.haml1
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml2
-rw-r--r--app/views/layouts/header/_super_sidebar_logged_out.haml2
-rw-r--r--app/views/layouts/minimal.html.haml2
-rw-r--r--app/views/layouts/oauth_error.html.haml2
-rw-r--r--app/views/layouts/organization.html.haml6
-rw-r--r--app/views/layouts/project.html.haml1
-rw-r--r--app/views/layouts/service_desk.html.haml8
-rw-r--r--app/views/layouts/terms.html.haml1
-rw-r--r--app/views/notify/in_product_marketing_email.html.haml51
-rw-r--r--app/views/notify/in_product_marketing_email.text.erb36
-rw-r--r--app/views/notify/member_access_granted_email.html.haml5
-rw-r--r--app/views/notify/member_invited_email.html.haml5
-rw-r--r--app/views/notify/new_email_address_added_email.html.haml (renamed from app/views/notify/new_email_address_added_email.haml)0
-rw-r--r--app/views/notify/new_email_address_added_email.text.erb (renamed from app/views/notify/new_email_address_added_email.erb)0
-rw-r--r--app/views/notify/resource_access_tokens_about_to_expire_email.html.haml13
-rw-r--r--app/views/notify/resource_access_tokens_about_to_expire_email.text.erb11
-rw-r--r--app/views/organizations/organizations/groups_and_projects.html.haml2
-rw-r--r--app/views/organizations/organizations/index.html.haml2
-rw-r--r--app/views/organizations/organizations/new.html.haml3
-rw-r--r--app/views/organizations/organizations/show.html.haml2
-rw-r--r--app/views/profiles/gpg_keys/index.html.haml2
-rw-r--r--app/views/profiles/keys/_key_details.html.haml2
-rw-r--r--app/views/profiles/notifications/_email_settings.html.haml3
-rw-r--r--app/views/profiles/notifications/show.html.haml4
-rw-r--r--app/views/profiles/preferences/show.html.haml9
-rw-r--r--app/views/projects/_export.html.haml4
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/_invite_members_empty_project.html.haml2
-rw-r--r--app/views/projects/_merge_request_merge_checks_settings.html.haml8
-rw-r--r--app/views/projects/_merge_request_merge_suggestions_settings.html.haml14
-rw-r--r--app/views/projects/_merge_request_settings.html.haml18
-rw-r--r--app/views/projects/_service_desk_settings.html.haml2
-rw-r--r--app/views/projects/_transfer.html.haml8
-rw-r--r--app/views/projects/activity.html.haml2
-rw-r--r--app/views/projects/artifacts/_tree_directory.html.haml2
-rw-r--r--app/views/projects/blob/_header_content.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_markup.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_metrics_dashboard_yml_loading.html.haml2
-rw-r--r--app/views/projects/branch_defaults/_branch_names_fields.html.haml2
-rw-r--r--app/views/projects/branch_defaults/_default_branch_fields.html.haml3
-rw-r--r--app/views/projects/buttons/_clone.html.haml4
-rw-r--r--app/views/projects/commit/_commit_box.html.haml3
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml6
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/compare/index.html.haml2
-rw-r--r--app/views/projects/deployments/_commit.html.haml17
-rw-r--r--app/views/projects/deployments/_deployment.html.haml49
-rw-r--r--app/views/projects/deployments/_rollback.haml4
-rw-r--r--app/views/projects/diffs/_file_header.html.haml4
-rw-r--r--app/views/projects/edit.html.haml6
-rw-r--r--app/views/projects/environments/show.html.haml31
-rw-r--r--app/views/projects/hook_logs/show.html.haml2
-rw-r--r--app/views/projects/incidents/show.html.haml12
-rw-r--r--app/views/projects/issuable/_show.html.haml10
-rw-r--r--app/views/projects/issues/_details_content.html.haml (renamed from app/views/shared/issue_type/_details_content.html.haml)17
-rw-r--r--app/views/projects/issues/_emoji_block.html.haml (renamed from app/views/shared/issue_type/_emoji_block.html.haml)0
-rw-r--r--app/views/projects/issues/_related_issues.html.haml1
-rw-r--r--app/views/projects/issues/_sentry_stack_trace.html.haml (renamed from app/views/shared/issue_type/_sentry_stack_trace.html.haml)0
-rw-r--r--app/views/projects/issues/service_desk.html.haml3
-rw-r--r--app/views/projects/issues/service_desk/_issue.html.haml4
-rw-r--r--app/views/projects/issues/show.html.haml12
-rw-r--r--app/views/projects/jobs/_table.html.haml37
-rw-r--r--app/views/projects/labels/edit.html.haml3
-rw-r--r--app/views/projects/labels/index.html.haml1
-rw-r--r--app/views/projects/merge_requests/_code_dropdown.html.haml8
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml4
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml2
-rw-r--r--app/views/projects/merge_requests/_page.html.haml7
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml4
-rw-r--r--app/views/projects/merge_requests/creations/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/tabs/_tab.html.haml4
-rw-r--r--app/views/projects/milestones/_form.html.haml2
-rw-r--r--app/views/projects/mirrors/_authentication_method.html.haml11
-rw-r--r--app/views/projects/mirrors/_instructions.html.haml2
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml2
-rw-r--r--app/views/projects/mirrors/_mirror_repos_form.html.haml2
-rw-r--r--app/views/projects/mirrors/_mirror_repos_list.html.haml2
-rw-r--r--app/views/projects/new.html.haml5
-rw-r--r--app/views/projects/notes/_more_actions_dropdown.html.haml2
-rw-r--r--app/views/projects/pages/_pages_settings.html.haml17
-rw-r--r--app/views/projects/pages_domains/_dns.html.haml4
-rw-r--r--app/views/projects/pipeline_schedules/_form.html.haml43
-rw-r--r--app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml45
-rw-r--r--app/views/projects/pipeline_schedules/_table.html.haml12
-rw-r--r--app/views/projects/pipeline_schedules/_tabs.html.haml12
-rw-r--r--app/views/projects/pipeline_schedules/edit.html.haml6
-rw-r--r--app/views/projects/pipeline_schedules/index.html.haml26
-rw-r--r--app/views/projects/pipeline_schedules/new.html.haml6
-rw-r--r--app/views/projects/project_members/index.html.haml4
-rw-r--r--app/views/projects/protected_tags/_create_protected_tag.html.haml6
-rw-r--r--app/views/projects/protected_tags/_protected_tag_create_access_levels.haml5
-rw-r--r--app/views/projects/settings/access_tokens/index.html.haml5
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml11
-rw-r--r--app/views/projects/settings/integrations/_form.html.haml5
-rw-r--r--app/views/projects/settings/integrations/index.html.haml6
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_merge_checks_settings.html.haml8
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_merge_commit_template.html.haml (renamed from app/views/projects/_merge_request_merge_commit_template.html.haml)5
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_merge_method_settings.html.haml (renamed from app/views/projects/_merge_request_merge_method_settings.html.haml)0
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_merge_options_settings.html.haml (renamed from app/views/projects/_merge_request_merge_options_settings.html.haml)4
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_merge_suggestions_settings.html.haml13
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_pipelines_and_threads_options.html.haml (renamed from app/views/projects/_merge_request_pipelines_and_threads_options.html.haml)0
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_settings.html.haml18
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_settings_description_text.html.haml (renamed from app/views/projects/_merge_request_settings_description_text.html.haml)0
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_squash_commit_template.html.haml (renamed from app/views/projects/_merge_request_squash_commit_template.html.haml)5
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_squash_options_settings.html.haml (renamed from app/views/projects/_merge_request_squash_options_settings.html.haml)0
-rw-r--r--app/views/projects/settings/merge_requests/_merge_request_target_project_settings.html.haml (renamed from app/views/projects/_merge_request_target_project_settings.html.haml)0
-rw-r--r--app/views/projects/settings/merge_requests/show.html.haml5
-rw-r--r--app/views/projects/settings/operations/_alert_management.html.haml2
-rw-r--r--app/views/projects/show.html.haml1
-rw-r--r--app/views/projects/tags/new.html.haml5
-rw-r--r--app/views/projects/tracing/index.html.haml4
-rw-r--r--app/views/projects/tracing/show.html.haml5
-rw-r--r--app/views/projects/usage_quotas/index.html.haml3
-rw-r--r--app/views/protected_branches/_create_protected_branch.html.haml11
-rw-r--r--app/views/protected_branches/shared/_index.html.haml7
-rw-r--r--app/views/protected_branches/shared/_update_protected_branch.html.haml8
-rw-r--r--app/views/registrations/welcome/show.html.haml2
-rw-r--r--app/views/repository_check_mailer/notify.html.haml2
-rw-r--r--app/views/repository_check_mailer/notify.text.haml2
-rw-r--r--app/views/search/_results.html.haml2
-rw-r--r--app/views/search/_results_list.html.haml2
-rw-r--r--app/views/search/_results_status.html.haml53
-rw-r--r--app/views/search/show.html.haml4
-rw-r--r--app/views/shared/_clone_panel.html.haml2
-rw-r--r--app/views/shared/_label_row.html.haml3
-rw-r--r--app/views/shared/_logo.svg2
-rw-r--r--app/views/shared/_mobile_clone_panel.html.haml2
-rw-r--r--app/views/shared/_outdated_browser.html.haml4
-rw-r--r--app/views/shared/_silent_mode_banner.html.haml9
-rw-r--r--app/views/shared/_visibility_level.html.haml5
-rw-r--r--app/views/shared/builds/_build_output.html.haml6
-rw-r--r--app/views/shared/builds/_tabs.html.haml15
-rw-r--r--app/views/shared/deploy_keys/_index.html.haml4
-rw-r--r--app/views/shared/deploy_keys/_project_group_form.html.haml26
-rw-r--r--app/views/shared/deploy_tokens/_form.html.haml5
-rw-r--r--app/views/shared/deploy_tokens/_new_deploy_token.html.haml4
-rw-r--r--app/views/shared/doorkeeper/applications/_show.html.haml2
-rw-r--r--app/views/shared/form_elements/_description.html.haml2
-rw-r--r--app/views/shared/groups/_empty_state.html.haml8
-rw-r--r--app/views/shared/hook_logs/_content.html.haml33
-rw-r--r--app/views/shared/icons/_icon_empty_groups.svg1
-rw-r--r--app/views/shared/integrations/mattermost_slash_commands/_detailed_help.html.haml43
-rw-r--r--app/views/shared/integrations/mattermost_slash_commands/_installation_info.html.haml8
-rw-r--r--app/views/shared/integrations/prometheus/_custom_metrics.html.haml1
-rw-r--r--app/views/shared/integrations/prometheus/_metrics.html.haml4
-rw-r--r--app/views/shared/integrations/slack_slash_commands/_help.html.haml10
-rw-r--r--app/views/shared/issuable/_form.html.haml7
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml6
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar_reviewers.html.haml2
-rw-r--r--app/views/shared/issuable/_status_box.html.haml3
-rw-r--r--app/views/shared/issuable/form/_default_templates.html.haml5
-rw-r--r--app/views/shared/issuable/form/_title.html.haml2
-rw-r--r--app/views/shared/issue_type/_details_header.html.haml21
-rw-r--r--app/views/shared/labels/_form.html.haml9
-rw-r--r--app/views/shared/members/_access_request_links.html.haml2
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml4
-rw-r--r--app/views/shared/packages/_no_packages.html.haml5
-rw-r--r--app/views/shared/projects/_project.html.haml4
-rw-r--r--app/views/shared/web_hooks/_hook_errors.html.haml19
-rw-r--r--app/views/shared/web_hooks/_title_and_docs.html.haml8
-rw-r--r--app/views/users/_deletion_guidance.html.haml4
-rw-r--r--app/views/users/_profile_basic_info.html.haml2
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--app/workers/all_queues.yml97
-rw-r--r--app/workers/background_migration/single_database_worker.rb5
-rw-r--r--app/workers/build_success_worker.rb12
-rw-r--r--app/workers/bulk_import_worker.rb55
-rw-r--r--app/workers/bulk_imports/finish_project_import_worker.rb20
-rw-r--r--app/workers/click_house/events_sync_worker.rb93
-rw-r--r--app/workers/concerns/gitlab/bitbucket_import/object_importer.rb98
-rw-r--r--app/workers/concerns/gitlab/bitbucket_import/stage_methods.rb76
-rw-r--r--app/workers/concerns/gitlab/bitbucket_server_import/object_importer.rb4
-rw-r--r--app/workers/concerns/gitlab/github_import/object_importer.rb4
-rw-r--r--app/workers/concerns/gitlab/github_import/rescheduling_methods.rb2
-rw-r--r--app/workers/concerns/gitlab/github_import/stage_methods.rb2
-rw-r--r--app/workers/concerns/gitlab/import/notify_upon_death.rb32
-rw-r--r--app/workers/concerns/gitlab/notify_upon_death.rb29
-rw-r--r--app/workers/database/batched_background_migration/execution_worker.rb2
-rw-r--r--app/workers/database/batched_background_migration/single_database_worker.rb2
-rw-r--r--app/workers/database/lock_tables_worker.rb66
-rw-r--r--app/workers/database/monitor_locked_tables_worker.rb7
-rw-r--r--app/workers/environments/stop_job_success_worker.rb8
-rw-r--r--app/workers/gitlab/bitbucket_import/advance_stage_worker.rb37
-rw-r--r--app/workers/gitlab/bitbucket_import/import_pull_request_worker.rb13
-rw-r--r--app/workers/gitlab/bitbucket_import/stage/finish_import_worker.rb19
-rw-r--r--app/workers/gitlab/bitbucket_import/stage/import_pull_requests_worker.rb30
-rw-r--r--app/workers/gitlab/bitbucket_import/stage/import_repository_worker.rb29
-rw-r--r--app/workers/gitlab/github_gists_import/import_gist_worker.rb4
-rw-r--r--app/workers/gitlab/import/advance_stage.rb16
-rw-r--r--app/workers/gitlab/jira_import/import_issue_worker.rb4
-rw-r--r--app/workers/incident_management/close_incident_worker.rb2
-rw-r--r--app/workers/incident_management/process_alert_worker_v2.rb2
-rw-r--r--app/workers/loose_foreign_keys/cleanup_worker.rb24
-rw-r--r--app/workers/members_destroyer/unassign_issuables_worker.rb2
-rw-r--r--app/workers/merge_requests/ensure_prepared_worker.rb34
-rw-r--r--app/workers/merge_worker.rb2
-rw-r--r--app/workers/metrics/global_metrics_update_worker.rb4
-rw-r--r--app/workers/namespaces/in_product_marketing_emails_worker.rb33
-rw-r--r--app/workers/new_merge_request_worker.rb5
-rw-r--r--app/workers/pages/invalidate_domain_cache_worker.rb37
-rw-r--r--app/workers/personal_access_tokens/expiring_worker.rb16
-rw-r--r--app/workers/post_receive.rb2
-rw-r--r--app/workers/projects/inactive_projects_deletion_cron_worker.rb2
-rw-r--r--app/workers/projects/record_target_platforms_worker.rb4
-rw-r--r--app/workers/users/deactivate_dormant_users_worker.rb2
-rw-r--r--app/workers/users/track_namespace_visits_worker.rb30
-rw-r--r--app/workers/x509_issuer_crl_check_worker.rb2
1760 files changed, 17086 insertions, 14995 deletions
diff --git a/app/assets/images/auth_buttons/salesforce_64.png b/app/assets/images/auth_buttons/salesforce_64.png
deleted file mode 100644
index b562e09c20f..00000000000
--- a/app/assets/images/auth_buttons/salesforce_64.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
index 85b3c994e02..9a7296b6b1f 100644
--- a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
+++ b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
@@ -33,7 +33,7 @@ export default {
emptyField: __('Never'),
expired: __('Expired'),
modalMessage: __(
- 'Are you sure you want to revoke this %{accessTokenType}? This action cannot be undone.',
+ 'Are you sure you want to revoke the %{accessTokenType} "%{tokenName}"? This action cannot be undone.',
),
revokeButton: __('Revoke'),
tokenValidity: __('Token valid until revoked'),
@@ -72,11 +72,6 @@ export default {
return FIELDS.filter(({ key }) => !ignoredFields.includes(key));
},
- modalMessage() {
- return sprintf(this.$options.i18n.modalMessage, {
- accessTokenType: this.accessTokenType,
- });
- },
showPagination() {
return this.activeAccessTokens.length > PAGE_SIZE;
},
@@ -87,6 +82,12 @@ export default {
this.activeAccessTokens = convertObjectPropsToCamelCase(activeAccessTokens, { deep: true });
this.currentPage = INITIAL_PAGE;
},
+ modalMessage(tokenName) {
+ return sprintf(this.$options.i18n.modalMessage, {
+ accessTokenType: this.accessTokenType,
+ tokenName,
+ });
+ },
sortingChanged(aRow, bRow, key) {
if (['createdAt', 'lastUsedAt', 'expiresAt'].includes(key)) {
// Transform `null` value to the latest possible date
@@ -149,13 +150,13 @@ export default {
}}</span>
</template>
- <template #cell(action)="{ item: { revokePath } }">
+ <template #cell(action)="{ item: { name, revokePath } }">
<gl-button
v-if="revokePath"
category="tertiary"
:title="$options.i18n.revokeButton"
:aria-label="$options.i18n.revokeButton"
- :data-confirm="modalMessage"
+ :data-confirm="modalMessage(name)"
data-confirm-btn-variant="danger"
data-qa-selector="revoke_button"
data-method="put"
diff --git a/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
index c1ec46cfc50..9bcdcec8b78 100644
--- a/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
+++ b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
@@ -267,7 +267,8 @@ export default {
});
}
});
- } else if (this.uniqueCommits.length > 0) {
+ }
+ if (this.uniqueCommits.length > 0) {
return this.createContextCommits({ commits: this.uniqueCommits, forceReload: true });
}
diff --git a/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue b/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
index 1490d7e64f5..3c46de7c2be 100644
--- a/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
@@ -1,9 +1,12 @@
<script>
import { GlAlert } from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReportHeader from './report_header.vue';
import UserDetails from './user_details.vue';
+import ReportDetails from './report_details.vue';
import ReportedContent from './reported_content.vue';
-import HistoryItems from './history_items.vue';
+import ActivityEventsList from './activity_events_list.vue';
+import ActivityHistoryItem from './activity_history_item.vue';
const alertDefaults = {
visible: false,
@@ -17,9 +20,12 @@ export default {
GlAlert,
ReportHeader,
UserDetails,
+ ReportDetails,
ReportedContent,
- HistoryItems,
+ ActivityEventsList,
+ ActivityHistoryItem,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
abuseReport: {
type: Object,
@@ -31,6 +37,11 @@ export default {
alert: { ...alertDefaults },
};
},
+ computed: {
+ similarOpenReports() {
+ return this.abuseReport.user?.similarOpenReports || [];
+ },
+ },
methods: {
showAlert(variant, message) {
this.alert.visible = true;
@@ -49,6 +60,7 @@ export default {
<gl-alert v-if="alert.visible" :variant="alert.variant" class="gl-mt-4" @dismiss="closeAlert">{{
alert.message
}}</gl-alert>
+
<report-header
v-if="abuseReport.user"
:user="abuseReport.user"
@@ -56,7 +68,33 @@ export default {
@showAlert="showAlert"
/>
<user-details v-if="abuseReport.user" :user="abuseReport.user" />
- <reported-content :report="abuseReport.report" :reporter="abuseReport.reporter" />
- <history-items :report="abuseReport.report" :reporter="abuseReport.reporter" />
+
+ <report-details
+ v-if="glFeatures.abuseReportLabels"
+ :report-id="abuseReport.report.globalId"
+ class="gl-mt-6"
+ />
+
+ <reported-content :report="abuseReport.report" data-testid="reported-content" />
+
+ <div
+ v-for="report in similarOpenReports"
+ :key="report.id"
+ data-testid="reported-content-similar-open-reports"
+ >
+ <reported-content :report="report" />
+ </div>
+
+ <activity-events-list>
+ <template #history-items>
+ <activity-history-item :report="abuseReport.report" data-testid="activity" />
+ <activity-history-item
+ v-for="report in similarOpenReports"
+ :key="report.id"
+ :report="report"
+ data-testid="activity-similar-open-reports"
+ />
+ </template>
+ </activity-events-list>
</section>
</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue b/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue
new file mode 100644
index 00000000000..8c4c1da28b8
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue
@@ -0,0 +1,19 @@
+<script>
+import { HISTORY_ITEMS_I18N } from '../constants';
+
+export default {
+ name: 'ActivityEventsList',
+ i18n: HISTORY_ITEMS_I18N,
+};
+</script>
+
+<template>
+ <!-- The styles `issuable-discussion`, `timeline`, `main-notes-list` and `notes` used below
+ are declared in app/assets/stylesheets/pages/notes.scss -->
+ <section class="gl-pt-6 issuable-discussion">
+ <h2 class="gl-font-lg gl-mt-0 gl-mb-2">{{ $options.i18n.activity }}</h2>
+ <ul class="timeline main-notes-list notes">
+ <slot name="history-items"></slot>
+ </ul>
+ </section>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/activity_history_item.vue b/app/assets/javascripts/admin/abuse_report/components/activity_history_item.vue
new file mode 100644
index 00000000000..5962203c382
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/activity_history_item.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
+import { HISTORY_ITEMS_I18N } from '../constants';
+
+export default {
+ name: 'ActivityHistoryItem',
+ components: {
+ GlSprintf,
+ TimeAgoTooltip,
+ HistoryItem,
+ },
+ props: {
+ report: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ reporter() {
+ return this.report.reporter;
+ },
+ reporterName() {
+ return this.reporter?.name || this.$options.i18n.deletedReporter;
+ },
+ },
+ i18n: HISTORY_ITEMS_I18N,
+};
+</script>
+
+<template>
+ <history-item icon="warning">
+ <div class="gl-display-flex gl-xs-flex-direction-column">
+ <gl-sprintf :message="$options.i18n.reportedByForCategory">
+ <template #name>{{ reporterName }}</template>
+ <template #category>{{ report.category }}</template>
+ </gl-sprintf>
+ <time-ago-tooltip :time="report.reportedAt" class="gl-text-secondary gl-sm-ml-3" />
+ </div>
+ </history-item>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql b/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql
new file mode 100644
index 00000000000..f5b075cb9af
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql
@@ -0,0 +1,13 @@
+query abuseReportQuery($id: AbuseReportID!) {
+ abuseReport(id: $id) {
+ labels {
+ nodes {
+ id
+ title
+ description
+ color
+ textColor
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql b/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql
new file mode 100644
index 00000000000..4e724b4db2c
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql
@@ -0,0 +1,11 @@
+query abuseReportLabelsQuery($searchTerm: String) {
+ labels: abuseReportLabels(searchTerm: $searchTerm) {
+ nodes {
+ id
+ title
+ description
+ color
+ textColor
+ }
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql b/app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql
new file mode 100644
index 00000000000..0781b8e634b
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql
@@ -0,0 +1,10 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
+mutation createAbuseReportLabel($title: String!, $color: String) {
+ labelCreate: abuseReportLabelCreate(input: { title: $title, color: $color }) {
+ label {
+ ...Label
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/components/history_items.vue b/app/assets/javascripts/admin/abuse_report/components/history_items.vue
deleted file mode 100644
index 28b66db84a2..00000000000
--- a/app/assets/javascripts/admin/abuse_report/components/history_items.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<script>
-import { GlSprintf } from '@gitlab/ui';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
-import { HISTORY_ITEMS_I18N } from '../constants';
-
-export default {
- name: 'HistoryItems',
- components: {
- GlSprintf,
- TimeAgoTooltip,
- HistoryItem,
- },
- props: {
- report: {
- type: Object,
- required: true,
- },
- reporter: {
- type: Object,
- required: false,
- default: null,
- },
- },
- computed: {
- reporterName() {
- return this.reporter?.name || this.$options.i18n.deletedReporter;
- },
- },
- i18n: HISTORY_ITEMS_I18N,
-};
-</script>
-
-<template>
- <!-- The styles `issuable-discussion`, `timeline`, `main-notes-list` and `notes` used below
- are declared in app/assets/stylesheets/pages/notes.scss -->
- <section class="gl-pt-6 issuable-discussion">
- <h2 class="gl-font-size-h1 gl-mt-0 gl-mb-2">{{ $options.i18n.activity }}</h2>
- <ul class="timeline main-notes-list notes">
- <history-item icon="warning">
- <div class="gl-display-flex gl-xs-flex-direction-column">
- <gl-sprintf :message="$options.i18n.reportedByForCategory">
- <template #name>{{ reporterName }}</template>
- <template #category>{{ report.category }}</template>
- </gl-sprintf>
- <time-ago-tooltip :time="report.reportedAt" class="gl-text-secondary gl-sm-ml-3" />
- </div>
- </history-item>
- </ul>
- </section>
-</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/labels_select.vue b/app/assets/javascripts/admin/abuse_report/components/labels_select.vue
new file mode 100644
index 00000000000..747c9a1a947
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/labels_select.vue
@@ -0,0 +1,235 @@
+<script>
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { createAlert } from '~/alert';
+import axios from '~/lib/utils/axios_utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { __, s__, sprintf } from '~/locale';
+import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue';
+import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
+import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue';
+import DropdownHeader from '~/sidebar/components/labels/labels_select_widget/dropdown_header.vue';
+import DropdownFooter from '~/sidebar/components/labels/labels_select_widget/dropdown_footer.vue';
+import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
+import abuseReportLabelsQuery from './graphql/abuse_report_labels.query.graphql';
+
+export default {
+ components: {
+ DropdownWidget,
+ GlButton,
+ GlLoadingIcon,
+ LabelItem,
+ DropdownValue,
+ DropdownContentsCreateView,
+ DropdownHeader,
+ DropdownFooter,
+ },
+ inject: ['updatePath', 'listPath'],
+ props: {
+ report: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ search: '',
+ labels: [],
+ selected: this.report.labels,
+ initialLoading: true,
+ isEditing: false,
+ isUpdating: false,
+ showCreateView: false,
+ };
+ },
+ apollo: {
+ labels: {
+ query() {
+ return abuseReportLabelsQuery;
+ },
+ variables() {
+ return { searchTerm: this.search };
+ },
+ skip() {
+ return !this.isEditing;
+ },
+ update(data) {
+ return data.labels?.nodes;
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.searchError });
+ },
+ },
+ },
+ computed: {
+ isLabelsEmpty() {
+ return this.selected.length === 0;
+ },
+ selectedLabelIds() {
+ return this.selected.map((label) => label.id);
+ },
+ isLoading() {
+ return this.$apollo.queries.labels.loading;
+ },
+ selectText() {
+ if (!this.selected.length) {
+ return this.$options.i18n.labelsListTitle;
+ }
+ if (this.selected.length > 1) {
+ return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
+ firstLabelName: this.selected[0].title,
+ remainingLabelCount: this.selected.length - 1,
+ });
+ }
+ return this.selected[0].title;
+ },
+ },
+ watch: {
+ report({ labels }) {
+ this.selected = labels;
+ this.initialLoading = false;
+ },
+ },
+ created() {
+ const setSearch = (search) => {
+ this.search = search;
+ };
+ this.debouncedSetSearch = debounce(setSearch, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ methods: {
+ toggleEdit() {
+ return this.isEditing ? this.hideDropdown() : this.showDropdown();
+ },
+ showDropdown() {
+ this.isEditing = true;
+ this.$refs.editDropdown.showDropdown();
+ },
+ hideDropdown() {
+ this.saveSelectedLabels();
+ this.isEditing = false;
+ },
+ saveSelectedLabels() {
+ this.isUpdating = true;
+
+ axios
+ .put(this.updatePath, { label_ids: this.selectedLabelIds })
+ .catch((error) => {
+ createAlert({
+ message: __('An error occurred while updating labels.'),
+ captureError: true,
+ error,
+ });
+ })
+ .finally(() => {
+ this.isUpdating = false;
+ });
+ },
+ isLabelSelected(label) {
+ return this.selectedLabelIds.includes(label.id);
+ },
+ filterSelected(id) {
+ return this.selected.filter(({ id: labelId }) => labelId !== id);
+ },
+ toggleLabelSelection(label) {
+ this.selected = this.isLabelSelected(label)
+ ? this.filterSelected(label.id)
+ : [...this.selected, label];
+ },
+ removeLabel(labelId) {
+ this.selected = this.filterSelected(labelId);
+ this.saveSelectedLabels();
+ },
+ toggleCreateView() {
+ this.showCreateView = !this.showCreateView;
+ },
+ onLabelCreated(label) {
+ this.toggleLabelSelection(label);
+ this.toggleCreateView();
+ },
+ },
+ i18n: {
+ label: __('Labels'),
+ noLabels: __('None'),
+ labelsListTitle: __('Assign labels'),
+ searchError: __('An error occurred while searching for labels, please try again.'),
+ edit: __('Edit'),
+ },
+};
+</script>
+<template>
+ <div class="labels-select-wrapper">
+ <div class="gl-display-flex gl-align-items-center gl-gap-3 gl-mb-2">
+ <span>{{ $options.i18n.label }}</span>
+ <gl-loading-icon v-if="initialLoading" size="sm" inline class="gl-ml-2" />
+ <gl-button
+ category="tertiary"
+ size="small"
+ :disabled="isUpdating || initialLoading"
+ class="edit-link gl-ml-auto"
+ @click="toggleEdit"
+ >
+ {{ $options.i18n.edit }}
+ </gl-button>
+ </div>
+ <div class="gl-text-gray-500 gl-mb-2" data-testid="selected-labels">
+ <template v-if="isLabelsEmpty">{{ $options.i18n.noLabels }}</template>
+ <dropdown-value
+ v-else
+ :disable-labels="isLoading"
+ :selected-labels="selected"
+ :allow-label-remove="!isUpdating"
+ :labels-filter-base-path="listPath"
+ :labels-filter-param="'label_name'"
+ @onLabelRemove="removeLabel"
+ />
+ </div>
+
+ <dropdown-widget
+ v-show="isEditing"
+ ref="editDropdown"
+ :select-text="selectText"
+ :options="labels"
+ :is-loading="isLoading"
+ :selected="selected"
+ :search-term="search"
+ :allow-multiselect="true"
+ :no-options-text="__('No labels found')"
+ @hide="hideDropdown"
+ @set-option="toggleLabelSelection"
+ @set-search="debouncedSetSearch"
+ >
+ <template #header>
+ <dropdown-header
+ ref="header"
+ :search-key="search"
+ labels-create-title=""
+ :labels-list-title="$options.i18n.labelsListTitle"
+ :show-dropdown-contents-create-view="showCreateView"
+ @toggleDropdownContentsCreateView="toggleCreateView"
+ @closeDropdown="hideDropdown"
+ @input="debouncedSetSearch"
+ />
+ </template>
+ <template #item="{ item }">
+ <label-item v-if="item" :label="item" />
+ </template>
+ <template v-if="showCreateView" #default>
+ <dropdown-contents-create-view
+ attr-workspace-path=""
+ full-path=""
+ label-create-type=""
+ workspace-type="abuseReport"
+ @hideCreateView="toggleCreateView"
+ @labelCreated="onLabelCreated"
+ />
+ </template>
+ <template #footer>
+ <dropdown-footer
+ v-if="!showCreateView"
+ :footer-create-label-title="__('Create label')"
+ @toggleDropdownContentsCreateView="toggleCreateView"
+ />
+ </template>
+ </dropdown-widget>
+ </div>
+</template>
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 92478e10289..560d733c10c 100644
--- a/app/assets/javascripts/admin/abuse_report/components/report_actions.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/report_actions.vue
@@ -95,12 +95,8 @@ export default {
return;
}
- // TODO: In 16.4 use moderateUserPath without falling back to using updatePath
- // See https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/167?work_item_iid=443
- const { moderateUserPath, updatePath } = this.report;
- const path = moderateUserPath || updatePath;
-
- axios.put(path, this.form).then(this.handleResponse).catch(this.handleError);
+ const { moderateUserPath } = this.report;
+ axios.put(moderateUserPath, this.form).then(this.handleResponse).catch(this.handleError);
},
handleResponse({ data }) {
this.toggleActionsDrawer();
diff --git a/app/assets/javascripts/admin/abuse_report/components/report_details.vue b/app/assets/javascripts/admin/abuse_report/components/report_details.vue
new file mode 100644
index 00000000000..10e1dca7f91
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/report_details.vue
@@ -0,0 +1,49 @@
+<script>
+import { __ } from '~/locale';
+import { createAlert } from '~/alert';
+import LabelsSelect from './labels_select.vue';
+import abuseReportQuery from './graphql/abuse_report.query.graphql';
+
+export default {
+ name: 'ReportDetails',
+ components: {
+ LabelsSelect,
+ },
+ props: {
+ reportId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ report: { labels: [] },
+ };
+ },
+ apollo: {
+ report: {
+ query() {
+ return abuseReportQuery;
+ },
+ variables() {
+ return { id: this.reportId };
+ },
+ update({ abuseReport }) {
+ return {
+ labels: abuseReport.labels?.nodes,
+ };
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.fetchError });
+ },
+ },
+ },
+ i18n: {
+ fetchError: __('An error occurred while fetching labels, please try again.'),
+ },
+};
+</script>
+
+<template>
+ <labels-select :report="report" />
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/report_header.vue b/app/assets/javascripts/admin/abuse_report/components/report_header.vue
index 624dcd47650..90c1943cb27 100644
--- a/app/assets/javascripts/admin/abuse_report/components/report_header.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/report_header.vue
@@ -32,9 +32,6 @@ export default {
isOpen() {
return this.state === STATUS_OPEN;
},
- badgeClass() {
- return this.isOpen ? 'issuable-status-badge-open' : 'issuable-status-badge-closed';
- },
badgeVariant() {
return this.isOpen ? 'success' : 'info';
},
@@ -58,21 +55,16 @@ export default {
<header
class="gl-py-4 gl-border-b gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column"
>
- <div class="gl-display-flex gl-align-items-center">
- <gl-badge
- class="issuable-status-badge gl-mr-3"
- :class="badgeClass"
- :variant="badgeVariant"
- :aria-label="badgeText"
- >
+ <div class="gl-display-flex gl-align-items-center gl-gap-3">
+ <gl-badge :variant="badgeVariant" :aria-label="badgeText">
<gl-icon :name="badgeIcon" class="gl-badge-icon" />
<span class="gl-display-none gl-sm-display-block gl-ml-2">{{ badgeText }}</span>
</gl-badge>
<gl-avatar :size="48" :src="user.avatarUrl" />
- <h1 class="gl-font-size-h-display gl-my-0 gl-ml-3">
+ <h1 class="gl-font-size-h-display gl-my-0">
{{ user.name }}
</h1>
- <gl-link :href="user.path" class="gl-ml-3"> @{{ user.username }} </gl-link>
+ <gl-link :href="user.path"> @{{ user.username }} </gl-link>
</div>
<nav
class="gl-display-flex gl-sm-align-items-center gl-mt-4 gl-sm-mt-0 gl-xs-flex-direction-column"
diff --git a/app/assets/javascripts/admin/abuse_report/components/reported_content.vue b/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
index f4f0fcac58f..84d6f25ac05 100644
--- a/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
@@ -26,11 +26,6 @@ export default {
type: Object,
required: true,
},
- reporter: {
- type: Object,
- required: false,
- default: null,
- },
},
data() {
return {
@@ -38,6 +33,9 @@ export default {
};
},
computed: {
+ reporter() {
+ return this.report.reporter;
+ },
reporterName() {
return this.reporter?.name || this.$options.i18n.deletedReporter;
},
@@ -67,11 +65,12 @@ export default {
<template>
<div class="gl-pt-6">
<div
- class="gl-pb-3 gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column"
+ class="gl-pb-3 gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column gl-align-items-center"
>
- <h2 class="gl-font-size-h1 gl-mt-0 gl-mb-2">
+ <h2 class="gl-font-lg gl-mt-2 gl-mb-2">
{{ $options.i18n.reportTypes[reportType] }}
</h2>
+
<div
class="gl-display-flex gl-align-items-stretch gl-xs-flex-direction-column gl-mt-3 gl-sm-mt-0"
>
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 3dc03a8748f..fe0add1ba8d 100644
--- a/app/assets/javascripts/admin/abuse_report/components/user_details.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/user_details.vue
@@ -39,19 +39,27 @@ export default {
<template>
<div class="gl-mt-6">
- <user-detail data-testid="createdAt" :label="$options.i18n.createdAt">
+ <user-detail data-testid="created-at" :label="$options.i18n.createdAt">
<time-ago-tooltip :time="user.createdAt" />
</user-detail>
+
<user-detail data-testid="email" :label="$options.i18n.email">
<gl-link :href="`mailto:${user.email}`">{{ user.email }}</gl-link>
</user-detail>
+
<user-detail data-testid="plan" :label="$options.i18n.plan" :value="user.plan" />
+
<user-detail
data-testid="verification"
:label="$options.i18n.verification"
:value="verificationState"
/>
- <user-detail v-if="user.creditCard" data-testid="creditCard" :label="$options.i18n.creditCard">
+
+ <user-detail
+ v-if="user.creditCard"
+ data-testid="credit-card-verification"
+ :label="$options.i18n.creditCard"
+ >
<gl-sprintf :message="$options.i18n.registeredWith">
<template #name>{{ user.creditCard.name }}</template>
</gl-sprintf>
@@ -65,17 +73,18 @@ export default {
</template>
</gl-sprintf>
</user-detail>
+
<user-detail
- v-if="user.otherReports.length"
- data-testid="otherReports"
- :label="$options.i18n.otherReports"
+ v-if="user.pastClosedReports.length"
+ data-testid="past-closed-reports"
+ :label="$options.i18n.pastReports"
>
<div
- v-for="(report, index) in user.otherReports"
+ v-for="(report, index) in user.pastClosedReports"
:key="index"
- :data-testid="`other-report-${index}`"
+ :data-testid="`past-report-${index}`"
>
- <gl-sprintf :message="$options.i18n.otherReport">
+ <gl-sprintf :message="$options.i18n.reportedFor">
<template #reportLink="{ content }">
<gl-link :href="report.reportPath">{{ content }}</gl-link>
</template>
@@ -86,28 +95,33 @@ export default {
</gl-sprintf>
</div>
</user-detail>
+
<user-detail
- data-testid="normalLocation"
+ data-testid="normal-location"
:label="$options.i18n.normalLocation"
:value="user.mostUsedIp || user.lastSignInIp"
/>
+
<user-detail
- data-testid="lastSignInIp"
+ data-testid="last-sign-in-ip"
:label="$options.i18n.lastSignInIp"
:value="user.lastSignInIp"
/>
+
<user-detail
- data-testid="snippets"
+ data-testid="user-snippets-count"
:label="$options.i18n.snippets"
:value="$options.i18n.snippetsCount(user.snippetsCount)"
/>
+
<user-detail
- data-testid="groups"
+ data-testid="user-groups-count"
:label="$options.i18n.groups"
:value="$options.i18n.groupsCount(user.groupsCount)"
/>
+
<user-detail
- data-testid="notes"
+ data-testid="user-notes-count"
:label="$options.i18n.notes"
:value="$options.i18n.notesCount(user.notesCount)"
/>
diff --git a/app/assets/javascripts/admin/abuse_report/constants.js b/app/assets/javascripts/admin/abuse_report/constants.js
index b290581598a..6cae6b24f20 100644
--- a/app/assets/javascripts/admin/abuse_report/constants.js
+++ b/app/assets/javascripts/admin/abuse_report/constants.js
@@ -58,7 +58,7 @@ export const USER_DETAILS_I18N = {
plan: s__('AbuseReport|Tier'),
verification: s__('AbuseReport|Verification'),
creditCard: s__('AbuseReport|Credit card'),
- otherReports: s__('AbuseReport|Abuse reports'),
+ pastReports: s__('AbuseReport|Past abuse reports'),
normalLocation: s__('AbuseReport|Normal location'),
lastSignInIp: s__('AbuseReport|Last login'),
snippets: s__('AbuseReport|Snippets'),
@@ -72,7 +72,7 @@ export const USER_DETAILS_I18N = {
phone: s__('AbuseReport|Phone'),
creditCard: s__('AbuseReport|Credit card'),
},
- otherReport: s__(
+ reportedFor: s__(
'AbuseReport|%{reportLinkStart}Reported%{reportLinkEnd} for %{category} %{timeAgo}.',
),
registeredWith: s__('AbuseReport|Registered with name %{name}.'),
diff --git a/app/assets/javascripts/admin/abuse_report/index.js b/app/assets/javascripts/admin/abuse_report/index.js
index 8ff3e690127..c2117130d26 100644
--- a/app/assets/javascripts/admin/abuse_report/index.js
+++ b/app/assets/javascripts/admin/abuse_report/index.js
@@ -1,7 +1,15 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { defaultClient } from '~/graphql_shared/issuable_client';
import AbuseReportApp from './components/abuse_report_app.vue';
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient,
+});
+
export const initAbuseReportApp = () => {
const el = document.querySelector('#js-abuse-reports-detail-view');
@@ -9,14 +17,22 @@ export const initAbuseReportApp = () => {
return null;
}
- const { abuseReportData } = el.dataset;
+ const { abuseReportData, abuseReportsListPath } = el.dataset;
const abuseReport = convertObjectPropsToCamelCase(JSON.parse(abuseReportData), {
deep: true,
});
return new Vue({
el,
+ apolloProvider,
name: 'AbuseReportAppRoot',
+ provide: {
+ allowScopedLabels: false,
+ updatePath: abuseReport.report.updatePath,
+ listPath: abuseReportsListPath,
+ labelsManagePath: '',
+ allowLabelCreate: true,
+ },
render: (createElement) =>
createElement(AbuseReportApp, {
props: {
diff --git a/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue b/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue
index f24e491a745..b9fef57c2a2 100644
--- a/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue
+++ b/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue
@@ -1,7 +1,7 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { GlLabel, GlLink } from '@gitlab/ui';
import { getTimeago } from '~/lib/utils/datetime_utility';
-import { queryToObject } from '~/lib/utils/url_utility';
+import { mergeUrlParams, queryToObject } from '~/lib/utils/url_utility';
import { s__, __, sprintf } from '~/locale';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { SORT_UPDATED_AT } from '../constants';
@@ -10,6 +10,7 @@ import AbuseCategory from './abuse_category.vue';
export default {
name: 'AbuseReportRow',
components: {
+ GlLabel,
GlLink,
ListItem,
AbuseCategory,
@@ -53,6 +54,11 @@ export default {
});
},
},
+ methods: {
+ labelTarget(labelName) {
+ return mergeUrlParams({ 'label_name[]': labelName }, window.location.href);
+ },
+ },
};
</script>
@@ -68,7 +74,16 @@ export default {
</gl-link>
</template>
<template #left-secondary>
- <abuse-category :category="report.category" class="gl-mt-2 gl-mb-3" />
+ <abuse-category :category="report.category" class="gl-mr-2" />
+ <gl-label
+ v-for="label in report.labels"
+ :key="label.id"
+ class="gl-mr-2"
+ size="sm"
+ :background-color="label.color"
+ :title="label.title"
+ :target="labelTarget(label.title)"
+ />
</template>
<template #right-secondary>
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
index 109df943c42..2c555aca3c0 100644
--- a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
+++ b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
@@ -234,7 +234,8 @@ export default {
initialTarget() {
if (this.targetAccessLevels.length > 0) {
return TARGET_ROLES;
- } else if (this.targetPath !== '') {
+ }
+ if (this.targetPath !== '') {
return TARGET_ALL_MATCHING_PATH;
}
return TARGET_ALL;
diff --git a/app/assets/javascripts/admin/users/components/actions/approve.vue b/app/assets/javascripts/admin/users/components/actions/approve.vue
index 5b13bd177ae..bcd17570b95 100644
--- a/app/assets/javascripts/admin/users/components/actions/approve.vue
+++ b/app/assets/javascripts/admin/users/components/actions/approve.vue
@@ -44,7 +44,7 @@ export default {
},
actionPrimary: {
text: I18N_USER_ACTIONS.approve,
- attributes: { variant: 'confirm', 'data-qa-selector': 'approve_user_confirm_button' },
+ attributes: { variant: 'confirm', 'data-testid': 'approve-user-confirm-button' },
},
messageHtml,
},
@@ -55,7 +55,7 @@ export default {
</script>
<template>
- <gl-disclosure-dropdown-item data-qa-selector="approve_user_button" @action="onClick">
+ <gl-disclosure-dropdown-item @action="onClick">
<template #list-item>
<slot></slot>
</template>
diff --git a/app/assets/javascripts/admin/users/components/user_actions.vue b/app/assets/javascripts/admin/users/components/user_actions.vue
index 38c7d3f9b90..a9482d479b6 100644
--- a/app/assets/javascripts/admin/users/components/user_actions.vue
+++ b/app/assets/javascripts/admin/users/components/user_actions.vue
@@ -116,8 +116,7 @@ export default {
category="tertiary"
:toggle-text="$options.i18n.userAdministration"
text-sr-only
- data-testid="dropdown-toggle"
- data-qa-selector="user_actions_dropdown_toggle"
+ data-testid="user-actions-dropdown-toggle"
:data-qa-username="user.username"
no-caret
>
diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue
index 170bd6895aa..9e57b834c88 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -313,7 +313,7 @@ export default {
<template #table>
<gl-table
class="alert-management-table"
- data-qa-selector="alert_table_container"
+ data-testid="alert-table-container"
:items="
alerts
? alerts.list
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_form.vue
index b9e37b9ede7..6baa431f2d9 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_form.vue
@@ -72,7 +72,7 @@ export default {
</p>
<form ref="settingsForm" @submit.prevent="updateAlertsIntegrationSettings">
<gl-form-group class="gl-pl-0">
- <gl-form-checkbox v-model="createIssueEnabled" data-qa-selector="create_incident_checkbox">
+ <gl-form-checkbox v-model="createIssueEnabled" data-testid="create-incident-checkbox">
<span>{{ $options.i18n.createIncident.label }}</span>
</gl-form-checkbox>
</gl-form-group>
@@ -93,14 +93,14 @@ export default {
v-model="issueTemplate"
:items="templates"
block
- data-qa-selector="incident_templates_dropdown"
+ data-testid="incident-templates-dropdown"
/>
</gl-form-group>
<gl-form-group class="gl-pl-0 gl-mb-5">
<gl-form-checkbox
v-model="sendEmailEnabled"
- data-qa-selector="enable_email_notification_checkbox"
+ data-testid="enable-email-notification-checkbox"
>
<span>{{ $options.i18n.sendEmail.label }}</span>
</gl-form-checkbox>
@@ -112,7 +112,7 @@ export default {
</gl-form-group>
<gl-button
ref="submitBtn"
- data-qa-selector="save_changes_button"
+ data-testid="save-changes-button"
:disabled="loading"
variant="confirm"
type="submit"
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
index 56740e436ca..fb872243e5e 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -406,7 +406,7 @@ export default {
v-model="integrationForm.type"
:disabled="isSelectDisabled"
class="gl-max-w-full"
- data-qa-selector="integration_type_dropdown"
+ data-testid="integration-type-dropdown"
:options="integrationTypesOptions"
autofocus
/>
@@ -439,7 +439,7 @@ export default {
v-model="integrationForm.name"
type="text"
:placeholder="$options.i18n.integrationFormSteps.nameIntegration.placeholder"
- data-qa-selector="integration_name_field"
+ data-testid="integration-name-field"
@input="validateName"
/>
</gl-form-group>
@@ -462,7 +462,7 @@ export default {
v-model="integrationForm.active"
:is-loading="loading"
:label="$options.i18n.integrationFormSteps.nameIntegration.activeToggle"
- data-qa-selector="active_toggle_container"
+ data-testid="active-toggle-container"
class="gl-mt-4 gl-font-weight-normal"
/>
</gl-form-group>
@@ -552,7 +552,7 @@ export default {
variant="confirm"
category="secondary"
class="gl-ml-3 js-no-auto-disable"
- data-qa-selector="save_and_create_alert_button"
+ data-testid="save-and-create-alert-button"
@click="submit(true)"
>
{{ $options.i18n.saveAndTestIntegration }}
@@ -654,7 +654,7 @@ export default {
:debounce="$options.JSON_VALIDATE_DELAY"
rows="6"
max-rows="10"
- data-qa-selector="test_payload_field"
+ data-testid="test-payload-field"
@input="validateJson(false)"
/>
</gl-form-group>
@@ -666,7 +666,6 @@ export default {
data-testid="send-test-alert"
variant="confirm"
class="js-no-auto-disable"
- data-qa-selector="send_test_alert_button"
@click="isFormDirty ? null : sendTestAlert()"
>
{{ $options.i18n.send }}
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
index e4fc37f9760..e735ee466ad 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -385,8 +385,7 @@ export default {
<gl-button
v-if="canAddIntegration && !formVisible"
size="small"
- data-testid="add-integration-btn"
- data-qa-selector="add_integration_button"
+ data-testid="add-integration-button"
@click="setFormVisibility(true)"
>
{{ $options.i18n.addNewIntegration }}
diff --git a/app/assets/javascripts/analytics/cycle_analytics/components/formatted_stage_count.vue b/app/assets/javascripts/analytics/cycle_analytics/components/formatted_stage_count.vue
index b622b0441e2..724e9c91305 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/components/formatted_stage_count.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/formatted_stage_count.vue
@@ -13,7 +13,8 @@ export default {
formattedStageCount() {
if (!this.stageCount) {
return '-';
- } else if (this.stageCount > 1000) {
+ }
+ if (this.stageCount > 1000) {
return sprintf(s__('ValueStreamAnalytics|%{stageCount}+ items'), {
stageCount: formatNumber(1000),
});
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 f881c924ae5..ddfc6baafa9 100644
--- a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
+++ b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
@@ -69,7 +69,8 @@ export default {
selectedProjectsLabel() {
if (this.selectedProjects.length === 1) {
return this.selectedProjects[0].name;
- } else if (this.selectedProjects.length > 1) {
+ }
+ if (this.selectedProjects.length > 1) {
return n__(
'CycleAnalytics|Project selected',
'CycleAnalytics|%d projects selected',
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 185cdaa1c99..6dfc1c609de 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -33,6 +33,7 @@ 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',
@@ -177,6 +178,19 @@ 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/api/application_settings_api.js b/app/assets/javascripts/api/application_settings_api.js
new file mode 100644
index 00000000000..839636c36f4
--- /dev/null
+++ b/app/assets/javascripts/api/application_settings_api.js
@@ -0,0 +1,14 @@
+import axios from '../lib/utils/axios_utils';
+import { buildApiUrl } from './api_utils';
+
+const APPLICATION_SETTINGS_PATH = '/api/:version/application/settings';
+
+export function getApplicationSettings() {
+ const url = buildApiUrl(APPLICATION_SETTINGS_PATH);
+ return axios.get(url);
+}
+
+export function updateApplicationSettings(data) {
+ const url = buildApiUrl(APPLICATION_SETTINGS_PATH);
+ return axios.put(url, data);
+}
diff --git a/app/assets/javascripts/authentication/webauthn/error.js b/app/assets/javascripts/authentication/webauthn/error.js
index 40dbecd8bc9..ce6c79a1f11 100644
--- a/app/assets/javascripts/authentication/webauthn/error.js
+++ b/app/assets/javascripts/authentication/webauthn/error.js
@@ -14,11 +14,14 @@ export default class WebAuthnError {
message() {
if (this.errorName === 'NotSupportedError') {
return __('Your device is not compatible with GitLab. Please try another device');
- } else if (this.errorName === 'InvalidStateError' && this.flowType === WEBAUTHN_AUTHENTICATE) {
+ }
+ if (this.errorName === 'InvalidStateError' && this.flowType === WEBAUTHN_AUTHENTICATE) {
return __('This device has not been registered with us.');
- } else if (this.errorName === 'InvalidStateError' && this.flowType === WEBAUTHN_REGISTER) {
+ }
+ if (this.errorName === 'InvalidStateError' && this.flowType === WEBAUTHN_REGISTER) {
return __('This device has already been registered with us.');
- } else if (this.errorName === 'SecurityError' && this.httpsDisabled) {
+ }
+ if (this.errorName === 'SecurityError' && this.httpsDisabled) {
return __(
'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.',
);
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
index 095634340c1..e1c1bd58ee2 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
@@ -20,7 +20,8 @@ export default () => ({
const checkbox = el.querySelector('input[type=checkbox].task-list-item-checkbox');
if (checkbox?.matches('[data-inapplicable]')) {
return { state: 'inapplicable' };
- } else if (checkbox?.checked) {
+ }
+ if (checkbox?.checked) {
return { state: 'done' };
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
index 689f2f0898e..e8c486f6e74 100644
--- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js
+++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
@@ -636,6 +636,7 @@ const MR_SHORTCUTS_GROUP = {
MR_NEXT_UNRESOLVED_DISCUSSION,
MR_PREVIOUS_UNRESOLVED_DISCUSSION,
MR_COPY_SOURCE_BRANCH_NAME,
+ MR_TOGGLE_FILE_BROWSER,
],
};
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue b/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
index cb7c6f9f6bc..e81ceae57c0 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
@@ -1,15 +1,16 @@
<script>
-import { GlModal, GlSearchBoxByType } from '@gitlab/ui';
+import { GlModal, GlSearchBoxByType, GlLink, GlSprintf } from '@gitlab/ui';
import { s__, __ } from '~/locale';
+import { joinPaths } from '../../lib/utils/url_utility';
import { keybindingGroups } from './keybindings';
import Shortcut from './shortcut.vue';
-import ShortcutsToggle from './shortcuts_toggle.vue';
export default {
components: {
GlModal,
GlSearchBoxByType,
- ShortcutsToggle,
+ GlLink,
+ GlSprintf,
Shortcut,
},
data() {
@@ -39,6 +40,9 @@ export default {
return mapped.filter((group) => group.keybindings.length);
},
+ absoluteUserPreferencesPath() {
+ return joinPaths(gon.relative_url_root || '/', '/-/profile/preferences');
+ },
},
i18n: {
title: __(`Keyboard shortcuts`),
@@ -66,7 +70,21 @@ export default {
:aria-label="$options.i18n.search"
class="gl-w-half gl-mr-3"
/>
- <shortcuts-toggle class="gl-w-half gl-ml-3" />
+ <span>
+ <gl-sprintf
+ :message="
+ __(
+ 'Enable or disable keyboard shortcuts in your %{linkStart}user preferences%{linkEnd}.',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link :href="absoluteUserPreferencesPath">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
</div>
<div v-if="filteredKeybindings.length === 0" class="gl-px-5">
{{ $options.i18n.noMatch }}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.js
index 3f3e0c51de5..22f2478c530 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.js
@@ -3,13 +3,7 @@ import 'mousetrap/plugins/pause/mousetrap-pause';
const shorcutsDisabledKey = 'shortcutsDisabled';
-export const shouldDisableShortcuts = () => {
- try {
- return localStorage.getItem(shorcutsDisabledKey) === 'true';
- } catch (e) {
- return false;
- }
-};
+export const shouldDisableShortcuts = () => !window.gon.keyboard_shortcuts_enabled;
export function enableShortcuts() {
localStorage.setItem(shorcutsDisabledKey, false);
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 30424fee46a..4e643f71c4b 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { fixTitle } from '~/tooltips';
import { getLocationHash } from '../lib/utils/url_utility';
// Toggle button. Show/hide content inside parent container.
@@ -29,9 +30,22 @@ $(() => {
$container.find('.js-toggle-content').toggle(toggleState);
}
+ function updateTitle(el, container) {
+ const $container = $(container);
+ const isExpanded = $container.data('is-expanded');
+
+ el.setAttribute('title', isExpanded ? el.dataset.collapseTitle : el.dataset.expandTitle);
+
+ fixTitle(el);
+ }
+
$('body').on('click', '.js-toggle-button', function toggleButton(e) {
e.currentTarget.classList.toggle(e.currentTarget.dataset.toggleOpenClass || 'selected');
- toggleContainer($(this).closest('.js-toggle-container'));
+
+ const containerEl = this.closest('.js-toggle-container');
+
+ toggleContainer(containerEl);
+ updateTitle(this, containerEl);
const targetTag = e.currentTarget.tagName.toLowerCase();
if (targetTag === 'a' || targetTag === 'button') {
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index 4e47aa99fd8..699a0491183 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -1,5 +1,8 @@
<script>
import DefaultActions from 'jh_else_ce/blob/components/blob_header_default_actions.vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import userInfoQuery from '../queries/user_info.query.graphql';
+import applicationInfoQuery from '../queries/application_info.query.graphql';
import BlobFilepath from './blob_header_filepath.vue';
import ViewerSwitcher from './blob_header_viewer_switcher.vue';
import { SIMPLE_BLOB_VIEWER } from './constants';
@@ -11,6 +14,21 @@ export default {
DefaultActions,
BlobFilepath,
TableOfContents,
+ WebIdeLink: () => import('ee_else_ce/vue_shared/components/web_ide_link.vue'),
+ },
+ apollo: {
+ currentUser: {
+ query: userInfoQuery,
+ error() {
+ this.$emit('error');
+ },
+ },
+ gitpodEnabled: {
+ query: applicationInfoQuery,
+ error() {
+ this.$emit('error');
+ },
+ },
},
props: {
blob: {
@@ -52,10 +70,26 @@ export default {
required: false,
default: false,
},
+ showForkSuggestion: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ projectId: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
viewer: this.hideViewerSwitcher ? null : this.activeViewerType,
+ gitpodEnabled: false,
};
},
computed: {
@@ -65,12 +99,18 @@ export default {
showDefaultActions() {
return !this.hideDefaultActions;
},
+ showWebIdeLink() {
+ return !this.blob.archived && this.blob.editBlobPath;
+ },
isEmpty() {
return this.blob.rawSize === '0';
},
blobSwitcherDocIcon() {
return this.blob.richViewer?.fileType === 'csv' ? 'table' : 'document';
},
+ projectIdAsNumber() {
+ return getIdFromGraphQLId(this.projectId);
+ },
},
watch: {
viewer(newVal, oldVal) {
@@ -100,6 +140,27 @@ export default {
<div class="gl-display-flex gl-flex-wrap file-actions">
<viewer-switcher v-if="showViewerSwitcher" v-model="viewer" :doc-icon="blobSwitcherDocIcon" />
+ <web-ide-link
+ v-if="showWebIdeLink"
+ :show-edit-button="!isBinary"
+ class="gl-mr-3"
+ :edit-url="blob.editBlobPath"
+ :web-ide-url="blob.ideEditPath"
+ :needs-to-fork="showForkSuggestion"
+ :show-pipeline-editor-button="Boolean(blob.pipelineEditorPath)"
+ :pipeline-editor-url="blob.pipelineEditorPath"
+ :gitpod-url="blob.gitpodBlobUrl"
+ :show-gitpod-button="gitpodEnabled"
+ :gitpod-enabled="currentUser && currentUser.gitpodEnabled"
+ :project-path="projectPath"
+ :project-id="projectIdAsNumber"
+ :user-preferences-gitpod-path="currentUser && currentUser.preferencesGitpodPath"
+ :user-profile-enable-gitpod-path="currentUser && currentUser.profileEnableGitpodPath"
+ is-blob
+ disable-fork-modal
+ v-on="$listeners"
+ />
+
<slot name="actions"></slot>
<default-actions
diff --git a/app/assets/javascripts/blob/line_highlighter.js b/app/assets/javascripts/blob/line_highlighter.js
index 4258d16b69f..9c6a5958e1f 100644
--- a/app/assets/javascripts/blob/line_highlighter.js
+++ b/app/assets/javascripts/blob/line_highlighter.js
@@ -59,7 +59,7 @@ LineHighlighter.prototype.bindEvents = function () {
}
};
-LineHighlighter.prototype.highlightHash = function (newHash) {
+LineHighlighter.prototype.highlightHash = function (newHash, scrollEnabled = true) {
let range;
if (newHash && typeof newHash === 'string') this._hash = newHash;
@@ -71,12 +71,14 @@ LineHighlighter.prototype.highlightHash = function (newHash) {
this.highlightRange(range);
const lineSelector = `#L${range[0]}`;
- scrollToElement(lineSelector, {
- // Scroll to the first highlighted line on initial load
- // Add an offset of -100 for some context
- offset: -100,
- behavior: this.options.scrollBehavior,
- });
+ if (scrollEnabled) {
+ scrollToElement(lineSelector, {
+ // Scroll to the first highlighted line on initial load
+ // Add an offset of -100 for some context
+ offset: -100,
+ behavior: this.options.scrollBehavior,
+ });
+ }
}
}
};
@@ -94,7 +96,8 @@ LineHighlighter.prototype.clickHandler = function (event) {
// treat this like a single-line selection.
this.setHash(lineNumber);
return this.highlightLine(lineNumber);
- } else if (event.shiftKey) {
+ }
+ if (event.shiftKey) {
if (lineNumber < current[0]) {
range = [lineNumber, current[0]];
} else {
diff --git a/app/assets/javascripts/blob/openapi/index.js b/app/assets/javascripts/blob/openapi/index.js
index 94ae281cada..9c22d960bf5 100644
--- a/app/assets/javascripts/blob/openapi/index.js
+++ b/app/assets/javascripts/blob/openapi/index.js
@@ -5,6 +5,7 @@ import {
relativePathToAbsolute,
joinPaths,
setUrlParams,
+ getParameterByName,
} from '~/lib/utils/url_utility';
const SANDBOX_FRAME_PATH = '/-/sandbox/swagger';
@@ -12,10 +13,14 @@ const SANDBOX_FRAME_PATH = '/-/sandbox/swagger';
const getSandboxFrameSrc = () => {
const path = joinPaths(gon.relative_url_root || '', SANDBOX_FRAME_PATH);
const absoluteUrl = relativePathToAbsolute(path, getBaseURL());
+ const displayOperationId = getParameterByName('displayOperationId');
+ const params = { displayOperationId };
+
if (window.gon?.relative_url_root) {
- return setUrlParams({ relativeRootPath: window.gon.relative_url_root }, absoluteUrl);
+ params.relativeRootPath = window.gon.relative_url_root;
}
- return absoluteUrl;
+
+ return setUrlParams(params, absoluteUrl);
};
const createSandbox = () => {
diff --git a/app/assets/javascripts/repository/queries/application_info.query.graphql b/app/assets/javascripts/blob/queries/application_info.query.graphql
index fd69de39f75..fd69de39f75 100644
--- a/app/assets/javascripts/repository/queries/application_info.query.graphql
+++ b/app/assets/javascripts/blob/queries/application_info.query.graphql
diff --git a/app/assets/javascripts/repository/queries/user_info.query.graphql b/app/assets/javascripts/blob/queries/user_info.query.graphql
index 114947a423d..114947a423d 100644
--- a/app/assets/javascripts/repository/queries/user_info.query.graphql
+++ b/app/assets/javascripts/blob/queries/user_info.query.graphql
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index 692ca6bf59b..c441a718dd8 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -20,6 +20,7 @@ import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
import { ListType } from '../constants';
import eventHub from '../eventhub';
+import { setError } from '../graphql/cache_updates';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue';
@@ -45,6 +46,7 @@ export default {
},
mixins: [boardCardInner],
inject: [
+ 'allowSubEpics',
'rootPath',
'scopedLabelsAvailable',
'isEpicBoard',
@@ -85,7 +87,7 @@ export default {
};
},
computed: {
- ...mapState(['isShowingLabels', 'allowSubEpics']),
+ ...mapState(['isShowingLabels']),
isLoading() {
return this.item.isLoading || this.item.iid === '-1';
},
@@ -175,7 +177,8 @@ export default {
},
},
methods: {
- ...mapActions(['performSearch', 'setError']),
+ ...mapActions(['performSearch']),
+ setError,
isIndexLessThanlimit(index) {
return index < this.limitBeforeCounter;
},
@@ -288,7 +291,7 @@ export default {
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-5" />
<span
v-if="item.referencePath && !isLoading"
- class="board-card-number gl-overflow-hidden gl-display-flex gl-mr-3 gl-mt-3 gl-font-sm gl-text-secondary"
+ class="board-card-number gl-overflow-hidden gl-display-flex gl-gap-2 gl-mr-3 gl-mt-3 gl-font-sm gl-text-secondary"
:class="{ 'gl-font-base': isEpicBoard }"
>
<work-item-type-icon
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 5e1e46dd198..bb740c0e7eb 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -222,7 +222,7 @@ export default {
<template #default>
<board-sidebar-title :active-item="activeBoardIssuable" data-testid="sidebar-title" />
<sidebar-assignees-widget
- v-if="activeBoardItem.assignees"
+ v-if="activeBoardIssuable.assignees"
:iid="activeBoardIssuable.iid"
:full-path="projectPathForActiveIssue"
:initial-assignees="activeBoardIssuable.assignees"
@@ -232,7 +232,7 @@ export default {
/>
<sidebar-dropdown-widget
v-if="epicFeatureAvailable && !isIncidentSidebar"
- :key="`epic-${activeBoardItem.iid}`"
+ :key="`epic-${activeBoardIssuable.iid}`"
:iid="activeBoardIssuable.iid"
issuable-attribute="epic"
:workspace-path="projectPathForActiveIssue"
@@ -242,7 +242,7 @@ export default {
/>
<div>
<sidebar-dropdown-widget
- :key="`milestone-${activeBoardItem.iid}`"
+ :key="`milestone-${activeBoardIssuable.iid}`"
:iid="activeBoardIssuable.iid"
issuable-attribute="milestone"
:workspace-path="projectPathForActiveIssue"
@@ -252,7 +252,7 @@ export default {
/>
<sidebar-iteration-widget
v-if="iterationFeatureAvailable && !isIncidentSidebar"
- :key="`iteration-${activeBoardItem.iid}`"
+ :key="`iteration-${activeBoardIssuable.iid}`"
:iid="activeBoardIssuable.iid"
:workspace-path="projectPathForActiveIssue"
:attr-workspace-path="groupPathForActiveIssue"
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index 4986c3780e5..d12478b42d8 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -178,6 +178,9 @@ export default {
},
methods: {
...mapActions(['setError', 'unsetError', 'setBoard']),
+ isFocusMode() {
+ return Boolean(document.querySelector('.content-wrapper > .js-focus-mode-board.is-focused'));
+ },
cancel() {
this.$emit('cancel');
},
@@ -281,6 +284,7 @@ export default {
modal-class="board-config-modal"
content-class="gl-absolute gl-top-7"
visible
+ :static="isFocusMode()"
:hide-footer="readonly"
:title="title"
:action-primary="primaryProps"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 67bfcfb9d97..1bb7e88122a 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -88,6 +88,7 @@ export default {
toListId: null,
toList: {},
addItemToListInProgress: false,
+ updateIssueOrderInProgress: false,
};
},
apollo: {
@@ -253,7 +254,9 @@ export default {
return this.canMoveIssue ? options : {};
},
disableScrollingWhenMutationInProgress() {
- return this.hasNextPage && this.isUpdateIssueOrderInProgress;
+ return (
+ this.hasNextPage && (this.isUpdateIssueOrderInProgress || this.updateIssueOrderInProgress)
+ );
},
showMoveToPosition() {
return !this.disabled && this.list.listType !== ListType.closed;
@@ -343,7 +346,7 @@ export default {
sortableStart();
this.track('drag_card', { label: 'board' });
},
- handleDragOnEnd({
+ async handleDragOnEnd({
newIndex: originalNewIndex,
oldIndex,
from,
@@ -394,7 +397,8 @@ export default {
}
if (this.isApolloBoard) {
- this.moveBoardItem(
+ this.updateIssueOrderInProgress = true;
+ await this.moveBoardItem(
{
epicId: itemId,
iid: itemIid,
@@ -404,7 +408,9 @@ export default {
moveAfterId,
},
newIndex,
- );
+ ).finally(() => {
+ this.updateIssueOrderInProgress = false;
+ });
} else {
this.moveItem({
itemId,
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 068db98a750..42c30dc8245 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -490,7 +490,11 @@ export default {
>
<span class="gl-display-inline-flex" :class="{ 'gl-rotate-90': list.collapsed }">
<gl-tooltip :target="() => $refs.itemCount" :title="itemsTooltipLabel" />
- <span ref="itemCount" class="gl-display-inline-flex gl-align-items-center">
+ <span
+ ref="itemCount"
+ class="gl-display-inline-flex gl-align-items-center"
+ data-testid="item-count"
+ >
<gl-icon class="gl-mr-2" :name="countIcon" :size="14" />
<item-count
v-if="!isLoading"
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 58db2c9ac2a..89e13625210 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -166,7 +166,7 @@ export default {
<mounting-portal mount-to="#js-right-sidebar-portal" name="board-settings-sidebar" append>
<gl-drawer
v-bind="$attrs"
- class="js-board-settings-sidebar gl-absolute boards-sidebar"
+ class="js-board-settings-sidebar boards-sidebar"
:open="showSidebar"
variant="sidebar"
@close="unsetActiveListId"
diff --git a/app/assets/javascripts/boards/components/board_top_bar.vue b/app/assets/javascripts/boards/components/board_top_bar.vue
index 2b8418333a8..7fd1a934381 100644
--- a/app/assets/javascripts/boards/components/board_top_bar.vue
+++ b/app/assets/javascripts/boards/components/board_top_bar.vue
@@ -93,6 +93,9 @@ export default {
});
return hasScope;
},
+ isLoading() {
+ return this.$apollo.queries.board.loading;
+ },
},
};
</script>
@@ -105,7 +108,11 @@ export default {
<div
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-flex-grow-1 gl-lg-mb-0 gl-mb-3 gl-w-full gl-min-w-0"
>
- <boards-selector :board-apollo="board" @switchBoard="$emit('switchBoard', $event)" />
+ <boards-selector
+ :board-apollo="board"
+ :is-current-board-loading="isLoading"
+ @switchBoard="$emit('switchBoard', $event)"
+ />
<new-board-button />
<issue-board-filtered-search
v-if="isIssueBoard"
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index b3fe52944dc..cc6fde92f9b 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -70,6 +70,11 @@ export default {
required: false,
default: () => ({}),
},
+ isCurrentBoardLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -92,6 +97,9 @@ export default {
boardToUse() {
return this.isApolloBoard ? this.boardApollo : this.board;
},
+ isBoardToUseLoading() {
+ return this.isApolloBoard ? this.isCurrentBoardLoading : this.isBoardLoading;
+ },
parentType() {
return this.boardType;
},
@@ -301,7 +309,7 @@ export default {
data-qa-selector="boards_dropdown"
toggle-class="dropdown-menu-toggle"
menu-class="flex-column dropdown-extended-height"
- :loading="isBoardLoading"
+ :loading="isBoardToUseLoading"
:text="boardToUse.name"
@show="loadBoards"
>
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 f60f00be368..a7b3f5536a4 100644
--- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -62,7 +62,7 @@ export default {
tokensCE() {
const { issue, incident } = this.$options.i18n;
const { types } = this.$options;
- const { fetchUsers, fetchLabels, fetchMilestones } = issueBoardFilters(
+ const { fetchUsers, fetchLabels } = issueBoardFilters(
this.$apollo,
this.fullPath,
this.isGroupBoard,
@@ -148,7 +148,8 @@ export default {
token: MilestoneToken,
unique: true,
shouldSkipSort: true,
- fetchMilestones,
+ isProject: !this.isGroupBoard,
+ fullPath: this.fullPath,
},
{
icon: 'issues',
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
index 1f28974afd1..46009df2bd3 100644
--- a/app/assets/javascripts/boards/components/issue_due_date.vue
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -52,11 +52,14 @@ export default {
if (timeDifference === 0) {
return __('Today');
- } else if (timeDifference === 1) {
+ }
+ if (timeDifference === 1) {
return __('Tomorrow');
- } else if (timeDifference === -1) {
+ }
+ if (timeDifference === -1) {
return __('Yesterday');
- } else if (timeDifference > 0 && timeDifference < 7) {
+ }
+ if (timeDifference > 0 && timeDifference < 7) {
return dateFormat(issueDueDate, 'dddd');
}
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
index 1c2c0022ddf..a2c4b42b6c5 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
@@ -7,6 +7,7 @@ import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { titleQueries } from 'ee_else_ce/boards/constants';
+import { setError } from '../../graphql/cache_updates';
export default {
components: {
@@ -65,7 +66,7 @@ export default {
},
},
methods: {
- ...mapActions(['setActiveItemTitle', 'setError']),
+ ...mapActions(['setActiveItemTitle']),
getPendingChangesKey(item) {
if (!item) {
return '';
@@ -130,7 +131,7 @@ export default {
this.showChangesAlert = false;
} catch (e) {
this.title = this.item.title;
- this.setError({ error: e, message: this.$options.i18n.updateTitleError });
+ setError({ error: e, message: this.$options.i18n.updateTitleError });
} finally {
this.loading = false;
}
diff --git a/app/assets/javascripts/boards/graphql/cache_updates.js b/app/assets/javascripts/boards/graphql/cache_updates.js
index e54701a63c0..3551c3ed982 100644
--- a/app/assets/javascripts/boards/graphql/cache_updates.js
+++ b/app/assets/javascripts/boards/graphql/cache_updates.js
@@ -109,9 +109,9 @@ export function updateEpicsCount({
epicBoardList: {
...epicBoardList,
metadata: {
+ ...epicBoardList.metadata,
epicsCount: epicBoardList.metadata.epicsCount - 1,
totalWeight: epicBoardList.metadata.totalWeight - epicWeight,
- ...epicBoardList.metadata,
},
},
}),
@@ -127,9 +127,9 @@ export function updateEpicsCount({
epicBoardList: {
...epicBoardList,
metadata: {
+ ...epicBoardList.metadata,
epicsCount: epicBoardList.metadata.epicsCount + 1,
totalWeight: epicBoardList.metadata.totalWeight + epicWeight,
- ...epicBoardList.metadata,
},
},
}),
diff --git a/app/assets/javascripts/boards/graphql/group_board_members.query.graphql b/app/assets/javascripts/boards/graphql/group_board_members.query.graphql
deleted file mode 100644
index 252e8c1ab06..00000000000
--- a/app/assets/javascripts/boards/graphql/group_board_members.query.graphql
+++ /dev/null
@@ -1,15 +0,0 @@
-#import "~/graphql_shared/fragments/user.fragment.graphql"
-
-query GroupBoardMembers($fullPath: ID!, $search: String) {
- workspace: group(fullPath: $fullPath) {
- id
- assignees: groupMembers(search: $search, relations: [DIRECT, DESCENDANTS, INHERITED]) {
- nodes {
- id
- user {
- ...User
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/boards/graphql/project_board_members.query.graphql b/app/assets/javascripts/boards/graphql/project_board_members.query.graphql
deleted file mode 100644
index 5279680b03c..00000000000
--- a/app/assets/javascripts/boards/graphql/project_board_members.query.graphql
+++ /dev/null
@@ -1,15 +0,0 @@
-#import "~/graphql_shared/fragments/user.fragment.graphql"
-
-query ProjectBoardMembers($fullPath: ID!, $search: String) {
- workspace: project(fullPath: $fullPath) {
- id
- assignees: projectMembers(search: $search) {
- nodes {
- id
- user {
- ...User
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index a03ec9193ea..9d7b7a38c6d 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -102,6 +102,7 @@ function mountBoardApp(el) {
swimlanesFeatureAvailable: gon.licensed_features?.swimlanes,
multipleIssueBoardsAvailable: parseBoolean(el.dataset.multipleBoardsAvailable),
scopedIssueBoardFeatureEnabled: parseBoolean(el.dataset.scopedIssueBoardFeatureEnabled),
+ allowSubEpics: false,
},
render: (createComponent) => createComponent(BoardApp),
});
diff --git a/app/assets/javascripts/boards/issue_board_filters.js b/app/assets/javascripts/boards/issue_board_filters.js
index 27efb3f775c..ba5da70c6ec 100644
--- a/app/assets/javascripts/boards/issue_board_filters.js
+++ b/app/assets/javascripts/boards/issue_board_filters.js
@@ -1,7 +1,5 @@
-import groupBoardMembers from '~/boards/graphql/group_board_members.query.graphql';
-import projectBoardMembers from '~/boards/graphql/project_board_members.query.graphql';
-import groupBoardMilestonesQuery from './graphql/group_board_milestones.query.graphql';
-import projectBoardMilestonesQuery from './graphql/project_board_milestones.query.graphql';
+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) {
@@ -9,20 +7,15 @@ export default function issueBoardFilters(apollo, fullPath, isGroupBoard) {
return isGroupBoard ? data.group?.labels.nodes || [] : data.project?.labels.nodes || [];
};
- const boardAssigneesQuery = () => {
- return isGroupBoard ? groupBoardMembers : projectBoardMembers;
- };
-
const fetchUsers = (usersSearchTerm) => {
+ const namespace = isGroupBoard ? BoardType.group : BoardType.project;
+
return apollo
.query({
- query: boardAssigneesQuery(),
- variables: {
- fullPath,
- search: usersSearchTerm,
- },
+ query: usersAutocompleteQuery,
+ variables: { fullPath, search: usersSearchTerm, isProject: !isGroupBoard },
})
- .then(({ data }) => data.workspace?.assignees.nodes.map(({ user }) => user));
+ .then(({ data }) => data[namespace]?.autocompleteUsers);
};
const fetchLabels = (labelSearchTerm) => {
@@ -39,27 +32,8 @@ export default function issueBoardFilters(apollo, fullPath, isGroupBoard) {
.then(transformLabels);
};
- const fetchMilestones = (searchTerm) => {
- const variables = {
- fullPath,
- searchTerm,
- };
-
- const query = isGroupBoard ? groupBoardMilestonesQuery : projectBoardMilestonesQuery;
-
- return apollo
- .query({
- query,
- variables,
- })
- .then(({ data }) => {
- return data.workspace?.milestones.nodes;
- });
- };
-
return {
fetchLabels,
fetchUsers,
- fetchMilestones,
};
}
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue b/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
index daa4119f44d..89582e64f3a 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
@@ -1,13 +1,17 @@
<script>
import { GlAlert, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { setUrlParams, updateHistory, queryToObject } from '~/lib/utils/url_utility';
-import { validateQueryString } from '~/jobs/components/filtered_search/utils';
-import JobsTable from '~/jobs/components/table/jobs_table.vue';
-import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
-import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
-import JobsTableEmptyState from '~/jobs/components/table/jobs_table_empty_state.vue';
+import { validateQueryString } from '~/ci/common/private/jobs_filtered_search/utils';
+import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
+import JobsTableTabs from '~/ci/jobs_page/components/jobs_table_tabs.vue';
+import JobsFilteredSearch from '~/ci/common/private/jobs_filtered_search/app.vue';
+import JobsTableEmptyState from '~/ci/jobs_page/components/jobs_table_empty_state.vue';
import { createAlert } from '~/alert';
-import JobsSkeletonLoader from '../jobs_skeleton_loader.vue';
+import {
+ TOKEN_TYPE_STATUS,
+ TOKEN_TYPE_JOBS_RUNNER_TYPE,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
DEFAULT_FIELDS_ADMIN,
RAW_TEXT_WARNING_ADMIN,
@@ -15,10 +19,11 @@ import {
JOBS_FETCH_ERROR_MSG,
LOADING_ARIA_LABEL,
CANCELABLE_JOBS_ERROR_MSG,
-} from '../constants';
+} from './constants';
+import JobsSkeletonLoader from './components/jobs_skeleton_loader.vue';
import GetAllJobs from './graphql/queries/get_all_jobs.query.graphql';
import GetAllJobsCount from './graphql/queries/get_all_jobs_count.query.graphql';
-import CancelableJobs from './graphql/queries/get_cancelable_jobs_count.query.graphql';
+import getCancelableJobs from './graphql/queries/get_cancelable_jobs_count.query.graphql';
export default {
i18n: {
@@ -39,6 +44,7 @@ export default {
GlIntersectionObserver,
GlLoadingIcon,
},
+ mixins: [glFeatureFlagsMixin()],
inject: {
jobStatuses: {
default: null,
@@ -72,6 +78,9 @@ export default {
},
jobsCount: {
query: GetAllJobsCount,
+ variables() {
+ return this.variables;
+ },
update(data) {
return data?.jobs?.count || 0;
},
@@ -83,7 +92,7 @@ export default {
},
},
cancelable: {
- query: CancelableJobs,
+ query: getCancelableJobs,
update(data) {
this.isCancelable = data.cancelable.count !== 0;
},
@@ -150,11 +159,11 @@ export default {
},
},
methods: {
- updateHistoryAndFetchCount(status = null) {
- this.$apollo.queries.jobsCount.refetch({ statuses: status });
+ updateHistoryAndFetchCount(filterParams = {}) {
+ this.$apollo.queries.jobsCount.refetch(filterParams);
updateHistory({
- url: setUrlParams({ statuses: status }, window.location.href, true),
+ url: setUrlParams(filterParams, window.location.href, true),
});
},
fetchJobsByStatus(scope) {
@@ -184,33 +193,36 @@ export default {
this.infiniteScrollingTriggered = false;
this.filterSearchTriggered = true;
- // all filters have been cleared reset query param
- // and refetch jobs/count with defaults
- if (!filters.length) {
- this.updateHistoryAndFetchCount();
- this.$apollo.queries.jobs.refetch({ statuses: null });
-
- return;
- }
-
- // Eventually there will be more tokens available
- // this code is written to scale for those tokens
- filters.forEach((filter) => {
+ if (filters.some((filter) => !filter.type)) {
// Raw text input in filtered search does not have a type
// when a user enters raw text we alert them that it is
// not supported and we do not make an additional API call
- if (!filter.type) {
- createAlert({
- message: RAW_TEXT_WARNING_ADMIN,
- type: 'warning',
- });
- }
+ createAlert({ message: RAW_TEXT_WARNING_ADMIN, type: 'warning' });
+ return;
+ }
+
+ const defaultFilterParams = this.glFeatures.adminJobsFilterRunnerType
+ ? { statuses: null, runnerTypes: null }
+ : { statuses: null };
- if (filter.type === 'status') {
- this.updateHistoryAndFetchCount(filter.value.data);
- this.$apollo.queries.jobs.refetch({ statuses: filter.value.data });
+ const filterParams = filters.reduce((acc, filter) => {
+ switch (filter.type) {
+ case TOKEN_TYPE_STATUS:
+ return { ...acc, statuses: filter.value.data };
+
+ case TOKEN_TYPE_JOBS_RUNNER_TYPE:
+ if (this.glFeatures.adminJobsFilterRunnerType) {
+ return { ...acc, runnerTypes: filter.value.data };
+ }
+ return acc;
+
+ default:
+ return acc;
}
- });
+ }, defaultFilterParams);
+
+ this.updateHistoryAndFetchCount(filterParams);
+ this.$apollo.queries.jobs.refetch(filterParams);
},
},
};
diff --git a/app/assets/javascripts/pages/admin/jobs/components/cancel_jobs.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs.vue
index 72cfc005782..fb13fd4b03e 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/cancel_jobs.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
+import { CANCEL_JOBS_MODAL_ID, CANCEL_JOBS_BUTTON_TEXT, CANCEL_BUTTON_TOOLTIP } from '../constants';
import CancelJobsModal from './cancel_jobs_modal.vue';
-import { CANCEL_JOBS_MODAL_ID, CANCEL_JOBS_BUTTON_TEXT, CANCEL_BUTTON_TOOLTIP } from './constants';
export default {
name: 'CancelJobs',
diff --git a/app/assets/javascripts/pages/admin/jobs/components/cancel_jobs_modal.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs_modal.vue
index b2c5326fefd..7c1cd75609a 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/cancel_jobs_modal.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs_modal.vue
@@ -9,7 +9,7 @@ import {
CANCEL_JOBS_MODAL_TITLE,
CANCEL_JOBS_WARNING,
PRIMARY_ACTION_TEXT,
-} from './constants';
+} from '../constants';
export default {
components: {
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/cell/project_cell.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue
index cbb80a5175f..cbb80a5175f 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/table/cell/project_cell.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/cells/runner_cell.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue
index 33bcee5b34b..a76829aa129 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/table/cells/runner_cell.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue
@@ -1,6 +1,6 @@
<script>
import { GlLink } from '@gitlab/ui';
-import { RUNNER_EMPTY_TEXT, RUNNER_NO_DESCRIPTION } from '~/pages/admin/jobs/components/constants';
+import { RUNNER_EMPTY_TEXT, RUNNER_NO_DESCRIPTION } from '../../constants';
export default {
i18n: {
diff --git a/app/assets/javascripts/pages/admin/jobs/components/jobs_skeleton_loader.vue b/app/assets/javascripts/ci/admin/jobs_table/components/jobs_skeleton_loader.vue
index c305e09af0d..c305e09af0d 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/jobs_skeleton_loader.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/jobs_skeleton_loader.vue
diff --git a/app/assets/javascripts/pages/admin/jobs/components/constants.js b/app/assets/javascripts/ci/admin/jobs_table/constants.js
index 4af8cb355fc..ff0efdb1f5b 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/constants.js
+++ b/app/assets/javascripts/ci/admin/jobs_table/constants.js
@@ -1,5 +1,5 @@
import { s__, __ } from '~/locale';
-import { RAW_TEXT_WARNING } from '~/jobs/components/table/constants';
+import { RAW_TEXT_WARNING } from '~/ci/jobs_page/constants';
export const JOBS_COUNT_ERROR_MESSAGE = __('There was an error fetching the number of jobs.');
export const JOBS_FETCH_ERROR_MSG = __('There was an error fetching the jobs.');
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/cache_config.js b/app/assets/javascripts/ci/admin/jobs_table/graphql/cache_config.js
index fd7ee2a6f8c..fd7ee2a6f8c 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/cache_config.js
+++ b/app/assets/javascripts/ci/admin/jobs_table/graphql/cache_config.js
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql
index 9e2795966e0..89fb1782e46 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql
+++ b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql
@@ -1,5 +1,10 @@
-query getAllJobs($after: String, $first: Int = 50, $statuses: [CiJobStatus!]) {
- jobs(after: $after, first: $first, statuses: $statuses) {
+query getAllJobs(
+ $after: String
+ $first: Int = 50
+ $statuses: [CiJobStatus!]
+ $runnerTypes: [CiRunnerType!]
+) {
+ jobs(after: $after, first: $first, statuses: $statuses, runnerTypes: $runnerTypes) {
pageInfo {
endCursor
hasNextPage
diff --git a/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs_count.query.graphql b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs_count.query.graphql
new file mode 100644
index 00000000000..bcb0123e9e3
--- /dev/null
+++ b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs_count.query.graphql
@@ -0,0 +1,5 @@
+query getAllJobsCount($statuses: [CiJobStatus!], $runnerTypes: [CiRunnerType!]) {
+ jobs(statuses: $statuses, runnerTypes: $runnerTypes) {
+ count
+ }
+}
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_cancelable_jobs_count.query.graphql b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_cancelable_jobs_count.query.graphql
index 9b90abebbf7..9bf5e1449b7 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_cancelable_jobs_count.query.graphql
+++ b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_cancelable_jobs_count.query.graphql
@@ -1,4 +1,4 @@
-query canelableJobs {
+query getCancelableJobsCount {
cancelable: jobs(statuses: [PENDING, RUNNING]) {
count
}
diff --git a/app/assets/javascripts/ci/artifacts/components/feedback_banner.vue b/app/assets/javascripts/ci/artifacts/components/feedback_banner.vue
deleted file mode 100644
index d2c96b1a201..00000000000
--- a/app/assets/javascripts/ci/artifacts/components/feedback_banner.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<script>
-import { GlBanner } from '@gitlab/ui';
-import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
-import {
- I18N_FEEDBACK_BANNER_TITLE,
- I18N_FEEDBACK_BANNER_BODY,
- I18N_FEEDBACK_BANNER_BUTTON,
- FEEDBACK_URL,
-} from '../constants';
-
-export default {
- components: {
- GlBanner,
- UserCalloutDismisser,
- },
- inject: ['artifactsManagementFeedbackImagePath'],
- FEEDBACK_URL,
- i18n: {
- title: I18N_FEEDBACK_BANNER_TITLE,
- body: I18N_FEEDBACK_BANNER_BODY,
- button: I18N_FEEDBACK_BANNER_BUTTON,
- },
-};
-</script>
-<template>
- <user-callout-dismisser feature-name="artifacts_management_page_feedback_banner">
- <template #default="{ dismiss, shouldShowCallout }">
- <gl-banner
- v-if="shouldShowCallout"
- class="gl-mb-6"
- :title="$options.i18n.title"
- :button-text="$options.i18n.button"
- :button-link="$options.FEEDBACK_URL"
- :svg-path="artifactsManagementFeedbackImagePath"
- @close="dismiss"
- >
- <p>{{ $options.i18n.body }}</p>
- </gl-banner>
- </template>
- </user-callout-dismisser>
-</template>
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 88334488fdd..e08470c62be 100644
--- a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
+++ b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
@@ -48,7 +48,6 @@ import JobCheckbox from './job_checkbox.vue';
import ArtifactsBulkDelete from './artifacts_bulk_delete.vue';
import BulkDeleteModal from './bulk_delete_modal.vue';
import ArtifactsTableRowDetails from './artifacts_table_row_details.vue';
-import FeedbackBanner from './feedback_banner.vue';
const INITIAL_PAGINATION_STATE = {
currentPage: INITIAL_CURRENT_PAGE,
@@ -76,7 +75,6 @@ export default {
ArtifactsBulkDelete,
BulkDeleteModal,
ArtifactsTableRowDetails,
- FeedbackBanner,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -374,7 +372,6 @@ export default {
</script>
<template>
<div>
- <feedback-banner />
<artifacts-bulk-delete
v-if="canBulkDestroyArtifacts"
:selected-artifacts="selectedArtifacts"
diff --git a/app/assets/javascripts/ci/artifacts/constants.js b/app/assets/javascripts/ci/artifacts/constants.js
index 2d89b6541f3..28c371cda1e 100644
--- a/app/assets/javascripts/ci/artifacts/constants.js
+++ b/app/assets/javascripts/ci/artifacts/constants.js
@@ -47,13 +47,6 @@ export const I18N_MODAL_BODY = s__(
export const I18N_MODAL_PRIMARY = s__('Artifacts|Delete artifact');
export const I18N_MODAL_CANCEL = __('Cancel');
-export const I18N_FEEDBACK_BANNER_TITLE = s__('Artifacts|Help us improve this page');
-export const I18N_FEEDBACK_BANNER_BODY = s__(
- 'Artifacts|We want you to be able to use this page to easily manage your CI/CD job artifacts. We are working to improve this experience and would appreciate any feedback you have about the improvements we are making.',
-);
-export const I18N_FEEDBACK_BANNER_BUTTON = s__('Artifacts|Take a quick survey');
-export const FEEDBACK_URL = 'https://gitlab.fra1.qualtrics.com/jfe/form/SV_cI9rAUI20Vo2St8';
-
export const SELECTED_ARTIFACTS_MAX_COUNT = 50;
export const I18N_BULK_DELETE_MAX_SELECTED = s__(
'Artifacts|Maximum selected artifacts limit reached',
diff --git a/app/assets/javascripts/ci/artifacts/index.js b/app/assets/javascripts/ci/artifacts/index.js
index 6e795fd9bd7..c6021eb056f 100644
--- a/app/assets/javascripts/ci/artifacts/index.js
+++ b/app/assets/javascripts/ci/artifacts/index.js
@@ -19,12 +19,7 @@ export const initArtifactsTable = () => {
return false;
}
- const {
- projectPath,
- projectId,
- canDestroyArtifacts,
- artifactsManagementFeedbackImagePath,
- } = el.dataset;
+ const { projectPath, projectId, canDestroyArtifacts } = el.dataset;
return new Vue({
el,
@@ -33,7 +28,6 @@ export const initArtifactsTable = () => {
projectPath,
projectId,
canDestroyArtifacts: parseBoolean(canDestroyArtifacts),
- artifactsManagementFeedbackImagePath,
},
render: (createElement) => createElement(App),
});
diff --git a/app/assets/javascripts/ci/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci/ci_variable_list/ci_variable_list.js
deleted file mode 100644
index 574a5e7fd99..00000000000
--- a/app/assets/javascripts/ci/ci_variable_list/ci_variable_list.js
+++ /dev/null
@@ -1,262 +0,0 @@
-import $ from 'jquery';
-import SecretValues from '~/behaviors/secret_values';
-import CreateItemDropdown from '~/create_item_dropdown';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import { s__ } from '~/locale';
-
-const ALL_ENVIRONMENTS_STRING = s__('CiVariable|All environments');
-
-function createEnvironmentItem(value) {
- return {
- title: value === '*' ? ALL_ENVIRONMENTS_STRING : value,
- id: value,
- text: value === '*' ? s__('CiVariable|* (All environments)') : value,
- };
-}
-
-export default class VariableList {
- constructor({ container, formField, maskableRegex }) {
- this.$container = $(container);
- this.formField = formField;
- this.maskableRegex = new RegExp(maskableRegex);
- this.environmentDropdownMap = new WeakMap();
-
- this.inputMap = {
- id: {
- selector: '.js-ci-variable-input-id',
- default: '',
- },
- variable_type: {
- selector: '.js-ci-variable-input-variable-type',
- default: 'env_var',
- },
- key: {
- selector: '.js-ci-variable-input-key',
- default: '',
- },
- secret_value: {
- selector: '.js-ci-variable-input-value',
- default: '',
- },
- protected: {
- selector: '.js-ci-variable-input-protected',
- // use `attr` instead of `data` as we don't want the value to be
- // converted. we need the value as a string.
- default: $('.js-ci-variable-input-protected').attr('data-default'),
- },
- masked: {
- selector: '.js-ci-variable-input-masked',
- // use `attr` instead of `data` as we don't want the value to be
- // converted. we need the value as a string.
- default: $('.js-ci-variable-input-masked').attr('data-default'),
- },
- environment_scope: {
- // We can't use a `.js-` class here because
- // deprecated_jquery_dropdown replaces the <input> and doesn't copy over the class
- // See https://gitlab.com/gitlab-org/gitlab-foss/issues/42458
- selector: `input[name="${this.formField}[variables_attributes][][environment_scope]"]`,
- default: '*',
- },
- _destroy: {
- selector: '.js-ci-variable-input-destroy',
- default: '',
- },
- };
-
- this.secretValues = new SecretValues({
- container: this.$container[0],
- valueSelector: '.js-row:not(:last-child) .js-secret-value',
- placeholderSelector: '.js-row:not(:last-child) .js-secret-value-placeholder',
- });
- }
-
- init() {
- this.bindEvents();
- this.secretValues.init();
- }
-
- bindEvents() {
- this.$container.find('.js-row').each((index, rowEl) => {
- this.initRow(rowEl);
- });
-
- this.$container.on('click', '.js-row-remove-button', (e) => {
- e.preventDefault();
- this.removeRow($(e.currentTarget).closest('.js-row'));
- });
-
- const inputSelector = Object.keys(this.inputMap)
- .map((name) => this.inputMap[name].selector)
- .join(',');
-
- // Remove any empty rows except the last row
- this.$container.on('blur', inputSelector, (e) => {
- const $row = $(e.currentTarget).closest('.js-row');
-
- if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) {
- this.removeRow($row);
- }
- });
-
- this.$container.on('input trigger-change', inputSelector, (e) => {
- // Always make sure there is an empty last row
- const $lastRow = this.$container.find('.js-row').last();
-
- if (this.checkIfRowTouched($lastRow)) {
- this.insertRow($lastRow);
- }
-
- // If masked, validate value against regex
- this.validateMaskability($(e.currentTarget).closest('.js-row'));
- });
- }
-
- initRow(rowEl) {
- const $row = $(rowEl);
-
- // Reset the resizable textarea
- $row.find(this.inputMap.secret_value.selector).css('height', '');
-
- const $environmentSelect = $row.find('.js-variable-environment-toggle');
- if ($environmentSelect.length) {
- const createItemDropdown = new CreateItemDropdown({
- $dropdown: $environmentSelect,
- defaultToggleLabel: ALL_ENVIRONMENTS_STRING,
- fieldName: `${this.formField}[variables_attributes][][environment_scope]`,
- getData: (term, callback) => callback(this.getEnvironmentValues()),
- createNewItemFromValue: createEnvironmentItem,
- onSelect: () => {
- // Refresh the other dropdowns in the variable list
- // so they have the new value we just picked
- this.refreshDropdownData();
-
- $row.find(this.inputMap.environment_scope.selector).trigger('trigger-change');
- },
- });
-
- // Clear out any data that might have been left-over from the row clone
- createItemDropdown.clearDropdown();
-
- this.environmentDropdownMap.set($row[0], createItemDropdown);
- }
- }
-
- insertRow($row) {
- const $rowClone = $row.clone();
- $rowClone.removeAttr('data-is-persisted');
-
- // Reset the inputs to their defaults
- Object.keys(this.inputMap).forEach((name) => {
- const entry = this.inputMap[name];
- $rowClone.find(entry.selector).val(entry.default);
- });
-
- // Close any dropdowns
- $rowClone.find('.dropdown-menu.show').each((index, $dropdown) => {
- $dropdown.classList.remove('show');
- });
-
- this.initRow($rowClone);
-
- $row.after($rowClone);
- }
-
- removeRow(row) {
- const $row = $(row);
- const isPersisted = parseBoolean($row.attr('data-is-persisted'));
-
- if (isPersisted) {
- $row.hide();
- $row
- // eslint-disable-next-line no-underscore-dangle
- .find(this.inputMap._destroy.selector)
- .val(true);
- } else {
- $row.remove();
- }
-
- // Refresh the other dropdowns in the variable list
- // so any value with the variable deleted is gone
- this.refreshDropdownData();
- }
-
- checkIfRowTouched($row) {
- return Object.keys(this.inputMap).some((name) => {
- // Row should not qualify as touched if only switches have been touched
- if (['protected', 'masked'].includes(name)) return false;
-
- const entry = this.inputMap[name];
- const $el = $row.find(entry.selector);
- return $el.length && $el.val() !== entry.default;
- });
- }
-
- validateMaskability($row) {
- const invalidInputClass = 'gl-field-error-outline';
-
- const variableValue = $row.find(this.inputMap.secret_value.selector).val();
- const isValueMaskable = this.maskableRegex.test(variableValue) || variableValue === '';
- const isMaskedChecked = $row.find(this.inputMap.masked.selector).val() === 'true';
-
- // Show a validation error if the user wants to mask an unmaskable variable value
- $row
- .find(this.inputMap.secret_value.selector)
- .toggleClass(invalidInputClass, isMaskedChecked && !isValueMaskable);
- $row
- .find('.js-secret-value-placeholder')
- .toggleClass(invalidInputClass, isMaskedChecked && !isValueMaskable);
- $row.find('.masking-validation-error').toggle(isMaskedChecked && !isValueMaskable);
- }
-
- toggleEnableRow(isEnabled = true) {
- this.$container.find(this.inputMap.key.selector).attr('disabled', !isEnabled);
- this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled);
- }
-
- hideValues() {
- this.secretValues.updateDom(false);
- }
-
- getAllData() {
- // Ignore the last empty row because we don't want to try persist
- // a blank variable and run into validation problems.
- const validRows = this.$container.find('.js-row').toArray().slice(0, -1);
-
- return validRows.map((rowEl) => {
- const resultant = {};
- Object.keys(this.inputMap).forEach((name) => {
- const entry = this.inputMap[name];
- const $input = $(rowEl).find(entry.selector);
- if ($input.length) {
- resultant[name] = $input.val();
- }
- });
-
- return resultant;
- });
- }
-
- getEnvironmentValues() {
- const valueMap = this.$container
- .find(this.inputMap.environment_scope.selector)
- .toArray()
- .reduce(
- (prevValueMap, envInput) => ({
- ...prevValueMap,
- [envInput.value]: envInput.value,
- }),
- {},
- );
-
- return Object.keys(valueMap).map(createEnvironmentItem);
- }
-
- refreshDropdownData() {
- this.$container.find('.js-row').each((index, rowEl) => {
- const environmentDropdown = this.environmentDropdownMap.get(rowEl);
- if (environmentDropdown) {
- environmentDropdown.refreshData();
- }
- });
- }
-}
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 0ce11da658c..c609e05bbb7 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
@@ -1,10 +1,12 @@
<script>
import {
+ GlAlert,
GlButton,
GlDrawer,
GlFormCheckbox,
GlFormCombobox,
GlFormGroup,
+ GlFormInput,
GlFormSelect,
GlFormTextarea,
GlIcon,
@@ -15,9 +17,14 @@ import { __, s__ } from '~/locale';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { helpPagePath } from '~/helpers/help_page_helper';
+import Tracking from '~/tracking';
import {
+ allEnvironments,
defaultVariableState,
+ DRAWER_EVENT_LABEL,
+ EDIT_VARIABLE_ACTION,
ENVIRONMENT_SCOPE_LINK_TITLE,
+ EVENT_ACTION,
EXPANDED_VARIABLES_NOTE,
FLAG_LINK_TITLE,
VARIABLE_ACTIONS,
@@ -26,9 +33,13 @@ import {
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
import { awsTokenList } from './ci_variable_autocomplete_tokens';
-const i18n = {
+const trackingMixin = Tracking.mixin({ label: DRAWER_EVENT_LABEL });
+
+export const i18n = {
addVariable: s__('CiVariables|Add Variable'),
cancel: __('Cancel'),
+ defaultScope: allEnvironments.text,
+ editVariable: s__('CiVariables|Edit Variable'),
environments: __('Environments'),
environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
expandedField: s__('CiVariables|Expand variable reference'),
@@ -44,39 +55,60 @@ const i18n = {
protectedDescription: s__(
'CiVariables|Export variable to pipelines running on protected branches and tags only.',
),
+ valueFeedback: {
+ rawHelpText: s__('CiVariables|Variable value will be evaluated as raw string.'),
+ maskedReqsNotMet: s__(
+ 'CiVariables|This variable value does not meet the masking requirements.',
+ ),
+ },
+ variableReferenceTitle: s__('CiVariables|Value might contain a variable reference'),
+ variableReferenceDescription: s__(
+ 'CiVariables|Unselect "Expand variable reference" if you want to use the variable value as a raw string.',
+ ),
type: __('Type'),
value: __('Value'),
};
+const VARIABLE_REFERENCE_REGEX = /\$/;
+
export default {
DRAWER_Z_INDEX,
components: {
CiEnvironmentsDropdown,
+ GlAlert,
GlButton,
GlDrawer,
GlFormCheckbox,
GlFormCombobox,
GlFormGroup,
+ GlFormInput,
GlFormSelect,
GlFormTextarea,
GlIcon,
GlLink,
GlSprintf,
},
- inject: ['environmentScopeLink'],
+ mixins: [trackingMixin],
+ inject: ['environmentScopeLink', 'isProtectedByDefault', 'maskableRawRegex', 'maskableRegex'],
props: {
areEnvironmentsLoading: {
type: Boolean,
required: true,
},
+ areScopedVariablesAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
environments: {
type: Array,
required: false,
default: () => [],
},
- hasEnvScopeQuery: {
+ hideEnvironmentScope: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
mode: {
type: String,
@@ -85,22 +117,107 @@ export default {
return VARIABLE_ACTIONS.includes(val);
},
},
+ selectedVariable: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
},
data() {
return {
- key: defaultVariableState.key,
- variableType: defaultVariableState.variableType,
+ variable: { ...defaultVariableState, ...this.selectedVariable },
+ trackedValidationErrorProperty: undefined,
};
},
computed: {
+ isValueMaskable() {
+ return this.variable.masked && !this.isValueMasked;
+ },
+ isValueMasked() {
+ const regex = RegExp(this.maskedRegexToUse);
+ return regex.test(this.variable.value);
+ },
+ canSubmit() {
+ return this.variable.key.length > 0 && this.isValueValid;
+ },
getDrawerHeaderHeight() {
return getContentWrapperHeight();
},
+ hasVariableReference() {
+ return this.isExpanded && VARIABLE_REFERENCE_REGEX.test(this.variable.value);
+ },
+ isExpanded() {
+ return !this.variable.raw;
+ },
+ isMaskedReqsMet() {
+ return !this.variable.masked || this.isValueMasked;
+ },
+ isValueEmpty() {
+ return this.variable.value === '';
+ },
+ isValueValid() {
+ return this.isValueEmpty || this.isMaskedReqsMet;
+ },
+ isEditing() {
+ return this.mode === EDIT_VARIABLE_ACTION;
+ },
+ maskedRegexToUse() {
+ return this.variable.raw ? this.maskableRawRegex : this.maskableRegex;
+ },
+ maskedReqsNotMetText() {
+ return !this.isMaskedReqsMet ? this.$options.i18n.valueFeedback.maskedReqsNotMet : '';
+ },
+ modalActionText() {
+ return this.isEditing ? this.$options.i18n.editVariable : this.$options.i18n.addVariable;
+ },
+ },
+ watch: {
+ variable: {
+ handler() {
+ this.trackVariableValidationErrors();
+ },
+ deep: true,
+ },
+ },
+ mounted() {
+ if (this.isProtectedByDefault && !this.isEditing) {
+ this.variable = { ...this.variable, protected: true };
+ }
},
methods: {
close() {
this.$emit('close-form');
},
+ getTrackingErrorProperty() {
+ if (this.isValueEmpty) {
+ return null;
+ }
+
+ let property;
+ if (this.isValueMaskable) {
+ const supportedChars = this.maskedRegexToUse.replace('^', '').replace(/{(\d,)}\$/, '');
+ const regex = new RegExp(supportedChars, 'g');
+ property = this.variable.value.replace(regex, '');
+ } else if (this.hasVariableReference) {
+ property = '$';
+ }
+
+ return property;
+ },
+ setRaw(expanded) {
+ this.variable = { ...this.variable, raw: !expanded };
+ },
+ submit() {
+ this.$emit(this.isEditing ? 'update-variable' : 'add-variable', this.variable);
+ this.close();
+ },
+ trackVariableValidationErrors() {
+ const property = this.getTrackingErrorProperty();
+ if (property && !this.trackedValidationErrorProperty) {
+ this.track(EVENT_ACTION, { property });
+ this.trackedValidationErrorProperty = property;
+ }
+ },
},
awsTokenList,
flagLink: helpPagePath('ci/variables/index', {
@@ -119,20 +236,25 @@ export default {
@close="close"
>
<template #title>
- <h2 class="gl-m-0">{{ $options.i18n.addVariable }}</h2>
+ <h2 class="gl-m-0">{{ modalActionText }}</h2>
</template>
<gl-form-group
:label="$options.i18n.type"
label-for="ci-variable-type"
- class="gl-border-none gl-mb-n5"
+ class="gl-border-none"
+ :class="{
+ 'gl-mb-n5': !hideEnvironmentScope,
+ 'gl-mb-n1': hideEnvironmentScope,
+ }"
>
<gl-form-select
id="ci-variable-type"
- v-model="variableType"
+ 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"
@@ -154,11 +276,18 @@ export default {
</div>
</template>
<ci-environments-dropdown
+ v-if="areScopedVariablesAvailable"
class="gl-mb-5"
+ has-env-scope-query
:are-environments-loading="areEnvironmentsLoading"
:environments="environments"
- :has-env-scope-query="hasEnvScopeQuery"
- selected-environment-scope=""
+ :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>
<gl-form-group class="gl-border-none gl-mb-n8">
@@ -177,17 +306,21 @@ export default {
</gl-link>
</div>
</template>
- <gl-form-checkbox data-testid="ci-variable-protected-checkbox">
+ <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 data-testid="ci-variable-masked-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">
+ <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">
@@ -199,34 +332,56 @@ export default {
</gl-form-checkbox>
</gl-form-group>
<gl-form-combobox
- v-model="key"
+ 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="pipeline-form-ci-variable-key"
+ 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="pipeline-form-ci-variable-value"
+ 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"
+ >
+ {{ $options.i18n.variableReferenceDescription }}
+ </gl-alert>
<div class="gl-display-flex gl-justify-content-end">
- <gl-button category="primary" class="gl-mr-3" data-testid="cancel-button" @click="close"
+ <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" data-testid="confirm-button"
- >{{ $options.i18n.addVariable }}
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :disabled="!canSubmit"
+ data-testid="ci-variable-confirm-btn"
+ @click="submit"
+ >{{ modalActionText }}
</gl-button>
</div>
</gl-drawer>
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 f4e1da9b34f..482f6da5617 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
@@ -139,7 +139,10 @@ export default {
<ci-variable-drawer
v-if="showDrawer"
:are-environments-loading="areEnvironmentsLoading"
- :has-env-scope-query="hasEnvScopeQuery"
+ :are-scoped-variables-available="areScopedVariablesAvailable"
+ :environments="environments"
+ :hide-environment-scope="hideEnvironmentScope"
+ :selected-variable="selectedVariable"
:mode="mode"
v-on="$listeners"
@close-form="closeForm"
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 9786f25ed87..3d5ed327dc7 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,8 @@
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { mapEnvironmentNames, reportMessageToSentry } from '../utils';
+import { reportMessageToSentry } from '~/ci/utils';
+import { mapEnvironmentNames } from '../utils';
import {
ADD_MUTATION_ACTION,
DELETE_MUTATION_ACTION,
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
index a14cd1e387a..3d62313815c 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
@@ -32,24 +32,20 @@ export default {
label: s__('CiVariables|Key'),
tdClass: 'text-plain',
sortable: true,
+ thClass: 'gl-w-40p',
},
{
key: 'value',
label: s__('CiVariables|Value'),
},
{
- key: 'Attributes',
- label: s__('CiVariables|Attributes'),
- thClass: 'gl-w-40p',
- },
- {
key: 'environmentScope',
label: s__('CiVariables|Environments'),
},
{
key: 'actions',
label: __('Actions'),
- thClass: 'gl-text-right',
+ thClass: 'gl-text-right gl-w-15',
},
],
inheritedVarsFields: [
@@ -287,7 +283,7 @@ export default {
</template>
<template #cell(key)="{ item }">
<div
- class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3"
+ class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-md-justify-content-start gl-mr-n3"
>
<span
:id="`ci-variable-key-${item.id}`"
@@ -298,16 +294,28 @@ export default {
v-gl-tooltip
category="tertiary"
icon="copy-to-clipboard"
- class="gl-my-n3 gl-ml-2"
+ class="gl-my-n2 gl-ml-2"
+ size="small"
:title="__('Copy key')"
:data-clipboard-text="item.key"
:aria-label="__('Copy to clipboard')"
/>
</div>
+ <div data-testid="ci-variable-table-row-attributes" class="gl-mt-2">
+ <gl-badge
+ v-for="attribute in item.attributes"
+ :key="`${item.key}-${attribute}`"
+ class="gl-mr-2"
+ variant="info"
+ size="sm"
+ >
+ {{ attribute }}
+ </gl-badge>
+ </div>
</template>
<template v-if="!isInheritedGroupVars" #cell(value)="{ item }">
<div
- class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3"
+ class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-md-justify-content-start gl-mr-n3"
>
<span v-if="areValuesHidden" data-testid="hiddenValue">*****</span>
<span
@@ -321,29 +329,17 @@ export default {
v-gl-tooltip
category="tertiary"
icon="copy-to-clipboard"
- class="gl-my-n3 gl-ml-2"
+ class="gl-my-n2 gl-ml-2"
+ size="small"
:title="__('Copy value')"
:data-clipboard-text="item.value"
:aria-label="__('Copy to clipboard')"
/>
</div>
</template>
- <template #cell(attributes)="{ item }">
- <span data-testid="ci-variable-table-row-attributes">
- <gl-badge
- v-for="attribute in item.attributes"
- :key="`${item.key}-${attribute}`"
- class="gl-mr-2"
- variant="info"
- size="sm"
- >
- {{ attribute }}
- </gl-badge>
- </span>
- </template>
<template #cell(environmentScope)="{ item }">
<div
- class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3"
+ class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-md-justify-content-start gl-mr-n3"
>
<span
:id="`ci-variable-env-${item.id}`"
@@ -354,7 +350,8 @@ export default {
v-gl-tooltip
category="tertiary"
icon="copy-to-clipboard"
- class="gl-my-n3 gl-ml-2"
+ class="gl-my-n2 gl-ml-2"
+ size="small"
:title="__('Copy environment')"
:data-clipboard-text="convertEnvironmentScopeValue(item.environmentScope)"
:aria-label="__('Copy to clipboard')"
@@ -363,7 +360,7 @@ export default {
</template>
<template v-if="isInheritedGroupVars" #cell(group)="{ item }">
<div
- class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3"
+ class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-md-justify-content-start gl-mr-n3"
>
<gl-link
:id="`ci-variable-group-${item.id}`"
diff --git a/app/assets/javascripts/ci/ci_variable_list/constants.js b/app/assets/javascripts/ci/ci_variable_list/constants.js
index 825b39e0cf9..fc37b62299d 100644
--- a/app/assets/javascripts/ci/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci/ci_variable_list/constants.js
@@ -46,6 +46,7 @@ export const AWS_TIP_MESSAGE = s__(
);
export const EVENT_LABEL = 'ci_variable_modal';
+export const DRAWER_EVENT_LABEL = 'ci_variable_drawer';
export const EVENT_ACTION = 'validation_error';
// AWS TOKEN CONSTANTS
diff --git a/app/assets/javascripts/ci/ci_variable_list/native_form_variable_list.js b/app/assets/javascripts/ci/ci_variable_list/native_form_variable_list.js
deleted file mode 100644
index fdbefd8c313..00000000000
--- a/app/assets/javascripts/ci/ci_variable_list/native_form_variable_list.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import $ from 'jquery';
-import VariableList from './ci_variable_list';
-
-// Used for the variable list on scheduled pipeline edit page
-export default function setupNativeFormVariableList({ container, formField = 'variables' }) {
- const $container = $(container);
-
- const variableList = new VariableList({
- container: $container,
- formField,
- });
- variableList.init();
-
- // Clear out the names in the empty last row so it
- // doesn't get submitted and throw validation errors
- $container.closest('form').on('submit trigger-submit', () => {
- const $lastRow = $container.find('.js-row').last();
-
- const isTouched = variableList.checkIfRowTouched($lastRow);
- if (!isTouched) {
- $lastRow.find('input, textarea').attr('name', '');
- $lastRow.find('select').attr('name', '');
- }
- });
-}
diff --git a/app/assets/javascripts/ci/ci_variable_list/utils.js b/app/assets/javascripts/ci/ci_variable_list/utils.js
index eeca69274ce..1faa97a5f73 100644
--- a/app/assets/javascripts/ci/ci_variable_list/utils.js
+++ b/app/assets/javascripts/ci/ci_variable_list/utils.js
@@ -1,4 +1,3 @@
-import * as Sentry from '@sentry/browser';
import { uniq } from 'lodash';
import { allEnvironments } from './constants';
@@ -49,12 +48,3 @@ export const convertEnvironmentScope = (environmentScope = '') => {
export const mapEnvironmentNames = (nodes = []) => {
return nodes.map((env) => env.name);
};
-
-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);
- });
-};
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/ci/common/pipelines_table.vue
index c03085e6419..807128d2341 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
+++ b/app/assets/javascripts/ci/common/pipelines_table.vue
@@ -4,16 +4,16 @@ 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 { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
-import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
-import PipelineFailedJobsWidget from '~/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue';
-import eventHub from '../../event_hub';
-import { TRACKING_CATEGORIES } from '../../constants';
-import PipelineOperations from './pipeline_operations.vue';
-import PipelineStopModal from './pipeline_stop_modal.vue';
-import PipelineTriggerer from './pipeline_triggerer.vue';
-import PipelineUrl from './pipeline_url.vue';
-import PipelinesStatusBadge from './pipelines_status_badge.vue';
+import { 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';
const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!';
const DEFAULT_TH_CLASSES =
@@ -95,10 +95,10 @@ export default {
},
{
key: 'triggerer',
- label: s__('Pipeline|Triggerer'),
+ label: s__('Pipeline|Created by'),
thClass: DEFAULT_TH_CLASSES,
tdClass: `${this.tdClasses} ${HIDE_TD_ON_MOBILE}`,
- columnClass: 'gl-w-10p',
+ columnClass: 'gl-w-15p',
thAttr: { 'data-testid': 'triggerer-th' },
},
{
@@ -113,7 +113,7 @@ export default {
key: 'actions',
thClass: DEFAULT_TH_CLASSES,
tdClass: this.tdClasses,
- columnClass: 'gl-w-15p',
+ columnClass: 'gl-w-20p',
thAttr: { 'data-testid': 'actions-th' },
},
];
diff --git a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue b/app/assets/javascripts/ci/common/private/job_action_component.vue
index ffb6ab71b22..f649750ce8a 100644
--- a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
+++ b/app/assets/javascripts/ci/common/private/job_action_component.vue
@@ -5,7 +5,7 @@ import axios from '~/lib/utils/axios_utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { dasherize } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
-import { reportToSentry } from '../../utils';
+import { reportToSentry } from '~/ci/utils';
/**
* Renders either a cancel, retry or play icon button and handles the post request
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue b/app/assets/javascripts/ci/common/private/job_links_layer.vue
index ef24694e494..59260ca3f81 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
+++ b/app/assets/javascripts/ci/common/private/job_links_layer.vue
@@ -1,8 +1,8 @@
<script>
import { memoize } from 'lodash';
-import { reportToSentry } from '../../utils';
-import { parseData } from '../parsing_utils';
-import LinksInner from './links_inner.vue';
+import { reportToSentry } from '~/ci/utils';
+import { parseData } from '~/ci/pipeline_details/utils/parsing_utils';
+import LinksInner from '~/ci/pipeline_details/graph/components/links_inner.vue';
const parseForLinksBare = (pipeline) => {
const arrayOfJobs = pipeline.flatMap(({ groups }) => groups);
diff --git a/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue b/app/assets/javascripts/ci/common/private/job_name_component.vue
index 1c7f5a7476d..1c7f5a7476d 100644
--- a/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue
+++ b/app/assets/javascripts/ci/common/private/job_name_component.vue
diff --git a/app/assets/javascripts/ci/common/private/jobs_filtered_search/app.vue b/app/assets/javascripts/ci/common/private/jobs_filtered_search/app.vue
new file mode 100644
index 00000000000..86ccdb2c87b
--- /dev/null
+++ b/app/assets/javascripts/ci/common/private/jobs_filtered_search/app.vue
@@ -0,0 +1,99 @@
+<script>
+import { GlFilteredSearch } from '@gitlab/ui';
+import {
+ OPERATOR_IS,
+ OPERATORS_IS,
+ TOKEN_TITLE_STATUS,
+ TOKEN_TYPE_STATUS,
+ TOKEN_TITLE_JOBS_RUNNER_TYPE,
+ TOKEN_TYPE_JOBS_RUNNER_TYPE,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import JobStatusToken from './tokens/job_status_token.vue';
+import JobRunnerTypeToken from './tokens/job_runner_type_token.vue';
+
+export default {
+ components: {
+ GlFilteredSearch,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ props: {
+ queryString: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ tokens() {
+ const tokens = [
+ {
+ type: TOKEN_TYPE_STATUS,
+ icon: 'status',
+ title: TOKEN_TITLE_STATUS,
+ unique: true,
+ token: JobStatusToken,
+ operators: OPERATORS_IS,
+ },
+ ];
+
+ if (this.glFeatures.adminJobsFilterRunnerType) {
+ tokens.push({
+ type: TOKEN_TYPE_JOBS_RUNNER_TYPE,
+ title: TOKEN_TITLE_JOBS_RUNNER_TYPE,
+ unique: true,
+ token: JobRunnerTypeToken,
+ operators: OPERATORS_IS,
+ });
+ }
+
+ return tokens;
+ },
+ filteredSearchValue() {
+ return Object.entries(this.queryString || {}).reduce(
+ (acc, [queryStringKey, queryStringValue]) => {
+ switch (queryStringKey) {
+ case 'statuses':
+ return [
+ ...acc,
+ {
+ type: TOKEN_TYPE_STATUS,
+ value: { data: queryStringValue, operator: OPERATOR_IS },
+ },
+ ];
+ case 'runnerTypes':
+ if (!this.glFeatures.adminJobsFilterRunnerType) {
+ return acc;
+ }
+
+ return [
+ ...acc,
+ {
+ type: TOKEN_TYPE_JOBS_RUNNER_TYPE,
+ value: { data: queryStringValue, operator: OPERATOR_IS },
+ },
+ ];
+ default:
+ return acc;
+ }
+ },
+ [],
+ );
+ },
+ },
+ methods: {
+ onSubmit(filters) {
+ this.$emit('filterJobsBySearch', filters);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search
+ :placeholder="s__('Jobs|Filter jobs')"
+ :available-tokens="tokens"
+ :value="filteredSearchValue"
+ @submit="onSubmit"
+ />
+</template>
diff --git a/app/assets/javascripts/ci/common/private/jobs_filtered_search/constants.js b/app/assets/javascripts/ci/common/private/jobs_filtered_search/constants.js
new file mode 100644
index 00000000000..86b8290864c
--- /dev/null
+++ b/app/assets/javascripts/ci/common/private/jobs_filtered_search/constants.js
@@ -0,0 +1,23 @@
+export const jobStatusValues = [
+ 'CANCELED',
+ 'CREATED',
+ 'FAILED',
+ 'MANUAL',
+ 'SUCCESS',
+ 'PENDING',
+ 'PREPARING',
+ 'RUNNING',
+ 'SCHEDULED',
+ 'SKIPPED',
+ 'WAITING_FOR_RESOURCE',
+];
+
+export const JOB_RUNNER_TYPE_INSTANCE_TYPE = 'INSTANCE_TYPE';
+export const JOB_RUNNER_TYPE_GROUP_TYPE = 'GROUP_TYPE';
+export const JOB_RUNNER_TYPE_PROJECT_TYPE = 'PROJECT_TYPE';
+
+export const jobRunnerTypeValues = [
+ JOB_RUNNER_TYPE_INSTANCE_TYPE,
+ JOB_RUNNER_TYPE_GROUP_TYPE,
+ JOB_RUNNER_TYPE_PROJECT_TYPE,
+];
diff --git a/app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_runner_type_token.vue b/app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_runner_type_token.vue
new file mode 100644
index 00000000000..5bd3693b4d9
--- /dev/null
+++ b/app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_runner_type_token.vue
@@ -0,0 +1,79 @@
+<script>
+import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import {
+ JOB_RUNNER_TYPE_INSTANCE_TYPE,
+ JOB_RUNNER_TYPE_GROUP_TYPE,
+ JOB_RUNNER_TYPE_PROJECT_TYPE,
+} from '../constants';
+
+export default {
+ components: {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlIcon,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ runnerTypes() {
+ return [
+ {
+ class: 'ci-runner-runner-type-instance',
+ icon: 'users',
+ text: s__('Runners|Instance'),
+ value: JOB_RUNNER_TYPE_INSTANCE_TYPE,
+ },
+ {
+ class: 'ci-runner-runner-type-group',
+ icon: 'group',
+ text: s__('Runners|Group'),
+ value: JOB_RUNNER_TYPE_GROUP_TYPE,
+ },
+ {
+ class: 'ci-runner-runner-type-project',
+ icon: 'project',
+ text: s__('Runners|Project'),
+ value: JOB_RUNNER_TYPE_PROJECT_TYPE,
+ },
+ ];
+ },
+ findActiveRunnerType() {
+ return this.runnerTypes.find((runnerType) => runnerType.value === this.value.data);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
+ <template #view>
+ <div class="gl-display-flex gl-align-items-center">
+ <div :class="findActiveRunnerType.class">
+ <gl-icon :name="findActiveRunnerType.icon" class="gl-mr-2 gl-display-block" />
+ </div>
+ <span>{{ findActiveRunnerType.text }}</span>
+ </div>
+ </template>
+ <template #suggestions>
+ <gl-filtered-search-suggestion
+ v-for="(runnerType, index) in runnerTypes"
+ :key="index"
+ :value="runnerType.value"
+ >
+ <div class="gl-display-flex" :class="runnerType.class">
+ <gl-icon :name="runnerType.icon" class="gl-mr-3" />
+ <span>{{ runnerType.text }}</span>
+ </div>
+ </gl-filtered-search-suggestion>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/javascripts/jobs/components/filtered_search/tokens/job_status_token.vue b/app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_status_token.vue
index aad86ded80a..aad86ded80a 100644
--- a/app/assets/javascripts/jobs/components/filtered_search/tokens/job_status_token.vue
+++ b/app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_status_token.vue
diff --git a/app/assets/javascripts/ci/common/private/jobs_filtered_search/utils.js b/app/assets/javascripts/ci/common/private/jobs_filtered_search/utils.js
new file mode 100644
index 00000000000..43c0da72d3d
--- /dev/null
+++ b/app/assets/javascripts/ci/common/private/jobs_filtered_search/utils.js
@@ -0,0 +1,22 @@
+import { jobStatusValues, jobRunnerTypeValues } from './constants';
+
+// validates query string used for filtered search
+// on jobs table to ensure GraphQL query is called correctly
+export const validateQueryString = (queryStringObj) => {
+ return Object.entries(queryStringObj).reduce((acc, [queryStringKey, queryStringValue]) => {
+ switch (queryStringKey) {
+ case 'statuses': {
+ const statusValue = queryStringValue.toUpperCase();
+ const statusValueValid = jobStatusValues.includes(statusValue);
+ return statusValueValid ? { ...acc, statuses: statusValue } : acc;
+ }
+ case 'runnerTypes': {
+ const runnerTypesValue = queryStringValue.toUpperCase();
+ const runnerTypesValueValid = jobRunnerTypeValues.includes(runnerTypesValue);
+ return runnerTypesValueValid ? { ...acc, runnerTypes: runnerTypesValue } : acc;
+ }
+ default:
+ return acc;
+ }
+ }, null);
+};
diff --git a/app/assets/javascripts/ci/constants.js b/app/assets/javascripts/ci/constants.js
new file mode 100644
index 00000000000..93c2504dd5d
--- /dev/null
+++ b/app/assets/javascripts/ci/constants.js
@@ -0,0 +1,51 @@
+import { __, s__ } from '~/locale';
+
+export const forwardDeploymentFailureModalId = 'forward-deployment-failure';
+
+export const BUTTON_TOOLTIP_RETRY = __('Retry all failed or cancelled jobs');
+export const BUTTON_TOOLTIP_CANCEL = __('Cancel the running pipeline');
+
+export const FILTER_TAG_IDENTIFIER = 'tag';
+
+export const JOB_GRAPHQL_ERRORS = {
+ jobMutationErrorText: __('There was an error running the job. Please try again.'),
+ jobQueryErrorText: __('There was an error fetching the job.'),
+};
+
+export const ICONS = {
+ TAG: 'tag',
+ MR: 'git-merge',
+ BRANCH: 'branch',
+ RETRY: 'retry',
+ SUCCESS: 'success',
+};
+
+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 RAW_TEXT_WARNING = s__(
+ 'Pipeline|Raw text search is not currently supported. Please use the available search tokens.',
+);
+
+export const TRACKING_CATEGORIES = {
+ table: 'pipelines_table_component',
+ tabs: 'pipelines_filter_tabs',
+ search: 'pipelines_filtered_search',
+ failed: 'pipeline_failed_jobs_tab',
+ tests: 'pipeline_tests_tab',
+};
diff --git a/app/assets/javascripts/jobs/components/table/event_hub.js b/app/assets/javascripts/ci/event_hub.js
index e31806ad199..e31806ad199 100644
--- a/app/assets/javascripts/jobs/components/table/event_hub.js
+++ b/app/assets/javascripts/ci/event_hub.js
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 27ee1b794f6..f02d59af1d9 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/ci_variable_list/utils';
+import { reportMessageToSentry } 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';
diff --git a/app/assets/javascripts/jobs/components/job/empty_state.vue b/app/assets/javascripts/ci/job_details/components/empty_state.vue
index d0a39025807..5756d4a71df 100644
--- a/app/assets/javascripts/jobs/components/job/empty_state.vue
+++ b/app/assets/javascripts/ci/job_details/components/empty_state.vue
@@ -1,6 +1,6 @@
<script>
import { GlLink } from '@gitlab/ui';
-import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue';
+import ManualVariablesForm from '~/ci/job_details/components/manual_variables_form.vue';
export default {
components: {
diff --git a/app/assets/javascripts/jobs/components/job/environments_block.vue b/app/assets/javascripts/ci/job_details/components/environments_block.vue
index 4046e1ade82..4046e1ade82 100644
--- a/app/assets/javascripts/jobs/components/job/environments_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/environments_block.vue
diff --git a/app/assets/javascripts/jobs/components/job/erased_block.vue b/app/assets/javascripts/ci/job_details/components/erased_block.vue
index a815689659e..a815689659e 100644
--- a/app/assets/javascripts/jobs/components/job/erased_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/erased_block.vue
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/ci/job_details/components/job_header.vue
index 1adda905006..13f3eebd447 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/ci/job_details/components/job_header.vue
@@ -4,16 +4,9 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { glEmojiTag } from '~/emoji';
import { __, sprintf } from '~/locale';
-import CiBadgeLink from './ci_badge_link.vue';
-import TimeagoTooltip from './time_ago_tooltip.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-/**
- * Renders header component for job and pipeline page based on UI mockups
- *
- * Used in:
- * - job show page
- * - pipeline show page
- */
export default {
components: {
CiBadgeLink,
@@ -33,33 +26,21 @@ export default {
type: Object,
required: true,
},
- itemName: {
+ name: {
type: String,
required: true,
},
- itemId: {
- type: String,
- required: false,
- default: '',
- },
time: {
type: String,
required: true,
},
user: {
type: Object,
- required: false,
- default: () => ({}),
- },
- hasSidebarButton: {
- type: Boolean,
- required: false,
- default: false,
+ required: true,
},
shouldRenderTriggeredLabel: {
type: Boolean,
- required: false,
- default: true,
+ required: true,
},
},
@@ -92,13 +73,6 @@ export default {
message() {
return this.user?.status?.message;
},
- item() {
- if (this.itemId) {
- return `${this.itemName} #${this.itemId}`;
- }
-
- return this.itemName;
- },
userId() {
return isGid(this.user?.id) ? getIdFromGraphQLId(this.user?.id) : this.user?.id;
},
@@ -114,13 +88,16 @@ export default {
</script>
<template>
- <header class="page-content-header gl-md-display-flex gl-min-h-7" data-testid="ci-header-content">
+ <header
+ class="page-content-header gl-md-display-flex gl-min-h-7"
+ data-testid="job-header-content"
+ >
<section class="header-main-content gl-mr-3">
<ci-badge-link class="gl-mr-3" :status="status" />
- <strong data-testid="ci-header-item-text">{{ item }}</strong>
+ <strong data-testid="job-name">{{ name }}</strong>
- <template v-if="shouldRenderTriggeredLabel">{{ __('triggered') }}</template>
+ <template v-if="shouldRenderTriggeredLabel">{{ __('started') }}</template>
<template v-else>{{ __('created') }}</template>
<timeago-tooltip :time="time" />
@@ -158,11 +135,10 @@ export default {
</section>
<!-- eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots -->
- <section v-if="$slots.default" data-testid="ci-header-action-buttons" class="gl-display-flex">
+ <section v-if="$slots.default" data-testid="job-header-action-buttons" class="gl-display-flex">
<slot></slot>
</section>
<gl-button
- v-if="hasSidebarButton"
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')"
diff --git a/app/assets/javascripts/jobs/components/job/job_log_controllers.vue b/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue
index efd4eed2a9f..419efcba46d 100644
--- a/app/assets/javascripts/jobs/components/job/job_log_controllers.vue
+++ b/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue
@@ -3,6 +3,7 @@ import { GlTooltipDirective, GlLink, GlButton, GlSearchBoxByClick } from '@gitla
import { scrollToElement, backOff } from '~/lib/utils/common_utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __, s__, sprintf } from '~/locale';
+import { compactJobLog } from '~/ci/job_details/utils';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -130,17 +131,7 @@ export default {
if (!this.searchTerm) return;
- const compactedLog = [];
-
- this.jobLog.forEach((obj) => {
- if (obj.lines && obj.lines.length > 0) {
- compactedLog.push(...obj.lines);
- }
-
- if (!obj.lines && obj.content.length > 0) {
- compactedLog.push(obj);
- }
- });
+ const compactedLog = compactJobLog(this.jobLog);
compactedLog.forEach((line) => {
const lineText = line.content[0].text;
@@ -155,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(`.js-line ${firstSearchResult}`);
+ const logLine = document.querySelector(`.log-line ${firstSearchResult}`);
if (logLine) {
setTimeout(() => scrollToElement(logLine));
@@ -224,7 +215,7 @@ export default {
:aria-label="$options.i18n.showRawButtonLabel"
:href="rawPath"
data-testid="job-raw-link-controller"
- icon="doc-text"
+ icon="doc-code"
/>
<!-- eo links -->
diff --git a/app/assets/javascripts/jobs/components/log/collapsible_section.vue b/app/assets/javascripts/ci/job_details/components/log/collapsible_section.vue
index 13716b4d391..39c612bc600 100644
--- a/app/assets/javascripts/jobs/components/log/collapsible_section.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/collapsible_section.vue
@@ -27,11 +27,24 @@ export default {
badgeDuration() {
return this.section.line && this.section.line.section_duration;
},
+ highlightedLines() {
+ return this.searchResults.map((result) => result.lineNumber);
+ },
+ headerIsHighlighted() {
+ const {
+ line: { lineNumber },
+ } = this.section;
+
+ return this.highlightedLines.includes(lineNumber);
+ },
},
methods: {
handleOnClickCollapsibleLine(section) {
this.$emit('onClickCollapsibleLine', section);
},
+ lineIsHighlighted({ lineNumber }) {
+ return this.highlightedLines.includes(lineNumber);
+ },
},
};
</script>
@@ -42,6 +55,7 @@ export default {
:duration="badgeDuration"
:path="jobLogEndpoint"
:is-closed="section.isClosed"
+ :is-highlighted="headerIsHighlighted"
@toggleLine="handleOnClickCollapsibleLine(section)"
/>
<template v-if="!section.isClosed">
@@ -50,7 +64,7 @@ export default {
:key="line.offset"
:line="line"
:path="jobLogEndpoint"
- :search-results="searchResults"
+ :is-highlighted="lineIsHighlighted(line)"
/>
</template>
</div>
diff --git a/app/assets/javascripts/jobs/components/log/duration_badge.vue b/app/assets/javascripts/ci/job_details/components/log/duration_badge.vue
index 54b76fd9edd..54b76fd9edd 100644
--- a/app/assets/javascripts/jobs/components/log/duration_badge.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/duration_badge.vue
diff --git a/app/assets/javascripts/jobs/components/log/line.vue b/app/assets/javascripts/ci/job_details/components/log/line.vue
index 3c9c5097122..fa4a12b3dd3 100644
--- a/app/assets/javascripts/jobs/components/log/line.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line.vue
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { getLocationHash } from '~/lib/utils/url_utility';
-import { linkRegex } from '../../utils';
+import { linkRegex } from './utils';
import LineNumber from './line_number.vue';
export default {
@@ -15,14 +15,14 @@ export default {
type: String,
required: true,
},
- searchResults: {
- type: Array,
+ isHighlighted: {
+ type: Boolean,
required: false,
- default: () => [],
+ default: false,
},
},
render(h, { props }) {
- const { line, path, searchResults } = props;
+ const { line, path, isHighlighted } = props;
const chars = line.content.map((content) => {
return h(
@@ -52,31 +52,21 @@ export default {
);
});
- let applyHighlight = false;
-
- if (searchResults.length > 0) {
- const linesToHighlight = searchResults.map((searchResultLine) => searchResultLine.lineNumber);
-
- linesToHighlight.forEach((num) => {
- if (num === line.lineNumber) {
- applyHighlight = true;
- }
- });
- }
+ let applyHashHighlight = false;
if (window.location.hash) {
const hash = getLocationHash();
const lineToMatch = `L${line.lineNumber + 1}`;
if (hash === lineToMatch) {
- applyHighlight = true;
+ applyHashHighlight = true;
}
}
return h(
'div',
{
- class: ['js-line', 'log-line', applyHighlight ? 'gl-bg-gray-700' : ''],
+ class: ['js-line', 'log-line', { 'gl-bg-gray-700': isHighlighted || applyHashHighlight }],
},
[
h(LineNumber, {
diff --git a/app/assets/javascripts/jobs/components/log/line_header.vue b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
index 115b090b32a..e647ab4ac0b 100644
--- a/app/assets/javascripts/jobs/components/log/line_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
@@ -28,17 +28,29 @@ export default {
required: false,
default: '',
},
+ isHighlighted: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ applyHashHighlight: false,
+ };
},
computed: {
iconName() {
return this.isClosed ? 'chevron-lg-right' : 'chevron-lg-down';
},
- applyHighlight() {
- const hash = getLocationHash();
- const lineToMatch = `L${this.line.lineNumber + 1}`;
+ },
+ mounted() {
+ const hash = getLocationHash();
+ const lineToMatch = `L${this.line.lineNumber + 1}`;
- return hash === lineToMatch;
- },
+ if (hash === lineToMatch) {
+ this.applyHashHighlight = true;
+ }
},
methods: {
handleOnClick() {
@@ -50,12 +62,12 @@ export default {
<template>
<div
- class="log-line collapsible-line d-flex justify-content-between ws-normal gl-align-items-flex-start"
- :class="{ 'gl-bg-gray-700': applyHighlight }"
+ class="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"
>
- <gl-icon :name="iconName" class="arrow position-absolute" />
+ <gl-icon :name="iconName" class="arrow gl-absolute gl-top-2" />
<line-number :line-number="line.lineNumber" :path="path" />
<span
v-for="(content, i) in line.content"
diff --git a/app/assets/javascripts/jobs/components/log/line_number.vue b/app/assets/javascripts/ci/job_details/components/log/line_number.vue
index 7ca9154d2fe..7ca9154d2fe 100644
--- a/app/assets/javascripts/jobs/components/log/line_number.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line_number.vue
diff --git a/app/assets/javascripts/jobs/components/log/log.vue b/app/assets/javascripts/ci/job_details/components/log/log.vue
index 6a1101bf297..fb6a6a58074 100644
--- a/app/assets/javascripts/jobs/components/log/log.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/log.vue
@@ -26,6 +26,9 @@ export default {
'isJobLogComplete',
'isScrolledToBottomBeforeReceivingJobLog',
]),
+ highlightedLines() {
+ return this.searchResults.map((result) => result.lineNumber);
+ },
},
updated() {
this.$nextTick(() => {
@@ -68,11 +71,14 @@ export default {
}, 0);
}
},
+ isHighlighted({ lineNumber }) {
+ return this.highlightedLines.includes(lineNumber);
+ },
},
};
</script>
<template>
- <code class="job-log d-block" data-qa-selector="job_log_content">
+ <code class="job-log d-block" data-testid="job-log-content">
<template v-for="(section, index) in jobLog">
<collapsible-log-section
v-if="section.isHeader"
@@ -87,7 +93,7 @@ export default {
:key="section.offset"
:line="section"
:path="jobLogEndpoint"
- :search-results="searchResults"
+ :is-highlighted="isHighlighted(section)"
/>
</template>
diff --git a/app/assets/javascripts/ci/job_details/components/log/utils.js b/app/assets/javascripts/ci/job_details/components/log/utils.js
new file mode 100644
index 00000000000..1ccecf3eb53
--- /dev/null
+++ b/app/assets/javascripts/ci/job_details/components/log/utils.js
@@ -0,0 +1,12 @@
+/**
+ * capture anything starting with http:// or https://
+ * https?:\/\/
+ *
+ * up until a disallowed character or whitespace
+ * [^"<>()\\^`{|}\s]+
+ *
+ * and a disallowed character or whitespace, including non-ending chars .,:;!?
+ * [^"<>()\\^`{|}\s.,:;!?]
+ */
+export const linkRegex = /(https?:\/\/[^"<>()\\^`{|}\s]+[^"<>()\\^`{|}\s.,:;!?])/g;
+export default { linkRegex };
diff --git a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue b/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue
index 356d65e1d14..1232ffffb57 100644
--- a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue
+++ b/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue
@@ -14,16 +14,16 @@ import { fetchPolicies } from '~/lib/graphql';
import { createAlert } from '~/alert';
import { TYPENAME_CI_BUILD, TYPENAME_COMMIT_STATUS } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { JOB_GRAPHQL_ERRORS } from '~/jobs/constants';
+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 '~/jobs/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';
+import { reportMessageToSentry } 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';
-// This component is a port of ~/jobs/components/job/legacy_manual_variables_form.vue
+// This component is a port of ~/ci/job_details/components/legacy_manual_variables_form.vue
// It is meant to fetch/update the job information via GraphQL instead of REST API.
export default {
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/artifacts_block.vue b/app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue
index a78cacf110f..4c81a9bd033 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/artifacts_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue
@@ -75,7 +75,7 @@ export default {
data-testid="artifacts-remove-timeline"
>
<span v-if="isExpired">{{ $options.i18n.expiredText }}</span>
- <span v-if="willExpire" data-qa-selector="artifacts_unlocked_message_content">
+ <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" />
@@ -89,7 +89,7 @@ export default {
</gl-link>
</p>
<p v-else-if="isLocked" class="build-detail-row">
- <span data-testid="job-locked-message" data-qa-selector="artifacts_locked_message_content">
+ <span data-testid="artifacts-locked-message-content">
{{ $options.i18n.lockedText }}
</span>
</p>
@@ -112,8 +112,7 @@ export default {
<gl-button
v-if="artifact.browse_path"
:href="artifact.browse_path"
- data-testid="browse-artifacts"
- data-qa-selector="browse_artifacts_button"
+ data-testid="browse-artifacts-button"
>{{ $options.i18n.browseText }}</gl-button
>
</gl-button-group>
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
new file mode 100644
index 00000000000..95616a4c706
--- /dev/null
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue
@@ -0,0 +1,54 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
+export default {
+ components: {
+ ClipboardButton,
+ GlLink,
+ },
+ props: {
+ commit: {
+ type: Object,
+ required: true,
+ },
+ mergeRequest: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <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"
+ >
+ {{ commit.short_id }}
+ </gl-link>
+
+ <clipboard-button
+ :text="commit.id"
+ :title="__('Copy commit SHA')"
+ category="tertiary"
+ size="small"
+ class="gl-align-self-center"
+ />
+
+ <span v-if="mergeRequest">
+ {{ __('in') }}
+ <gl-link :href="mergeRequest.path" class="gl-text-blue-500!" data-testid="link-commit"
+ >!{{ mergeRequest.iid }}</gl-link
+ >
+ </span>
+ </p>
+
+ <p class="gl-mb-0">{{ commit.title }}</p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/external_links_block.vue b/app/assets/javascripts/ci/job_details/components/sidebar/external_links_block.vue
new file mode 100644
index 00000000000..a87f4b8467e
--- /dev/null
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/external_links_block.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlIcon, GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ externalLinks: {
+ type: Array,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <div class="title gl-font-weight-bold">{{ s__('Job|External links') }}</div>
+ <ul class="gl-list-style-none gl-p-0 gl-m-0">
+ <li v-for="(externalLink, index) in externalLinks" :key="index">
+ <gl-link
+ :href="externalLink.url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="gl-text-blue-600!"
+ >
+ <gl-icon name="external-link" class="flex-shrink-0" />
+ {{ externalLink.label }}
+ </gl-link>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/job_container_item.vue b/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
index 097ab3b4cf6..8e87f118fa4 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/job_container_item.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
@@ -1,6 +1,6 @@
<script>
import { GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
+import delayedJobMixin from '~/ci/mixins/delayed_job_mixin';
import { sprintf } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
@@ -40,7 +40,7 @@ export default {
},
classes() {
return {
- retried: this.job.retried,
+ 'retried gl-text-secondary': this.job.retried,
'gl-font-weight-bold': this.isActive,
};
},
@@ -57,7 +57,7 @@ export default {
v-gl-tooltip.left.viewport
:href="job.status.details_path"
:title="tooltipText"
- class="gl-display-flex gl-align-items-center"
+ class="gl-display-flex gl-align-items-center gl-py-3 gl-pl-7"
:data-testid="dataTestId"
>
<gl-icon
@@ -67,11 +67,11 @@ export default {
:size="14"
/>
- <ci-icon :status="job.status" class="gl-mr-2" :size="14" />
+ <ci-icon :status="job.status" class="gl-mr-3" :size="14" />
<span class="gl-text-truncate gl-w-full">{{ jobName }}</span>
- <gl-icon v-if="job.retried" name="retry" />
+ <gl-icon v-if="job.retried" name="retry" class="gl-mr-4" />
</gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue b/app/assets/javascripts/ci/job_details/components/sidebar/job_retry_forward_deployment_modal.vue
index a3f1a2c4be8..58e49c71830 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/job_retry_forward_deployment_modal.vue
@@ -1,6 +1,6 @@
<script>
import { GlLink, GlModal } from '@gitlab/ui';
-import { JOB_RETRY_FORWARD_DEPLOYMENT_MODAL } from '~/jobs/constants';
+import { __, s__ } from '~/locale';
export default {
name: 'JobRetryForwardDeploymentModal',
@@ -9,7 +9,15 @@ export default {
GlModal,
},
i18n: {
- ...JOB_RETRY_FORWARD_DEPLOYMENT_MODAL,
+ cancel: __('Cancel'),
+ info: s__(
+ `Jobs|You're about to retry a job that failed because it attempted to deploy code that is older than the latest deployment.
+ Retrying this job could result in overwriting the environment with the older source code.`,
+ ),
+ areYouSure: s__('Jobs|Are you sure you want to proceed?'),
+ moreInfo: __('More information'),
+ primaryText: __('Retry job'),
+ title: s__('Jobs|Are you sure you want to retry this job?'),
},
inject: {
retryOutdatedJobDocsUrl: {
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue b/app/assets/javascripts/ci/job_details/components/sidebar/job_sidebar_retry_button.vue
index 87c47f592aa..26676123dc3 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/job_sidebar_retry_button.vue
@@ -2,12 +2,14 @@
import { GlButton, GlDisclosureDropdown, GlModalDirective } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters } from 'vuex';
-import { JOB_SIDEBAR_COPY } from '~/jobs/constants';
+import { s__ } from '~/locale';
export default {
name: 'JobSidebarRetryButton',
i18n: {
- ...JOB_SIDEBAR_COPY,
+ retryJobLabel: s__('Job|Retry'),
+ runAgainJobButtonLabel: s__('Job|Run again'),
+ updateVariables: s__('Job|Update CI/CD variables'),
},
components: {
GlButton,
@@ -65,6 +67,7 @@ export default {
icon="retry"
category="primary"
placement="right"
+ positioning-strategy="fixed"
variant="confirm"
:items="dropdownItems"
/>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/jobs_container.vue b/app/assets/javascripts/ci/job_details/components/sidebar/jobs_container.vue
index df64b6422c7..18bd2593c2a 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/jobs_container.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/jobs_container.vue
@@ -24,7 +24,8 @@ export default {
};
</script>
<template>
- <div class="builds-container">
+ <div class="block builds-container">
+ <b class="gl-display-flex gl-mb-2 gl-font-weight-semibold">{{ __('Related jobs') }}</b>
<job-container-item
v-for="job in jobs"
:key="job.id"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
index 92e1557ada2..7f2f4fc0331 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
@@ -1,11 +1,12 @@
<script>
-import { GlButton, GlIcon } from '@gitlab/ui';
import { isEmpty } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
-import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants';
+import { forwardDeploymentFailureModalId } from '~/ci/constants';
+import { filterAnnotations } from '~/ci/job_details/utils';
import ArtifactsBlock from './artifacts_block.vue';
import CommitBlock from './commit_block.vue';
+import ExternalLinksBlock from './external_links_block.vue';
import JobsContainer from './jobs_container.vue';
import JobRetryForwardDeploymentModal from './job_retry_forward_deployment_modal.vue';
import JobSidebarDetailsContainer from './sidebar_job_details_container.vue';
@@ -15,22 +16,17 @@ import TriggerBlock from './trigger_block.vue';
export default {
name: 'JobSidebar',
- i18n: {
- ...JOB_SIDEBAR_COPY,
- },
- borderTopClass: ['gl-border-t-solid', 'gl-border-t-1', 'gl-border-t-gray-100'],
forwardDeploymentFailureModalId,
components: {
ArtifactsBlock,
CommitBlock,
- GlButton,
- GlIcon,
JobsContainer,
JobRetryForwardDeploymentModal,
JobSidebarDetailsContainer,
SidebarHeader,
StagesDropdown,
TriggerBlock,
+ ExternalLinksBlock,
},
props: {
artifactHelpUrl: {
@@ -46,6 +42,9 @@ export default {
// the artifact object will always have a locked property
return Object.keys(this.job.artifact).length > 1;
},
+ hasExternalLinks() {
+ return this.externalLinks.length > 0;
+ },
hasTriggers() {
return !isEmpty(this.job.trigger);
},
@@ -58,6 +57,9 @@ export default {
shouldShowJobRetryForwardDeploymentModal() {
return this.job.retry_path && this.hasForwardDeploymentFailure;
},
+ externalLinks() {
+ return filterAnnotations(this.job.annotations, 'external_link');
+ },
},
watch: {
job(value, oldValue) {
@@ -77,73 +79,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">
+ <div class="blocks-container gl-p-4">
<sidebar-header
+ class="block gl-pb-4! gl-mb-2"
:rest-job="job"
:job-id="job.id"
@updateVariables="$emit('updateVariables')"
/>
- <div
- v-if="job.terminal_path || job.new_issue_path"
- class="gl-py-5"
- :class="$options.borderTopClass"
- >
- <gl-button
- v-if="job.new_issue_path"
- :href="job.new_issue_path"
- category="secondary"
- variant="confirm"
- data-testid="job-new-issue"
- >
- {{ $options.i18n.newIssue }}
- </gl-button>
- <gl-button
- v-if="job.terminal_path"
- :href="job.terminal_path"
- target="_blank"
- data-testid="terminal-link"
- >
- {{ $options.i18n.debug }}
- <gl-icon name="external-link" />
- </gl-button>
- </div>
- <job-sidebar-details-container class="gl-py-5" :class="$options.borderTopClass" />
+ <job-sidebar-details-container class="block gl-mb-2" />
<artifacts-block
v-if="hasArtifact"
- class="gl-py-5"
- :class="$options.borderTopClass"
+ class="block gl-mb-2"
:artifact="job.artifact"
:help-url="artifactHelpUrl"
/>
- <trigger-block
- v-if="hasTriggers"
- class="gl-py-5"
- :class="$options.borderTopClass"
- :trigger="job.trigger"
+ <external-links-block
+ v-if="hasExternalLinks"
+ class="block gl-mb-2"
+ :external-links="externalLinks"
/>
- <commit-block
- :commit="commit"
- class="gl-py-5"
- :class="$options.borderTopClass"
- :merge-request="job.merge_request"
- />
+ <trigger-block v-if="hasTriggers" class="block gl-mb-2" :trigger="job.trigger" />
+
+ <commit-block class="block gl-mb-2" :commit="commit" :merge-request="job.merge_request" />
<stages-dropdown
v-if="job.pipeline"
- class="gl-py-5"
- :class="$options.borderTopClass"
+ class="block gl-mb-2"
:pipeline="job.pipeline"
:selected-stage="selectedStage"
:stages="stages"
@requestSidebarStageDropdown="fetchJobsForStage"
/>
- </div>
- <jobs-container v-if="jobs.length" :job-id="job.id" :jobs="jobs" />
+ <jobs-container v-if="jobs.length" :job-id="job.id" :jobs="jobs" />
+ </div>
</div>
<job-retry-forward-deployment-modal
v-if="shouldShowJobRetryForwardDeploymentModal"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_detail_row.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue
index 0ba34eafa58..5b1bf354fd4 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_detail_row.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue
@@ -39,21 +39,26 @@ export default {
};
</script>
<template>
- <p class="gl-display-flex gl-justify-content-space-between gl-mb-2">
- <span v-if="hasTitle">
- <b>{{ title }}:</b>
+ <p class="build-sidebar-item gl-mb-2">
+ <b v-if="hasTitle" class="gl-display-flex">{{ title }}:</b>
+ <gl-link
+ v-if="path"
+ :href="path"
+ class="gl-text-blue-600!"
+ data-testid="job-sidebar-value-link"
+ >
+ {{ value }}
+ </gl-link>
+ <span v-else
+ >{{ value }}
<gl-link
- v-if="path"
- :href="path"
- class="gl-text-blue-600!"
- data-testid="job-sidebar-value-link"
+ v-if="hasHelpURL"
+ :href="helpUrl"
+ target="_blank"
+ data-testid="job-sidebar-help-link"
>
- {{ value }}
+ <gl-icon name="question-o" class="gl-ml-2 gl-text-blue-500" />
</gl-link>
- <span v-else>{{ value }}</span>
</span>
- <gl-link v-if="hasHelpURL" :href="helpUrl" target="_blank" data-testid="job-sidebar-help-link">
- <gl-icon name="question-o" />
- </gl-link>
</p>
</template>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue
index 56fcd8738d7..77e3ecb9b3c 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue
@@ -5,20 +5,23 @@ import { mapActions } from 'vuex';
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,
- JOB_SIDEBAR_COPY,
- forwardDeploymentFailureModalId,
- PASSED_STATUS,
-} from '~/jobs/constants';
-import GetJob from '../graphql/queries/get_job.query.graphql';
+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';
export default {
name: 'SidebarHeader',
i18n: {
- ...JOB_SIDEBAR_COPY,
+ cancelJobButtonLabel: s__('Job|Cancel'),
+ debug: __('Debug'),
+ eraseLogButtonLabel: s__('Job|Erase job log and artifacts'),
+ 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,
directives: {
@@ -90,27 +93,47 @@ export default {
</script>
<template>
- <div class="gl-py-5 gl-display-flex gl-align-items-center">
+ <div>
<tooltip-on-truncate :title="job.name" truncate-target="child"
- ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate" data-testid="job-name">{{ job.name }}</h4>
+ ><h4 class="gl-mt-0 gl-mb-3 gl-text-truncate" data-testid="job-name">{{ job.name }}</h4>
</tooltip-on-truncate>
- <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right">
+ <div class="gl-display-flex gl-gap-3">
<gl-button
v-if="restJob.erase_path"
- v-gl-tooltip.left
+ v-gl-tooltip.bottom
:title="$options.i18n.eraseLogButtonLabel"
:aria-label="$options.i18n.eraseLogButtonLabel"
:href="restJob.erase_path"
:data-confirm="$options.i18n.eraseLogConfirmText"
- class="gl-mr-2"
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.left
+ v-gl-tooltip.bottom
:title="buttonTitle"
:aria-label="buttonTitle"
:is-manual-job="isManualJob"
@@ -118,13 +141,12 @@ export default {
:href="restJob.retry_path"
:modal-id="$options.forwardDeploymentFailureModalId"
variant="confirm"
- data-qa-selector="retry_button"
data-testid="retry-button"
@updateVariablesClicked="$emit('updateVariables')"
/>
<gl-button
v-if="restJob.cancel_path"
- v-gl-tooltip.left
+ v-gl-tooltip.bottom
:title="$options.i18n.cancelJobButtonLabel"
:aria-label="$options.i18n.cancelJobButtonLabel"
:href="restJob.cancel_path"
@@ -136,7 +158,7 @@ export default {
/>
<gl-button
:aria-label="$options.i18n.toggleSidebar"
- category="tertiary"
+ category="secondary"
class="gl-md-display-none gl-ml-2"
icon="chevron-double-lg-right"
@click="toggleSidebar"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue
index 09335476008..ebef3ecaa3f 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue
@@ -44,10 +44,14 @@ 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;
@@ -81,8 +85,9 @@ export default {
ERASED: __('Erased'),
QUEUED: __('Queued'),
RUNNER: __('Runner'),
- TAGS: __('Tags:'),
+ 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',
@@ -108,6 +113,7 @@ 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"
@@ -117,8 +123,8 @@ export default {
<detail-row v-if="job.coverage" :value="coverage" :title="$options.i18n.COVERAGE" />
<p v-if="hasTags" class="build-detail-row" data-testid="job-tags">
- <span class="font-weight-bold">{{ $options.i18n.TAGS }}</span>
- <gl-badge v-for="(tag, i) in job.tags" :key="i" variant="info">{{ tag }}</gl-badge>
+ <span class="font-weight-bold">{{ $options.i18n.TAGS }}:</span>
+ <gl-badge v-for="(tag, i) in job.tags" :key="i" variant="info" size="sm">{{ tag }}</gl-badge>
</p>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/stages_dropdown.vue b/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
index c1f84adf664..7744395734f 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/stages_dropdown.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
@@ -1,20 +1,20 @@
<script>
import { GlLink, GlDisclosureDropdown, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { Mousetrap } from '~/lib/mousetrap';
import { s__ } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { clickCopyToClipboardButton } from '~/behaviors/copy_to_clipboard';
import { keysFor, MR_COPY_SOURCE_BRANCH_NAME } from '~/behaviors/shortcuts/keybindings';
export default {
components: {
- CiIcon,
ClipboardButton,
GlDisclosureDropdown,
GlLink,
GlSprintf,
+ CiBadgeLink,
},
props: {
pipeline: {
@@ -51,11 +51,13 @@ export default {
},
pipelineInfo() {
if (!this.hasRef) {
- return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id}');
- } else if (!this.isTriggeredByMergeRequest) {
- return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id} for %{ref}');
- } else if (!this.isMergeRequestPipeline) {
- return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id} for %{mrId} with %{source}');
+ return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id} %{status}');
+ }
+ if (!this.isTriggeredByMergeRequest) {
+ return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id} %{status} for %{ref}');
+ }
+ if (!this.isMergeRequestPipeline) {
+ return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id} %{status} for %{mrId} with %{source}');
}
return s__(
@@ -78,7 +80,8 @@ export default {
if (!this.hasRef) {
return;
- } else if (!this.isTriggeredByMergeRequest) {
+ }
+ if (!this.isTriggeredByMergeRequest) {
button = this.$refs['copy-source-ref-link'];
} else {
button = this.$refs['copy-source-branch-link'];
@@ -91,25 +94,30 @@ export default {
</script>
<template>
<div class="dropdown">
- <div class="js-pipeline-info" data-testid="pipeline-info">
- <ci-icon :status="pipeline.details.status" />
+ <div class="gl-display-flex gl-flex-wrap gl-gap-2 js-pipeline-info" data-testid="pipeline-info">
<gl-sprintf :message="pipelineInfo">
<template #bold="{ content }">
- <span class="font-weight-bold">{{ content }}</span>
+ <span class="gl-display-flex gl-font-weight-bold">{{ content }}</span>
</template>
<template #id>
<gl-link
:href="pipeline.path"
- class="js-pipeline-path link-commit"
+ class="js-pipeline-path link-commit gl-text-blue-500!"
data-testid="pipeline-path"
- data-qa-selector="pipeline_path"
>#{{ pipeline.id }}</gl-link
>
</template>
+ <template #status>
+ <ci-badge-link
+ :status="pipeline.details.status"
+ size="sm"
+ data-testid="pipeline-status-link"
+ />
+ </template>
<template #mrId>
<gl-link
:href="pipeline.merge_request.path"
- class="link-commit ref-name"
+ class="link-commit gl-text-blue-500!"
data-testid="mr-link"
>!{{ pipeline.merge_request.iid }}</gl-link
>
@@ -117,7 +125,7 @@ export default {
<template #ref>
<gl-link
:href="pipeline.ref.path"
- class="link-commit ref-name"
+ class="link-commit ref-name gl-mt-1"
data-testid="source-ref-link"
>{{ pipeline.ref.name }}</gl-link
><clipboard-button
@@ -132,7 +140,7 @@ export default {
<template #source>
<gl-link
:href="pipeline.merge_request.source_branch_path"
- class="link-commit ref-name"
+ class="link-commit ref-name gl-mt-1"
data-testid="source-branch-link"
>{{ pipeline.merge_request.source_branch }}</gl-link
><clipboard-button
@@ -147,7 +155,7 @@ export default {
<template #target>
<gl-link
:href="pipeline.merge_request.target_branch_path"
- class="link-commit ref-name"
+ class="link-commit ref-name gl-mt-1"
data-testid="target-branch-link"
>{{ pipeline.merge_request.target_branch }}</gl-link
><clipboard-button
@@ -165,7 +173,7 @@ export default {
:toggle-text="selectedStage"
:items="dropdownItems"
block
- class="gl-mt-3"
+ class="gl-mt-2"
/>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue b/app/assets/javascripts/ci/job_details/components/sidebar/trigger_block.vue
index c9172fe0322..315587a3376 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/trigger_block.vue
@@ -68,7 +68,7 @@ export default {
<template v-if="hasVariables">
<p class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
- <span class="gl-font-weight-bold">{{ __('Trigger variables:') }}</span>
+ <span class="gl-display-flex gl-font-weight-bold">{{ __('Trigger variables') }}</span>
<gl-button
v-if="hasValues"
diff --git a/app/assets/javascripts/jobs/components/job/stuck_block.vue b/app/assets/javascripts/ci/job_details/components/stuck_block.vue
index 1a678ce69a8..8c73f09daea 100644
--- a/app/assets/javascripts/jobs/components/job/stuck_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/stuck_block.vue
@@ -43,7 +43,8 @@ export default {
dataTestId: 'job-stuck-with-tags',
showTags: true,
};
- } else if (this.hasOfflineRunnersForProject) {
+ }
+ if (this.hasOfflineRunnersForProject) {
return {
text: s__(`Job|This job is stuck because the project
doesn't have any runners online assigned to it.`),
diff --git a/app/assets/javascripts/jobs/components/job/unmet_prerequisites_block.vue b/app/assets/javascripts/ci/job_details/components/unmet_prerequisites_block.vue
index c9747ca9f02..c9747ca9f02 100644
--- a/app/assets/javascripts/jobs/components/job/unmet_prerequisites_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/unmet_prerequisites_block.vue
diff --git a/app/assets/javascripts/jobs/components/job/graphql/fragments/ci_job.fragment.graphql b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql
index f4a0b10672e..7fb887b2dd4 100644
--- a/app/assets/javascripts/jobs/components/job/graphql/fragments/ci_job.fragment.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql
@@ -1,4 +1,4 @@
-#import "~/jobs/components/job/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/ci/job_details/graphql/fragments/ci_variable.fragment.graphql"
fragment BaseCiJob on CiJob {
id
diff --git a/app/assets/javascripts/jobs/components/job/graphql/fragments/ci_variable.fragment.graphql b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql
index 0479df7bc4c..0479df7bc4c 100644
--- a/app/assets/javascripts/jobs/components/job/graphql/fragments/ci_variable.fragment.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql
diff --git a/app/assets/javascripts/jobs/components/job/graphql/mutations/job_play_with_variables.mutation.graphql b/app/assets/javascripts/ci/job_details/graphql/mutations/job_play_with_variables.mutation.graphql
index 520deef5136..5d8a7b4c6f6 100644
--- a/app/assets/javascripts/jobs/components/job/graphql/mutations/job_play_with_variables.mutation.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/mutations/job_play_with_variables.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/jobs/components/job/graphql/fragments/ci_job.fragment.graphql"
+#import "~/ci/job_details/graphql/fragments/ci_job.fragment.graphql"
mutation playJobWithVariables($id: CiBuildID!, $variables: [CiVariableInput!]) {
jobPlay(input: { id: $id, variables: $variables }) {
diff --git a/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql b/app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql
index e35d603ea71..cd66a30ce63 100644
--- a/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/jobs/components/job/graphql/fragments/ci_job.fragment.graphql"
+#import "~/ci/job_details/graphql/fragments/ci_job.fragment.graphql"
mutation retryJobWithVariables($id: CiBuildID!, $variables: [CiVariableInput!]) {
jobRetry(input: { id: $id, variables: $variables }) {
diff --git a/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql b/app/assets/javascripts/ci/job_details/graphql/queries/get_job.query.graphql
index 95e3521091d..a521ec2bb72 100644
--- a/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/queries/get_job.query.graphql
@@ -1,4 +1,4 @@
-#import "~/jobs/components/job/graphql/fragments/ci_job.fragment.graphql"
+#import "~/ci/job_details/graphql/fragments/ci_job.fragment.graphql"
query getJob($fullPath: ID!, $id: JobID!) {
project(fullPath: $fullPath) {
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/ci/job_details/index.js
index 8cd69f25218..5a1ecf2fff3 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/ci/job_details/index.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
-import JobApp from './components/job/job_app.vue';
+import JobApp from './job_app.vue';
import createStore from './store';
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/jobs/components/job/job_app.vue b/app/assets/javascripts/ci/job_details/job_app.vue
index 52030a0f830..5137ebfeaa8 100644
--- a/app/assets/javascripts/jobs/components/job/job_app.vue
+++ b/app/assets/javascripts/ci/job_details/job_app.vue
@@ -4,25 +4,25 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { throttle, isEmpty } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapState, mapActions } from 'vuex';
-import LogTopBar from 'ee_else_ce/jobs/components/job/job_log_controllers.vue';
+import LogTopBar from 'ee_else_ce/ci/job_details/components/job_log_controllers.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
import { __, sprintf } from '~/locale';
-import CiHeader from '~/vue_shared/components/header_ci_component.vue';
-import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
-import Log from '~/jobs/components/log/log.vue';
-import { MANUAL_STATUS } from '~/jobs/constants';
-import EmptyState from './empty_state.vue';
-import EnvironmentsBlock from './environments_block.vue';
-import ErasedBlock from './erased_block.vue';
-import StuckBlock from './stuck_block.vue';
-import UnmetPrerequisitesBlock from './unmet_prerequisites_block.vue';
-import Sidebar from './sidebar/sidebar.vue';
+import delayedJobMixin from '~/ci/mixins/delayed_job_mixin';
+import Log from '~/ci/job_details/components/log/log.vue';
+import { MANUAL_STATUS } from '~/ci/constants';
+import EmptyState from './components/empty_state.vue';
+import EnvironmentsBlock from './components/environments_block.vue';
+import ErasedBlock from './components/erased_block.vue';
+import JobHeader from './components/job_header.vue';
+import StuckBlock from './components/stuck_block.vue';
+import UnmetPrerequisitesBlock from './components/unmet_prerequisites_block.vue';
+import Sidebar from './components/sidebar/sidebar.vue';
export default {
name: 'JobPageApp',
components: {
- CiHeader,
+ JobHeader,
EmptyState,
EnvironmentsBlock,
ErasedBlock,
@@ -33,7 +33,7 @@ export default {
UnmetPrerequisitesBlock,
Sidebar,
GlLoadingIcon,
- SharedRunner: () => import('ee_component/jobs/components/shared_runner_limit_block.vue'),
+ SharedRunner: () => import('ee_component/ci/runner/components/shared_runner_limit_block.vue'),
GlAlert,
},
directives: {
@@ -129,7 +129,7 @@ export default {
return Boolean(this.job.retry_path);
},
- itemName() {
+ jobName() {
return sprintf(__('Job %{jobName}'), { jobName: this.job.name });
},
},
@@ -225,13 +225,12 @@ export default {
<!-- Header Section -->
<header>
<div class="build-header top-area">
- <ci-header
+ <job-header
:status="job.status"
:time="headerTime"
:user="job.user"
- :has-sidebar-button="true"
:should-render-triggered-label="shouldRenderTriggeredLabel"
- :item-name="itemName"
+ :name="jobName"
@clickedSidebarButton="toggleSidebar"
/>
</div>
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/ci/job_details/store/actions.js
index b348478ccda..33d83689e61 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/ci/job_details/store/actions.js
@@ -12,7 +12,7 @@ import {
scrollUp,
} from '~/lib/utils/scroll_utils';
import { __ } from '~/locale';
-import { reportToSentry } from '../utils';
+import { reportToSentry } from '~/ci/utils';
import * as types from './mutation_types';
export const init = ({ dispatch }, { endpoint, logState, pagePath }) => {
diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/ci/job_details/store/getters.js
index a0f9db7409d..a0f9db7409d 100644
--- a/app/assets/javascripts/jobs/store/getters.js
+++ b/app/assets/javascripts/ci/job_details/store/getters.js
diff --git a/app/assets/javascripts/jobs/store/index.js b/app/assets/javascripts/ci/job_details/store/index.js
index b9d76765d8d..b9d76765d8d 100644
--- a/app/assets/javascripts/jobs/store/index.js
+++ b/app/assets/javascripts/ci/job_details/store/index.js
diff --git a/app/assets/javascripts/jobs/store/mutation_types.js b/app/assets/javascripts/ci/job_details/store/mutation_types.js
index 4915a826b84..4915a826b84 100644
--- a/app/assets/javascripts/jobs/store/mutation_types.js
+++ b/app/assets/javascripts/ci/job_details/store/mutation_types.js
diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/ci/job_details/store/mutations.js
index b7d7006ee61..b7d7006ee61 100644
--- a/app/assets/javascripts/jobs/store/mutations.js
+++ b/app/assets/javascripts/ci/job_details/store/mutations.js
diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/ci/job_details/store/state.js
index dfff65c364d..dfff65c364d 100644
--- a/app/assets/javascripts/jobs/store/state.js
+++ b/app/assets/javascripts/ci/job_details/store/state.js
diff --git a/app/assets/javascripts/jobs/store/utils.js b/app/assets/javascripts/ci/job_details/store/utils.js
index bc76901026d..bc76901026d 100644
--- a/app/assets/javascripts/jobs/store/utils.js
+++ b/app/assets/javascripts/ci/job_details/store/utils.js
diff --git a/app/assets/javascripts/ci/job_details/utils.js b/app/assets/javascripts/ci/job_details/utils.js
new file mode 100644
index 00000000000..4d06c241b4f
--- /dev/null
+++ b/app/assets/javascripts/ci/job_details/utils.js
@@ -0,0 +1,29 @@
+export const compactJobLog = (jobLog) => {
+ const compactedLog = [];
+
+ jobLog.forEach((obj) => {
+ // push header section line
+ if (obj.line && obj.isHeader) {
+ compactedLog.push(obj.line);
+ }
+
+ // push lines within section header
+ if (obj.lines?.length > 0) {
+ compactedLog.push(...obj.lines);
+ }
+
+ // push lines from plain log
+ if (!obj.lines && obj.content.length > 0) {
+ compactedLog.push(obj);
+ }
+ });
+
+ return compactedLog;
+};
+
+export const filterAnnotations = (annotations, type) => {
+ return [...annotations]
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .flatMap((annotationList) => annotationList.data)
+ .flatMap((annotation) => annotation[type] ?? []);
+};
diff --git a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
index d97f6f6ff8c..609f2790869 100644
--- a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
@@ -7,6 +7,7 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
+import { reportMessageToSentry } 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 {
@@ -23,13 +24,12 @@ import {
PLAY_JOB_CONFIRMATION_MESSAGE,
RUN_JOB_NOW_HEADER_TITLE,
FILE_TYPE_ARCHIVE,
-} from '../constants';
-import eventHub from '../event_hub';
-import cancelJobMutation from '../graphql/mutations/job_cancel.mutation.graphql';
-import playJobMutation from '../graphql/mutations/job_play.mutation.graphql';
-import retryJobMutation from '../graphql/mutations/job_retry.mutation.graphql';
-import unscheduleJobMutation from '../graphql/mutations/job_unschedule.mutation.graphql';
-import { reportMessageToSentry } from '../../../utils';
+} from '../../constants';
+import eventHub from '../../event_hub';
+import cancelJobMutation from '../../graphql/mutations/job_cancel.mutation.graphql';
+import playJobMutation from '../../graphql/mutations/job_play.mutation.graphql';
+import retryJobMutation from '../../graphql/mutations/job_retry.mutation.graphql';
+import unscheduleJobMutation from '../../graphql/mutations/job_unschedule.mutation.graphql';
export default {
ACTIONS_DOWNLOAD_ARTIFACTS,
diff --git a/app/assets/javascripts/jobs/components/table/cells/duration_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue
index 11593fa355a..dbf1dfe7a29 100644
--- a/app/assets/javascripts/jobs/components/table/cells/duration_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue
@@ -27,6 +27,9 @@ export default {
durationFormatted() {
return formatTime(this.duration * 1000);
},
+ hasDurationAndFinishedTime() {
+ return this.finishedTime && this.duration;
+ },
},
};
</script>
@@ -37,7 +40,11 @@ export default {
<gl-icon name="timer" :size="$options.iconSize" data-testid="duration-icon" />
{{ durationFormatted }}
</div>
- <div v-if="finishedTime" data-testid="job-finished-time">
+ <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" />
</div>
diff --git a/app/assets/javascripts/jobs/components/table/cells/job_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
index 27d286fc766..b435eb283fd 100644
--- a/app/assets/javascripts/jobs/components/table/cells/job_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
@@ -71,7 +71,7 @@ export default {
<template>
<div>
- <div class="gl-text-truncate gl-mb-2">
+ <div class="gl-text-truncate gl-p-3 gl-mt-n3 gl-mx-n3 gl-mb-n2">
<gl-link
v-if="canReadJob"
class="gl-text-blue-600!"
@@ -96,7 +96,7 @@ export default {
>
<div
v-if="jobRef"
- class="gl-px-2 gl-rounded-base gl-bg-gray-50 gl-max-w-15 gl-text-truncate"
+ class="gl-p-2 gl-rounded-base gl-bg-gray-50 gl-max-w-15 gl-text-truncate"
>
<gl-icon
v-if="createdByTag"
@@ -114,7 +114,7 @@ export default {
</div>
<span v-else>{{ __('none') }}</span>
- <div class="gl-ml-2 gl-rounded-base gl-px-2 gl-bg-gray-50">
+ <div class="gl-ml-2 gl-p-2 gl-rounded-base gl-bg-gray-50">
<gl-icon class="gl-mx-2" name="commit" :size="$options.iconSize" />
<gl-link
class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
diff --git a/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
index 1a6d1a341b0..18d68ee8a29 100644
--- a/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
@@ -36,12 +36,16 @@ export default {
<template>
<div>
- <div class="gl-text-truncate">
- <gl-link class="gl-text-gray-500!" :href="pipelinePath" data-testid="pipeline-id">
+ <div class="gl-p-3 gl-mt-n3">
+ <gl-link
+ class="gl-text-truncate gl-ml-n3 gl-text-gray-500!"
+ :href="pipelinePath"
+ data-testid="pipeline-id"
+ >
{{ pipelineId }}
</gl-link>
</div>
- <div>
+ <div class="gl-font-sm gl-text-secondary gl-mt-n2">
<span>{{ __('created by') }}</span>
<gl-link v-if="showAvatar" :href="userPath" data-testid="pipeline-user-link">
<gl-avatar :src="pipelineUserAvatar" :size="16" />
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table.vue b/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
index 84479ec421e..23100a3f3db 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
@@ -2,13 +2,13 @@
import { GlTable } from '@gitlab/ui';
import { s__ } from '~/locale';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
-import ProjectCell from '~/pages/admin/jobs/components/table/cell/project_cell.vue';
-import RunnerCell from '~/pages/admin/jobs/components/table/cells/runner_cell.vue';
-import ActionsCell from './cells/actions_cell.vue';
-import DurationCell from './cells/duration_cell.vue';
-import JobCell from './cells/job_cell.vue';
-import PipelineCell from './cells/pipeline_cell.vue';
-import { DEFAULT_FIELDS } from './constants';
+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 ActionsCell from './job_cells/actions_cell.vue';
+import DurationCell from './job_cells/duration_cell.vue';
+import JobCell from './job_cells/job_cell.vue';
+import PipelineCell from './job_cells/pipeline_cell.vue';
export default {
i18n: {
@@ -85,7 +85,7 @@ export default {
<template #cell(stage)="{ item }">
<div class="gl-text-truncate">
- <span data-testid="job-stage-name">{{ item.stage.name }}</span>
+ <span v-if="item.stage" data-testid="job-stage-name">{{ item.stage.name }}</span>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_empty_state.vue b/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue
index fcdd52b719c..d2cd27be034 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table_empty_state.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue
@@ -31,5 +31,6 @@ export default {
:svg-path="emptyStateSvgPath"
:primary-button-link="pipelineEditorPath"
:primary-button-text="$options.i18n.buttonText"
+ data-testid="jobs-empty-state"
/>
</template>
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue b/app/assets/javascripts/ci/jobs_page/components/jobs_table_tabs.vue
index 797facb1eb8..b753195da9a 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/jobs_table_tabs.vue
@@ -1,7 +1,7 @@
<script>
import { GlBadge, GlTab, GlTabs, GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
-import CancelJobs from '~/pages/admin/jobs/components/cancel_jobs.vue';
+import CancelJobs from '~/ci/admin/jobs_table/components/cancel_jobs.vue';
import { limitedCounterWithDelimiter } from '~/lib/utils/text_utility';
export default {
@@ -63,7 +63,7 @@ export default {
<template>
<div class="gl-display-flex align-items-lg-center">
- <gl-tabs content-class="gl-py-0">
+ <gl-tabs content-class="gl-py-0" class="gl-w-full">
<gl-tab
v-for="tab in tabs"
:key="tab.text"
diff --git a/app/assets/javascripts/jobs/components/table/constants.js b/app/assets/javascripts/ci/jobs_page/constants.js
index 1b572e60c58..1b572e60c58 100644
--- a/app/assets/javascripts/jobs/components/table/constants.js
+++ b/app/assets/javascripts/ci/jobs_page/constants.js
diff --git a/app/assets/javascripts/pipelines/event_hub.js b/app/assets/javascripts/ci/jobs_page/event_hub.js
index e31806ad199..e31806ad199 100644
--- a/app/assets/javascripts/pipelines/event_hub.js
+++ b/app/assets/javascripts/ci/jobs_page/event_hub.js
diff --git a/app/assets/javascripts/jobs/components/table/graphql/cache_config.js b/app/assets/javascripts/ci/jobs_page/graphql/cache_config.js
index 5390c023da4..5390c023da4 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/cache_config.js
+++ b/app/assets/javascripts/ci/jobs_page/graphql/cache_config.js
diff --git a/app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql b/app/assets/javascripts/ci/jobs_page/graphql/fragments/job.fragment.graphql
index 3038216fdfc..3038216fdfc 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/fragments/job.fragment.graphql
diff --git a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_cancel.mutation.graphql
index 20935514d51..20935514d51 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_cancel.mutation.graphql
diff --git a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_play.mutation.graphql b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_play.mutation.graphql
index c94b045ac40..c94b045ac40 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_play.mutation.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_play.mutation.graphql
diff --git a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_retry.mutation.graphql b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql
index 6e51f9a20fa..6e51f9a20fa 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_retry.mutation.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql
diff --git a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_unschedule.mutation.graphql
index 8be8c42f3c3..8be8c42f3c3 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_unschedule.mutation.graphql
diff --git a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql b/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql
index 69719011079..69719011079 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql
diff --git a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs_count.query.graphql b/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs_count.query.graphql
index a4e02ae721a..a4e02ae721a 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs_count.query.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs_count.query.graphql
diff --git a/app/assets/javascripts/jobs/components/table/index.js b/app/assets/javascripts/ci/jobs_page/index.js
index 88da1169e01..7e99157289b 100644
--- a/app/assets/javascripts/jobs/components/table/index.js
+++ b/app/assets/javascripts/ci/jobs_page/index.js
@@ -1,7 +1,7 @@
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue';
+import JobsTableApp from '~/ci/jobs_page/jobs_page_app.vue';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import cacheConfig from './graphql/cache_config';
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue b/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue
index 09fa006cb88..03e0f2dadc8 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
+++ b/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue
@@ -3,14 +3,14 @@ import { GlAlert, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { createAlert } from '~/alert';
import { setUrlParams, updateHistory, queryToObject } from '~/lib/utils/url_utility';
-import JobsSkeletonLoader from '~/pages/admin/jobs/components/jobs_skeleton_loader.vue';
-import JobsFilteredSearch from '../filtered_search/jobs_filtered_search.vue';
-import { validateQueryString } from '../filtered_search/utils';
+import JobsSkeletonLoader from '~/ci/admin/jobs_table/components/jobs_skeleton_loader.vue';
+import JobsFilteredSearch from '~/ci/common/private/jobs_filtered_search/app.vue';
+import { validateQueryString } from '~/ci/common/private/jobs_filtered_search/utils';
import GetJobs from './graphql/queries/get_jobs.query.graphql';
import GetJobsCount from './graphql/queries/get_jobs_count.query.graphql';
-import JobsTable from './jobs_table.vue';
-import JobsTableEmptyState from './jobs_table_empty_state.vue';
-import JobsTableTabs from './jobs_table_tabs.vue';
+import JobsTable from './components/jobs_table.vue';
+import JobsTableEmptyState from './components/jobs_table_empty_state.vue';
+import JobsTableTabs from './components/jobs_table_tabs.vue';
import { RAW_TEXT_WARNING } from './constants';
export default {
diff --git a/app/assets/javascripts/ci/merge_requests/components/pipelines_table_wrapper.vue b/app/assets/javascripts/ci/merge_requests/components/pipelines_table_wrapper.vue
new file mode 100644
index 00000000000..ee911d716e4
--- /dev/null
+++ b/app/assets/javascripts/ci/merge_requests/components/pipelines_table_wrapper.vue
@@ -0,0 +1,60 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import { createAlert } from '~/alert';
+import { __ } from '~/locale';
+import { getQueryHeaders } from '~/ci/pipeline_details/graph/utils';
+import { graphqlEtagMergeRequestPipelines } from '~/ci/pipeline_details/utils';
+import getMergeRequestPipelines from '../graphql/queries/get_merge_request_pipelines.query.graphql';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ },
+ inject: ['graphqlPath', 'mergeRequestId', 'targetProjectFullPath'],
+ data() {
+ return {
+ pipelines: [],
+ };
+ },
+ apollo: {
+ pipelines: {
+ query: getMergeRequestPipelines,
+ context() {
+ return getQueryHeaders(this.graphqlResourceEtag);
+ },
+ pollInterval: 10000,
+ variables() {
+ return {
+ fullPath: this.targetProjectFullPath,
+ mergeRequestIid: String(this.mergeRequestId),
+ };
+ },
+ update(data) {
+ return data?.project?.mergeRequest?.pipelines?.nodes || [];
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.fetchError });
+ },
+ },
+ },
+ computed: {
+ graphqlResourceEtag() {
+ return graphqlEtagMergeRequestPipelines(this.graphqlPath, this.mergeRequestId);
+ },
+ isLoading() {
+ return this.$apollo.queries.pipelines.loading;
+ },
+ },
+ i18n: {
+ fetchError: __("There was an error fetching this merge request's pipelines."),
+ },
+};
+</script>
+<template>
+ <div class="gl-mt-3">
+ <gl-loading-icon v-if="isLoading" size="lg" />
+ <ul v-else>
+ <li v-for="pipeline in pipelines" :key="pipeline.id">{{ pipeline.path }}</li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/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..022d461dbec 100644
--- a/app/assets/javascripts/pipelines/graphql/mutations/retry_mr_failed_job.mutation.graphql
+++ b/app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql
diff --git a/app/assets/javascripts/ci/merge_requests/graphql/queries/get_merge_request_pipelines.query.graphql b/app/assets/javascripts/ci/merge_requests/graphql/queries/get_merge_request_pipelines.query.graphql
new file mode 100644
index 00000000000..8c235032e6c
--- /dev/null
+++ b/app/assets/javascripts/ci/merge_requests/graphql/queries/get_merge_request_pipelines.query.graphql
@@ -0,0 +1,16 @@
+query getMergeRequestPipelines($mergeRequestIid: String!, $fullPath: ID!) {
+ project(fullPath: $fullPath) {
+ id
+ mergeRequest(iid: $mergeRequestIid) {
+ id
+ pipelines {
+ count
+ nodes {
+ id
+ iid
+ path
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/jobs/mixins/delayed_job_mixin.js b/app/assets/javascripts/ci/mixins/delayed_job_mixin.js
index 7b17dc7f693..7b17dc7f693 100644
--- a/app/assets/javascripts/jobs/mixins/delayed_job_mixin.js
+++ b/app/assets/javascripts/ci/mixins/delayed_job_mixin.js
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/ci/pipeline_details/constants.js
index 93ca3738ff0..bf312e66144 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/ci/pipeline_details/constants.js
@@ -1,22 +1,11 @@
-import { s__, __ } from '~/locale';
+import { __, s__ } from '~/locale';
export const CANCEL_REQUEST = 'CANCEL_REQUEST';
-export const FILTER_PIPELINES_SEARCH_DELAY = 200;
-export const ANY_TRIGGER_AUTHOR = 'Any';
export const SUPPORTED_FILTER_PARAMETERS = ['username', 'ref', 'status', 'source'];
-export const FILTER_TAG_IDENTIFIER = 'tag';
export const SCHEDULE_ORIGIN = 'schedule';
export const NEEDS_PROPERTY = 'needs';
export const EXPLICIT_NEEDS_PROPERTY = 'previousStageJobsOrNeeds';
-export const ICONS = {
- TAG: 'tag',
- MR: 'git-merge',
- BRANCH: 'branch',
- RETRY: 'retry',
- SUCCESS: 'success',
-};
-
export const TestStatus = {
FAILED: 'failed',
SKIPPED: 'skipped',
@@ -25,13 +14,6 @@ export const TestStatus = {
UNKNOWN: 'unknown',
};
-export const FETCH_AUTHOR_ERROR_MESSAGE = __('There was a problem fetching project users.');
-export const FETCH_BRANCH_ERROR_MESSAGE = __('There was a problem fetching project branches.');
-export const FETCH_TAG_ERROR_MESSAGE = __('There was a problem fetching project tags.');
-export const RAW_TEXT_WARNING = s__(
- 'Pipeline|Raw text search is not currently supported. Please use the available search tokens.',
-);
-
/* Error constants shared across graphs */
export const DEFAULT = 'default';
export const DELETE_FAILURE = 'delete_pipeline_failure';
@@ -64,25 +46,8 @@ export const validPipelineTabNames = [
codeQualityTabName,
];
-// 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 TOAST_MESSAGE = s__('Pipeline|Creating pipeline.');
-export const BUTTON_TOOLTIP_RETRY = __('Retry all failed or cancelled jobs');
-export const BUTTON_TOOLTIP_CANCEL = __('Cancel the running pipeline');
-
export const DEFAULT_FIELDS = [
{
key: 'name',
@@ -107,14 +72,6 @@ export const DEFAULT_FIELDS = [
},
];
-export const TRACKING_CATEGORIES = {
- table: 'pipelines_table_component',
- tabs: 'pipelines_filter_tabs',
- search: 'pipelines_filtered_search',
- failed: 'pipeline_failed_jobs_tab',
- tests: 'pipeline_tests_tab',
-};
-
// Pipeline Mini Graph
export const PIPELINE_MINI_GRAPH_POLL_INTERVAL = 5000;
diff --git a/app/assets/javascripts/pipelines/components/dag/dag_annotations.vue b/app/assets/javascripts/ci/pipeline_details/dag/components/dag_annotations.vue
index a1500166cdc..a1500166cdc 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag_annotations.vue
+++ b/app/assets/javascripts/ci/pipeline_details/dag/components/dag_annotations.vue
diff --git a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue b/app/assets/javascripts/ci/pipeline_details/dag/components/dag_graph.vue
index 7646c11773c..6e975d55a7f 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
+++ b/app/assets/javascripts/ci/pipeline_details/dag/components/dag_graph.vue
@@ -1,10 +1,10 @@
<script>
import * as d3 from 'd3';
import { uniqueId } from 'lodash';
+import { getMaxNodes, removeOrphanNodes } from '~/ci/pipeline_details/utils/parsing_utils';
import { PARSE_FAILURE } from '../../constants';
-import { getMaxNodes, removeOrphanNodes } from '../parsing_utils';
-import { LINK_SELECTOR, NODE_SELECTOR, ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants';
-import { calculateClip, createLinkPath, createSankey, labelPosition } from './drawing_utils';
+import { LINK_SELECTOR, NODE_SELECTOR, ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from '../constants';
+import { calculateClip, createLinkPath, createSankey, labelPosition } from '../utils/drawing_utils';
import {
currentIsLive,
getLiveLinksAsDict,
@@ -12,7 +12,7 @@ import {
restoreLinks,
toggleLinkHighlight,
togglePathHighlights,
-} from './interactions';
+} from '../utils/interactions';
export default {
viewOptions: {
diff --git a/app/assets/javascripts/pipelines/components/dag/constants.js b/app/assets/javascripts/ci/pipeline_details/dag/constants.js
index cd89055737f..cd89055737f 100644
--- a/app/assets/javascripts/pipelines/components/dag/constants.js
+++ b/app/assets/javascripts/ci/pipeline_details/dag/constants.js
diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/ci/pipeline_details/dag/dag.vue
index afb5aa05098..5415340c956 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag.vue
+++ b/app/assets/javascripts/ci/pipeline_details/dag/dag.vue
@@ -4,12 +4,17 @@ import { GlAlert, GlButton, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { fetchPolicies } from '~/lib/graphql';
import { __ } from '~/locale';
-import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from '../../constants';
-import getDagVisData from '../../graphql/queries/get_dag_vis_data.query.graphql';
-import { parseData } from '../parsing_utils';
+import {
+ DEFAULT,
+ PARSE_FAILURE,
+ LOAD_FAILURE,
+ UNSUPPORTED_DATA,
+} from '~/ci/pipeline_details/constants';
+import { parseData } from '~/ci/pipeline_details/utils/parsing_utils';
+import getDagVisData from './graphql/queries/get_dag_vis_data.query.graphql';
import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants';
-import DagAnnotations from './dag_annotations.vue';
-import DagGraph from './dag_graph.vue';
+import DagAnnotations from './components/dag_annotations.vue';
+import DagGraph from './components/dag_graph.vue';
export default {
// eslint-disable-next-line @gitlab/require-i18n-strings
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql b/app/assets/javascripts/ci/pipeline_details/dag/graphql/queries/get_dag_vis_data.query.graphql
index 2a0b13dd0cc..2a0b13dd0cc 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/dag/graphql/queries/get_dag_vis_data.query.graphql
diff --git a/app/assets/javascripts/pipelines/components/dag/drawing_utils.js b/app/assets/javascripts/ci/pipeline_details/dag/utils/drawing_utils.js
index 3cd09d57ffb..3cd09d57ffb 100644
--- a/app/assets/javascripts/pipelines/components/dag/drawing_utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/dag/utils/drawing_utils.js
diff --git a/app/assets/javascripts/pipelines/components/dag/interactions.js b/app/assets/javascripts/ci/pipeline_details/dag/utils/interactions.js
index 69f36feeee4..d2b7b7f9069 100644
--- a/app/assets/javascripts/pipelines/components/dag/interactions.js
+++ b/app/assets/javascripts/ci/pipeline_details/dag/utils/interactions.js
@@ -1,5 +1,5 @@
import * as d3 from 'd3';
-import { LINK_SELECTOR, NODE_SELECTOR, IS_HIGHLIGHTED } from './constants';
+import { LINK_SELECTOR, NODE_SELECTOR, IS_HIGHLIGHTED } from '../constants';
export const highlightIn = 1;
export const highlightOut = 0.2;
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/api.js b/app/assets/javascripts/ci/pipeline_details/graph/api_utils.js
index 0fe7d9ffda3..f9f47d1ea15 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/api.js
+++ b/app/assets/javascripts/ci/pipeline_details/graph/api_utils.js
@@ -1,5 +1,5 @@
import axios from '~/lib/utils/axios_utils';
-import { reportToSentry } from '../../utils';
+import { reportToSentry } from '~/ci/utils';
export const reportPerformance = (path, stats) => {
// FIXME: https://gitlab.com/gitlab-org/gitlab/-/issues/330245
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue
index 49df71beeec..f098d790736 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue
@@ -1,15 +1,15 @@
<script>
-import { reportToSentry } from '../../utils';
-import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue';
-import LinksLayer from '../graph_shared/links_layer.vue';
+import { reportToSentry } from '~/ci/utils';
import {
generateColumnsFromLayersListMemoized,
keepLatestDownstreamPipelines,
-} from '../parsing_utils';
-import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH, STAGE_VIEW } from './constants';
+} from '~/ci/pipeline_details/utils/parsing_utils';
+import LinksLayer from '../../../common/private/job_links_layer.vue';
+import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH, STAGE_VIEW } from '../constants';
+import { validateConfigPaths } from '../utils';
+import LinkedGraphWrapper from './linked_graph_wrapper.vue';
import LinkedPipelinesColumn from './linked_pipelines_column.vue';
import StageColumnComponent from './stage_column_component.vue';
-import { validateConfigPaths } from './utils';
export default {
name: 'PipelineGraph',
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue
index 73143c981ed..fb7dcb300f1 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue
@@ -1,7 +1,7 @@
<script>
import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { __, s__ } from '~/locale';
-import { STAGE_VIEW, LAYER_VIEW } from './constants';
+import { STAGE_VIEW, LAYER_VIEW } from '../constants';
export default {
name: 'GraphViewSelector',
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue
index d4852224df5..7538ad87af8 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue
@@ -1,6 +1,6 @@
<script>
-import { reportToSentry } from '../../utils';
-import { JOB_DROPDOWN, SINGLE_JOB } from './constants';
+import { reportToSentry } from '~/ci/utils';
+import { JOB_DROPDOWN, SINGLE_JOB } from '../constants';
import JobItem from './job_item.vue';
/**
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
index 22895a31082..4298052d1c0 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
@@ -1,14 +1,14 @@
<script>
import { GlBadge, GlForm, GlFormCheckbox, GlLink, GlModal, GlTooltipDirective } from '@gitlab/ui';
-import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
+import { reportToSentry } from '~/ci/utils';
+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 { reportToSentry } from '../../utils';
-import ActionComponent from '../jobs_shared/action_component.vue';
-import JobNameComponent from '../jobs_shared/job_name_component.vue';
-import { BRIDGE_KIND, RETRY_ACTION_TITLE, SINGLE_JOB, SKIP_RETRY_MODAL_KEY } from './constants';
+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';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/linked_graph_wrapper.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue
index fb2280d971a..fb2280d971a 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/linked_graph_wrapper.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
index d8b843bdfb0..d6adaf78da4 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
@@ -11,11 +11,11 @@ import { TYPENAME_CI_PIPELINE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
-import CancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
-import RetryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
+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 { reportToSentry } from '../../utils';
-import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from './constants';
+import { reportToSentry } from '~/ci/utils';
+import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from '../constants';
export default {
directives: {
@@ -72,7 +72,8 @@ export default {
method: this.cancelPipeline,
ariaLabel: __('Cancel downstream pipeline'),
};
- } else if (this.isRetryable) {
+ }
+ if (this.isRetryable) {
return {
icon: 'retry',
method: this.retryPipeline,
@@ -141,7 +142,8 @@ export default {
label() {
if (this.parentPipeline) {
return __('Parent');
- } else if (this.childPipeline) {
+ }
+ if (this.childPipeline) {
return __('Child');
}
return __('Multi-project');
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue
index 02e426064c9..2de7e43c9b1 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue
@@ -1,9 +1,8 @@
<script>
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
+import { reportToSentry } from '~/ci/utils';
import { LOAD_FAILURE } from '../../constants';
-import { reportToSentry } from '../../utils';
-import { ONE_COL_WIDTH, UPSTREAM, LAYER_VIEW, STAGE_VIEW } from './constants';
-import LinkedPipeline from './linked_pipeline.vue';
+import { ONE_COL_WIDTH, UPSTREAM, LAYER_VIEW, STAGE_VIEW } from '../constants';
import {
calculatePipelineLayersInfo,
getQueryHeaders,
@@ -11,7 +10,8 @@ import {
toggleQueryPollingByVisibility,
unwrapPipelineData,
validateConfigPaths,
-} from './utils';
+} from '../utils';
+import LinkedPipeline from './linked_pipeline.vue';
export default {
components: {
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/links_inner.vue
index 1189c2ebad8..09285525c38 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/links_inner.vue
@@ -1,9 +1,10 @@
<script>
import { isEmpty } from 'lodash';
+import { STAGE_VIEW } from '~/ci/pipeline_details/graph/constants';
+import { createJobsHash, generateJobNeedsDict } from '~/ci/pipeline_details/utils';
+import { reportToSentry } from '~/ci/utils';
import { DRAW_FAILURE } from '../../constants';
-import { createJobsHash, generateJobNeedsDict, reportToSentry } from '../../utils';
-import { STAGE_VIEW } from '../graph/constants';
-import { generateLinksData } from './drawing_utils';
+import { generateLinksData } from '../../utils/drawing_utils';
export default {
name: 'LinksInner',
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue
index bcd7705669e..bcd7705669e 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
index ffd0fec2ca8..1401bdba5ca 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
@@ -1,9 +1,9 @@
<script>
import { escape, isEmpty } from 'lodash';
+import ActionComponent from '~/ci/common/private/job_action_component.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { reportToSentry } from '../../utils';
-import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue';
-import ActionComponent from '../jobs_shared/action_component.vue';
+import { reportToSentry } from '~/ci/utils';
+import RootGraphLayout from './root_graph_layout.vue';
import JobGroupDropdown from './job_group_dropdown.vue';
import JobItem from './job_item.vue';
@@ -12,7 +12,7 @@ export default {
ActionComponent,
JobGroupDropdown,
JobItem,
- MainGraphWrapper,
+ RootGraphLayout,
},
mixins: [glFeatureFlagMixin()],
props: {
@@ -135,7 +135,7 @@ export default {
};
</script>
<template>
- <main-graph-wrapper :class="columnSpacingClass" data-testid="stage-column">
+ <root-graph-layout :class="columnSpacingClass" data-testid="stage-column">
<template #stages>
<div
data-testid="stage-column-title"
@@ -192,5 +192,5 @@ export default {
</div>
</div>
</template>
- </main-graph-wrapper>
+ </root-graph-layout>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/ci/pipeline_details/graph/constants.js
index e650a48bc2a..e650a48bc2a 100644
--- a/app/assets/javascripts/pipelines/components/graph/constants.js
+++ b/app/assets/javascripts/ci/pipeline_details/graph/constants.js
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue
index b2cef7c37b9..bd7325f7925 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue
@@ -4,10 +4,10 @@ import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.qu
import getUserCallouts from '~/graphql_shared/queries/get_user_callouts.query.graphql';
import { __, s__ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants';
-import DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql';
-import getPipelineQuery from '../../graphql/queries/get_pipeline_header_data.query.graphql';
-import { reportToSentry, reportMessageToSentry } from '../../utils';
+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 DismissPipelineGraphCallout from './graphql/mutations/dismiss_pipeline_notification.graphql';
import {
ACTION_FAILURE,
IID_FAILURE,
@@ -16,8 +16,8 @@ import {
STAGE_VIEW,
VIEW_TYPE_KEY,
} from './constants';
-import PipelineGraph from './graph_component.vue';
-import GraphViewSelector from './graph_view_selector.vue';
+import PipelineGraph from './components/graph_component.vue';
+import GraphViewSelector from './components/graph_view_selector.vue';
import {
calculatePipelineLayersInfo,
getQueryHeaders,
diff --git a/app/assets/javascripts/pipelines/graphql/mutations/dismiss_pipeline_notification.graphql b/app/assets/javascripts/ci/pipeline_details/graph/graphql/mutations/dismiss_pipeline_notification.graphql
index e8af1db9592..e8af1db9592 100644
--- a/app/assets/javascripts/pipelines/graphql/mutations/dismiss_pipeline_notification.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/graph/graphql/mutations/dismiss_pipeline_notification.graphql
diff --git a/app/assets/javascripts/pipelines/components/graph/perf_utils.js b/app/assets/javascripts/ci/pipeline_details/graph/perf_utils.js
index 3737a209f5c..511dcbe6889 100644
--- a/app/assets/javascripts/pipelines/components/graph/perf_utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/graph/perf_utils.js
@@ -8,7 +8,7 @@ import {
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
-import { reportPerformance } from '../graph_shared/api';
+import { reportPerformance } from './api_utils';
export const beginPerfMeasure = () => {
performanceMarkAndMeasure({ mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START });
diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/ci/pipeline_details/graph/utils.js
index c888c8a5537..9a8d6440d4d 100644
--- a/app/assets/javascripts/pipelines/components/graph/utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/graph/utils.js
@@ -1,8 +1,9 @@
import { isEmpty } from 'lodash';
import { getIdFromGraphQLId, etagQueryHeaders } from '~/graphql_shared/utils';
-import { reportToSentry } from '../../utils';
-import { listByLayers } from '../parsing_utils';
-import { unwrapStagesWithNeedsAndLookup } from '../unwrapping_utils';
+import { reportToSentry } from '~/ci/utils';
+
+import { listByLayers } from '~/ci/pipeline_details/utils/parsing_utils';
+import { unwrapStagesWithNeedsAndLookup } from '~/ci/pipeline_details/utils/unwrapping_utils';
import { beginPerfMeasure, finishPerfMeasureAndSend } from './perf_utils';
export { toggleQueryPollingByVisibility } from '~/graphql_shared/utils';
diff --git a/app/assets/javascripts/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql b/app/assets/javascripts/ci/pipeline_details/graphql/fragments/pipeline_stages_connection.fragment.graphql
index f93908aeb04..f93908aeb04 100644
--- a/app/assets/javascripts/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/graphql/fragments/pipeline_stages_connection.fragment.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql b/app/assets/javascripts/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql
index 9afb474cb17..9afb474cb17 100644
--- a/app/assets/javascripts/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/mutations/delete_pipeline.mutation.graphql b/app/assets/javascripts/ci/pipeline_details/graphql/mutations/delete_pipeline.mutation.graphql
index a52cecafcaf..a52cecafcaf 100644
--- a/app/assets/javascripts/pipelines/graphql/mutations/delete_pipeline.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/graphql/mutations/delete_pipeline.mutation.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/mutations/retry_pipeline.mutation.graphql b/app/assets/javascripts/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql
index 2b3b0822653..2b3b0822653 100644
--- a/app/assets/javascripts/pipelines/graphql/mutations/retry_pipeline.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/provider.js b/app/assets/javascripts/ci/pipeline_details/graphql/provider.js
index ef96b443da8..ef96b443da8 100644
--- a/app/assets/javascripts/pipelines/graphql/provider.js
+++ b/app/assets/javascripts/ci/pipeline_details/graphql/provider.js
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_linked_pipelines.query.graphql b/app/assets/javascripts/ci/pipeline_details/graphql/queries/get_linked_pipelines.query.graphql
index 9257cc7de7b..9257cc7de7b 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_linked_pipelines.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/graphql/queries/get_linked_pipelines.query.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql b/app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql
index eb5643126a2..eb5643126a2 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql
diff --git a/app/assets/javascripts/pipelines/components/pipeline_details_header.vue b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
index c53321f82bd..3a6a655bfa6 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_details_header.vue
+++ b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
@@ -11,6 +11,7 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
+import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '~/ci/constants';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { __, s__, sprintf, formatNumber } from '~/locale';
@@ -19,19 +20,12 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
-import {
- LOAD_FAILURE,
- POST_FAILURE,
- DELETE_FAILURE,
- DEFAULT,
- BUTTON_TOOLTIP_RETRY,
- BUTTON_TOOLTIP_CANCEL,
-} from '../constants';
+import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants';
import cancelPipelineMutation from '../graphql/mutations/cancel_pipeline.mutation.graphql';
import deletePipelineMutation from '../graphql/mutations/delete_pipeline.mutation.graphql';
import retryPipelineMutation from '../graphql/mutations/retry_pipeline.mutation.graphql';
-import getPipelineQuery from '../graphql/queries/get_pipeline_header_data.query.graphql';
-import { getQueryHeaders } from './graph/utils';
+import { getQueryHeaders } from '../graph/utils';
+import getPipelineQuery from './graphql/queries/get_pipeline_header_data.query.graphql';
const DELETE_MODAL_ID = 'pipeline-delete-modal';
const POLL_INTERVAL = 10000;
@@ -63,7 +57,7 @@ export default {
},
i18n: {
scheduleBadgeText: s__('Pipelines|Scheduled'),
- scheduleBadgeTooltip: __('This pipeline was triggered by a schedule'),
+ scheduleBadgeTooltip: __('This pipeline was created by a schedule'),
childBadgeText: s__('Pipelines|Child pipeline (%{linkStart}parent%{linkEnd})'),
childBadgeTooltip: __('This is a child pipeline within the parent pipeline'),
latestBadgeText: s__('Pipelines|latest'),
@@ -272,7 +266,7 @@ export default {
});
},
triggeredText() {
- return sprintf(__('triggered pipeline for commit %{linkStart}%{shortId}%{linkEnd}'), {
+ return sprintf(__('created pipeline for commit %{linkStart}%{shortId}%{linkEnd}'), {
shortId: this.shortId,
});
},
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue b/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
index f84ae13180d..4752fbb3e96 100644
--- a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
@@ -6,8 +6,9 @@ import { createAlert } from '~/alert';
import Tracking from '~/tracking';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
-import RetryFailedJobMutation from '../../graphql/mutations/retry_failed_job.mutation.graphql';
-import { DEFAULT_FIELDS, TRACKING_CATEGORIES } from '../../constants';
+import { TRACKING_CATEGORIES } from '~/ci/constants';
+import RetryFailedJobMutation from '../graphql/mutations/retry_failed_job.mutation.graphql';
+import { DEFAULT_FIELDS } from '../../constants';
export default {
fields: DEFAULT_FIELDS,
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue b/app/assets/javascripts/ci/pipeline_details/jobs/failed_jobs_app.vue
index c24862f828b..b946a40e590 100644
--- a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/failed_jobs_app.vue
@@ -2,8 +2,8 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import { createAlert } from '~/alert';
-import GetFailedJobsQuery from '../../graphql/queries/get_failed_jobs.query.graphql';
-import FailedJobsTable from './failed_jobs_table.vue';
+import GetFailedJobsQuery from './graphql/queries/get_failed_jobs.query.graphql';
+import FailedJobsTable from './components/failed_jobs_table.vue';
export default {
components: {
diff --git a/app/assets/javascripts/pipelines/graphql/mutations/retry_failed_job.mutation.graphql b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql
index 1955cc9b0ac..1955cc9b0ac 100644
--- a/app/assets/javascripts/pipelines/graphql/mutations/retry_failed_job.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/queries/get_failed_jobs.query.graphql
index c1f994ece24..c1f994ece24 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/queries/get_failed_jobs.query.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/queries/get_pipeline_jobs.query.graphql
index b0f875160d4..b0f875160d4 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/queries/get_pipeline_jobs.query.graphql
diff --git a/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue b/app/assets/javascripts/ci/pipeline_details/jobs/jobs_app.vue
index 61748860983..81b6152347d 100644
--- a/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/jobs_app.vue
@@ -3,10 +3,10 @@ import { GlIntersectionObserver, GlLoadingIcon, GlSkeletonLoader } from '@gitlab
import produce from 'immer';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
-import eventHub from '~/jobs/components/table/event_hub';
-import JobsTable from '~/jobs/components/table/jobs_table.vue';
-import { JOBS_TAB_FIELDS } from '~/jobs/components/table/constants';
-import getPipelineJobs from '../../graphql/queries/get_pipeline_jobs.query.graphql';
+import eventHub from '~/ci/jobs_page/event_hub';
+import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
+import { JOBS_TAB_FIELDS } from '~/ci/jobs_page/constants';
+import getPipelineJobs from './graphql/queries/get_pipeline_jobs.query.graphql';
export default {
fields: JOBS_TAB_FIELDS,
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js b/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js
index 481953608e9..53f755fda37 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
+++ b/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js
@@ -1,13 +1,13 @@
import Visibility from 'visibilityjs';
import { createAlert } from '~/alert';
+import eventHub from '~/ci/event_hub';
import { helpPagePath } from '~/helpers/help_page_helper';
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
import { HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
-import { validateParams } from '~/pipelines/utils';
+import { validateParams } from '~/ci/pipeline_details/utils';
import { CANCEL_REQUEST, TOAST_MESSAGE } from '../constants';
-import eventHub from '../event_hub';
export default {
data() {
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/ci/pipeline_details/pipeline_details_bundle.js
index f9c027539f2..da09852a7f4 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipeline_details_bundle.js
@@ -33,9 +33,15 @@ export default async function initPipelineDetailsBundle() {
if (tabsEl) {
const { dataset } = tabsEl;
- const { createAppOptions } = await import('ee_else_ce/pipelines/pipeline_tabs');
+ const dismissalDescriptions = JSON.parse(dataset.dismissalDescriptions || '{}');
+ const { createAppOptions } = await import('ee_else_ce/ci/pipeline_details/pipeline_tabs');
const { createPipelineTabs } = await import('./pipeline_tabs');
- const { routes } = await import('ee_else_ce/pipelines/routes');
+ const { routes } = await import('ee_else_ce/ci/pipeline_details/routes');
+
+ const securityRoute = routes.find((route) => route.path === '/security');
+ if (securityRoute) {
+ securityRoute.props = { dismissalDescriptions };
+ }
const router = new VueRouter({
mode: 'history',
diff --git a/app/assets/javascripts/pipelines/pipeline_details_header.js b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
index c79aaef23e8..067ec3f305e 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_header.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { parseBoolean } from '~/lib/utils/common_utils';
-import PipelineDetailsHeader from './components/pipeline_details_header.vue';
+import PipelineDetailsHeader from './header/pipeline_details_header.vue';
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/pipelines/pipeline_shared_client.js b/app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js
index c3be487caae..c3be487caae 100644
--- a/app/assets/javascripts/pipelines/pipeline_shared_client.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js
diff --git a/app/assets/javascripts/pipelines/pipeline_tabs.js b/app/assets/javascripts/ci/pipeline_details/pipeline_tabs.js
index 00a1810926c..0ca9a68e70d 100644
--- a/app/assets/javascripts/pipelines/pipeline_tabs.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipeline_tabs.js
@@ -4,10 +4,11 @@ import VueRouter from 'vue-router';
import Vuex from 'vuex';
import VueApollo from 'vue-apollo';
import { GlToast } from '@gitlab/ui';
-import PipelineTabs from 'ee_else_ce/pipelines/components/pipeline_tabs.vue';
+import PipelineTabs from 'ee_else_ce/ci/pipeline_details/tabs/pipeline_tabs.vue';
+import { reportToSentry } from '~/ci/utils';
import { parseBoolean } from '~/lib/utils/common_utils';
import createTestReportsStore from './stores/test_reports';
-import { getPipelineDefaultTab, reportToSentry } from './utils';
+import { getPipelineDefaultTab } from './utils';
Vue.use(GlToast);
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/pipelines/pipelines_index.js b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
index 20fd0915e28..d38397e7479 100644
--- a/app/assets/javascripts/pipelines/pipelines_index.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
@@ -10,7 +10,7 @@ import {
import { doesHashExistInUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import Translate from '~/vue_shared/translate';
-import Pipelines from './components/pipelines_list/pipelines.vue';
+import Pipelines from '~/ci/pipelines_page/pipelines.vue';
import PipelinesStore from './stores/pipelines_store';
Vue.use(Translate);
diff --git a/app/assets/javascripts/pipelines/routes.js b/app/assets/javascripts/ci/pipeline_details/routes.js
index 0e1414ec390..84207f3ab0c 100644
--- a/app/assets/javascripts/pipelines/routes.js
+++ b/app/assets/javascripts/ci/pipeline_details/routes.js
@@ -1,8 +1,8 @@
-import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue';
-import Dag from './components/dag/dag.vue';
-import FailedJobsApp from './components/jobs/failed_jobs_app.vue';
-import JobsApp from './components/jobs/jobs_app.vue';
-import TestReports from './components/test_reports/test_reports.vue';
+import PipelineGraphWrapper from './graph/graph_component_wrapper.vue';
+import Dag from './dag/dag.vue';
+import FailedJobsApp from './jobs/failed_jobs_app.vue';
+import JobsApp from './jobs/jobs_app.vue';
+import TestReports from './test_reports/test_reports.vue';
import {
pipelineTabName,
needsTabName,
diff --git a/app/assets/javascripts/pipelines/stores/pipelines_store.js b/app/assets/javascripts/ci/pipeline_details/stores/pipelines_store.js
index 765441560d8..765441560d8 100644
--- a/app/assets/javascripts/pipelines/stores/pipelines_store.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/pipelines_store.js
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/actions.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/actions.js
index 1b51bb804d0..1b51bb804d0 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/actions.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/actions.js
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/constants.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/constants.js
index 83d14e1a109..83d14e1a109 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/constants.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/constants.js
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/getters.js
index e6a88bb4175..e6a88bb4175 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/getters.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/getters.js
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/index.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/index.js
index f45a53f47b7..f45a53f47b7 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/index.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/index.js
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/mutation_types.js
index 7651a2f4327..7651a2f4327 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/mutation_types.js
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/mutations.js
index 466574157f5..466574157f5 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/mutations.js
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/state.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/state.js
index 3ec9418c14e..3ec9418c14e 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/state.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/state.js
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/utils.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js
index 6b616601bc5..6b616601bc5 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js
diff --git a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue b/app/assets/javascripts/ci/pipeline_details/tabs/pipeline_tabs.vue
index 35dde6379dd..9783a9b5937 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
+++ b/app/assets/javascripts/ci/pipeline_details/tabs/pipeline_tabs.vue
@@ -1,5 +1,6 @@
<script>
import { GlBadge, GlTabs, GlTab } from '@gitlab/ui';
+import { TRACKING_CATEGORIES } from '~/ci/constants';
import { __ } from '~/locale';
import Tracking from '~/tracking';
import {
@@ -8,7 +9,6 @@ import {
needsTabName,
pipelineTabName,
testReportTabName,
- TRACKING_CATEGORIES,
} from '../constants';
export default {
diff --git a/app/assets/javascripts/pipelines/components/test_reports/empty_state.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/empty_state.vue
index 3e7827dc416..055b6742ae1 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/empty_state.vue
+++ b/app/assets/javascripts/ci/pipeline_details/test_reports/empty_state.vue
@@ -54,6 +54,7 @@ export default {
:title="emptyStateText.title"
:description="emptyStateText.description"
:svg-path="emptyStateImagePath"
+ :svg-height="150"
:primary-button-link="testReportDocPath"
:primary-button-text="emptyStateText.button"
/>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/test_case_details.vue
index 10db3e1c56b..3e6faa6b346 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
+++ b/app/assets/javascripts/ci/pipeline_details/test_reports/test_case_details.vue
@@ -29,6 +29,11 @@ export default {
return {};
},
},
+ visible: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
failureHistoryMessage() {
@@ -77,6 +82,8 @@ export default {
:modal-id="modalId"
:title="testCase.classname"
:action-primary="$options.modalCloseButton"
+ :visible="visible"
+ @hidden="$emit('hidden')"
>
<div class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
<strong class="gl-text-right col-sm-3">{{ $options.text.name }}</strong>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue
index a7737d33285..a7737d33285 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
+++ b/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/test_suite_table.vue
index d8af926a796..d8af926a796 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
+++ b/app/assets/javascripts/ci/pipeline_details/test_reports/test_suite_table.vue
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/test_summary.vue
index 6b723ad5481..f6090678ca4 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
+++ b/app/assets/javascripts/ci/pipeline_details/test_reports/test_summary.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlProgressBar } from '@gitlab/ui';
import { __ } from '~/locale';
-import { formattedTime } from '../../stores/test_reports/utils';
+import { formattedTime } from '../stores/test_reports/utils';
export default {
name: 'TestSummary',
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/test_summary_table.vue
index 9141947ea04..9141947ea04 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
+++ b/app/assets/javascripts/ci/pipeline_details/test_reports/test_summary_table.vue
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js b/app/assets/javascripts/ci/pipeline_details/utils/drawing_utils.js
index d6d9ea94c13..d6d9ea94c13 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/utils/drawing_utils.js
diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/ci/pipeline_details/utils/index.js
index 38be5becfb8..9109342707e 100644
--- a/app/assets/javascripts/pipelines/utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/utils/index.js
@@ -1,4 +1,3 @@
-import * as Sentry from '@sentry/browser';
import { pickBy } from 'lodash';
import { parseUrlPathname } from '~/lib/utils/url_utility';
import {
@@ -6,7 +5,7 @@ import {
SUPPORTED_FILTER_PARAMETERS,
validPipelineTabNames,
pipelineTabName,
-} from './constants';
+} from '../constants';
/*
The following functions are the main engine in transforming the data as
received from the endpoint into the format the d3 graph expects.
@@ -128,22 +127,6 @@ export const generateJobNeedsDict = (jobs = {}) => {
}, {});
};
-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);
- });
-};
-
export const getPipelineDefaultTab = (url) => {
const strippedUrl = parseUrlPathname(url);
const regexp = /\w*$/;
@@ -154,3 +137,11 @@ export const getPipelineDefaultTab = (url) => {
return null;
};
+
+export const graphqlEtagPipelinePath = (graphqlPath, pipelineId) => {
+ return `${graphqlPath}pipelines/id/${pipelineId}`;
+};
+
+export const graphqlEtagMergeRequestPipelines = (graphqlPath, mergeRequestId) => {
+ return `${graphqlPath}merge_requests/id/${mergeRequestId}`;
+};
diff --git a/app/assets/javascripts/pipelines/components/parsing_utils.js b/app/assets/javascripts/ci/pipeline_details/utils/parsing_utils.js
index e158f8809b5..0a2a6d16498 100644
--- a/app/assets/javascripts/pipelines/components/parsing_utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/utils/parsing_utils.js
@@ -1,7 +1,7 @@
import { memoize } from 'lodash';
-import { createNodeDict } from '../utils';
import { EXPLICIT_NEEDS_PROPERTY, NEEDS_PROPERTY } from '../constants';
-import { createSankey } from './dag/drawing_utils';
+import { createSankey } from '../dag/utils/drawing_utils';
+import { createNodeDict } from './index';
/*
A peformant alternative to lodash's isEqual. Because findIndex always finds
diff --git a/app/assets/javascripts/pipelines/components/unwrapping_utils.js b/app/assets/javascripts/ci/pipeline_details/utils/unwrapping_utils.js
index d42a11c3aba..7ac813bd527 100644
--- a/app/assets/javascripts/pipelines/components/unwrapping_utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/utils/unwrapping_utils.js
@@ -1,4 +1,4 @@
-import { reportToSentry } from '../utils';
+import { reportToSentry } from '~/ci/utils';
import { EXPLICIT_NEEDS_PROPERTY, NEEDS_PROPERTY } from '../constants';
const unwrapGroups = (stages) => {
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue
index 3fe9103c2b3..baf3dbfa090 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue
@@ -108,7 +108,6 @@ export default {
<gl-form-group
id="commit-group"
:label="$options.i18n.commitMessage"
- label-cols-sm="2"
label-for="commit-message"
>
<gl-form-textarea
@@ -122,7 +121,6 @@ export default {
<gl-form-group
id="source-branch-group"
:label="$options.i18n.sourceBranch"
- label-cols-sm="2"
label-for="source-branch-field"
>
<gl-form-input
@@ -130,13 +128,12 @@ export default {
v-model="sourceBranch"
class="gl-font-monospace!"
required
- data-qa-selector="source_branch_field"
+ data-testid="source-branch-field"
/>
<gl-form-checkbox
v-if="!isCurrentBranchSourceBranch"
v-model="openMergeRequest"
data-testid="new-mr-checkbox"
- data-qa-selector="new_mr_checkbox"
class="gl-mt-3"
>
<gl-sprintf :message="$options.i18n.startMergeRequest">
@@ -152,7 +149,7 @@ export default {
class="js-no-auto-disable gl-mr-3"
category="primary"
variant="confirm"
- data-qa-selector="commit_changes_button"
+ data-testid="commit-changes-button"
:disabled="isSubmitDisabled"
:loading="isSaving"
>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
index bc0cad75c60..8f4d566e7e6 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
@@ -89,7 +89,6 @@ export default {
icon="external-link"
target="_blank"
data-testid="template-repo-link"
- data-qa-selector="template_repo_link"
@click="trackTemplateBrowsing"
>
{{ $options.i18n.browseTemplates }}
@@ -98,7 +97,6 @@ export default {
icon="information-o"
size="small"
data-testid="drawer-toggle"
- data-qa-selector="drawer_toggle"
@click="toggleHelpDrawer"
>
{{ $options.i18n.help }}
@@ -107,7 +105,6 @@ export default {
v-if="glFeatures.ciJobAssistantDrawer"
icon="bulb"
size="small"
- data-qa-selector="job_assistant_drawer_toggle"
@click="toggleJobAssistantDrawer"
>
{{ $options.i18n.jobAssistant }}
@@ -117,7 +114,6 @@ export default {
icon="bulb"
size="small"
data-testid="ai-assistant-drawer-toggle"
- data-qa-selector="ai_assistant_drawer_toggle"
@click="toggleAiAssistantDrawer"
>
{{ $options.i18n.aiAssistant }}
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
index a410e4c933c..221a45d4d9a 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
@@ -218,7 +218,6 @@ export default {
:text="currentBranch"
:disabled="!enableBranchSwitcher"
icon="branch"
- data-qa-selector="branch_selector_button"
data-testid="branch-selector"
>
<gl-search-box-by-type :debounce="$options.inputDebounce" @input="setSearchTerm" />
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
index 7368d1a3a91..20b42e26f08 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
@@ -52,7 +52,7 @@ export default {
};
</script>
<template>
- <div class="gl-mb-4 gl-display-flex gl-flex-wrap gl-gap-3">
+ <div class="gl-display-flex gl-flex-wrap gl-gap-3 gl-mb-4">
<gl-button
v-if="showFileTreeToggle"
id="file-tree-toggle"
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue b/app/assets/javascripts/ci/pipeline_editor/components/graph/job_pill.vue
index 3f1d7255a2b..3f1d7255a2b 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/graph/job_pill.vue
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/ci/pipeline_editor/components/graph/pipeline_graph.vue
index 8daf85e2b2e..eb906cfc486 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/graph/pipeline_graph.vue
@@ -1,8 +1,8 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { __ } from '~/locale';
-import { DRAW_FAILURE, DEFAULT } from '../../constants';
-import LinksLayer from '../graph_shared/links_layer.vue';
+import { DRAW_FAILURE, DEFAULT } from '~/ci/pipeline_details/constants';
+import LinksLayer from '~/ci/common/private/job_links_layer.vue';
import JobPill from './job_pill.vue';
import StageName from './stage_name.vue';
@@ -132,7 +132,6 @@ export default {
:ref="$options.CONTAINER_REF"
class="gl-bg-gray-10 gl-overflow-auto"
data-testid="graph-container"
- data-qa-selector="pipeline_graph_container"
>
<links-layer
:pipeline-data="pipelineStages"
@@ -148,10 +147,7 @@ export default {
:key="`${stage.name}-${index}`"
class="gl-flex-direction-column"
>
- <div
- class="gl-display-flex gl-align-items-center gl-w-full gl-px-9 gl-py-4 gl-mb-5"
- data-qa-selector="stage_container"
- >
+ <div class="gl-display-flex gl-align-items-center gl-w-full gl-px-9 gl-py-4 gl-mb-5">
<stage-name :stage-name="stage.name" />
</div>
<div :class="$options.jobWrapperClasses">
@@ -162,7 +158,6 @@ export default {
:pipeline-id="$options.PIPELINE_ID"
:is-hovered="highlightedJob === group.name"
:is-faded-out="isFadedOut(group.name)"
- data-qa-selector="job_container"
@on-mouse-enter="setHoveredJob"
@on-mouse-leave="removeHoveredJob"
/>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue b/app/assets/javascripts/ci/pipeline_editor/components/graph/stage_name.vue
index 600832b7633..600832b7633 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/graph/stage_name.vue
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_header.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_header.vue
index ec6ee52b6b2..665ca907ed9 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_header.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_header.vue
@@ -1,30 +1,11 @@
<script>
+import { GlCard } from '@gitlab/ui';
import PipelineStatus from './pipeline_status.vue';
import ValidationSegment from './validation_segment.vue';
-const baseClasses = ['gl-p-5', 'gl-bg-gray-10', 'gl-border-solid', 'gl-border-gray-100'];
-
-const pipelineStatusClasses = [
- ...baseClasses,
- 'gl-border-1',
- 'gl-border-b-0!',
- 'gl-rounded-top-base',
-];
-
-const validationSegmentClasses = [...baseClasses, 'gl-border-1', 'gl-rounded-base'];
-
-const validationSegmentWithPipelineStatusClasses = [
- ...baseClasses,
- 'gl-border-1',
- 'gl-rounded-bottom-left-base',
- 'gl-rounded-bottom-right-base',
-];
-
export default {
- pipelineStatusClasses,
- validationSegmentClasses,
- validationSegmentWithPipelineStatusClasses,
components: {
+ GlCard,
PipelineStatus,
ValidationSegment,
},
@@ -47,24 +28,19 @@ export default {
showPipelineStatus() {
return !this.isNewCiConfigFile;
},
- // make sure corners are rounded correctly depending on if
- // pipeline status is rendered
- validationStyling() {
- return this.showPipelineStatus
- ? this.$options.validationSegmentWithPipelineStatusClasses
- : this.$options.validationSegmentClasses;
- },
},
};
</script>
<template>
- <div class="gl-mb-5">
- <pipeline-status
- v-if="showPipelineStatus"
- :commit-sha="commitSha"
- :class="$options.pipelineStatusClasses"
- v-on="$listeners"
- />
- <validation-segment :class="validationStyling" :ci-config="ciConfigData" />
- </div>
+ <gl-card
+ class="gl-new-card gl-mb-3 gl-mt-0"
+ header-class="gl-new-card-header"
+ body-class="gl-new-card-body gl-py-4 gl-px-5"
+ >
+ <template v-if="showPipelineStatus" #header>
+ <pipeline-status :commit-sha="commitSha" v-on="$listeners" />
+ </template>
+
+ <validation-segment :ci-config="ciConfigData" />
+ </gl-card>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
index f1c9770714a..f00098105d3 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
@@ -1,8 +1,8 @@
<script>
import { __ } from '~/locale';
-import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
-import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
-import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
+import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
+import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
+import getLinkedPipelinesQuery from '~/ci/pipeline_details/graphql/queries/get_linked_pipelines.query.graphql';
import { PIPELINE_FAILURE } from '../../constants';
export default {
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 3bce50224d9..58b5c0004e0 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
@@ -5,13 +5,10 @@ import { truncateSha } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.query.graphql';
import getPipelineEtag from '~/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
-import {
- getQueryHeaders,
- toggleQueryPollingByVisibility,
-} from '~/pipelines/components/graph/utils';
+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 PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
+import PipelineMiniGraph from '~/ci/pipeline_mini_graph/pipeline_mini_graph.vue';
import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
const POLL_INTERVAL = 10000;
@@ -141,7 +138,9 @@ export default {
</script>
<template>
- <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-wrap">
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-wrap gl-w-full"
+ >
<template v-if="showLoadingState">
<div>
<gl-loading-icon class="gl-mr-auto gl-display-inline-block" size="sm" />
@@ -149,20 +148,20 @@ export default {
</div>
</template>
<template v-else-if="hasError">
- <gl-icon class="gl-mr-auto" name="warning-solid" />
- <span data-testid="pipeline-error-msg">{{ $options.i18n.fetchError }}</span>
+ <div>
+ <gl-icon class="gl-mr-auto" name="warning-solid" />
+ <span data-testid="pipeline-error-msg">{{ $options.i18n.fetchError }}</span>
+ </div>
</template>
<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" />
+ <ci-icon :status="status" :size="16" data-testid="pipeline-status-icon" class="gl-mr-2" />
</a>
<span class="gl-font-weight-bold">
<gl-sprintf :message="$options.i18n.pipelineInfo">
<template #id="{ content }">
- <span data-testid="pipeline-id" data-qa-selector="pipeline_id_content">
- {{ content }}{{ pipelineId }}
- </span>
+ <span data-testid="pipeline-id"> {{ content }}{{ pipelineId }} </span>
</template>
<template #status>{{ status.text }}</template>
<template #commit>
@@ -187,9 +186,8 @@ export default {
/>
<pipeline-editor-mini-graph v-else :pipeline="pipeline" v-on="$listeners" />
<gl-button
- class="gl-ml-3"
- category="secondary"
- variant="confirm"
+ class="gl-ml-3 gl-align-self-center"
+ size="small"
:href="status.detailsPath"
data-testid="pipeline-view-btn"
>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue
index 8553256f13a..d54ad78b3d3 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue
@@ -112,8 +112,8 @@ export default {
{{ $options.i18n.loading }}
</template>
<span v-else data-testid="validation-segment">
- <span class="gl-max-w-full" data-qa-selector="validation_message_content">
- <gl-icon :name="icon" />
+ <span class="gl-max-w-full">
+ <gl-icon :name="icon" class="gl-mr-2" />
<gl-sprintf :message="message">
<template v-if="hasLink" #link="{ content }">
<gl-link :href="helpPath">{{ content }}</gl-link>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js
index a604d79259d..32eda355e66 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js
@@ -10,7 +10,8 @@ const trimText = (val) => (isString(val) ? trim(val) : val);
export const removeEmptyObj = (obj) => {
if (isArray(obj)) {
return reject(map(obj, removeEmptyObj), isEmptyValue);
- } else if (isObject(obj)) {
+ }
+ if (isObject(obj)) {
return omitBy(mapValues(obj, removeEmptyObj), isEmptyValue);
}
return obj;
@@ -19,7 +20,8 @@ export const removeEmptyObj = (obj) => {
export const trimFields = (data) => {
if (isArray(data)) {
return data.map(trimFields);
- } else if (isObject(data)) {
+ }
+ if (isObject(data)) {
return mapValues(data, trimFields);
}
return trimText(data);
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
index a954615ca8a..c7c15cdd76e 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -2,7 +2,7 @@
import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import CiEditorHeader from 'ee_else_ce/ci/pipeline_editor/components/editor/ci_editor_header.vue';
import { s__, __ } from '~/locale';
-import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
+import PipelineGraph from '~/ci/pipeline_editor/components/graph/pipeline_graph.vue';
import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import {
CREATE_TAB,
@@ -182,7 +182,7 @@ export default {
<template>
<gl-tabs
class="file-editor gl-mb-3"
- data-qa-selector="file_editor_container"
+ data-testid="file-editor-container"
:query-param-name="$options.query.TAB_QUERY_PARAM"
sync-active-tab-with-query-params
>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/popovers/file_tree_popover.vue b/app/assets/javascripts/ci/pipeline_editor/components/popovers/file_tree_popover.vue
index efa6a54c638..57694bbcd77 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/popovers/file_tree_popover.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/popovers/file_tree_popover.vue
@@ -42,7 +42,6 @@ export default {
target="file-tree-toggle"
triggers="manual"
placement="right"
- data-qa-selector="file_tree_popover"
@close-button-clicked="dismissPermanently"
>
<div v-outside="dismissPermanently" class="gl-font-base gl-mb-3">
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue b/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
index 25e4e99bf54..90402a89280 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
@@ -61,8 +61,7 @@ export default {
<gl-button
variant="confirm"
class="gl-mt-3"
- data-testid="create_new_ci_button"
- data-qa-selector="create_new_ci_button"
+ data-testid="create-new-ci-button"
@click="createEmptyConfigFile"
>
{{ $options.i18n.btnText }}
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue b/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
index 7583fa7a3b5..617088f303b 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
@@ -246,7 +246,6 @@ export default {
class="gl-mt-3"
:disabled="isInitialCiContentLoading"
data-testid="simulate-pipeline-button"
- data-qa-selector="simulate_pipeline_button"
@click="validateYaml"
>
{{ $options.i18n.cta }}
diff --git a/app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.query.graphql
index 5354ed7c2d5..3570fc1f008 100644
--- a/app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.query.graphql
@@ -1,4 +1,4 @@
-#import "~/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql"
+#import "~/ci/pipeline_details/graphql/fragments/pipeline_stages_connection.fragment.graphql"
query getCiConfigData($projectPath: ID!, $sha: String, $content: String!) {
ciConfig(projectPath: $projectPath, sha: $sha, content: $content) {
diff --git a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue
index de8e5a1a284..49562b0be28 100644
--- a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue
@@ -4,7 +4,7 @@ import { fetchPolicies } from '~/lib/graphql';
import { mergeUrlParams, queryToObject, redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { __, s__ } from '~/locale';
-import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
+import { unwrapStagesWithNeeds } from '~/ci/pipeline_details/utils/unwrapping_utils';
import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue';
import PipelineEditorEmptyState from './components/ui/pipeline_editor_empty_state.vue';
@@ -394,7 +394,7 @@ export default {
</script>
<template>
- <div class="gl-mt-4 gl-relative" data-qa-selector="pipeline_editor_app">
+ <div class="gl-mt-4 gl-relative">
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
<pipeline-editor-empty-state
v-else-if="showStartScreen || usesExternalConfig"
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/accessors/linked_pipelines_accessors.js b/app/assets/javascripts/ci/pipeline_mini_graph/accessors/linked_pipelines_accessors.js
index 1ca9e35c008..1ca9e35c008 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/accessors/linked_pipelines_accessors.js
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/accessors/linked_pipelines_accessors.js
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_stage.query.graphql b/app/assets/javascripts/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stage.query.graphql
index 64a5964dbeb..64a5964dbeb 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_stage.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stage.query.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_stages.query.graphql b/app/assets/javascripts/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stages.query.graphql
index 69a29947b16..69a29947b16 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_stages.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stages.query.graphql
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue b/app/assets/javascripts/ci/pipeline_mini_graph/job_item.vue
index 7f97097def6..7f97097def6 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/job_item.vue
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_job_item.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
index d6e585d093b..d20d4aec59d 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
@@ -1,11 +1,11 @@
<script>
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
-import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
+import ActionComponent from '~/ci/common/private/job_action_component.vue';
+import JobNameComponent from '~/ci/common/private/job_name_component.vue';
+import { ICONS } from '~/ci/constants';
+import delayedJobMixin from '~/ci/mixins/delayed_job_mixin';
import { s__, sprintf } from '~/locale';
-import { reportToSentry } from '../../utils';
-import ActionComponent from '../jobs_shared/action_component.vue';
-import JobNameComponent from '../jobs_shared/job_name_component.vue';
-import { ICONS } from '../../constants';
+import { reportToSentry } from '~/ci/utils';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue
index 8c0e65d1d39..8c0e65d1d39 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
index 048e42731c7..bbe0f1fbefc 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
@@ -15,9 +15,9 @@
import { GlDropdown, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { createAlert } from '~/alert';
+import eventHub from '~/ci/event_hub';
import axios from '~/lib/utils/axios_utils';
import { __, s__, sprintf } from '~/locale';
-import eventHub from '../../event_hub';
import LegacyJobItem from './legacy_job_item.vue';
export default {
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
index a5c6dc98694..8567654a89e 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
@@ -105,7 +105,7 @@ export default {
v-gl-tooltip="{ title: pipelineTooltipText(pipeline) }"
:href="pipeline.path"
:class="triggerButtonClass(pipeline)"
- class="linked-pipeline-mini-item gl-display-inline-block gl-h-6 gl-mr-2 gl-my-2 gl-rounded-full gl-vertical-align-middle"
+ class="linked-pipeline-mini-item gl-display-inline-flex gl-mr-2 gl-my-2 gl-rounded-full gl-vertical-align-middle"
data-testid="linked-pipeline-mini-item"
>
<ci-icon
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue b/app/assets/javascripts/ci/pipeline_mini_graph/pipeline_mini_graph.vue
index 7cdaec81466..358d3dc826e 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/pipeline_mini_graph.vue
@@ -2,14 +2,11 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
-import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
-import {
- getQueryHeaders,
- toggleQueryPollingByVisibility,
-} from '~/pipelines/components/graph/utils';
-import { PIPELINE_MINI_GRAPH_POLL_INTERVAL } from '~/pipelines/constants';
-import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
-import getPipelineStagesQuery from '~/pipelines/graphql/queries/get_pipeline_stages.query.graphql';
+import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
+import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
+import { PIPELINE_MINI_GRAPH_POLL_INTERVAL } from '~/ci/pipeline_details/constants';
+import getLinkedPipelinesQuery from '~/ci/pipeline_details/graphql/queries/get_linked_pipelines.query.graphql';
+import getPipelineStagesQuery from './graphql/queries/get_pipeline_stages.query.graphql';
import LegacyPipelineMiniGraph from './legacy_pipeline_mini_graph.vue';
export default {
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue b/app/assets/javascripts/ci/pipeline_mini_graph/pipeline_stage.vue
index 8e22f440089..747b5d33b1a 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/pipeline_stage.vue
@@ -1,12 +1,9 @@
<script>
import { createAlert } from '~/alert';
import { __ } from '~/locale';
-import { PIPELINE_MINI_GRAPH_POLL_INTERVAL } from '~/pipelines/constants';
-import {
- getQueryHeaders,
- toggleQueryPollingByVisibility,
-} from '~/pipelines/components/graph/utils';
-import getPipelineStageQuery from '~/pipelines/graphql/queries/get_pipeline_stage.query.graphql';
+import { PIPELINE_MINI_GRAPH_POLL_INTERVAL } from '~/ci/pipeline_details/constants';
+import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
+import getPipelineStageQuery from './graphql/queries/get_pipeline_stage.query.graphql';
import JobItem from './job_item.vue';
export default {
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue b/app/assets/javascripts/ci/pipeline_mini_graph/pipeline_stages.vue
index 02dba9ba30f..f883833f7ea 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/pipeline_stages.vue
@@ -42,7 +42,7 @@ export default {
<div
v-for="stage in stages"
:key="stage.name"
- class="pipeline-mini-graph-stage-container dropdown gl-display-inline-block gl-mr-2 gl-my-2 gl-vertical-align-middle"
+ class="pipeline-mini-graph-stage-container dropdown gl-display-inline-flex gl-mr-2 gl-my-2 gl-vertical-align-middle"
>
<pipeline-stage
v-if="isGraphql"
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue
index fbdb60f61f1..f701bedc74d 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue
@@ -41,6 +41,7 @@ export default {
<template>
<gl-empty-state
:svg-path="$options.SCHEDULE_MD_SVG_URL"
+ :svg-height="150"
:primary-button-text="$options.i18n.createNew"
:primary-button-link="newSchedulePath"
>
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 396ff9808f2..0c3ede47015 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
@@ -1,8 +1,7 @@
<script>
import {
GlButton,
- GlDropdown,
- GlDropdownItem,
+ GlCollapsibleListbox,
GlFormCheckbox,
GlForm,
GlFormGroup,
@@ -27,8 +26,7 @@ const scheduleId = queryToObject(window.location.search).id;
export default {
components: {
GlButton,
- GlDropdown,
- GlDropdownItem,
+ GlCollapsibleListbox,
GlForm,
GlFormCheckbox,
GlFormGroup,
@@ -81,7 +79,7 @@ export default {
this.description = schedule.description;
this.cron = schedule.cron;
this.cronTimezone = schedule.cronTimezone;
- this.scheduleRef = schedule.ref;
+ this.scheduleRef = schedule.ref || this.defaultBranch;
this.variables = variables.map((variable) => {
return {
id: variable.id,
@@ -144,10 +142,6 @@ export default {
revealText: __('Reveal values'),
hideText: __('Hide values'),
},
- typeOptions: {
- [VARIABLE_TYPE]: __('Variable'),
- [FILE_TYPE]: __('File'),
- },
formElementClasses: 'gl-md-mr-3 gl-mb-3 gl-flex-basis-quarter gl-flex-shrink-0 gl-flex-grow-0',
computed: {
dropdownTranslations() {
@@ -155,7 +149,7 @@ export default {
dropdownHeader: this.$options.i18n.targetBranchTag,
};
},
- typeOptionsListbox() {
+ typeOptions() {
return [
{
text: __('Variable'),
@@ -232,9 +226,9 @@ export default {
empty: true,
});
},
- setVariableAttribute(key, attribute, value) {
+ setVariableType(typeValue, key) {
const variable = this.variables.find((v) => v.key === key);
- variable[attribute] = value;
+ variable.variableType = typeValue;
},
removeVariable(index) {
this.variables[index].destroy = true;
@@ -387,19 +381,15 @@ export default {
class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row gl-mb-3 gl-pb-2"
data-testid="ci-variable-row"
>
- <gl-dropdown
- :text="$options.typeOptions[variable.variableType]"
+ <gl-collapsible-listbox
+ :items="typeOptions"
+ :selected="variable.variableType"
:class="$options.formElementClasses"
+ block
data-testid="pipeline-form-ci-variable-type"
- >
- <gl-dropdown-item
- v-for="type in Object.keys($options.typeOptions)"
- :key="type"
- @click="setVariableAttribute(variable.key, 'variableType', type)"
- >
- {{ $options.typeOptions[type] }}
- </gl-dropdown-item>
- </gl-dropdown>
+ @select="setVariableType($event, variable.key)"
+ />
+
<gl-form-input
v-model="variable.key"
:placeholder="s__('CiVariables|Input variable key')"
@@ -414,7 +404,6 @@ export default {
value="*****************"
disabled
class="gl-mb-3 gl-h-7!"
- :style="$options.textAreaStyle"
:no-resize="false"
data-testid="pipeline-form-ci-variable-hidden-value"
/>
@@ -424,7 +413,6 @@ export default {
v-model="variable.value"
:placeholder="s__('CiVariables|Input variable value')"
class="gl-mb-3 gl-h-7!"
- :style="$options.textAreaStyle"
:no-resize="false"
data-testid="pipeline-form-ci-variable-value"
data-qa-selector="ci_variable_value_field"
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue
index 08efa794bcc..56d50026f17 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue
@@ -27,10 +27,13 @@ export default {
</script>
<template>
- <div>
- <gl-icon :name="iconName" />
+ <div data-testid="pipeline-schedule-target">
<span v-if="refPath">
+ <gl-icon :name="iconName" />
<gl-link :href="refPath" class="gl-text-gray-900">{{ refDisplay }}</gl-link>
</span>
+ <span v-else>
+ {{ s__('PipelineSchedules|None') }}
+ </span>
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue b/app/assets/javascripts/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue
deleted file mode 100644
index b4d84309c5f..00000000000
--- a/app/assets/javascripts/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-<script>
-import { GlModal } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
-
-export default {
- components: {
- GlModal,
- },
- props: {
- ownershipUrl: {
- type: String,
- required: true,
- },
- },
- modalId: 'pipeline-take-ownership-modal',
- i18n: {
- takeOwnership: s__('PipelineSchedules|Take ownership'),
- ownershipMessage: s__(
- 'PipelineSchedules|Only the owner of a pipeline schedule can make changes to it. Do you want to take ownership of this schedule?',
- ),
- cancelLabel: __('Cancel'),
- },
- computed: {
- actionCancel() {
- return { text: this.$options.i18n.cancelLabel };
- },
- actionPrimary() {
- return {
- text: this.$options.i18n.takeOwnership,
- attributes: {
- variant: 'confirm',
- category: 'primary',
- href: this.ownershipUrl,
- 'data-method': 'post',
- },
- };
- },
- },
-};
-</script>
-<template>
- <gl-modal
- :modal-id="$options.modalId"
- :action-primary="actionPrimary"
- :action-cancel="actionCancel"
- :title="$options.i18n.takeOwnership"
- >
- <p>{{ $options.i18n.ownershipMessage }}</p>
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/ci_templates.vue
index 439dc0eb253..439dc0eb253 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/empty_state/ci_templates.vue
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue
index 5208f9a3ce7..1a2021df9c8 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue
@@ -4,7 +4,7 @@ import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import { mergeUrlParams, DOCS_URL } from '~/lib/utils/url_utility';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
-import apolloProvider from '~/pipelines/graphql/provider';
+import apolloProvider from '~/ci/pipeline_details/graphql/provider';
import CiTemplates from './ci_templates.vue';
export default {
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
index 3bbdfc73e1b..6e7d6908cd9 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
@@ -2,8 +2,8 @@
import { GlEmptyState } from '@gitlab/ui';
import { s__ } from '~/locale';
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
-import PipelinesCiTemplates from './empty_state/pipelines_ci_templates.vue';
-import IosTemplates from './empty_state/ios_templates.vue';
+import PipelinesCiTemplates from './pipelines_ci_templates.vue';
+import IosTemplates from './ios_templates.vue';
export default {
i18n: {
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue
index a6297213402..a6297213402 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue
index edf4cc87a87..82f1d57912a 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue
@@ -5,8 +5,8 @@ import { __, s__, sprintf } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
-import { BRIDGE_KIND } from '~/pipelines/components/graph/constants';
-import RetryMrFailedJobMutation from '../../../graphql/mutations/retry_mr_failed_job.mutation.graphql';
+import { BRIDGE_KIND } from '~/ci/pipeline_details/graph/constants';
+import RetryMrFailedJobMutation from '~/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql';
export default {
components: {
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_jobs_list.vue
index 2c5aa84bc4f..138269bdb8a 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_jobs_list.vue
@@ -2,9 +2,10 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { __, s__, sprintf } from '~/locale';
-import { getQueryHeaders } from '~/pipelines/components/graph/utils';
-import getPipelineFailedJobs from '../../../graphql/queries/get_pipeline_failed_jobs.query.graphql';
-import { graphqlEtagPipelinePath, sortJobsByStatus } from './utils';
+import { getQueryHeaders } from '~/ci/pipeline_details/graph/utils';
+import { graphqlEtagPipelinePath } from '~/ci/pipeline_details/utils';
+import getPipelineFailedJobs from '~/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs.query.graphql';
+import { sortJobsByStatus } from './utils';
import FailedJobDetails from './failed_job_details.vue';
const POLL_INTERVAL = 10000;
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue
index 60c429459bf..c01037e9791 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue
@@ -91,7 +91,7 @@ export default {
<template #header>
<gl-button
variant="link"
- class="gl-text-gray-700! gl-font-weight-semibold"
+ class="gl-text-gray-500! gl-font-weight-semibold"
@click="toggleWidget"
>
<gl-icon :name="iconName" />
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/utils.js b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/utils.js
index 2d0c467c54f..3f395fff7e0 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/utils.js
+++ b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/utils.js
@@ -13,7 +13,3 @@ export const sortJobsByStatus = (jobs = []) => {
return 1;
});
};
-
-export const graphqlEtagPipelinePath = (graphqlPath, pipelineId) => {
- return `${graphqlPath}pipelines/id/${pipelineId}`;
-};
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue b/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue
index 235126fea0c..235126fea0c 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_labels.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
index 40b2454b8c1..082ede60244 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_labels.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { SCHEDULE_ORIGIN } from '../../constants';
+import { SCHEDULE_ORIGIN } from '~/ci/pipeline_details/constants';
export default {
components: {
@@ -54,7 +54,7 @@ export default {
v-gl-tooltip
:href="pipelineScheduleUrl"
target="__blank"
- :title="__('This pipeline was triggered by a schedule.')"
+ :title="__('This pipeline was created by a schedule.')"
variant="info"
size="sm"
data-testid="pipeline-url-scheduled"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_multi_actions.vue
index 747d94d92f2..78acead95f4 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_multi_actions.vue
@@ -1,8 +1,7 @@
<script>
import {
GlAlert,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
GlSearchBoxByType,
GlLoadingIcon,
GlTooltipDirective,
@@ -14,6 +13,7 @@ import Tracking from '~/tracking';
import { TRACKING_CATEGORIES } from '../../constants';
export const i18n = {
+ searchPlaceholder: __('Search artifacts'),
downloadArtifacts: __('Download artifacts'),
artifactsFetchErrorMessage: s__('Pipelines|Could not load artifacts.'),
artifactsFetchWarningMessage: s__(
@@ -29,8 +29,7 @@ export default {
},
components: {
GlAlert,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
GlSearchBoxByType,
GlLoadingIcon,
},
@@ -67,6 +66,17 @@ export default {
? fuzzaldrinPlus.filter(this.artifacts, this.searchQuery, { key: 'name' })
: this.artifacts;
},
+ items() {
+ return this.filteredArtifacts.map(({ name, path }) => ({
+ text: name,
+ href: path,
+ extraAttrs: {
+ download: '',
+ rel: 'nofollow',
+ 'data-testid': 'artifact-item',
+ },
+ }));
+ },
},
watch: {
pipelineId() {
@@ -107,66 +117,73 @@ export default {
this.isLoading = false;
});
},
- handleDropdownShown() {
- if (this.hasArtifacts) {
- this.$refs.searchInput.focusInput();
- }
+ onDisclosureDropdownShown() {
+ this.fetchArtifacts();
+ },
+ onDisclosureDropdownHidden() {
+ this.searchQuery = '';
},
},
};
</script>
<template>
- <gl-dropdown
+ <gl-disclosure-dropdown
v-gl-tooltip
+ class="gl-text-left"
:title="$options.i18n.downloadArtifacts"
- :text="$options.i18n.downloadArtifacts"
+ :toggle-text="$options.i18n.downloadArtifacts"
:aria-label="$options.i18n.downloadArtifacts"
- :header-text="$options.i18n.downloadArtifacts"
+ :items="items"
icon="download"
- data-testid="pipeline-multi-actions-dropdown"
- right
- lazy
+ placement="right"
text-sr-only
- @show="fetchArtifacts"
- @shown="handleDropdownShown"
+ data-testid="pipeline-multi-actions-dropdown"
+ @shown="onDisclosureDropdownShown"
+ @hidden="onDisclosureDropdownHidden"
>
- <gl-alert v-if="hasError && !hasArtifacts" variant="danger" :dismissible="false">
- {{ $options.i18n.artifactsFetchErrorMessage }}
- </gl-alert>
-
- <gl-loading-icon v-else-if="isLoading" size="sm" />
-
- <gl-dropdown-item v-else-if="!hasArtifacts" data-testid="artifacts-empty-message">
- {{ $options.i18n.emptyArtifactsMessage }}
- </gl-dropdown-item>
-
<template #header>
- <gl-search-box-by-type v-if="hasArtifacts" ref="searchInput" v-model.trim="searchQuery" />
+ <div
+ aria-hidden="true"
+ class="gl-display-flex gl-align-items-center gl-p-4! gl-min-h-8 gl-font-sm gl-font-weight-bold gl-text-gray-900 gl-border-b-1 gl-border-b-solid gl-border-b-gray-200"
+ >
+ {{ $options.i18n.downloadArtifacts }}
+ </div>
+ <div v-if="hasArtifacts" class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-200">
+ <gl-search-box-by-type
+ ref="searchInput"
+ v-model.trim="searchQuery"
+ :placeholder="$options.i18n.searchPlaceholder"
+ borderless
+ autofocus
+ />
+ </div>
+ <gl-alert
+ v-if="hasError && !hasArtifacts"
+ variant="danger"
+ :dismissible="false"
+ data-testid="artifacts-fetch-error"
+ >
+ {{ $options.i18n.artifactsFetchErrorMessage }}
+ </gl-alert>
</template>
- <gl-dropdown-item
- v-for="(artifact, i) in filteredArtifacts"
- :key="i"
- :href="artifact.path"
- rel="nofollow"
- download
- class="gl-word-break-word"
- data-testid="artifact-item"
+ <gl-loading-icon v-if="isLoading" class="gl-m-3" size="sm" />
+ <p
+ v-else-if="filteredArtifacts.length === 0"
+ class="gl-px-4 gl-py-3 gl-m-0 gl-text-gray-600"
+ data-testid="artifacts-empty-message"
>
- {{ artifact.name }}
- </gl-dropdown-item>
+ {{ $options.i18n.emptyArtifactsMessage }}
+ </p>
<template #footer>
- <gl-dropdown-item
+ <p
v-if="hasError && hasArtifacts"
- class="gl-list-style-none"
- disabled
+ class="gl-font-sm gl-text-secondary gl-py-4 gl-px-5 gl-mb-0 gl-border-t"
data-testid="artifacts-fetch-warning"
>
- <span class="gl-font-sm">
- {{ $options.i18n.artifactsFetchWarningMessage }}
- </span>
- </gl-dropdown-item>
+ {{ $options.i18n.artifactsFetchWarningMessage }}
+ </p>
</template>
- </gl-dropdown>
+ </gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue
index caeee7edefe..b05bdae65c4 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue
@@ -1,8 +1,8 @@
<script>
import { GlButton, GlTooltipDirective, GlModalDirective } 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 { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL, TRACKING_CATEGORIES } from '../../constants';
import PipelineMultiActions from './pipeline_multi_actions.vue';
import PipelinesManualActions from './pipelines_manual_actions.vue';
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
index 9f38be668f2..9f38be668f2 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue
index 2a73795db0a..2a73795db0a 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue
index ff1a01d5037..edaeb481d7b 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/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 '../../constants';
+import { ICONS, TRACKING_CATEGORIES } from '~/ci/constants';
import PipelineLabels from './pipeline_labels.vue';
export default {
@@ -151,7 +151,10 @@ export default {
<div v-if="!pipelineName" class="commit-title gl-mb-2" data-testid="commit-title-container">
<span v-if="commitTitle" class="gl-display-flex">
- <tooltip-on-truncate :title="commitTitle" class="gl-flex-grow-1 gl-text-truncate">
+ <tooltip-on-truncate
+ :title="commitTitle"
+ class="gl-flex-grow-1 gl-text-truncate gl-p-3 gl-ml-n3 gl-mr-n3 gl-mt-n3 gl-mb-n3"
+ >
<gl-link
:href="commitUrl"
class="commit-row-message gl-text-blue-600!"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue b/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue
index 4452db64b0a..3021b4a2ef8 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue
@@ -60,7 +60,7 @@ export default {
<gl-disclosure-dropdown
v-if="shouldShowDropdown"
v-gl-tooltip
- class="build-artifacts js-pipeline-dropdown-download"
+ class="gl-text-left"
:title="$options.i18n.artifacts"
:toggle-text="$options.i18n.artifacts"
:aria-label="$options.i18n.artifacts"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue b/app/assets/javascripts/ci/pipelines_page/components/pipelines_filtered_search.vue
index 7dc1e60610e..6aadb6b73c8 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipelines_filtered_search.vue
@@ -5,11 +5,11 @@ import { s__ } from '~/locale';
import Tracking from '~/tracking';
import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { TRACKING_CATEGORIES } from '../../constants';
-import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue';
-import PipelineSourceToken from './tokens/pipeline_source_token.vue';
-import PipelineStatusToken from './tokens/pipeline_status_token.vue';
-import PipelineTagNameToken from './tokens/pipeline_tag_name_token.vue';
-import PipelineTriggerAuthorToken from './tokens/pipeline_trigger_author_token.vue';
+import PipelineBranchNameToken from '../tokens/pipeline_branch_name_token.vue';
+import PipelineSourceToken from '../tokens/pipeline_source_token.vue';
+import PipelineStatusToken from '../tokens/pipeline_status_token.vue';
+import PipelineTagNameToken from '../tokens/pipeline_tag_name_token.vue';
+import PipelineTriggerAuthorToken from '../tokens/pipeline_trigger_author_token.vue';
export default {
userType: 'username',
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue b/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue
index 262e82677a7..4dacd474bde 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue
@@ -8,7 +8,7 @@ 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';
+import getPipelineActionsQuery from '../graphql/queries/get_pipeline_actions.query.graphql';
export default {
name: 'PipelinesManualActions',
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue b/app/assets/javascripts/ci/pipelines_page/components/pipelines_status_badge.vue
index 00ab8a25ca1..2da9141df8e 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipelines_status_badge.vue
@@ -1,5 +1,6 @@
<script>
-import { CHILD_VIEW, TRACKING_CATEGORIES } from '~/pipelines/constants';
+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';
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue b/app/assets/javascripts/ci/pipelines_page/components/time_ago.vue
index 70343544638..70343544638 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/time_ago.vue
diff --git a/app/assets/javascripts/ci/pipelines_page/constants.js b/app/assets/javascripts/ci/pipelines_page/constants.js
new file mode 100644
index 00000000000..aa6ef8a25ee
--- /dev/null
+++ b/app/assets/javascripts/ci/pipelines_page/constants.js
@@ -0,0 +1,2 @@
+export const ANY_TRIGGER_AUTHOR = 'Any';
+export const FILTER_PIPELINES_SEARCH_DELAY = 200;
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_actions.query.graphql b/app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_actions.query.graphql
index d1878c01e91..d1878c01e91 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_actions.query.graphql
+++ b/app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_actions.query.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_failed_jobs.query.graphql b/app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs.query.graphql
index 6b553866f63..6b553866f63 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_failed_jobs.query.graphql
+++ b/app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs.query.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_failed_jobs_count.query.graphql b/app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs_count.query.graphql
index b70e95deab6..b70e95deab6 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_failed_jobs_count.query.graphql
+++ b/app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs_count.query.graphql
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
index 574d291a767..87ee5463bb0 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
@@ -7,29 +7,29 @@ import { createAlert, VARIANT_INFO, VARIANT_WARNING } from '~/alert';
import { getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
-import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
-import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
-import { isLoggedIn } from '~/lib/utils/common_utils';
-import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
import {
- ANY_TRIGGER_AUTHOR,
- RAW_TEXT_WARNING,
FILTER_TAG_IDENTIFIER,
PipelineKeyOptions,
+ RAW_TEXT_WARNING,
TRACKING_CATEGORIES,
-} from '../../constants';
-import PipelinesMixin from '../../mixins/pipelines_mixin';
-import PipelinesService from '../../services/pipelines_service';
-import { validateParams } from '../../utils';
-import EmptyState from './empty_state.vue';
-import NavigationControls from './nav_controls.vue';
-import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
-import PipelinesTableComponent from './pipelines_table.vue';
+} from '~/ci/constants';
+import PipelinesTableComponent 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';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
+import { isLoggedIn } from '~/lib/utils/common_utils';
+import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
+import PipelinesService from './services/pipelines_service';
+import { ANY_TRIGGER_AUTHOR } from './constants';
+import NoCiEmptyState from './components/empty_state/no_ci_empty_state.vue';
+import NavigationControls from './components/nav_controls.vue';
+import PipelinesFilteredSearch from './components/pipelines_filtered_search.vue';
export default {
PipelineKeyOptions,
components: {
- EmptyState,
+ NoCiEmptyState,
GlCollapsibleListbox,
GlEmptyState,
GlIcon,
@@ -409,7 +409,7 @@ export default {
class="prepend-top-20"
/>
- <empty-state
+ <no-ci-empty-state
v-else-if="stateToRender === $options.stateMap.emptyState"
:empty-state-svg-path="emptyStateSvgPath"
:can-set-ci="canCreatePipeline"
@@ -426,6 +426,7 @@ export default {
<gl-empty-state
v-else-if="stateToRender === $options.stateMap.emptyTab"
:svg-path="noPipelinesSvgPath"
+ :svg-height="150"
:title="emptyTabMessage"
/>
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/ci/pipelines_page/services/pipelines_service.js
index 3ec563c95bb..c38fa07c7e3 100644
--- a/app/assets/javascripts/pipelines/services/pipelines_service.js
+++ b/app/assets/javascripts/ci/pipelines_page/services/pipelines_service.js
@@ -1,6 +1,6 @@
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
-import { validateParams } from '../utils';
+import { validateParams } from '../../pipeline_details/utils';
export default class PipelinesService {
/**
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js b/app/assets/javascripts/ci/pipelines_page/tokens/constants.js
index d8f15cfde91..d8f15cfde91 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js
+++ b/app/assets/javascripts/ci/pipelines_page/tokens/constants.js
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_branch_name_token.vue
index 81f46d5f2f9..45b6fb380a9 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
+++ b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_branch_name_token.vue
@@ -3,7 +3,8 @@ import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from
import { debounce } from 'lodash';
import Api from '~/api';
import { createAlert } from '~/alert';
-import { FETCH_BRANCH_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
+import { __ } from '~/locale';
+import { FILTER_PIPELINES_SEARCH_DELAY } from '../constants';
export default {
components: {
@@ -46,7 +47,7 @@ export default {
})
.catch((err) => {
createAlert({
- message: FETCH_BRANCH_ERROR_MESSAGE,
+ message: __('There was a problem fetching project branches.'),
});
this.loading = false;
throw err;
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_source_token.vue
index 9643ddfbd21..b4b5c5c1b37 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue
+++ b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_source_token.vue
@@ -1,6 +1,6 @@
<script>
import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
-import { PIPELINE_SOURCES } from 'ee_else_ce/pipelines/components/pipelines_list/tokens/constants';
+import { PIPELINE_SOURCES } from 'ee_else_ce/ci/pipelines_page/tokens/constants';
export default {
PIPELINE_SOURCES,
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_status_token.vue
index 020a08b8cee..020a08b8cee 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue
+++ b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_status_token.vue
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_tag_name_token.vue
index b32f5de2d7e..a6034e78b6d 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
+++ b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_tag_name_token.vue
@@ -3,7 +3,8 @@ import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from
import { debounce } from 'lodash';
import Api from '~/api';
import { createAlert } from '~/alert';
-import { FETCH_TAG_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
+import { __ } from '~/locale';
+import { FILTER_PIPELINES_SEARCH_DELAY } from '../constants';
export default {
components: {
@@ -39,7 +40,7 @@ export default {
})
.catch((err) => {
createAlert({
- message: FETCH_TAG_ERROR_MESSAGE,
+ message: __('There was a problem fetching project tags.'),
});
this.loading = false;
throw err;
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_trigger_author_token.vue
index a89354c671a..20c5e1557a7 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue
+++ b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_trigger_author_token.vue
@@ -9,11 +9,8 @@ import {
import { debounce } from 'lodash';
import Api from '~/api';
import { createAlert } from '~/alert';
-import {
- ANY_TRIGGER_AUTHOR,
- FETCH_AUTHOR_ERROR_MESSAGE,
- FILTER_PIPELINES_SEARCH_DELAY,
-} from '../../../constants';
+import { __ } from '~/locale';
+import { ANY_TRIGGER_AUTHOR, FILTER_PIPELINES_SEARCH_DELAY } from '../constants';
export default {
anyTriggerAuthor: ANY_TRIGGER_AUTHOR,
@@ -62,7 +59,7 @@ export default {
})
.catch((err) => {
createAlert({
- message: FETCH_AUTHOR_ERROR_MESSAGE,
+ message: __('There was a problem fetching project users.'),
});
this.loading = false;
throw err;
diff --git a/app/assets/javascripts/ci/reports/components/issue_status_icon.vue b/app/assets/javascripts/ci/reports/components/issue_status_icon.vue
index bd41b8d23f1..f2346a5512e 100644
--- a/app/assets/javascripts/ci/reports/components/issue_status_icon.vue
+++ b/app/assets/javascripts/ci/reports/components/issue_status_icon.vue
@@ -22,7 +22,8 @@ export default {
iconName() {
if (this.isStatusFailed) {
return 'status_failed_borderless';
- } else if (this.isStatusSuccess) {
+ }
+ if (this.isStatusSuccess) {
return 'status_success_borderless';
}
@@ -49,6 +50,6 @@ export default {
}"
class="report-block-list-icon"
>
- <gl-icon :name="iconName" :size="statusIconSize" :data-qa-selector="`status_${status}_icon`" />
+ <gl-icon :name="iconName" :size="statusIconSize" />
</div>
</template>
diff --git a/app/assets/javascripts/ci/reports/components/report_section.vue b/app/assets/javascripts/ci/reports/components/report_section.vue
index a4ec7b6a325..fd6c6cca6b7 100644
--- a/app/assets/javascripts/ci/reports/components/report_section.vue
+++ b/app/assets/javascripts/ci/reports/components/report_section.vue
@@ -159,7 +159,8 @@ export default {
slotName() {
if (this.isSuccess) {
return SLOT_SUCCESS;
- } else if (this.isLoading) {
+ }
+ if (this.isLoading) {
return SLOT_LOADING;
}
diff --git a/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
index e6813211fe9..0ec94dc865f 100644
--- a/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
@@ -178,6 +178,22 @@ export default {
</script>
<template>
<div>
+ <header class="gl-my-5 gl-display-flex gl-justify-content-space-between">
+ <h2 class="gl-my-0 header-title">
+ {{ s__('Runners|Runners') }}
+ </h2>
+ <div class="gl-display-flex gl-gap-3">
+ <runner-dashboard-link />
+ <gl-button :href="newRunnerPath" variant="confirm">
+ {{ s__('Runners|New instance runner') }}
+ </gl-button>
+ <registration-dropdown
+ :registration-token="registrationToken"
+ :type="$options.INSTANCE_TYPE"
+ placement="right"
+ />
+ </div>
+ </header>
<div
class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
>
@@ -189,18 +205,6 @@ export default {
content-class="gl-display-none"
nav-class="gl-border-none!"
/>
-
- <div class="gl-w-full gl-md-w-auto gl-display-flex gl-gap-3">
- <runner-dashboard-link />
- <gl-button :href="newRunnerPath" variant="confirm">
- {{ s__('Runners|New instance runner') }}
- </gl-button>
- <registration-dropdown
- :registration-token="registrationToken"
- :type="$options.INSTANCE_TYPE"
- placement="right"
- />
- </div>
</div>
<runner-filtered-search-bar
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_owner_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_owner_cell.vue
index cb43760b2d6..8f1c7234b84 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_owner_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_owner_cell.vue
@@ -50,12 +50,7 @@ export default {
<template>
<div>
- <gl-link
- v-if="cell.href"
- v-gl-tooltip="cell.tooltip"
- :href="cell.href"
- class="gl-text-body gl-text-decoration-underline"
- >
+ <gl-link v-if="cell.href" v-gl-tooltip="cell.tooltip" :href="cell.href" class="gl-text-body">
{{ cell.text }}
</gl-link>
<span v-else>{{ cell.text }}</span>
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
index cc31afea88c..a80d6207be8 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
@@ -12,7 +12,6 @@ import RunnerManagersBadge from '../runner_managers_badge.vue';
import { formatJobCount } from '../../utils';
import {
- I18N_NO_DESCRIPTION,
I18N_LOCKED_RUNNER_DESCRIPTION,
I18N_VERSION_LABEL,
I18N_LAST_CONTACT_LABEL,
@@ -73,7 +72,6 @@ export default {
formatNumber,
},
i18n: {
- I18N_NO_DESCRIPTION,
I18N_LOCKED_RUNNER_DESCRIPTION,
I18N_VERSION_LABEL,
I18N_LAST_CONTACT_LABEL,
@@ -100,7 +98,10 @@ export default {
<runner-type-badge :type="runner.runnerType" size="sm" class="gl-vertical-align-middle" />
</div>
- <div class="gl-mb-3 gl-ml-auto gl-display-inline-flex gl-max-w-full">
+ <div
+ v-if="runner.version || runner.description"
+ class="gl-mb-3 gl-ml-auto gl-display-inline-flex gl-max-w-full gl-font-sm gl-align-items-center"
+ >
<template v-if="runner.version">
<div class="gl-flex-shrink-0">
<runner-upgrade-status-icon :upgrade-status="runner.upgradeStatus" />
@@ -108,19 +109,20 @@ export default {
<template #version>{{ runner.version }}</template>
</gl-sprintf>
</div>
- <div class="gl-text-secondary gl-mx-2" aria-hidden="true">·</div>
+ <div v-if="runner.description" class="gl-text-secondary gl-mx-2" aria-hidden="true">·</div>
</template>
<tooltip-on-truncate
+ v-if="runner.description"
class="gl-text-truncate gl-display-block"
:class="{ 'gl-text-secondary': !runner.description }"
:title="runner.description"
>
- {{ runner.description || $options.i18n.I18N_NO_DESCRIPTION }}
+ {{ runner.description }}
</tooltip-on-truncate>
</div>
- <div>
- <runner-summary-field icon="clock">
+ <div class="gl-font-sm">
+ <runner-summary-field icon="clock" icon-size="sm">
<gl-sprintf :message="$options.i18n.I18N_LAST_CONTACT_LABEL">
<template #timeAgo>
<time-ago v-if="runner.contactedAt" :time="runner.contactedAt" />
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue
index 742259ee491..b1b61e03eec 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue
@@ -25,7 +25,7 @@ export default {
<template>
<div v-gl-tooltip="tooltip" class="gl-display-inline-block gl-text-secondary gl-mb-3 gl-mr-4">
- <gl-icon v-if="icon" :name="icon" />
+ <gl-icon v-if="icon" :name="icon" :size="12" />
<!-- display tooltip as a label for screen readers -->
<span class="gl-sr-only">{{ tooltip }}</span>
<slot></slot>
diff --git a/app/assets/javascripts/ci/runner/components/runner_create_form.vue b/app/assets/javascripts/ci/runner/components/runner_create_form.vue
index 1b363174d28..adaed77055a 100644
--- a/app/assets/javascripts/ci/runner/components/runner_create_form.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_create_form.vue
@@ -120,7 +120,7 @@ export default {
</script>
<template>
<gl-form @submit.prevent="onSubmit">
- <runner-form-fields v-model="runner" />
+ <runner-form-fields v-model="runner" :runner-type="runnerType" />
<div class="gl-display-flex gl-mt-6">
<gl-button type="submit" variant="confirm" class="js-no-auto-disable" :loading="saving">
diff --git a/app/assets/javascripts/ci/runner/components/runner_filtered_search_bar.vue b/app/assets/javascripts/ci/runner/components/runner_filtered_search_bar.vue
index 3634dcf1c93..81b2a17631e 100644
--- a/app/assets/javascripts/ci/runner/components/runner_filtered_search_bar.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_filtered_search_bar.vue
@@ -92,7 +92,6 @@ export default {
:initial-filter-value="initialFilterValue"
:tokens="validTokens"
:initial-sort-by="initialSortBy"
- :search-input-placeholder="__('Search or filter results...')"
:search-text-option-label="s__('Runners|Search description...')"
terms-as-tokens
data-testid="runners-filtered-search"
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 d090a562ff7..38e36733045 100644
--- a/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
@@ -10,7 +10,12 @@ import {
GlSkeletonLoader,
} from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants';
+import {
+ ACCESS_LEVEL_NOT_PROTECTED,
+ ACCESS_LEVEL_REF_PROTECTED,
+ PROJECT_TYPE,
+ RUNNER_TYPES,
+} from '../constants';
export default {
name: 'RunnerFormFields',
@@ -26,6 +31,12 @@ export default {
import('ee_component/ci/runner/components/runner_maintenance_note_field.vue'),
},
props: {
+ runnerType: {
+ type: String,
+ required: false,
+ default: null,
+ validator: (t) => RUNNER_TYPES.includes(t),
+ },
value: {
type: Object,
default: null,
@@ -44,7 +55,7 @@ export default {
},
computed: {
canBeLockedToProject() {
- return this.value?.runnerType === PROJECT_TYPE;
+ return this.runnerType === PROJECT_TYPE;
},
},
watch: {
diff --git a/app/assets/javascripts/ci/runner/components/runner_managers_table.vue b/app/assets/javascripts/ci/runner/components/runner_managers_table.vue
index 10790c398b0..58244e1f2df 100644
--- a/app/assets/javascripts/ci/runner/components/runner_managers_table.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_managers_table.vue
@@ -6,6 +6,7 @@ import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { tableField } from '../utils';
import { I18N_STATUS_NEVER_CONTACTED } from '../constants';
import RunnerStatusBadge from './runner_status_badge.vue';
+import RunnerJobStatusBadge from './runner_job_status_badge.vue';
export default {
name: 'RunnerManagersTable',
@@ -15,6 +16,7 @@ export default {
HelpPopover,
GlIntersperse,
RunnerStatusBadge,
+ RunnerJobStatusBadge,
RunnerUpgradeStatusIcon: () =>
import('ee_component/ci/runner/components/runner_upgrade_status_icon.vue'),
},
@@ -52,7 +54,15 @@ export default {
</help-popover>
</template>
<template #cell(status)="{ item = {} }">
- <runner-status-badge :contacted-at="item.contactedAt" :status="item.status" />
+ <runner-status-badge
+ class="gl-vertical-align-middle"
+ :contacted-at="item.contactedAt"
+ :status="item.status"
+ />
+ <runner-job-status-badge
+ class="gl-vertical-align-middle"
+ :job-status="item.jobExecutionStatus"
+ />
</template>
<template #cell(version)="{ item = {} }">
{{ item.version }}
diff --git a/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue b/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue
index a49641194a7..a841f66b566 100644
--- a/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue
@@ -55,7 +55,7 @@ export default {
<div class="gl-mt-3 gl-mb-6">
<label>{{ s__('Runners|Operating systems') }}</label>
- <div class="gl-display-flex gl-flex-wrap gl-gap-5">
+ <div class="gl-display-flex gl-flex-wrap gl-gap-3">
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
<runner-platforms-radio
v-model="model"
@@ -76,7 +76,7 @@ export default {
<div class="gl-mt-3 gl-mb-6">
<label>{{ s__('Runners|Containers') }}</label>
- <div class="gl-display-flex gl-flex-wrap gl-gap-5">
+ <div class="gl-display-flex gl-flex-wrap gl-gap-3">
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
<runner-platforms-radio :image="$options.DOCKER_LOGO_URL">
<gl-link :href="$options.DOCKER_HELP_URL" target="_blank">
diff --git a/app/assets/javascripts/ci/runner/components/runner_type_tabs.vue b/app/assets/javascripts/ci/runner/components/runner_type_tabs.vue
index 70226074993..3af10c59e31 100644
--- a/app/assets/javascripts/ci/runner/components/runner_type_tabs.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_type_tabs.vue
@@ -112,7 +112,12 @@ export default {
:scope="countScope"
:variables="tabBadgeCountVariables(tab.runnerType)"
>
- <gl-badge v-if="tabCount(count)" class="gl-ml-1" size="sm">
+ <gl-badge
+ v-if="tabCount(count)"
+ class="gl-ml-1"
+ size="sm"
+ :data-testid="`runner-count-${tab.title.toLowerCase()}`"
+ >
{{ tabCount(count) }}
</gl-badge>
</runner-count>
diff --git a/app/assets/javascripts/ci/runner/components/runner_update_form.vue b/app/assets/javascripts/ci/runner/components/runner_update_form.vue
index 6b94e594f1c..4278615ba66 100644
--- a/app/assets/javascripts/ci/runner/components/runner_update_form.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_update_form.vue
@@ -96,7 +96,7 @@ export default {
</script>
<template>
<gl-form @submit.prevent="onSubmit">
- <runner-form-fields v-model="model" :loading="loading" />
+ <runner-form-fields v-model="model" :loading="loading" :runner-type="runnerType" />
<runner-update-cost-factor-fields v-model="model" :runner-type="runnerType" />
<div class="gl-mt-6">
diff --git a/app/assets/javascripts/ci/runner/components/stat/runner_count.vue b/app/assets/javascripts/ci/runner/components/stat/runner_count.vue
index c33c42f3afe..cee1088d90b 100644
--- a/app/assets/javascripts/ci/runner/components/stat/runner_count.vue
+++ b/app/assets/javascripts/ci/runner/components/stat/runner_count.vue
@@ -55,7 +55,8 @@ export default {
query() {
if (this.scope === INSTANCE_TYPE) {
return allRunnersCountQuery;
- } else if (this.scope === GROUP_TYPE) {
+ }
+ if (this.scope === GROUP_TYPE) {
return groupRunnersCountQuery;
}
return null;
@@ -74,7 +75,8 @@ export default {
update(data) {
if (this.scope === INSTANCE_TYPE) {
return data?.runners?.count;
- } else if (this.scope === GROUP_TYPE) {
+ }
+ if (this.scope === GROUP_TYPE) {
return data?.group?.runners?.count;
}
return null;
diff --git a/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue b/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue
index e0a6f4b1e67..6c49263ac82 100644
--- a/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue
+++ b/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue
@@ -90,6 +90,7 @@ export default {
:scope="scope"
v-bind="stat.props"
class="gl-px-5"
+ :data-testid="`runner-stats-${stat.key.toLowerCase()}`"
/>
<runner-upgrade-status-stats
diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js
index 203f97876de..3293c68ddb8 100644
--- a/app/assets/javascripts/ci/runner/constants.js
+++ b/app/assets/javascripts/ci/runner/constants.js
@@ -15,7 +15,7 @@ export const I18N_CREATE_ERROR = s__(
);
export const FILTER_CSS_CLASSES =
- 'gl-bg-gray-10 gl-p-5 gl-border-solid gl-border-gray-100 gl-border-0 gl-border-t-1 gl-border-b-1';
+ 'gl-bg-gray-10 gl-p-5 gl-border-solid gl-border-gray-100 gl-border-0 gl-border-t-1';
// Type
@@ -96,7 +96,6 @@ export const I18N_DELETE_RUNNER = s__('Runners|Delete runner');
export const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted');
// List
-export const I18N_NO_DESCRIPTION = s__('Runners|No description');
export const I18N_LOCKED_RUNNER_DESCRIPTION = s__(
'Runners|Runner is locked and available for currently assigned projects only. Only administrators can change the assigned projects.',
);
diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql
index ead005d1252..84d32e24f24 100644
--- a/app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql
@@ -9,4 +9,5 @@ fragment CiRunnerManagerShared on CiRunnerManager {
platformName
ipAddress
contactedAt
+ jobExecutionStatus
}
diff --git a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
index 71584c40a38..dcaf8635f5c 100644
--- a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
@@ -212,6 +212,27 @@ export default {
<template>
<div>
+ <header class="gl-my-5 gl-display-flex gl-justify-content-space-between">
+ <h2 class="gl-my-0 header-title">
+ {{ s__('Runners|Runners') }}
+ </h2>
+ <div class="gl-display-flex gl-gap-3">
+ <gl-button
+ v-if="newRunnerPath"
+ :href="newRunnerPath"
+ variant="confirm"
+ data-testid="new-group-runner-button"
+ >
+ {{ s__('Runners|New group runner') }}
+ </gl-button>
+ <registration-dropdown
+ v-if="registrationToken"
+ :registration-token="registrationToken"
+ :type="$options.GROUP_TYPE"
+ placement="right"
+ />
+ </div>
+ </header>
<div
class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
>
@@ -225,19 +246,6 @@ export default {
content-class="gl-display-none"
nav-class="gl-border-none!"
/>
-
- <div class="gl-w-full gl-md-w-auto gl-display-flex">
- <gl-button v-if="newRunnerPath" :href="newRunnerPath" variant="confirm">
- {{ s__('Runners|New group runner') }}
- </gl-button>
- <registration-dropdown
- v-if="registrationToken"
- class="gl-ml-3"
- :registration-token="registrationToken"
- :type="$options.GROUP_TYPE"
- placement="right"
- />
- </div>
</div>
<div
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-gap-3"
diff --git a/app/assets/javascripts/ci/runner/project_runners/index.js b/app/assets/javascripts/ci/runner/project_runners/index.js
deleted file mode 100644
index 3be2b4a7422..00000000000
--- a/app/assets/javascripts/ci/runner/project_runners/index.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import Vue from 'vue';
-import ProjectRunnersApp from './project_runners_app.vue';
-
-export const initProjectRunners = (selector = '#js-project-runners') => {
- const el = document.querySelector(selector);
-
- if (!el) {
- return null;
- }
-
- const { projectFullPath } = el.dataset;
-
- return new Vue({
- el,
- render(h) {
- return h(ProjectRunnersApp, {
- props: {
- projectFullPath,
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/ci/runner/project_runners/project_runners_app.vue b/app/assets/javascripts/ci/runner/project_runners/project_runners_app.vue
deleted file mode 100644
index c7bf5e521a1..00000000000
--- a/app/assets/javascripts/ci/runner/project_runners/project_runners_app.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-<script>
-export default {
- props: {
- projectFullPath: {
- required: true,
- type: String,
- },
- },
-};
-</script>
-<template>
- <div>
- <!--
- Under development
- Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/33803
- Feature rollout: https://gitlab.com/gitlab-org/gitlab/-/issues/386573
- -->
- </div>
-</template>
diff --git a/app/assets/javascripts/ci/runner/runner_search_utils.js b/app/assets/javascripts/ci/runner/runner_search_utils.js
index 3dc99baa329..8915198350f 100644
--- a/app/assets/javascripts/ci/runner/runner_search_utils.js
+++ b/app/assets/javascripts/ci/runner/runner_search_utils.js
@@ -97,7 +97,8 @@ const outdatedStatusParams = (status) => {
[PARAM_KEY_PAUSED]: ['false'],
[PARAM_KEY_STATUS]: [], // Important! clear PARAM_KEY_STATUS to avoid a redirection loop!
};
- } else if (status === STATUS_PAUSED) {
+ }
+ if (status === STATUS_PAUSED) {
return {
[PARAM_KEY_PAUSED]: ['true'],
[PARAM_KEY_STATUS]: [], // Important! clear PARAM_KEY_STATUS to avoid a redirection loop!
diff --git a/app/assets/javascripts/jobs/utils.js b/app/assets/javascripts/ci/utils.js
index a4e695518f1..eb9e9538b75 100644
--- a/app/assets/javascripts/jobs/utils.js
+++ b/app/assets/javascripts/ci/utils.js
@@ -1,18 +1,5 @@
import * as Sentry from '@sentry/browser';
-/**
- * capture anything starting with http:// or https://
- * https?:\/\/
- *
- * up until a disallowed character or whitespace
- * [^"<>()\\^`{|}\s]+
- *
- * and a disallowed character or whitespace, including non-ending chars .,:;!?
- * [^"<>()\\^`{|}\s.,:;!?]
- */
-export const linkRegex = /(https?:\/\/[^"<>()\\^`{|}\s]+[^"<>()\\^`{|}\s.,:;!?])/g;
-export default { linkRegex };
-
export const reportToSentry = (component, failureType) => {
Sentry.withScope((scope) => {
scope.setTag('component', component);
diff --git a/app/assets/javascripts/ci_secure_files/components/metadata/modal.vue b/app/assets/javascripts/ci_secure_files/components/metadata/modal.vue
index fdf720a5f94..cbb9c31b54b 100644
--- a/app/assets/javascripts/ci_secure_files/components/metadata/modal.vue
+++ b/app/assets/javascripts/ci_secure_files/components/metadata/modal.vue
@@ -101,10 +101,12 @@ export default {
if (this.fileExtension === 'cer') {
this.track('load_secure_file_metadata_cer');
return this.cerItems();
- } else if (this.fileExtension === 'p12') {
+ }
+ if (this.fileExtension === 'p12') {
this.track('load_secure_file_metadata_p12');
return this.p12Items();
- } else if (this.fileExtension === 'mobileprovision') {
+ }
+ if (this.fileExtension === 'mobileprovision') {
this.track('load_secure_file_metadata_mobileprovision');
return this.mobileprovisionItems(this.metadata);
}
diff --git a/app/assets/javascripts/clusters_list/components/agent_table.vue b/app/assets/javascripts/clusters_list/components/agent_table.vue
index 529be7169db..7482aaca36e 100644
--- a/app/assets/javascripts/clusters_list/components/agent_table.vue
+++ b/app/assets/javascripts/clusters_list/components/agent_table.vue
@@ -191,9 +191,11 @@ export default {
getVersionPopoverTitle(agent) {
if (this.isVersionMismatch(agent) && this.isVersionOutdated(agent)) {
return this.$options.i18n.versionMismatchOutdatedTitle;
- } else if (this.isVersionMismatch(agent)) {
+ }
+ if (this.isVersionMismatch(agent)) {
return this.$options.i18n.versionMismatchTitle;
- } else if (this.isVersionOutdated(agent)) {
+ }
+ if (this.isVersionOutdated(agent)) {
return this.$options.i18n.versionOutdatedTitle;
}
diff --git a/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue b/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue
index 75850cbb108..96ecbe9fa12 100644
--- a/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue
+++ b/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue
@@ -30,7 +30,8 @@ export default {
dropdownText() {
if (this.isRegistering) {
return this.$options.i18n.registeringAgent;
- } else if (this.selectedAgent === null) {
+ }
+ if (this.selectedAgent === null) {
return this.$options.i18n.selectAgent;
}
diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue
index 590fdb947b3..0258d8e0da0 100644
--- a/app/assets/javascripts/clusters_list/components/clusters.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters.vue
@@ -125,9 +125,11 @@ export default {
k8sQuantityToGb(quantity) {
if (!quantity) {
return 0;
- } else if (quantity.endsWith(__('Ki'))) {
+ }
+ if (quantity.endsWith(__('Ki'))) {
return parseInt(quantity.substr(0, quantity.length - 2), 10) * 0.000001024;
- } else if (quantity.endsWith(__('Mi'))) {
+ }
+ if (quantity.endsWith(__('Mi'))) {
return parseInt(quantity.substr(0, quantity.length - 2), 10) * 0.001048576;
}
@@ -138,9 +140,11 @@ export default {
k8sQuantityToCpu(quantity) {
if (!quantity) {
return 0;
- } else if (quantity.endsWith('m')) {
+ }
+ if (quantity.endsWith('m')) {
return parseInt(quantity.substr(0, quantity.length - 1), 10) / 1000.0;
- } else if (quantity.endsWith('n')) {
+ }
+ if (quantity.endsWith('n')) {
return parseInt(quantity.substr(0, quantity.length - 1), 10) / 1000000000.0;
}
diff --git a/app/assets/javascripts/clusters_list/components/clusters_actions.vue b/app/assets/javascripts/clusters_list/components/clusters_actions.vue
index c388d3fee71..e92b98946d0 100644
--- a/app/assets/javascripts/clusters_list/components/clusters_actions.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters_actions.vue
@@ -37,7 +37,8 @@ export default {
if (!this.displayClusterAgents) {
return connectClusterDeprecated;
- } else if (!this.certificateBasedClustersEnabled) {
+ }
+ if (!this.certificateBasedClustersEnabled) {
return connectCluster;
}
return connectWithAgent;
diff --git a/app/assets/javascripts/commit/constants.js b/app/assets/javascripts/commit/constants.js
index 4f865e99e46..e28009ab996 100644
--- a/app/assets/javascripts/commit/constants.js
+++ b/app/assets/javascripts/commit/constants.js
@@ -80,14 +80,14 @@ export const typeConfig = {
keyNamespace: 'gpgKeyPrimaryKeyid',
helpLink: {
label: __('Learn about signing commits'),
- path: 'user/project/repository/gpg_signed_commits/index.md',
+ path: 'user/project/repository/signed_commits/index.md',
},
},
[signatureTypes.X509]: {
keyLabel: '',
helpLink: {
label: __('Learn more about X.509 signed commits'),
- path: '/user/project/repository/x509_signed_commits/index.md',
+ path: '/user/project/repository/signed_commits/x509.md',
},
subjectTitle: __('Certificate Subject'),
issuerTitle: __('Certificate Issuer'),
@@ -98,7 +98,7 @@ export const typeConfig = {
keyNamespace: 'keyFingerprintSha256',
helpLink: {
label: __('Learn about signing commits with SSH keys.'),
- path: '/user/project/repository/ssh_signed_commits/index.md',
+ path: '/user/project/repository/signed_commits/ssh.md',
},
},
};
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue
index 8c8293eb09e..5e84dcbe48e 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue
@@ -2,12 +2,12 @@
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 '~/pipelines/components/pipelines_list/pipelines_table.vue';
-import { PipelineKeyOptions } from '~/pipelines/constants';
-import eventHub from '~/pipelines/event_hub';
-import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin';
-import PipelinesService from '~/pipelines/services/pipelines_service';
-import PipelineStore from '~/pipelines/stores/pipelines_store';
+import PipelinesTableComponent from '~/ci/common/pipelines_table.vue';
+import { PipelineKeyOptions } 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';
+import PipelineStore from '~/ci/pipeline_details/stores/pipelines_store';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__, __ } from '~/locale';
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index 96c274225d8..beeb9b9ada4 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -26,7 +26,8 @@ export default () => {
if (pipelineTableViewEl.dataset.disableInitialization === undefined) {
const table = new Vue({
components: {
- CommitPipelinesTable: () => import('~/commit/pipelines/pipelines_table.vue'),
+ CommitPipelinesTable: () =>
+ import('~/commit/pipelines/legacy_pipelines_table_wrapper.vue'),
},
apolloProvider,
provide: {
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table_wrapper.vue b/app/assets/javascripts/commit/pipelines/pipelines_table_wrapper.vue
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/app/assets/javascripts/commit/pipelines/pipelines_table_wrapper.vue
+++ /dev/null
diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js
new file mode 100644
index 00000000000..4a9f79460da
--- /dev/null
+++ b/app/assets/javascripts/commons/gitlab_ui.js
@@ -0,0 +1,10 @@
+import applyGitLabUIConfig from '@gitlab/ui/dist/config';
+import { __ } from '~/locale';
+
+applyGitLabUIConfig({
+ translations: {
+ 'GlSearchBoxByType.input.placeholder': __('Search'),
+ 'GlSearchBoxByType.clearButtonTitle': __('Clear'),
+ 'ClearIconButton.title': __('Clear'),
+ },
+});
diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js
index 77c85d85e27..d2a5ef83faf 100644
--- a/app/assets/javascripts/commons/index.js
+++ b/app/assets/javascripts/commons/index.js
@@ -1,6 +1,7 @@
import './polyfills';
import './bootstrap';
import './vue';
+import './gitlab_ui';
import '../lib/utils/axios_utils';
import { openUserCountsBroadcast } from './nav/user_merge_requests';
diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_created.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_created.vue
index 7915cd6679d..fe9b2f81f85 100644
--- a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_created.vue
+++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_created.vue
@@ -24,7 +24,7 @@ export default {
return this.event.resource_parent;
},
message() {
- if (!this.target) {
+ if (!this.target.type) {
return EVENT_CREATED_I18N[this.resourceParent.type] || EVENT_CREATED_I18N[TYPE_FALLBACK];
}
diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_destroyed.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_destroyed.vue
new file mode 100644
index 00000000000..11b6affb944
--- /dev/null
+++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_destroyed.vue
@@ -0,0 +1,28 @@
+<script>
+import { EVENT_DESTROYED_I18N, EVENT_DESTROYED_ICONS } from '../../constants';
+import { getValueByEventTarget } from '../../utils';
+import ContributionEventBase from './contribution_event_base.vue';
+
+export default {
+ name: 'ContributionEventDestroyed',
+ components: { ContributionEventBase },
+ props: {
+ event: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ message() {
+ return getValueByEventTarget(EVENT_DESTROYED_I18N, this.event);
+ },
+ iconName() {
+ return getValueByEventTarget(EVENT_DESTROYED_ICONS, this.event);
+ },
+ },
+};
+</script>
+
+<template>
+ <contribution-event-base :event="event" :message="message" :icon-name="iconName" />
+</template>
diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_pushed.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_pushed.vue
index 557f2912f17..a2516f37c92 100644
--- a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_pushed.vue
+++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_pushed.vue
@@ -53,7 +53,8 @@ export default {
message() {
if (this.ref.is_new) {
return this.$options.i18n.new[this.ref.type];
- } else if (this.ref.is_removed) {
+ }
+ if (this.ref.is_removed) {
return this.$options.i18n.removed[this.ref.type];
}
diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_updated.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_updated.vue
new file mode 100644
index 00000000000..e795e611ee5
--- /dev/null
+++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_updated.vue
@@ -0,0 +1,25 @@
+<script>
+import { EVENT_UPDATED_I18N } from '../../constants';
+import { getValueByEventTarget } from '../../utils';
+import ContributionEventBase from './contribution_event_base.vue';
+
+export default {
+ name: 'ContributionEventUpdated',
+ components: { ContributionEventBase },
+ props: {
+ event: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ message() {
+ return getValueByEventTarget(EVENT_UPDATED_I18N, this.event);
+ },
+ },
+};
+</script>
+
+<template>
+ <contribution-event-base :event="event" :message="message" icon-name="pencil" />
+</template>
diff --git a/app/assets/javascripts/contribution_events/components/contribution_events.vue b/app/assets/javascripts/contribution_events/components/contribution_events.vue
index 8b42d77675f..f3116717783 100644
--- a/app/assets/javascripts/contribution_events/components/contribution_events.vue
+++ b/app/assets/javascripts/contribution_events/components/contribution_events.vue
@@ -12,6 +12,8 @@ import {
EVENT_TYPE_CLOSED,
EVENT_TYPE_REOPENED,
EVENT_TYPE_COMMENTED,
+ EVENT_TYPE_UPDATED,
+ EVENT_TYPE_DESTROYED,
} from '../constants';
import ContributionEventApproved from './contribution_event/contribution_event_approved.vue';
import ContributionEventExpired from './contribution_event/contribution_event_expired.vue';
@@ -24,6 +26,8 @@ import ContributionEventCreated from './contribution_event/contribution_event_cr
import ContributionEventClosed from './contribution_event/contribution_event_closed.vue';
import ContributionEventReopened from './contribution_event/contribution_event_reopened.vue';
import ContributionEventCommented from './contribution_event/contribution_event_commented.vue';
+import ContributionEventUpdated from './contribution_event/contribution_event_updated.vue';
+import ContributionEventDestroyed from './contribution_event/contribution_event_destroyed.vue';
export default {
props: {
@@ -151,6 +155,12 @@ export default {
case EVENT_TYPE_COMMENTED:
return ContributionEventCommented;
+ case EVENT_TYPE_UPDATED:
+ return ContributionEventUpdated;
+
+ case EVENT_TYPE_DESTROYED:
+ return ContributionEventDestroyed;
+
default:
return EmptyComponent;
}
diff --git a/app/assets/javascripts/contribution_events/components/target_link.vue b/app/assets/javascripts/contribution_events/components/target_link.vue
index a14574ed826..86fddbb063e 100644
--- a/app/assets/javascripts/contribution_events/components/target_link.vue
+++ b/app/assets/javascripts/contribution_events/components/target_link.vue
@@ -27,5 +27,5 @@ export default {
</script>
<template>
- <gl-link v-if="target" v-bind="targetLinkAttributes">{{ targetLinkText }}</gl-link>
+ <gl-link v-if="target.type" v-bind="targetLinkAttributes">{{ targetLinkText }}</gl-link>
</template>
diff --git a/app/assets/javascripts/contribution_events/constants.js b/app/assets/javascripts/contribution_events/constants.js
index b5eddbf7e25..c0224ce94ff 100644
--- a/app/assets/javascripts/contribution_events/constants.js
+++ b/app/assets/javascripts/contribution_events/constants.js
@@ -119,14 +119,31 @@ export const EVENT_COMMENTED_I18N = Object.freeze({
[COMMIT_NOTEABLE_TYPE]: s__(
'ContributionEvent|Commented on commit %{noteableLink} in %{resourceParentLink}.',
),
- fallback: s__('ContributionEvent|Commented on %{noteableLink}.'),
+ [TYPE_FALLBACK]: s__('ContributionEvent|Commented on %{noteableLink}.'),
});
export const EVENT_COMMENTED_SNIPPET_I18N = Object.freeze({
[RESOURCE_PARENT_TYPE_PROJECT]: s__(
'ContributionEvent|Commented on snippet %{noteableLink} in %{resourceParentLink}.',
),
- fallback: s__('ContributionEvent|Commented on snippet %{noteableLink}.'),
+ [TYPE_FALLBACK]: s__('ContributionEvent|Commented on snippet %{noteableLink}.'),
+});
+
+export const EVENT_UPDATED_I18N = Object.freeze({
+ [TARGET_TYPE_DESIGN]: s__(
+ 'ContributionEvent|Updated design %{targetLink} in %{resourceParentLink}.',
+ ),
+ [TARGET_TYPE_WIKI]: s__(
+ 'ContributionEvent|Updated wiki page %{targetLink} in %{resourceParentLink}.',
+ ),
+ [TYPE_FALLBACK]: s__('ContributionEvent|Updated resource.'),
+});
+
+export const EVENT_DESTROYED_I18N = Object.freeze({
+ [TARGET_TYPE_DESIGN]: s__('ContributionEvent|Archived design in %{resourceParentLink}.'),
+ [TARGET_TYPE_WIKI]: s__('ContributionEvent|Deleted wiki page in %{resourceParentLink}.'),
+ [TARGET_TYPE_MILESTONE]: s__('ContributionEvent|Deleted milestone in %{resourceParentLink}.'),
+ [TYPE_FALLBACK]: s__('ContributionEvent|Deleted resource.'),
});
export const EVENT_CLOSED_ICONS = Object.freeze({
@@ -139,3 +156,8 @@ export const EVENT_REOPENED_ICONS = Object.freeze({
[TARGET_TYPE_MERGE_REQUEST]: 'merge-request-open',
[TYPE_FALLBACK]: 'status_open',
});
+
+export const EVENT_DESTROYED_ICONS = Object.freeze({
+ [TARGET_TYPE_DESIGN]: 'archive',
+ [TYPE_FALLBACK]: 'remove',
+});
diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js
index b39720c6094..08c2e235819 100644
--- a/app/assets/javascripts/create_item_dropdown.js
+++ b/app/assets/javascripts/create_item_dropdown.js
@@ -114,4 +114,8 @@ export default class CreateItemDropdown {
toggleFooter(toggleState) {
this.$dropdownFooter.toggleClass('hidden', toggleState);
}
+
+ close() {
+ this.$dropdown.data('deprecatedJQueryDropdown')?.close();
+ }
}
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 a56fce98f85..ccf4b064fa4 100644
--- a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
+++ b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
@@ -249,7 +249,12 @@ export default {
:description="$options.translations.addTokenNameDescription"
label-for="deploy_token_name"
>
- <gl-form-input id="deploy_token_name" v-model="name" name="deploy_token_name" />
+ <gl-form-input
+ id="deploy_token_name"
+ v-model="name"
+ class="gl-form-input-xl"
+ name="deploy_token_name"
+ />
</gl-form-group>
<gl-form-group
:label="$options.translations.addTokenExpiryLabel"
@@ -258,6 +263,7 @@ export default {
>
<gl-form-input
id="deploy_token_expires_at"
+ class="gl-form-input-xl"
name="deploy_token_expires_at"
:value="formattedExpiryDate"
data-qa-selector="deploy_token_expires_at_field"
@@ -277,7 +283,7 @@ export default {
</template>
</gl-sprintf>
</template>
- <gl-form-input id="deploy_token_username" v-model="username" />
+ <gl-form-input id="deploy_token_username" v-model="username" class="gl-form-input-xl" />
</gl-form-group>
<gl-form-group
:label="$options.translations.addTokenScopesLabel"
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
index 69f1d62539a..3f14a8a8a26 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
@@ -94,6 +94,7 @@ export class GitLabDropdown {
dataType: this.options.dataType,
beforeSend: this.toggleLoading.bind(this),
success: (data) => {
+ this.dropdown.trigger('done.remote.loading.gl.dropdown');
this.fullData = data;
this.parseData(this.fullData);
this.focusTextInput();
@@ -220,7 +221,12 @@ export class GitLabDropdown {
}
toggleLoading() {
- return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS);
+ const menu = this.dropdown[0].querySelector('.dropdown-menu');
+ const isLoading = menu.classList.contains(LOADING_CLASS);
+
+ this.dropdown.trigger(`toggle.${isLoading ? 'off' : 'on'}.loading.gl.dropdown`);
+
+ menu.classList.toggle(LOADING_CLASS);
}
togglePage() {
@@ -719,4 +725,8 @@ export class GitLabDropdown {
clearField(field, isInput) {
return isInput ? field.val('') : field.remove();
}
+
+ close() {
+ this.dropdown.find('.dropdown-menu-toggle').dropdown('hide');
+ }
}
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
index 2cb9e9a56a3..271347feebd 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
@@ -17,9 +17,11 @@ export class GitLabDropdownFilter {
const $inputContainer = this.input.parent();
const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
const filterRemoteDebounced = debounce(() => {
+ options.instance.dropdown.trigger('filtering.gl.dropdown');
$inputContainer.parent().addClass('is-loading');
return this.options.query(this.input.val(), (data) => {
+ options.instance.dropdown.trigger('done.filtering.gl.dropdown');
$inputContainer.parent().removeClass('is-loading');
return this.options.callback(data);
});
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js
index ae5d3298b62..bacd84048af 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js
@@ -11,7 +11,8 @@ export class GitLabDropdownRemote {
execute() {
if (typeof this.dataEndpoint === 'string') {
return this.fetchData();
- } else if (typeof this.dataEndpoint === 'function') {
+ }
+ if (typeof this.dataEndpoint === 'function') {
if (this.options.beforeSend) {
this.options.beforeSend();
}
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/index.js b/app/assets/javascripts/deprecated_jquery_dropdown/index.js
index 6a3d2026192..38236707e06 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/index.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/index.js
@@ -5,7 +5,10 @@ export default function initDeprecatedJQueryDropdown($el, opts) {
// eslint-disable-next-line func-names
return $el.each(function () {
if (!$.data(this, 'deprecatedJQueryDropdown')) {
- $.data(this, 'deprecatedJQueryDropdown', new GitLabDropdown(this, opts));
+ const instance = new GitLabDropdown(this, opts);
+
+ $.data(this, 'deprecatedJQueryDropdown', instance);
+ this.GitLabDropdownInstance = instance;
}
});
}
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/render.js b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
index 97698d55011..1b133c57302 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/render.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
@@ -89,7 +89,8 @@ function checkSelected(data, options) {
if (!options.parent) {
return !data.id;
- } else if (value) {
+ }
+ if (value) {
return (
options.parent.querySelector(`input[name='${options.fieldName}'][value='${value}']`) != null
);
diff --git a/app/assets/javascripts/deprecated_notes.js b/app/assets/javascripts/deprecated_notes.js
index 4e5e07c57e4..008e12abbcd 100644
--- a/app/assets/javascripts/deprecated_notes.js
+++ b/app/assets/javascripts/deprecated_notes.js
@@ -458,7 +458,8 @@ export default class Notes {
this.setupNewNote($newNote);
this.refresh();
return this.updateNotesCount(1);
- } else if (Notes.isUpdatedNote(noteEntity, $note)) {
+ }
+ if (Notes.isUpdatedNote(noteEntity, $note)) {
// The server can send the same update multiple times so we need to make sure to only update once per actual update.
const isEditing = $note.hasClass('is-editing');
const initialContent = normalizeNewlines($note.find('.original-note-content').text().trim());
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index e3882ce42c2..08306312c2e 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -19,7 +19,6 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import { Mousetrap } from '~/lib/mousetrap';
import { updateHistory } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import notesEventHub from '~/notes/event_hub';
import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
@@ -76,7 +75,6 @@ export default {
GenerateTestFileDrawer: () =>
import('ee_component/ai/components/generate_test_file_drawer.vue'),
},
- mixins: [glFeatureFlagsMixin()],
alerts: {
ALERT_OVERFLOW_HIDDEN,
ALERT_MERGE_CONFLICT,
@@ -580,11 +578,7 @@ export default {
<template>
<div v-show="shouldShow">
- <findings-drawer
- v-if="glFeatures.codeQualityInlineDrawer"
- :drawer="activeDrawer"
- @close="closeDrawer"
- />
+ <findings-drawer :drawer="activeDrawer" @close="closeDrawer" />
<div v-if="isLoading || !isTreeLoaded" class="loading"><gl-loading-icon size="lg" /></div>
<div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane">
<compare-versions :diff-files-count-text="numTotalFiles" />
@@ -631,6 +625,7 @@ export default {
<template #default="{ item, index, active }">
<dynamic-scroller-item :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/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index d62d0e11bff..a9e63ad53bb 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -337,7 +337,7 @@ export default {
:title="filePath"
class="file-title-name"
data-container="body"
- data-qa-selector="file_name_content"
+ data-testid="file-name-content"
>
{{ filePath }}
</strong>
diff --git a/app/assets/javascripts/diffs/components/diff_inline_findings_item.vue b/app/assets/javascripts/diffs/components/diff_inline_findings_item.vue
index 5cc2a3079b0..8a30d6ee7f8 100644
--- a/app/assets/javascripts/diffs/components/diff_inline_findings_item.vue
+++ b/app/assets/javascripts/diffs/components/diff_inline_findings_item.vue
@@ -1,23 +1,15 @@
<script>
-import { GlLink, GlIcon } from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapActions } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import { getSeverity } from '~/ci/reports/utils';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
- components: { GlLink, GlIcon },
- mixins: [glFeatureFlagsMixin()],
+ components: { GlIcon },
+
props: {
finding: {
type: Object,
required: true,
},
- link: {
- type: Boolean,
- required: false,
- default: true,
- },
},
computed: {
enhancedFinding() {
@@ -27,12 +19,6 @@ export default {
return `${this.finding.severity} - ${this.finding.description}`;
},
},
- methods: {
- toggleDrawer() {
- this.setDrawer(this.finding);
- },
- ...mapActions('findingsDrawer', ['setDrawer']),
- },
};
</script>
@@ -46,17 +32,7 @@ export default {
class="inline-findings-severity-icon"
/>
</span>
- <span
- v-if="glFeatures.codeQualityInlineDrawer"
- data-testid="description-button-section"
- class="gl-display-flex"
- >
- <gl-link v-if="link" category="primary" variant="link" @click="toggleDrawer">
- {{ listText }}</gl-link
- >
- <span v-else>{{ listText }}</span>
- </span>
- <span v-else data-testid="description-plain-text" class="gl-display-flex">
+ <span data-testid="description-plain-text" class="gl-display-flex">
{{ listText }}
</span>
</li>
diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue
index 318ecc89d14..ee6e9a2fc94 100644
--- a/app/assets/javascripts/diffs/components/diff_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_row.vue
@@ -24,8 +24,8 @@ import * as utils from './diff_row_utils';
export default {
DiffGutterAvatars,
- InlineFindingsGutterIcon: () =>
- import('ee_component/diffs/components/inline_findings_gutter_icon.vue'),
+ InlineFindingsFlagSwitcher: () =>
+ import('ee_component/diffs/components/inline_findings_flag_switcher.vue'),
// Temporary mixin for migration from Vue.js 2 to @vue/compat
mixins: [compatFunctionalMixin],
@@ -338,10 +338,11 @@ export default {
:class="$options.parallelViewLeftLineType(props)"
>
<component
- :is="$options.InlineFindingsGutterIcon"
+ :is="$options.InlineFindingsFlagSwitcher"
v-if="$options.showCodequalityLeft(props) || $options.showSecurityLeft(props)"
:inline-findings-expanded="props.inlineFindingsExpanded"
- :codequality="props.line.left.codequality"
+ :code-quality="props.line.left.codequality"
+ :sast="props.line.left.sast"
:file-path="props.filePath"
@showInlineFindings="
listeners.toggleCodeQualityFindings(
@@ -479,9 +480,10 @@ export default {
:class="$options.classNameMapCellRight(props)"
>
<component
- :is="$options.InlineFindingsGutterIcon"
+ :is="$options.InlineFindingsFlagSwitcher"
v-if="$options.showCodequalityRight(props) || $options.showSecurityRight(props)"
- :codequality="props.line.right.codequality"
+ :code-quality="props.line.right.codequality"
+ :sast="props.line.right.sast"
:file-path="props.filePath"
data-testid="inlineFindingsIcon"
@showInlineFindings="
diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue
index 88ea4e15552..641616a34f5 100644
--- a/app/assets/javascripts/diffs/components/diff_view.vue
+++ b/app/assets/javascripts/diffs/components/diff_view.vue
@@ -186,7 +186,8 @@ export default {
getCountBetweenIndex(index) {
if (index === 0) {
return -1;
- } else if (!this.diffLines[index + 1]) {
+ }
+ if (!this.diffLines[index + 1]) {
return -1;
}
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index bbc602aedf6..7c68b5f69f1 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -910,7 +910,8 @@ export const setSuggestPopoverDismissed = ({ commit, state }) =>
export function changeCurrentCommit({ dispatch, commit, state }, { commitId }) {
if (!commitId) {
return Promise.reject(new Error('`commitId` is a required argument'));
- } else if (!state.commit) {
+ }
+ if (!state.commit) {
return Promise.reject(new Error('`state` must already contain a valid `commit`')); // eslint-disable-line @gitlab/require-i18n-strings
}
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index d82959daa9d..bfafb4d281d 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -139,7 +139,8 @@ export const fileLineCoverage = (state) => (file, line) => {
if (lineCoverage === 0) {
return { text: __('No test coverage'), class: 'no-coverage' };
- } else if (lineCoverage >= 0) {
+ }
+ if (lineCoverage >= 0) {
return {
text: n__('Test coverage: %d hit', 'Test coverage: %d hits', lineCoverage),
class: 'coverage',
diff --git a/app/assets/javascripts/drawio/drawio_editor.js b/app/assets/javascripts/drawio/drawio_editor.js
index 3c411d8093c..37bfde0ed9f 100644
--- a/app/assets/javascripts/drawio/drawio_editor.js
+++ b/app/assets/javascripts/drawio/drawio_editor.js
@@ -1,6 +1,7 @@
import _ from 'lodash';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { darkModeEnabled } from '~/lib/utils/color_utils';
+import { base64DecodeUnicode } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
import { setAttributes } from '~/lib/utils/dom_utils';
import {
@@ -28,7 +29,7 @@ function disposeDrawioEditor(drawIOEditorState) {
}
function getSvg(data) {
- const svgPath = atob(data.substring(data.indexOf(',') + 1));
+ const svgPath = base64DecodeUnicode(data.substring(data.indexOf(',') + 1));
return `<?xml version="1.0" encoding="UTF-8"?>\n\
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n\
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 65091487c93..2dba919cf58 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -845,6 +845,9 @@
"if": {
"$ref": "#/definitions/if"
},
+ "changes": {
+ "$ref": "#/definitions/changes"
+ },
"exists": {
"$ref": "#/definitions/exists"
},
@@ -2084,6 +2087,10 @@
"publish": {
"description": "A path to a directory that contains the files to be published with Pages",
"type": "string"
+ },
+ "pages_path_prefix": {
+ "description": "The path prefix identifier for this version of pages. Allows creation of multiple versions of the same site with different path prefixes",
+ "type": "string"
}
},
"oneOf": [
diff --git a/app/assets/javascripts/entrypoints/analytics.js b/app/assets/javascripts/entrypoints/analytics.js
new file mode 100644
index 00000000000..8eb265cb1e8
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/analytics.js
@@ -0,0 +1,17 @@
+import { glClientSDK } from '@gitlab/application-sdk-browser';
+
+const { analytics_id: appId, analytics_url: host } = window.gon;
+
+if (appId && host) {
+ window.glClient = glClientSDK({
+ appId,
+ host,
+ hasCookieConsent: true,
+ plugins: {
+ clientHints: false,
+ linkTracking: false,
+ performanceTiming: false,
+ errorTracking: false,
+ },
+ });
+}
diff --git a/app/assets/javascripts/entrypoints/jira_connect_app.js b/app/assets/javascripts/entrypoints/jira_connect_app.js
new file mode 100644
index 00000000000..90ad39ea487
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/jira_connect_app.js
@@ -0,0 +1 @@
+import '../jira_connect/subscriptions';
diff --git a/app/assets/javascripts/entrypoints/main.js b/app/assets/javascripts/entrypoints/main.js
new file mode 100644
index 00000000000..6d59e89cfd0
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/main.js
@@ -0,0 +1,6 @@
+import '../main';
+import { runModules } from '~/run_modules';
+
+const modules = import.meta.glob('../pages/**/index.js');
+
+runModules(modules, '../pages/');
diff --git a/app/assets/javascripts/entrypoints/main_ee.js b/app/assets/javascripts/entrypoints/main_ee.js
new file mode 100644
index 00000000000..4a83be6be94
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/main_ee.js
@@ -0,0 +1,5 @@
+import { runModules } from '~/run_modules';
+
+const modules = import.meta.glob('../../../../ee/app/assets/javascripts/pages/**/index.js');
+
+runModules(modules, '../../../../ee/app/assets/javascripts/pages/');
diff --git a/app/assets/javascripts/entrypoints/main_jh.js b/app/assets/javascripts/entrypoints/main_jh.js
new file mode 100644
index 00000000000..92a42a9ac70
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/main_jh.js
@@ -0,0 +1,5 @@
+import { runModules } from '~/run_modules';
+
+const modules = import.meta.glob('../../../../jh/app/assets/javascripts/pages/**/index.js');
+
+runModules(modules, '../../../../jh/app/assets/javascripts/pages/');
diff --git a/app/assets/javascripts/entrypoints/performance_bar.js b/app/assets/javascripts/entrypoints/performance_bar.js
new file mode 100644
index 00000000000..3f6fc6272d0
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/performance_bar.js
@@ -0,0 +1 @@
+import '../performance_bar';
diff --git a/app/assets/javascripts/entrypoints/redirect_listbox.js b/app/assets/javascripts/entrypoints/redirect_listbox.js
new file mode 100644
index 00000000000..811a73fbf2f
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/redirect_listbox.js
@@ -0,0 +1 @@
+import './behaviors/redirect_listbox';
diff --git a/app/assets/javascripts/entrypoints/sandboxed_mermaid.js b/app/assets/javascripts/entrypoints/sandboxed_mermaid.js
new file mode 100644
index 00000000000..d3dd144ffba
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/sandboxed_mermaid.js
@@ -0,0 +1 @@
+import '../lib/mermaid';
diff --git a/app/assets/javascripts/entrypoints/sentry.js b/app/assets/javascripts/entrypoints/sentry.js
new file mode 100644
index 00000000000..debafc6fab3
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/sentry.js
@@ -0,0 +1 @@
+import '../sentry/index';
diff --git a/app/assets/javascripts/environments/components/deployment.vue b/app/assets/javascripts/environments/components/deployment.vue
index 96d2a8d9ba2..ef6d3b79198 100644
--- a/app/assets/javascripts/environments/components/deployment.vue
+++ b/app/assets/javascripts/environments/components/deployment.vue
@@ -209,7 +209,7 @@ export default {
</div>
<time-ago-tooltip v-if="createdAt" :time="createdAt" class="gl-display-flex">
<template #default="{ timeAgo }">
- <gl-icon name="calendar" />
+ <gl-icon name="calendar" class="gl-mr-2" />
<span class="gl-mr-2 gl-white-space-nowrap">{{ timeAgo }}</span>
</template>
</time-ago-tooltip>
diff --git a/app/assets/javascripts/environments/components/edit_environment.vue b/app/assets/javascripts/environments/components/edit_environment.vue
index f90a1dcd193..0eebd81b671 100644
--- a/app/assets/javascripts/environments/components/edit_environment.vue
+++ b/app/assets/javascripts/environments/components/edit_environment.vue
@@ -4,7 +4,6 @@ import { createAlert } from '~/alert';
import { visitUrl } from '~/lib/utils/url_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getEnvironment from '../graphql/queries/environment.query.graphql';
-import getEnvironmentWithFluxResource from '../graphql/queries/environment_with_flux_resource.query.graphql';
import updateEnvironment from '../graphql/mutations/update_environment.mutation.graphql';
import EnvironmentForm from './environment_form.vue';
@@ -17,11 +16,7 @@ export default {
inject: ['projectEnvironmentsPath', 'projectPath', 'environmentName'],
apollo: {
environment: {
- query() {
- return this.glFeatures?.fluxResourceForEnvironment
- ? getEnvironmentWithFluxResource
- : getEnvironment;
- },
+ query: getEnvironment,
variables() {
return {
environmentName: this.environmentName,
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index d49598d2f21..926c556966c 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -87,7 +87,7 @@ export default {
</script>
<template>
<gl-disclosure-dropdown
- :text="title"
+ :toggle-text="title"
:title="title"
:loading="isLoading"
:aria-label="title"
diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue
index d89dcf56b7c..846f2cf73b2 100644
--- a/app/assets/javascripts/environments/components/environment_form.vue
+++ b/app/assets/javascripts/environments/components/environment_form.vue
@@ -181,13 +181,8 @@ export default {
namespaceDropdownToggleText() {
return this.selectedNamespace || this.$options.i18n.namespaceHelpText;
},
- isKasFluxResourceAvailable() {
- return this.glFeatures?.fluxResourceForEnvironment;
- },
showFluxResourceSelector() {
- return Boolean(
- this.isKasFluxResourceAvailable && this.selectedNamespace && this.selectedAgentId,
- );
+ return Boolean(this.selectedNamespace && this.selectedAgentId);
},
k8sAccessConfiguration() {
if (!this.showNamespaceSelector) {
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index b02142c24cf..08a1eacec7a 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -1,6 +1,6 @@
<script>
import {
- GlDropdown,
+ GlDisclosureDropdown,
GlTooltipDirective,
GlIcon,
GlLink,
@@ -24,7 +24,6 @@ import PinComponent from './environment_pin.vue';
import RollbackComponent from './environment_rollback.vue';
import StopComponent from './environment_stop.vue';
import TerminalButtonComponent from './environment_terminal_button.vue';
-
/**
* Environment Item Component
*
@@ -36,7 +35,7 @@ export default {
ActionsComponent,
CommitComponent,
ExternalUrlComponent,
- GlDropdown,
+ GlDisclosureDropdown,
GlBadge,
GlIcon,
GlLink,
@@ -820,13 +819,13 @@ export default {
data-track-label="environment_stop"
/>
- <gl-dropdown
- v-if="hasExtraActions"
- icon="ellipsis_v"
+ <gl-disclosure-dropdown
text-sr-only
- :text="__('More actions')"
- category="secondary"
no-caret
+ icon="ellipsis_v"
+ category="secondary"
+ placement="right"
+ :toggle-text="__('More actions')"
>
<rollback-component
v-if="canRetry"
@@ -857,7 +856,7 @@ export default {
data-track-action="click_button"
data-track-label="environment_delete"
/>
- </gl-dropdown>
+ </gl-disclosure-dropdown>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
index e8857dfe459..c603d83db9c 100644
--- a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
@@ -145,15 +145,20 @@ export default {
syncStatusBadge() {
if (!this.fluxCRD.length && this.fluxApiError) {
return { ...SYNC_STATUS_BADGES.unavailable, popoverText: this.fluxApiError };
- } else if (!this.fluxCRD.length) {
+ }
+ if (!this.fluxCRD.length) {
return SYNC_STATUS_BADGES.unavailable;
- } else if (this.fluxAnyFailed) {
+ }
+ if (this.fluxAnyFailed) {
return { ...SYNC_STATUS_BADGES.failed, popoverText: this.fluxAnyFailed.message };
- } else if (this.fluxAnyStalled) {
+ }
+ if (this.fluxAnyStalled) {
return { ...SYNC_STATUS_BADGES.stalled, popoverText: this.fluxAnyStalled.message };
- } else if (this.fluxAnyReconciling) {
+ }
+ if (this.fluxAnyReconciling) {
return SYNC_STATUS_BADGES.reconciling;
- } else if (this.fluxAnyReconciled) {
+ }
+ if (this.fluxAnyReconciled) {
return SYNC_STATUS_BADGES.reconciled;
}
return SYNC_STATUS_BADGES.unknown;
diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue
index 2148343f690..149cab21acd 100644
--- a/app/assets/javascripts/environments/components/new_environment_item.vue
+++ b/app/assets/javascripts/environments/components/new_environment_item.vue
@@ -14,7 +14,6 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import isLastDeployment from '../graphql/queries/is_last_deployment.query.graphql';
import getEnvironmentClusterAgent from '../graphql/queries/environment_cluster_agent.query.graphql';
-import getEnvironmentClusterAgentWithFluxResource from '../graphql/queries/environment_cluster_agent_with_flux_resource.query.graphql';
import ExternalUrl from './environment_external_url.vue';
import Actions from './environment_actions.vue';
import StopComponent from './environment_stop.vue';
@@ -165,9 +164,6 @@ export default {
rolloutStatus() {
return this.environment?.rolloutStatus;
},
- isFluxResourceAvailable() {
- return this.glFeatures?.fluxResourceForEnvironment;
- },
},
methods: {
toggleEnvironmentCollapse() {
@@ -185,9 +181,7 @@ export default {
return { environmentName: this.environment.name, projectFullPath: this.projectPath };
},
query() {
- return this.isFluxResourceAvailable
- ? getEnvironmentClusterAgentWithFluxResource
- : getEnvironmentClusterAgent;
+ return getEnvironmentClusterAgent;
},
update(data) {
this.clusterAgent = data?.project?.environment?.clusterAgent;
diff --git a/app/assets/javascripts/environments/environment_details/deployments_table.vue b/app/assets/javascripts/environments/environment_details/deployments_table.vue
index f37f93798ae..261d8106438 100644
--- a/app/assets/javascripts/environments/environment_details/deployments_table.vue
+++ b/app/assets/javascripts/environments/environment_details/deployments_table.vue
@@ -36,7 +36,7 @@ export default {
<deployment-status-link :deployment-job="item.job" :status="item.status" />
</template>
<template #cell(id)="{ item }">
- <strong>{{ item.id }}</strong>
+ <strong data-testid="deployment-id">{{ item.id }}</strong>
</template>
<template #cell(triggerer)="{ item }">
<deployment-triggerer :triggerer="item.triggerer" />
diff --git a/app/assets/javascripts/environments/graphql/queries/environment.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment.query.graphql
index 53dfe5303f3..2d6faed5c88 100644
--- a/app/assets/javascripts/environments/graphql/queries/environment.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/environment.query.graphql
@@ -6,6 +6,7 @@ query getEnvironment($projectFullPath: ID!, $environmentName: String) {
name
externalUrl
kubernetesNamespace
+ fluxResourcePath
clusterAgent {
id
name
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql
index 19374ae7a81..3f8874f2a8d 100644
--- a/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql
@@ -3,6 +3,7 @@ query getEnvironmentClusterAgent($projectFullPath: ID!, $environmentName: String
id
environment(name: $environmentName) {
id
+ fluxResourcePath
kubernetesNamespace
clusterAgent {
id
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_flux_resource.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_flux_resource.query.graphql
deleted file mode 100644
index 80363a06d42..00000000000
--- a/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_flux_resource.query.graphql
+++ /dev/null
@@ -1,21 +0,0 @@
-query getEnvironmentClusterAgentWithFluxResource($projectFullPath: ID!, $environmentName: String) {
- project(fullPath: $projectFullPath) {
- id
- environment(name: $environmentName) {
- id
- kubernetesNamespace
- fluxResourcePath
- clusterAgent {
- id
- name
- webPath
- tokens {
- nodes {
- id
- lastUsedAt
- }
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_with_flux_resource.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_with_flux_resource.query.graphql
deleted file mode 100644
index 166cd64189f..00000000000
--- a/app/assets/javascripts/environments/graphql/queries/environment_with_flux_resource.query.graphql
+++ /dev/null
@@ -1,16 +0,0 @@
-query getEnvironmentWithFluxResource($projectFullPath: ID!, $environmentName: String) {
- project(fullPath: $projectFullPath) {
- id
- environment(name: $environmentName) {
- id
- name
- externalUrl
- kubernetesNamespace
- fluxResourcePath
- clusterAgent {
- id
- name
- }
- }
- }
-}
diff --git a/app/assets/javascripts/environments/mixins/environments_pagination_api_mixin.js b/app/assets/javascripts/environments/mixins/environments_pagination_api_mixin.js
index 55e2536e283..a64cc0405bb 100644
--- a/app/assets/javascripts/environments/mixins/environments_pagination_api_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_pagination_api_mixin.js
@@ -3,7 +3,7 @@
*
* Components need to have `scope`, `page` and `requestData`
*/
-import { validateParams } from '~/pipelines/utils';
+import { validateParams } from '~/ci/pipeline_details/utils';
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
export default {
diff --git a/app/assets/javascripts/environments/mount_show.js b/app/assets/javascripts/environments/mount_show.js
index fb9a7a02d07..9924e1c7d7b 100644
--- a/app/assets/javascripts/environments/mount_show.js
+++ b/app/assets/javascripts/environments/mount_show.js
@@ -59,9 +59,6 @@ export const initHeader = () => {
};
export const initPage = async () => {
- if (!gon.features.environmentDetailsVue) {
- return null;
- }
const EnvironmentsDetailPageModule = await import('./environment_details/index.vue');
const EnvironmentsDetailPage = EnvironmentsDetailPageModule.default;
const dataElement = document.getElementById('environments-detail-view');
diff --git a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
index 420c34a88f1..ad80ee099ad 100644
--- a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
+++ b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
@@ -1,25 +1,15 @@
<script>
-import {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- GlIcon,
- GlLoadingIcon,
- GlSearchBoxByType,
-} from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { GlCollapsibleListbox, GlButton } from '@gitlab/ui';
+import { debounce, memoize } from 'lodash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
-import { __, sprintf } from '~/locale';
+import { __, n__, sprintf } from '~/locale';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
export default {
components: {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- GlSearchBoxByType,
- GlIcon,
- GlLoadingIcon,
+ GlButton,
+ GlCollapsibleListbox,
},
inject: ['environmentsEndpoint'],
data() {
@@ -34,69 +24,96 @@ export default {
noResultsLabel: __('No matching results'),
},
computed: {
+ srOnlyResultsCount() {
+ return n__('%d environment found', '%d environments found', this.results.length);
+ },
createEnvironmentLabel() {
return sprintf(__('Create %{environment}'), { environment: this.environmentSearch });
},
+ isCreateEnvironmentShown() {
+ return !this.isLoading && this.results.length === 0 && Boolean(this.environmentSearch);
+ },
+ },
+ mounted() {
+ this.fetchEnvironments();
+ },
+ unmounted() {
+ // cancel debounce if the component is unmounted to avoid unnecessary fetches
+ this.fetchEnvironments.cancel();
+ },
+ created() {
+ this.fetch = memoize(async function fetchEnvironmentsFromApi(query) {
+ this.isLoading = true;
+ try {
+ const { data } = await axios.get(this.environmentsEndpoint, { params: { query } });
+
+ return data;
+ } catch {
+ createAlert({
+ message: __('Something went wrong on our end. Please try again.'),
+ });
+ return [];
+ } finally {
+ this.isLoading = false;
+ }
+ });
+
+ this.fetchEnvironments = debounce(function debouncedFetchEnvironments(query = '') {
+ this.fetch(query)
+ .then((data) => {
+ this.results = data.map((item) => ({ text: item, value: item }));
+ })
+ .catch(() => {
+ this.results = [];
+ });
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
methods: {
+ onSelect(selected) {
+ this.$emit('add', selected[0]);
+ },
addEnvironment(newEnvironment) {
this.$emit('add', newEnvironment);
- this.environmentSearch = '';
this.results = [];
},
- fetchEnvironments: debounce(function debouncedFetchEnvironments() {
- this.isLoading = true;
- axios
- .get(this.environmentsEndpoint, { params: { query: this.environmentSearch } })
- .then(({ data }) => {
- this.results = data || [];
- })
- .catch(() => {
- createAlert({
- message: __('Something went wrong on our end. Please try again.'),
- });
- })
- .finally(() => {
- this.isLoading = false;
- });
- }, 250),
- setFocus() {
- this.$refs.searchBox.focusInput();
+ onSearch(query) {
+ this.environmentSearch = query;
+ this.fetchEnvironments(query);
},
},
};
</script>
<template>
- <gl-dropdown class="js-new-environments-dropdown" @shown="setFocus">
- <template #button-content>
- <span class="d-md-none mr-1">
- {{ $options.translations.addEnvironmentsLabel }}
- </span>
- <gl-icon class="d-none d-md-inline-flex gl-mr-1" name="plus" />
+ <gl-collapsible-listbox
+ icon="plus"
+ data-testid="new-environments-dropdown"
+ :toggle-text="$options.translations.addEnvironmentsLabel"
+ :items="results"
+ :searching="isLoading"
+ :header-text="$options.translations.addEnvironmentsLabel"
+ searchable
+ multiple
+ @search="onSearch"
+ @select="onSelect"
+ >
+ <template #footer>
+ <div
+ v-if="isCreateEnvironmentShown"
+ class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-200 gl-p-2"
+ >
+ <gl-button
+ category="tertiary"
+ block
+ class="gl-justify-content-start!"
+ data-testid="add-environment-button"
+ @click="addEnvironment(environmentSearch)"
+ >
+ {{ createEnvironmentLabel }}
+ </gl-button>
+ </div>
</template>
- <gl-search-box-by-type
- ref="searchBox"
- v-model.trim="environmentSearch"
- @focus="fetchEnvironments"
- @keyup="fetchEnvironments"
- />
- <gl-loading-icon v-if="isLoading" size="sm" />
- <gl-dropdown-item
- v-for="environment in results"
- v-else-if="results.length"
- :key="environment"
- @click="addEnvironment(environment)"
- >
- {{ environment }}
- </gl-dropdown-item>
- <template v-else-if="environmentSearch.length">
- <span ref="noResults" class="text-secondary gl-p-3">
- {{ $options.translations.noMatchingResults }}
- </span>
- <gl-dropdown-divider />
- <gl-dropdown-item @click="addEnvironment(environmentSearch)">
- {{ createEnvironmentLabel }}
- </gl-dropdown-item>
+ <template #search-summary-sr-only>
+ {{ srOnlyResultsCount }}
</template>
- </gl-dropdown>
+ </gl-collapsible-listbox>
</template>
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 684375177bb..8ccf7ba92a5 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -42,7 +42,7 @@ export default class FilteredSearchManager {
useDefaultState = false,
filteredSearchTokenKeys = IssuableFilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters',
- placeholder = __('Search or filter results...'),
+ placeholder = __('Search or filter results…'),
anchor = null,
}) {
this.isGroup = isGroup;
diff --git a/app/assets/javascripts/frequent_items/utils.js b/app/assets/javascripts/frequent_items/utils.js
index 1c33c8b1084..f71405a5bc4 100644
--- a/app/assets/javascripts/frequent_items/utils.js
+++ b/app/assets/javascripts/frequent_items/utils.js
@@ -24,7 +24,8 @@ export const getTopFrequentItems = (items) => {
// and then by lastAccessedOn with recent most first
if (itemA.frequency !== itemB.frequency) {
return itemB.frequency - itemA.frequency;
- } else if (itemA.lastAccessedOn !== itemB.lastAccessedOn) {
+ }
+ if (itemA.lastAccessedOn !== itemB.lastAccessedOn) {
return itemB.lastAccessedOn - itemA.lastAccessedOn;
}
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 9e7006bb6e7..264427f5806 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -393,13 +393,16 @@ class GfmAutoComplete {
if (command === MEMBER_COMMAND.ASSIGN) {
// Only include members which are not assigned to Issuable currently
return data.filter((member) => !assignees.includes(member.search));
- } else if (command === MEMBER_COMMAND.UNASSIGN) {
+ }
+ if (command === MEMBER_COMMAND.UNASSIGN) {
// Only include members which are assigned to Issuable currently
return data.filter((member) => assignees.includes(member.search));
- } else if (command === MEMBER_COMMAND.ASSIGN_REVIEWER) {
+ }
+ if (command === MEMBER_COMMAND.ASSIGN_REVIEWER) {
// Only include members which are not assigned as a reviewer to Issuable currently
return data.filter((member) => !reviewers.includes(member.search));
- } else if (command === MEMBER_COMMAND.UNASSIGN_REVIEWER) {
+ }
+ if (command === MEMBER_COMMAND.UNASSIGN_REVIEWER) {
// Only include members which are not assigned as a reviewer to Issuable currently
return data.filter((member) => reviewers.includes(member.search));
}
@@ -617,11 +620,6 @@ class GfmAutoComplete {
if (labels.find((label) => label.title.startsWith(lastCandidate))) {
return lastCandidate;
}
- } else {
- // Load all labels into the autocompleter.
- // This needs to happen if e.g. editing a label in an existing comment, because normally
- // label data would only be loaded only once you type `~`.
- fetchData(this.$inputor, this.at);
}
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
@@ -642,7 +640,8 @@ class GfmAutoComplete {
if (command === LABEL_COMMAND.LABEL || command === LABEL_COMMAND.LABELS) {
// Return labels with set: undefined.
return data.filter((label) => !label.set);
- } else if (command === LABEL_COMMAND.UNLABEL) {
+ }
+ if (command === LABEL_COMMAND.UNLABEL) {
// Return labels with set: true.
return data.filter((label) => label.set);
}
@@ -751,7 +750,8 @@ class GfmAutoComplete {
if (command === CONTACTS_ADD_COMMAND) {
// Return contacts that are active and not already on the issue
return data.filter((contact) => contact.state === CONTACT_STATE_ACTIVE && !contact.set);
- } else if (command === CONTACTS_REMOVE_COMMAND) {
+ }
+ if (command === CONTACTS_REMOVE_COMMAND) {
// Return contacts already on the issue
return data.filter((contact) => contact.set);
}
@@ -779,10 +779,8 @@ class GfmAutoComplete {
if (GfmAutoComplete.isLoading(data)) {
self.fetchData(this.$inputor, this.at);
return data;
- } else if (
- GfmAutoComplete.isTypeWithBackendFiltering(this.at) &&
- self.previousQuery !== query
- ) {
+ }
+ if (GfmAutoComplete.isTypeWithBackendFiltering(this.at) && self.previousQuery !== query) {
self.fetchData(this.$inputor, this.at, query);
self.previousQuery = query;
return data;
diff --git a/app/assets/javascripts/gitlab_version_check/components/gitlab_version_check_badge.vue b/app/assets/javascripts/gitlab_version_check/components/gitlab_version_check_badge.vue
index 1536a9a525b..25ddfc911f3 100644
--- a/app/assets/javascripts/gitlab_version_check/components/gitlab_version_check_badge.vue
+++ b/app/assets/javascripts/gitlab_version_check/components/gitlab_version_check_badge.vue
@@ -30,9 +30,11 @@ export default {
title() {
if (this.status === STATUS_TYPES.SUCCESS) {
return s__('VersionCheck|Up to date');
- } else if (this.status === STATUS_TYPES.WARNING) {
+ }
+ if (this.status === STATUS_TYPES.WARNING) {
return s__('VersionCheck|Update available');
- } else if (this.status === STATUS_TYPES.DANGER) {
+ }
+ if (this.status === STATUS_TYPES.DANGER) {
return s__('VersionCheck|Update ASAP');
}
diff --git a/app/assets/javascripts/google_cloud/deployments/service_table.vue b/app/assets/javascripts/google_cloud/deployments/service_table.vue
index 26c9fd14dc6..9388b41127e 100644
--- a/app/assets/javascripts/google_cloud/deployments/service_table.vue
+++ b/app/assets/javascripts/google_cloud/deployments/service_table.vue
@@ -34,7 +34,7 @@ export default {
methods: {
actionUrl(key) {
if (key === cloudRun) return this.cloudRunUrl;
- else if (key === cloudStorage) return this.cloudStorageUrl;
+ if (key === cloudStorage) return this.cloudStorageUrl;
return '#';
},
},
diff --git a/app/assets/javascripts/google_cloud/service_accounts/list.vue b/app/assets/javascripts/google_cloud/service_accounts/list.vue
index 635b185d207..ebba9332c53 100644
--- a/app/assets/javascripts/google_cloud/service_accounts/list.vue
+++ b/app/assets/javascripts/google_cloud/service_accounts/list.vue
@@ -63,6 +63,7 @@ export default {
:primary-button-link="createUrl"
:primary-button-text="$options.i18n.createServiceAccount"
:svg-path="emptyIllustrationUrl"
+ :svg-height="150"
/>
<div v-else>
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index e6c0b86d9a6..37c1674cc5a 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -48,6 +48,10 @@
"ExternalAuditEventDestination",
"InstanceExternalAuditEventDestination"
],
+ "GoogleCloudLoggingConfigurationInterface": [
+ "GoogleCloudLoggingConfigurationType",
+ "InstanceGoogleCloudLoggingConfigurationType"
+ ],
"Issuable": [
"Epic",
"Issue",
@@ -139,6 +143,7 @@
"WorkItem"
],
"User": [
+ "AddOnUser",
"AutocompletedUser",
"MergeRequestAssignee",
"MergeRequestAuthor",
diff --git a/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users.query.graphql b/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users.query.graphql
new file mode 100644
index 00000000000..ed318ef1b8d
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users.query.graphql
@@ -0,0 +1,12 @@
+#import "../fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
+
+query projectAutocompleteUsersSearch($search: String!, $fullPath: ID!) {
+ workspace: project(fullPath: $fullPath) {
+ id
+ users: autocompleteUsers(search: $search) {
+ ...User
+ ...UserAvailability
+ }
+ }
+}
diff --git a/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users_with_mr_permissions.query.graphql b/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users_with_mr_permissions.query.graphql
new file mode 100644
index 00000000000..8155451fb7c
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users_with_mr_permissions.query.graphql
@@ -0,0 +1,19 @@
+#import "../fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
+
+query projectAutocompleteUsersSearchWithMRPermissions(
+ $search: String!
+ $fullPath: ID!
+ $mergeRequestId: MergeRequestID!
+) {
+ workspace: project(fullPath: $fullPath) {
+ id
+ users: autocompleteUsers(search: $search) {
+ ...User
+ ...UserAvailability
+ mergeRequestInteraction(id: $mergeRequestId) {
+ canMerge
+ }
+ }
+ }
+}
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
new file mode 100644
index 00000000000..470ff45f47a
--- /dev/null
+++ b/app/assets/javascripts/groups/components/empty_states/groups_dashboard_empty_state.vue
@@ -0,0 +1,24 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+
+export default {
+ components: { GlEmptyState },
+ inject: ['groupsEmptyStateIllustration'],
+ i18n: {
+ title: s__('GroupsEmptyState|A group is a collection of several projects'),
+ description: s__(
+ "GroupsEmptyState|If you organize your projects under a group, it works like a folder. You can manage your group member's permissions and access to each project in the group.",
+ ),
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="$options.i18n.title"
+ :description="$options.i18n.description"
+ :svg-path="groupsEmptyStateIllustration"
+ />
+</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
new file mode 100644
index 00000000000..f524b769802
--- /dev/null
+++ b/app/assets/javascripts/groups/components/empty_states/groups_explore_empty_state.vue
@@ -0,0 +1,17 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+
+import { __ } from '~/locale';
+
+export default {
+ components: { GlEmptyState },
+ inject: ['groupsEmptyStateIllustration'],
+ i18n: {
+ title: __('No public groups'),
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state :title="$options.i18n.title" :svg-path="groupsEmptyStateIllustration" />
+</template>
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
index e71ff6d9107..2539d899865 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -13,7 +13,7 @@ import GroupsStore from './store/groups_store';
Vue.use(Translate);
-export default () => {
+export default (EmptyStateComponent) => {
const el = document.getElementById('js-groups-tree');
// eslint-disable-next-line no-new
@@ -36,16 +36,19 @@ export default () => {
components: {
GroupsApp,
},
+ provide() {
+ const { groupsEmptyStateIllustration } = dataset;
+
+ return { groupsEmptyStateIllustration };
+ },
data() {
const showSchemaMarkup = parseBoolean(dataset.showSchemaMarkup);
- const renderEmptyState = parseBoolean(dataset.renderEmptyState);
const service = new GroupsService(dataset.endpoint);
const store = new GroupsStore({ hideProjects: true, showSchemaMarkup });
return {
store,
service,
- renderEmptyState,
loading: true,
};
},
@@ -74,7 +77,9 @@ export default () => {
props: {
store: this.store,
service: this.service,
- renderEmptyState: this.renderEmptyState,
+ },
+ scopedSlots: {
+ 'empty-state': () => createElement(EmptyStateComponent),
},
});
},
diff --git a/app/assets/javascripts/helpers/avatar_helper.js b/app/assets/javascripts/helpers/avatar_helper.js
deleted file mode 100644
index 35b09e9b027..00000000000
--- a/app/assets/javascripts/helpers/avatar_helper.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { escape } from 'lodash';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
-
-export const DEFAULT_SIZE_CLASS = 's40';
-export const IDENTICON_BG_COUNT = 7;
-
-export function getIdenticonBackgroundClass(entityId) {
- // If a GraphQL string id is passed in, convert it to the entity number
- const id = typeof entityId === 'string' ? getIdFromGraphQLId(entityId) : entityId;
- const type = (id % IDENTICON_BG_COUNT) + 1;
- return `bg${type}`;
-}
-
-export function getIdenticonTitle(entityName) {
- return getFirstCharacterCapitalized(entityName) || ' ';
-}
-
-export function renderIdenticon(entity, options = {}) {
- const { sizeClass = DEFAULT_SIZE_CLASS } = options;
-
- const bgClass = getIdenticonBackgroundClass(entity.id);
- const title = getIdenticonTitle(entity.name);
-
- return `<div class="avatar identicon ${escape(sizeClass)} ${escape(bgClass)}">${escape(
- title,
- )}</div>`;
-}
-
-export function renderAvatar(entity, options = {}) {
- if (!entity.avatar_url) {
- return renderIdenticon(entity, options);
- }
-
- const { sizeClass = DEFAULT_SIZE_CLASS } = options;
-
- return `<img src="${escape(entity.avatar_url)}" class="avatar ${escape(sizeClass)}" />`;
-}
diff --git a/app/assets/javascripts/ide/commit_icon.js b/app/assets/javascripts/ide/commit_icon.js
index 70ee9cff22b..07eef897910 100644
--- a/app/assets/javascripts/ide/commit_icon.js
+++ b/app/assets/javascripts/ide/commit_icon.js
@@ -3,7 +3,8 @@ import { commitItemIconMap } from './constants';
export default (file) => {
if (file.deleted) {
return commitItemIconMap.deleted;
- } else if (file.tempFile && !file.prevPath) {
+ }
+ if (file.tempFile && !file.prevPath) {
return commitItemIconMap.addition;
}
diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
deleted file mode 100644
index f58a35e7624..00000000000
--- a/app/assets/javascripts/ide/components/file_templates/dropdown.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-<!-- eslint-disable vue/multi-word-component-names -->
-<script>
-import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
-import $ from 'jquery';
-// eslint-disable-next-line no-restricted-imports
-import { mapActions, mapState } from 'vuex';
-import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
-
-export default {
- components: {
- DropdownButton,
- GlIcon,
- GlLoadingIcon,
- },
- props: {
- data: {
- type: Array,
- required: false,
- default: () => [],
- },
- label: {
- type: String,
- required: true,
- },
- title: {
- type: String,
- required: false,
- default: null,
- },
- isAsyncData: {
- type: Boolean,
- required: false,
- default: false,
- },
- searchable: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- search: '',
- };
- },
- computed: {
- ...mapState('fileTemplates', ['templates', 'isLoading']),
- outputData() {
- return (this.isAsyncData ? this.templates : this.data).filter((t) => {
- if (!this.searchable) return true;
-
- return t.name.toLowerCase().indexOf(this.search.toLowerCase()) >= 0;
- });
- },
- showLoading() {
- return this.isAsyncData ? this.isLoading : false;
- },
- },
- mounted() {
- $(this.$el).on('show.bs.dropdown', this.fetchTemplatesIfAsync);
- },
- beforeDestroy() {
- $(this.$el).off('show.bs.dropdown', this.fetchTemplatesIfAsync);
- },
- methods: {
- ...mapActions('fileTemplates', ['fetchTemplateTypes']),
- fetchTemplatesIfAsync() {
- if (this.isAsyncData) {
- this.fetchTemplateTypes();
- }
- },
- clickItem(item) {
- this.$emit('click', item);
- },
- },
-};
-</script>
-
-<template>
- <div class="dropdown">
- <dropdown-button :toggle-text="label" data-display="static" />
- <div class="dropdown-menu pb-0">
- <div v-if="title" class="dropdown-title ml-0 mr-0">{{ title }}</div>
- <div v-if="!showLoading && searchable" class="dropdown-input">
- <input
- v-model="search"
- :placeholder="__('Filter...')"
- type="search"
- class="dropdown-input-field"
- />
- <gl-icon name="search" class="dropdown-input-search" />
- </div>
- <div class="dropdown-content">
- <gl-loading-icon v-if="showLoading" size="lg" />
- <ul v-else>
- <li v-for="(item, index) in outputData" :key="index">
- <button type="button" @click="clickItem(item)">{{ item.name }}</button>
- </li>
- </ul>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 854daa20628..741845e3325 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -32,7 +32,8 @@ export default {
if (this.modalType === modalTypes.tree) {
return __('Create new directory');
- } else if (this.modalType === modalTypes.rename) {
+ }
+ if (this.modalType === modalTypes.rename) {
return entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
}
@@ -43,7 +44,8 @@ export default {
if (this.modalType === modalTypes.tree) {
return __('Create directory');
- } else if (this.modalType === modalTypes.rename) {
+ }
+ if (this.modalType === modalTypes.rename) {
return entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
}
diff --git a/app/assets/javascripts/ide/components/terminal/terminal.vue b/app/assets/javascripts/ide/components/terminal/terminal.vue
index 9e8b3d87397..b2e90a64758 100644
--- a/app/assets/javascripts/ide/components/terminal/terminal.vue
+++ b/app/assets/javascripts/ide/components/terminal/terminal.vue
@@ -37,7 +37,8 @@ export default {
loadingText() {
if (isStartingStatus(this.status)) {
return __('Starting...');
- } else if (this.status === STOPPING) {
+ }
+ if (this.status === STOPPING) {
return __('Stopping...');
}
diff --git a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue
index 38e53b64503..332408b9ecf 100644
--- a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue
+++ b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue
@@ -31,12 +31,14 @@ export default {
icon: '',
text: this.isStarted ? MSG_TERMINAL_SYNC_UPLOADING : MSG_TERMINAL_SYNC_CONNECTING,
};
- } else if (this.isError) {
+ }
+ if (this.isError) {
return {
icon: 'warning',
text: this.message,
};
- } else if (this.isStarted) {
+ }
+ if (this.isStarted) {
return {
icon: 'mobile-issue-close',
text: MSG_TERMINAL_SYNC_RUNNING,
diff --git a/app/assets/javascripts/ide/init_gitlab_web_ide.js b/app/assets/javascripts/ide/init_gitlab_web_ide.js
index 11a0095db92..2e113003f8a 100644
--- a/app/assets/javascripts/ide/init_gitlab_web_ide.js
+++ b/app/assets/javascripts/ide/init_gitlab_web_ide.js
@@ -62,9 +62,8 @@ export const initGitlabWebIDE = async (el) => {
filePath,
mrId,
mrTargetProject: getMRTargetProject(),
- // note: At the time of writing this, forkInfo isn't expected by `@gitlab/web-ide`,
- // but it will be soon.
forkInfo,
+ username: gon.current_username,
links: {
feedbackIssue: GITLAB_WEB_IDE_FEEDBACK_ISSUE,
userPreferences: el.dataset.userPreferencesPath,
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index 7595a1cedf1..ec28845d805 100644
--- a/app/assets/javascripts/ide/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
@@ -7,9 +7,11 @@ import DirtyDiffWorker from './diff_worker?worker';
export const getDiffChangeType = (change) => {
if (change.modified) {
return 'modified';
- } else if (change.added) {
+ }
+ if (change.added) {
return 'added';
- } else if (change.removed) {
+ }
+ if (change.removed) {
return 'removed';
}
diff --git a/app/assets/javascripts/ide/lib/errors.js b/app/assets/javascripts/ide/lib/errors.js
index a8a048e588f..5063cf5fd4f 100644
--- a/app/assets/javascripts/ide/lib/errors.js
+++ b/app/assets/javascripts/ide/lib/errors.js
@@ -55,9 +55,11 @@ export const parseCommitError = (e) => {
if (CODEOWNERS_REGEX.test(message)) {
return createCodeownersCommitError(message);
- } else if (BRANCH_CHANGED_REGEX.test(message)) {
+ }
+ if (BRANCH_CHANGED_REGEX.test(message)) {
return createBranchChangedCommitError(message);
- } else if (BRANCH_ALREADY_EXISTS.test(message)) {
+ }
+ if (BRANCH_ALREADY_EXISTS.test(message)) {
return branchAlreadyExistsCommitError(message);
}
diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js
index 3fdf012bbb2..415e34f56b8 100644
--- a/app/assets/javascripts/ide/lib/files.js
+++ b/app/assets/javascripts/ide/lib/files.js
@@ -35,7 +35,8 @@ export const decorateFiles = ({
const insertParent = (path) => {
if (!path) {
return null;
- } else if (entries[path]) {
+ }
+ if (entries[path]) {
return entries[path];
}
diff --git a/app/assets/javascripts/ide/lib/mirror.js b/app/assets/javascripts/ide/lib/mirror.js
index f437965b25a..286798d7560 100644
--- a/app/assets/javascripts/ide/lib/mirror.js
+++ b/app/assets/javascripts/ide/lib/mirror.js
@@ -32,7 +32,8 @@ const isErrorPayload = (payload) => payload && payload.status_code !== HTTP_STAT
const getErrorFromResponse = (data) => {
if (isErrorResponse(data.error)) {
return { message: data.error.Message };
- } else if (isErrorPayload(data.payload)) {
+ }
+ if (isErrorPayload(data.payload)) {
return { message: data.payload.error_message };
}
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index f4fa52b2d4d..11e3d8260f7 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -133,7 +133,8 @@ export const loadBranch = ({ dispatch, getters, state }, { projectId, branchId }
if (currentProject?.branches?.[branchId]) {
return Promise.resolve();
- } else if (getters.emptyRepo) {
+ }
+ if (getters.emptyRepo) {
return dispatch('loadEmptyBranch', { projectId, branchId });
}
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index c0f666c6652..74fe61b6e2f 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -30,7 +30,8 @@ const getCannotPushCodeViewModel = (state) => {
text: MSG_GO_TO_FORK,
},
};
- } else if (forkPath) {
+ }
+ if (forkPath) {
return {
message: MSG_CANNOT_PUSH_CODE_SHOULD_FORK,
action: {
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/messages.js b/app/assets/javascripts/ide/stores/modules/terminal/messages.js
index ad7ad35a98c..a2b45f9dc62 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/messages.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/messages.js
@@ -39,7 +39,8 @@ export const configCheckError = (status, helpUrl) => {
},
false,
);
- } else if (status === HTTP_STATUS_FORBIDDEN) {
+ }
+ if (status === HTTP_STATUS_FORBIDDEN) {
return ERROR_PERMISSION;
}
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index ec661fdb0d6..bac3803e68f 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -81,9 +81,11 @@ export const setPageTitleForFile = (state, file) => {
export const commitActionForFile = (file) => {
if (file.prevPath) {
return commitActionTypes.move;
- } else if (file.deleted) {
+ }
+ if (file.deleted) {
return commitActionTypes.delete;
- } else if (file.tempFile) {
+ }
+ if (file.tempFile) {
return commitActionTypes.create;
}
@@ -131,7 +133,8 @@ export const createNewMergeRequestUrl = (projectUrl, source, target) =>
const sortTreesByTypeAndName = (a, b) => {
if (a.type === 'tree' && b.type === 'blob') {
return -1;
- } else if (a.type === 'blob' && b.type === 'tree') {
+ }
+ if (a.type === 'blob' && b.type === 'tree') {
return 1;
}
if (a.name < b.name) return -1;
diff --git a/app/assets/javascripts/import_entities/components/import_status.vue b/app/assets/javascripts/import_entities/components/import_status.vue
index 94c04123112..91436457b03 100644
--- a/app/assets/javascripts/import_entities/components/import_status.vue
+++ b/app/assets/javascripts/import_entities/components/import_status.vue
@@ -133,9 +133,11 @@ export default {
if (fetched === imported) {
return { name: 'status-success', class: 'gl-text-green-400' };
- } else if (imported === 0) {
+ }
+ if (imported === 0) {
return { name: 'status-scheduled', class: 'gl-text-gray-400' };
- } else if (this.status === STATUSES.FINISHED) {
+ }
+ if (this.status === STATUSES.FINISHED) {
return { name: 'status-alert', class: 'gl-text-orange-400' };
}
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index e5a88cf9510..782f417a989 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -11,9 +11,9 @@ import {
GlIcon,
GlEmptyState,
} from '@gitlab/ui';
-import { isValidSlaDueAt } from 'ee_else_ce/vue_shared/components/incidents/utils';
import { STATUS_CLOSED } from '~/issues/constants';
import { visitUrl, mergeUrlParams, joinPaths } from '~/lib/utils/url_utility';
+import { isValidDateString } from '~/lib/utils/datetime_range';
import { s__, n__ } from '~/locale';
import { INCIDENT_SEVERITY } from '~/sidebar/constants';
import SeverityToken from '~/sidebar/components/severity/severity.vue';
@@ -330,7 +330,7 @@ export default {
item.assignees.nodes.length - MAX_VISIBLE_ASSIGNEES,
);
},
- isValidSlaDueAt,
+ isValidDateString,
},
};
</script>
@@ -357,8 +357,7 @@ export default {
<gl-button
v-if="isHeaderButtonVisible"
class="gl-my-3 gl-mr-5 create-incident-button"
- data-testid="createIncidentBtn"
- data-qa-selector="create_incident_button"
+ data-testid="create-incident-button"
:loading="redirecting"
:disabled="redirecting"
category="primary"
@@ -406,7 +405,6 @@ export default {
>
<gl-link
data-testid="incident-link"
- data-qa-selector="incident_link"
:href="showIncidentLink(item)"
class="gl-min-w-0"
>
@@ -443,7 +441,7 @@ export default {
<template v-if="slaFeatureAvailable" #cell(incidentSla)="{ item }">
<service-level-agreement-cell
- v-if="isValidSlaDueAt(item.slaDueAt)"
+ v-if="isValidDateString(item.slaDueAt)"
:issue-iid="item.iid"
:project-path="projectPath"
:sla-due-at="item.slaDueAt"
diff --git a/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue b/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
index fe3f4ed4bf9..0a5bb82f151 100644
--- a/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
+++ b/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
@@ -25,7 +25,7 @@ export default {
<template>
<section
id="incident-management-settings"
- data-qa-selector="incidents_settings_content"
+ data-testid="incidents-settings-content"
class="settings no-animate"
>
<div class="settings-header">
diff --git a/app/assets/javascripts/integrations/index/components/integrations_table.vue b/app/assets/javascripts/integrations/index/components/integrations_table.vue
index eff64ed7c42..c94c509e811 100644
--- a/app/assets/javascripts/integrations/index/components/integrations_table.vue
+++ b/app/assets/javascripts/integrations/index/components/integrations_table.vue
@@ -1,13 +1,22 @@
<script>
-import { GlIcon, GlLink, GlTable, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlAvatarLabeled,
+ GlAvatarLink,
+ GlButton,
+ GlIcon,
+ GlTable,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import { sprintf, s__, __ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
+ GlAvatarLabeled,
+ GlAvatarLink,
+ GlButton,
GlIcon,
- GlLink,
GlTable,
TimeAgoTooltip,
},
@@ -49,17 +58,12 @@ export default {
key: 'active',
label: '',
thClass: 'gl-w-7',
+ tdClass: 'gl-vertical-align-middle!',
},
{
key: 'title',
label: __('Integration'),
- thClass: 'gl-w-quarter gl-xs-w-full',
- },
- {
- key: 'description',
- label: __('Description'),
- thClass: 'gl-display-none d-sm-table-cell',
- tdClass: 'gl-display-none d-sm-table-cell',
+ thClass: 'd-sm-table-cell',
},
);
@@ -67,11 +71,17 @@ export default {
fields.push({
key: 'updated_at',
label: this.showUpdatedAt ? __('Last updated') : '',
- thClass: 'gl-w-20 gl-text-right',
- tdClass: 'gl-text-right',
+ thClass: 'gl-display-none d-sm-table-cell gl-text-right',
+ tdClass: 'gl-text-right gl-display-none d-sm-table-cell gl-vertical-align-middle!',
});
}
+ fields.push({
+ key: 'edit_path',
+ label: '',
+ thClass: 'gl-w-15',
+ });
+
return fields;
},
filteredIntegrations() {
@@ -83,8 +93,11 @@ export default {
},
methods: {
getStatusTooltipTitle(integration) {
- return sprintf(s__('Integrations|%{integrationTitle}: active'), {
+ const status = integration.active ? 'active' : 'inactive';
+
+ return sprintf(s__('Integrations|%{integrationTitle}: %{status}'), {
integrationTitle: integration.title,
+ status,
});
},
},
@@ -95,26 +108,41 @@ export default {
<gl-table :items="filteredIntegrations" :fields="fields" :empty-text="emptyText" show-empty fixed>
<template #cell(active)="{ item }">
<gl-icon
- v-if="item.active"
+ v-if="item.configured"
v-gl-tooltip
- name="check"
- class="gl-text-green-500"
+ :name="item.active ? 'status-success' : 'status-paused'"
+ :class="item.active ? 'gl-text-green-500' : 'gl-text-gray-500'"
:title="getStatusTooltipTitle(item)"
/>
</template>
<template #cell(title)="{ item }">
- <gl-link
+ <gl-avatar-link
:href="item.edit_path"
- class="gl-font-weight-bold"
+ :title="item.title"
:data-qa-selector="`${item.name}_link`"
>
- {{ item.title }}
- </gl-link>
+ <gl-avatar-labeled
+ :label="item.title"
+ :sub-label="item.description"
+ :entity-id="item.id"
+ :entity-name="item.title"
+ :src="item.icon"
+ :size="32"
+ shape="rect"
+ :label-link="item.edit_path"
+ />
+ </gl-avatar-link>
</template>
<template #cell(updated_at)="{ item }">
<time-ago-tooltip v-if="showUpdatedAt && item.updated_at" :time="item.updated_at" />
</template>
+
+ <template #cell(edit_path)="{ item }">
+ <gl-button :href="item.edit_path">
+ {{ __('Configure') }}
+ </gl-button>
+ </template>
</gl-table>
</template>
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 e99a61caf3f..e9d7acdc913 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -1,28 +1,14 @@
<script>
-import {
- GlAlert,
- GlCollapsibleListbox,
- GlLink,
- GlSprintf,
- GlFormCheckboxGroup,
- GlButton,
- GlCollapse,
- GlIcon,
-} from '@gitlab/ui';
+import { GlAlert, GlButton, GlCollapse, GlIcon } from '@gitlab/ui';
import { partition, isString, uniqueId, isEmpty } from 'lodash';
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,
- qualifiesForTasksToBeDone,
-} from 'ee_else_ce/invite_members/utils/member_utils';
+import { memberName, triggerExternalAlert } from 'ee_else_ce/invite_members/utils/member_utils';
import {
USERS_FILTER_ALL,
- INVITE_MEMBERS_FOR_TASK,
MEMBER_MODAL_LABELS,
INVITE_MEMBER_MODAL_TRACKING_CATEGORY,
} from '../constants';
@@ -41,10 +27,6 @@ export default {
name: 'InviteMembersModal',
components: {
GlAlert,
- GlLink,
- GlCollapsibleListbox,
- GlSprintf,
- GlFormCheckboxGroup,
GlButton,
GlCollapse,
GlIcon,
@@ -56,7 +38,6 @@ export default {
import('ee_component/invite_members/components/active_trial_notification.vue'),
},
mixins: [Tracking.mixin({ category: INVITE_MEMBER_MODAL_TRACKING_CATEGORY })],
- inject: ['newProjectPath'],
props: {
id: {
type: String,
@@ -96,14 +77,6 @@ export default {
required: false,
default: null,
},
- tasksToBeDoneOptions: {
- type: Array,
- required: true,
- },
- projects: {
- type: Array,
- required: true,
- },
fullPath: {
type: String,
required: true,
@@ -131,9 +104,6 @@ export default {
modalId: uniqueId('invite-members-modal-'),
newUsersToInvite: [],
invalidMembers: {},
- selectedTasksToBeDone: [],
- selectedTaskProject: this.projects[0],
- selectedTaskProjectId: this.projects[0]?.id,
source: 'unknown',
mode: 'default',
// Kept in sync with "base"
@@ -141,7 +111,6 @@ export default {
errorsLimit: 2,
isErrorsSectionExpanded: false,
shouldShowEmptyInvitesAlert: false,
- projectsForDropdown: this.projects.map((p) => ({ value: p.id, text: p.title, ...p })),
};
},
computed: {
@@ -170,26 +139,6 @@ export default {
this.errorList.length,
);
},
- tasksToBeDoneEnabled() {
- return qualifiesForTasksToBeDone(this.source) && this.tasksToBeDoneOptions.length;
- },
- showTasksToBeDone() {
- return (
- this.tasksToBeDoneEnabled &&
- this.selectedAccessLevel >= INVITE_MEMBERS_FOR_TASK.minimum_access_level
- );
- },
- showTaskProjects() {
- return !this.isProject && this.selectedTasksToBeDone.length;
- },
- tasksToBeDoneForPost() {
- return this.showTasksToBeDone ? this.selectedTasksToBeDone : [];
- },
- tasksProjectForPost() {
- return this.showTasksToBeDone && this.selectedTasksToBeDone.length
- ? this.selectedTaskProject.id
- : '';
- },
showUserLimitNotification() {
return !isEmpty(this.usersLimitDataset.alertVariant);
},
@@ -243,10 +192,6 @@ export default {
eventHub.$on('openModal', (options) => {
this.openModal(options);
});
-
- if (this.tasksToBeDoneEnabled) {
- this.openModal({ source: 'in_product_marketing_email' });
- }
},
methods: {
showInvalidFeedbackMessage(response) {
@@ -289,8 +234,6 @@ export default {
expires_at: expiresAt,
access_level: accessLevel,
invite_source: this.source,
- tasks_to_be_done: this.tasksToBeDoneForPost,
- tasks_project_id: this.tasksProjectForPost,
...email,
...userId,
};
@@ -304,8 +247,6 @@ export default {
return;
}
- this.trackInviteMembersForTask();
-
const apiAddByInvite = this.isProject
? Api.inviteProjectMembers.bind(Api)
: Api.inviteGroupMembers.bind(Api);
@@ -317,7 +258,7 @@ export default {
const { error, message } = responseFromSuccess(response);
if (error) {
- this.showMemberErrors(message);
+ this.showErrors(message);
} else {
this.onInviteSuccess();
}
@@ -327,19 +268,18 @@ export default {
this.isLoading = false;
}
},
- showMemberErrors(message) {
- this.invalidMembers = message;
- this.$refs.alerts.focus();
+ showErrors(message) {
+ if (isString(message)) {
+ this.invalidFeedbackMessage = message;
+ } else {
+ this.invalidMembers = message;
+ this.$refs.alerts.focus();
+ }
},
tokenName(username) {
// initial token creation hits this and nothing is found... so safe navigation
return this.newUsersToInvite.find((member) => memberName(member) === username)?.name;
},
- trackInviteMembersForTask() {
- const label = 'selected_tasks_to_be_done';
- const property = this.selectedTasksToBeDone.join(',');
- this.track(INVITE_MEMBERS_FOR_TASK.submit, { label, property });
- },
onCancel() {
this.track('click_cancel', { label: this.source });
},
@@ -351,11 +291,6 @@ export default {
this.isLoading = false;
this.shouldShowEmptyInvitesAlert = false;
this.newUsersToInvite = [];
- this.selectedTasksToBeDone = [];
- [this.selectedTaskProject] = this.projects;
- },
- changeSelectedTaskProject(projectId) {
- this.selectedTaskProject = this.projects.find((project) => project.id === projectId);
},
onInviteSuccess() {
this.track('invite_successful', { label: this.source });
@@ -513,46 +448,5 @@ export default {
@token-remove="removeToken"
/>
</template>
- <template #form-after>
- <div v-if="showTasksToBeDone" data-testid="invite-members-modal-tasks-to-be-done">
- <label class="gl-mt-5">
- {{ $options.labels.tasksToBeDone.title }}
- </label>
- <template v-if="projects.length">
- <gl-form-checkbox-group
- v-model="selectedTasksToBeDone"
- :options="tasksToBeDoneOptions"
- data-testid="invite-members-modal-tasks"
- />
- <template v-if="showTaskProjects">
- <label class="gl-mt-5 gl-display-block">
- {{ $options.labels.tasksProject.title }}
- </label>
- <gl-collapsible-listbox
- v-model="selectedTaskProjectId"
- :items="projectsForDropdown"
- :block="true"
- class="gl-w-half gl-xs-w-full"
- data-testid="invite-members-modal-project-select"
- @select="changeSelectedTaskProject"
- />
- </template>
- </template>
- <gl-alert
- v-else-if="tasksToBeDoneEnabled"
- variant="tip"
- :dismissible="false"
- data-testid="invite-members-modal-no-projects-alert"
- >
- <gl-sprintf :message="$options.labels.tasksToBeDone.noProjects">
- <template #link="{ content }">
- <gl-link :href="newProjectPath" target="_blank" class="gl-label-link">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
- </div>
- </template>
</invite-modal-base>
</template>
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 91b623821dd..5a891e23faf 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -330,8 +330,6 @@ export default {
:target="null"
/>
</gl-form-group>
-
- <slot name="form-after"></slot>
</template>
<template v-for="{ key } in extraSlots" #[key]>
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index 1cee0c32008..93386e5504b 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -5,10 +5,6 @@ export const PROJECT_SELECT_LABEL_ID = 'project-select';
export const SEARCH_DELAY = 200;
export const VALID_TOKEN_BACKGROUND = 'gl-bg-green-100';
export const INVALID_TOKEN_BACKGROUND = 'gl-bg-red-100';
-export const INVITE_MEMBERS_FOR_TASK = {
- minimum_access_level: 30,
- submit: 'submit',
-};
export const TOAST_MESSAGE_LOCALSTORAGE_KEY = 'members_invited_successfully';
export const GROUP_FILTERS = {
@@ -46,15 +42,6 @@ export const MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT = s__(
);
export const MEMBERS_SEARCH_FIELD = s__('InviteMembersModal|Username or email address');
export const MEMBERS_PLACEHOLDER = s__('InviteMembersModal|Select members or type email addresses');
-export const MEMBERS_TASKS_TO_BE_DONE_TITLE = s__(
- 'InviteMembersModal|Create issues for your new team member to work on (optional)',
-);
-export const MEMBERS_TASKS_TO_BE_DONE_NO_PROJECTS = s__(
- 'InviteMembersModal|To assign issues to a new team member, you need a project for the issues. %{linkStart}Create a project to get started.%{linkEnd}',
-);
-export const MEMBERS_TASKS_PROJECTS_TITLE = s__(
- 'InviteMembersModal|Choose a project for the issues',
-);
export const GROUP_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite a group');
export const GROUP_MODAL_TO_GROUP_DEFAULT_INTRO_TEXT = s__(
@@ -123,13 +110,6 @@ export const MEMBER_MODAL_LABELS = {
},
searchField: MEMBERS_SEARCH_FIELD,
placeHolder: MEMBERS_PLACEHOLDER,
- tasksToBeDone: {
- title: MEMBERS_TASKS_TO_BE_DONE_TITLE,
- noProjects: MEMBERS_TASKS_TO_BE_DONE_NO_PROJECTS,
- },
- tasksProject: {
- title: MEMBERS_TASKS_PROJECTS_TITLE,
- },
toastMessageSuccessful: TOAST_MESSAGE_SUCCESSFUL,
memberErrorListText: MEMBER_ERROR_LIST_TEXT,
collapsedErrors: COLLAPSED_ERRORS,
diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js
index 4f539cd8756..41ed0179364 100644
--- a/app/assets/javascripts/invite_members/init_invite_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js
@@ -25,7 +25,6 @@ export default (function initInviteMembersModal() {
name: 'InviteMembersModalRoot',
provide: {
name: el.dataset.name,
- newProjectPath: el.dataset.newProjectPath,
},
render: (createElement) =>
createElement(InviteMembersModal, {
@@ -34,8 +33,6 @@ export default (function initInviteMembersModal() {
isProject: parseBoolean(el.dataset.isProject),
accessLevels: JSON.parse(el.dataset.accessLevels),
defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10),
- tasksToBeDoneOptions: JSON.parse(el.dataset.tasksToBeDoneOptions || '[]'),
- projects: JSON.parse(el.dataset.projects || '[]'),
usersFilter: el.dataset.usersFilter,
filterId: parseInt(el.dataset.filterId, 10),
usersLimitDataset: convertObjectPropsToCamelCase(
diff --git a/app/assets/javascripts/invite_members/utils/member_utils.js b/app/assets/javascripts/invite_members/utils/member_utils.js
index 240a3a89686..7998cb69445 100644
--- a/app/assets/javascripts/invite_members/utils/member_utils.js
+++ b/app/assets/javascripts/invite_members/utils/member_utils.js
@@ -1,5 +1,3 @@
-import { getParameterValues } from '~/lib/utils/url_utility';
-
export function memberName(member) {
// user defined tokens(invites by email) will have email in `name` and will not contain `username`
return member.username || member.name;
@@ -8,7 +6,3 @@ export function memberName(member) {
export function triggerExternalAlert() {
return false;
}
-
-export function qualifiesForTasksToBeDone() {
- return getParameterValues('open_modal')[0] === 'invite_members_for_task';
-}
diff --git a/app/assets/javascripts/issuable/components/csv_export_modal.vue b/app/assets/javascripts/issuable/components/csv_export_modal.vue
index c1de507cd80..fd279a6a451 100644
--- a/app/assets/javascripts/issuable/components/csv_export_modal.vue
+++ b/app/assets/javascripts/issuable/components/csv_export_modal.vue
@@ -47,7 +47,7 @@ export default {
href: this.exportCsvPath,
variant: 'confirm',
'data-method': 'post',
- 'data-qa-selector': `export_issues_button`,
+ 'data-testid': 'export-issues-button',
'data-track-action': 'click_button',
'data-track-label': this.dataTrackLabel,
},
@@ -78,7 +78,7 @@ export default {
:action-cancel="$options.actionCancel"
body-class="gl-p-0!"
:title="exportText"
- data-qa-selector="export_issuable_modal"
+ data-testid="export-issuable-modal"
>
<div
class="gl-justify-content-start gl-align-items-center gl-p-4 gl-border-b-solid gl-border-1 gl-border-gray-50"
diff --git a/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue b/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue
index 872e1d4269d..8e2ed63613d 100644
--- a/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue
+++ b/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue
@@ -76,7 +76,6 @@ export default {
v-if="showExportButton"
v-gl-modal="exportModalId"
data-testid="export-as-csv-button"
- data-qa-selector="export_as_csv_button"
:item="dropdownItems.exportAsCSV"
/>
<gl-disclosure-dropdown-item
@@ -88,7 +87,6 @@ export default {
<gl-disclosure-dropdown-item
v-if="showImportButton && canEdit"
data-testid="import-from-jira-link"
- data-qa-selector="import_from_jira_link"
:item="dropdownItems.importFromJIRA"
/>
diff --git a/app/assets/javascripts/issuable/components/issue_assignees.vue b/app/assets/javascripts/issuable/components/issue_assignees.vue
index d8cbc45684b..2181b4e1a40 100644
--- a/app/assets/javascripts/issuable/components/issue_assignees.vue
+++ b/app/assets/javascripts/issuable/components/issue_assignees.vue
@@ -89,7 +89,7 @@ export default {
:img-size="iconSize"
class="js-no-trigger author-link"
tooltip-placement="bottom"
- data-qa-selector="assignee_link"
+ data-testid="assignee-link"
>
<span class="js-assignee-tooltip">
<span class="bold d-block">{{ s__('Label|Assignee') }}</span> {{ assignee.name }}
@@ -101,7 +101,7 @@ export default {
v-gl-tooltip.bottom
:title="assigneesCounterTooltip"
class="avatar-counter"
- data-qa-selector="avatar_counter_content"
+ data-testid="avatar-counter-content"
>{{ assigneeCounterLabel }}</span
>
</div>
diff --git a/app/assets/javascripts/issuable/components/issue_milestone.vue b/app/assets/javascripts/issuable/components/issue_milestone.vue
index c7da3e59098..3340ef2338c 100644
--- a/app/assets/javascripts/issuable/components/issue_milestone.vue
+++ b/app/assets/javascripts/issuable/components/issue_milestone.vue
@@ -42,7 +42,8 @@ export default {
milestoneDatesAbsolute() {
if (this.milestoneDue) {
return `(${dateInWords(this.milestoneDue)})`;
- } else if (this.milestoneStart) {
+ }
+ if (this.milestoneStart) {
return `(${dateInWords(this.milestoneStart)})`;
}
return '';
diff --git a/app/assets/javascripts/issuable/components/status_badge.vue b/app/assets/javascripts/issuable/components/status_badge.vue
new file mode 100644
index 00000000000..949fb3c1ce5
--- /dev/null
+++ b/app/assets/javascripts/issuable/components/status_badge.vue
@@ -0,0 +1,98 @@
+<script>
+import { GlBadge, GlIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
+import {
+ STATUS_CLOSED,
+ STATUS_LOCKED,
+ STATUS_MERGED,
+ STATUS_OPEN,
+ TYPE_EPIC,
+ TYPE_ISSUE,
+ TYPE_MERGE_REQUEST,
+} from '~/issues/constants';
+
+const badgePropertiesMap = {
+ [TYPE_EPIC]: {
+ [STATUS_OPEN]: {
+ icon: 'epic',
+ text: __('Open'),
+ variant: 'success',
+ },
+ [STATUS_CLOSED]: {
+ icon: 'epic-closed',
+ text: __('Closed'),
+ variant: 'info',
+ },
+ },
+ [TYPE_ISSUE]: {
+ [STATUS_OPEN]: {
+ icon: 'issues',
+ text: __('Open'),
+ variant: 'success',
+ },
+ [STATUS_CLOSED]: {
+ icon: 'issue-closed',
+ text: __('Closed'),
+ variant: 'info',
+ },
+ [STATUS_LOCKED]: {
+ icon: 'issues',
+ text: __('Open'),
+ variant: 'success',
+ },
+ },
+ [TYPE_MERGE_REQUEST]: {
+ [STATUS_OPEN]: {
+ icon: 'merge-request-open',
+ text: __('Open'),
+ variant: 'success',
+ },
+ [STATUS_CLOSED]: {
+ icon: 'merge-request-close',
+ text: __('Closed'),
+ variant: 'danger',
+ },
+ [STATUS_MERGED]: {
+ icon: 'merge',
+ text: __('Merged'),
+ variant: 'info',
+ },
+ [STATUS_LOCKED]: {
+ icon: 'merge-request-open',
+ text: __('Open'),
+ variant: 'success',
+ },
+ },
+};
+
+export default {
+ components: {
+ GlBadge,
+ GlIcon,
+ },
+ props: {
+ issuableType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ state: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ badgeProperties() {
+ return badgePropertiesMap[this.issuableType][this.state];
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-badge :variant="badgeProperties.variant" :aria-label="badgeProperties.text">
+ <gl-icon :name="badgeProperties.icon" />
+ <span class="gl-display-none gl-sm-display-block gl-ml-2">{{ badgeProperties.text }}</span>
+ </gl-badge>
+</template>
diff --git a/app/assets/javascripts/issuable/components/status_box.vue b/app/assets/javascripts/issuable/components/status_box.vue
deleted file mode 100644
index 0d7d0f020dd..00000000000
--- a/app/assets/javascripts/issuable/components/status_box.vue
+++ /dev/null
@@ -1,146 +0,0 @@
-<script>
-import { GlBadge, GlIcon } from '@gitlab/ui';
-import Vue from 'vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { fetchPolicies } from '~/lib/graphql';
-import { __ } from '~/locale';
-import {
- STATUS_CLOSED,
- STATUS_OPEN,
- TYPE_ISSUE,
- TYPE_MERGE_REQUEST,
- TYPE_EPIC,
-} from '~/issues/constants';
-
-export const badgeState = Vue.observable({
- state: '',
- updateStatus: null,
-});
-
-const CLASSES = {
- opened: 'issuable-status-badge-open',
- locked: 'issuable-status-badge-open',
- closed: 'issuable-status-badge-closed',
- merged: 'issuable-status-badge-merged',
-};
-
-const ICONS = {
- [TYPE_EPIC]: {
- opened: 'epic',
- closed: 'epic-closed',
- },
- [TYPE_ISSUE]: {
- opened: 'issues',
- locked: 'issues',
- closed: 'issue-closed',
- },
- [TYPE_MERGE_REQUEST]: {
- opened: 'merge-request-open',
- locked: 'merge-request-open',
- closed: 'merge-request-close',
- merged: 'merge',
- },
-};
-
-const STATUS = {
- opened: __('Open'),
- locked: __('Open'),
- closed: __('Closed'),
- merged: __('Merged'),
-};
-
-export default {
- components: {
- GlBadge,
- GlIcon,
- },
- mixins: [glFeatureFlagMixin()],
- 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 (this.initialState && !badgeState.state) {
- badgeState.state = this.initialState;
- }
-
- return badgeState;
- },
- computed: {
- badgeClass() {
- return [
- CLASSES[this.state],
- {
- 'gl-vertical-align-bottom': this.issuableType === TYPE_MERGE_REQUEST,
- },
- ];
- },
- badgeVariant() {
- if (this.state === STATUS_OPEN) {
- return 'success';
- } else if (this.state === STATUS_CLOSED) {
- return this.issuableType === TYPE_MERGE_REQUEST ? 'danger' : 'info';
- }
- return 'info';
- },
- badgeText() {
- return STATUS[this.state];
- },
- badgeIcon() {
- const type = this.issuableType || TYPE_MERGE_REQUEST;
- return ICONS[type][this.state];
- },
- },
- 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>
- <gl-badge
- class="issuable-status-badge gl-mr-3 gl-align-self-center"
- :class="badgeClass"
- :variant="badgeVariant"
- :aria-label="badgeText"
- >
- <gl-icon :name="badgeIcon" class="gl-badge-icon" />
- <span class="gl-display-none gl-sm-display-block gl-ml-2">{{ badgeText }}</span>
- </gl-badge>
-</template>
diff --git a/app/assets/javascripts/issuable/index.js b/app/assets/javascripts/issuable/index.js
index acc0161bf6a..40f92763b29 100644
--- a/app/assets/javascripts/issuable/index.js
+++ b/app/assets/javascripts/issuable/index.js
@@ -5,7 +5,6 @@ import Sidebar from '~/right_sidebar';
import { getSidebarOptions } from '~/sidebar/mount_sidebar';
import CsvImportExportButtons from './components/csv_import_export_buttons.vue';
import IssuableByEmail from './components/issuable_by_email.vue';
-import IssuableHeaderWarnings from './components/issuable_header_warnings.vue';
import issuableBulkUpdateActions from './issuable_bulk_update_actions';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableContext from './issuable_context';
@@ -97,24 +96,6 @@ export function initIssuableByEmail() {
});
}
-export function initIssuableHeaderWarnings(store) {
- const el = document.getElementById('js-issuable-header-warnings');
-
- if (!el) {
- return null;
- }
-
- const { hidden } = el.dataset;
-
- return new Vue({
- el,
- name: 'IssuableHeaderWarningsRoot',
- store,
- provide: { hidden: parseBoolean(hidden) },
- render: (createElement) => createElement(IssuableHeaderWarnings),
- });
-}
-
export function initIssuableSidebar() {
const el = document.querySelector('.js-sidebar-options');
diff --git a/app/assets/javascripts/issuable/issuable_context.js b/app/assets/javascripts/issuable/issuable_context.js
index 8c2e2a5df67..ef49bd29a40 100644
--- a/app/assets/javascripts/issuable/issuable_context.js
+++ b/app/assets/javascripts/issuable/issuable_context.js
@@ -8,6 +8,29 @@ export default class IssuableContext {
this.userSelect = new UsersSelect(currentUser);
this.reviewersSelect = new UsersSelect(currentUser, '.js-reviewer-search');
+ this.reviewersSelect.dropdowns.forEach((glDropdownInstance) => {
+ const jQueryWrapper = glDropdownInstance.dropdown;
+ const domElement = jQueryWrapper[0];
+ const content = domElement.querySelector('.dropdown-content');
+ const loader = domElement.querySelector('.dropdown-loading');
+ const spinner = loader.querySelector('.gl-spinner-container');
+ const realParent = loader.parentNode;
+
+ domElement.classList.add('non-blocking-loader');
+ spinner.classList.remove('gl-mt-7');
+ spinner.classList.add('gl-mt-2');
+
+ jQueryWrapper.on('shown.bs.dropdown', () => {
+ glDropdownInstance.filterInput.focus();
+ });
+ jQueryWrapper.on('toggle.on.loading.gl.dropdown filtering.gl.dropdown', () => {
+ content.appendChild(loader);
+ });
+ jQueryWrapper.on('done.remote.loading.gl.dropdown done.filtering.gl.dropdown', () => {
+ realParent.appendChild(loader);
+ });
+ });
+
$('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() {
return $(this).submit();
});
diff --git a/app/assets/javascripts/issuable/popover/components/issue_popover.vue b/app/assets/javascripts/issuable/popover/components/issue_popover.vue
index 044a1bba7ad..12d76ec4b54 100644
--- a/app/assets/javascripts/issuable/popover/components/issue_popover.vue
+++ b/app/assets/javascripts/issuable/popover/components/issue_popover.vue
@@ -3,12 +3,13 @@ import { GlIcon, GlPopover, GlSkeletonLoader, GlTooltipDirective } from '@gitlab
import query from 'ee_else_ce/issuable/popover/queries/issue.query.graphql';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import IssueMilestone from '~/issuable/components/issue_milestone.vue';
-import StatusBox from '~/issuable/components/status_box.vue';
-import { STATUS_CLOSED } from '~/issues/constants';
+import StatusBadge from '~/issuable/components/status_badge.vue';
+import { STATUS_CLOSED, TYPE_ISSUE } from '~/issues/constants';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
export default {
+ TYPE_ISSUE,
components: {
GlIcon,
GlPopover,
@@ -16,7 +17,7 @@ export default {
IssueDueDate,
IssueMilestone,
IssueWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
- StatusBox,
+ StatusBadge,
WorkItemTypeIcon,
},
directives: {
@@ -82,14 +83,14 @@ export default {
<gl-skeleton-loader v-if="$apollo.queries.issue.loading" :height="15">
<rect width="250" height="15" rx="4" />
</gl-skeleton-loader>
- <div v-else-if="showDetails" class="gl-display-flex gl-align-items-center">
- <status-box issuable-type="issue" :initial-state="issue.state" />
+ <div v-else-if="showDetails" class="gl-display-flex gl-align-items-center gl-gap-2">
+ <status-badge :issuable-type="$options.TYPE_ISSUE" :state="issue.state" />
<gl-icon
v-if="issue.confidential"
v-gl-tooltip
name="eye-slash"
:title="__('Confidential')"
- class="gl-text-orange-500 gl-mr-2"
+ class="gl-text-orange-500"
:aria-label="__('Confidential')"
/>
<span class="gl-text-secondary">
diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js
index 0a1a1324d7d..80344efc44c 100644
--- a/app/assets/javascripts/issues/constants.js
+++ b/app/assets/javascripts/issues/constants.js
@@ -30,6 +30,7 @@ export const issuableStatusText = {
export const IssuableTypeText = {
[TYPE_ISSUE]: __('issue'),
+ [TYPE_EPIC]: __('epic'),
[TYPE_MERGE_REQUEST]: __('merge request'),
[TYPE_ALERT]: __('alert'),
[TYPE_INCIDENT]: __('incident'),
diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
index 9febebf7e55..a756229e6ca 100644
--- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
+++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
@@ -495,7 +495,6 @@ export default {
:issuables-loading="isLoading"
namespace="dashboard"
recent-searches-storage-key="issues"
- :search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
:show-pagination-controls="showPaginationControls"
show-work-item-type-icon
diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js
index eec7c6bf842..3bd28c50800 100644
--- a/app/assets/javascripts/issues/index.js
+++ b/app/assets/javascripts/issues/index.js
@@ -3,21 +3,20 @@ import IssuableForm from 'ee_else_ce/issuable/issuable_form';
import IssuableLabelSelector from '~/issuable/issuable_label_selector';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import { initIssuableHeaderWarnings, initIssuableSidebar } from '~/issuable';
-import { TYPE_INCIDENT } from '~/issues/constants';
+import { initIssuableSidebar } from '~/issuable';
import Issue from '~/issues/issue';
import { initTitleSuggestions, initTypePopover, initTypeSelect } from '~/issues/new';
import { initRelatedMergeRequests } from '~/issues/related_merge_requests';
import { initRelatedIssues } from '~/related_issues';
-import { initIncidentApp, initIssueApp, initSentryErrorStackTrace } from '~/issues/show';
-import { parseIssuableData } from '~/issues/show/utils/parse_data';
+import { initIssuableApp, initSentryErrorStackTrace } from '~/issues/show';
import LabelsSelect from '~/labels/labels_select';
import initNotesApp from '~/notes';
import { store } from '~/notes/stores';
import { mountMilestoneDropdown } from '~/sidebar/mount_sidebar';
+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 initLinkedResources from '~/linked_resources';
import FilteredSearchServiceDesk from './filtered_search_service_desk';
export function initFilteredSearchServiceDesk() {
@@ -42,33 +41,20 @@ export function initForm() {
mountMilestoneDropdown();
}
-export function initShow({ notesParams } = {}) {
- const el = document.getElementById('js-issuable-app');
-
- if (!el) {
- return;
- }
-
- const { issueType, ...issuableData } = parseIssuableData(el);
-
- if (issueType === TYPE_INCIDENT) {
- initIncidentApp({ ...issuableData, issuableId: el.dataset.issuableId }, store);
- initLinkedResources();
- initRelatedIssues(TYPE_INCIDENT);
- } else {
- initIssueApp(issuableData, store);
- }
-
+export function initShow() {
new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
- initIssuableHeaderWarnings(store);
+
+ initAwardsApp(document.getElementById('js-vue-awards-block'));
+ initIssuableApp(store);
initIssuableSidebar();
- initNotesApp(notesParams);
+ initNotesApp();
+ initRelatedIssues();
initRelatedMergeRequests();
initSentryErrorStackTrace();
-
- initAwardsApp(document.getElementById('js-vue-awards-block'));
+ initSidebarBundle(store);
+ initWorkItemLinks();
import(/* webpackChunkName: 'design_management' */ '~/design_management')
.then((module) => module.default())
diff --git a/app/assets/javascripts/issues/issue.js b/app/assets/javascripts/issues/issue.js
index b7fd99d8042..06bbcdc12ea 100644
--- a/app/assets/javascripts/issues/issue.js
+++ b/app/assets/javascripts/issues/issue.js
@@ -49,13 +49,8 @@ export default class Issue {
issueFailMessage = __('Unable to update this issue at this time.'),
) {
if ('id' in data) {
- const isClosedBadge = $('.issuable-status-badge-closed');
- const isOpenBadge = $('.issuable-status-badge-open');
const projectIssuesCounter = $('.issue_counter');
- isClosedBadge.toggleClass('hidden', !isClosed);
- isOpenBadge.toggleClass('hidden', isClosed);
-
$(document).trigger('issuable:change', isClosed);
let numProjectIssues = Number(
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 9f7fca0ceca..3d62ea07f59 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
@@ -82,7 +82,7 @@ export default {
v-if="showCsvButtons"
class="gl-w-full gl-sm-w-auto gl-sm-mr-3"
:toggle-text="$options.i18n.importIssues"
- data-qa-selector="import_issues_dropdown"
+ data-testid="import-issues-dropdown"
>
<csv-import-export-buttons
:export-csv-path="exportCsvPathWithQuery"
diff --git a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
index dde1a4fd2d6..22c0984ebdb 100644
--- a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
+++ b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
@@ -10,6 +10,8 @@ import {
newDateAsLocaleTime,
} from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
+import { STATE_CLOSED } from '~/work_items/constants';
+import { isMilestoneWidget, isStartAndDueDateWidget } from '~/work_items/utils';
export default {
components: {
@@ -26,9 +28,12 @@ export default {
},
},
computed: {
+ milestone() {
+ return this.issue.milestone || this.issue.widgets?.find(isMilestoneWidget)?.milestone;
+ },
milestoneDate() {
- if (this.issue.milestone?.dueDate) {
- const { dueDate, startDate } = this.issue.milestone;
+ if (this.milestone.dueDate) {
+ const { dueDate, startDate } = this.milestone;
const date = dateInWords(newDateAsLocaleTime(dueDate), true);
const remainingTime = this.milestoneRemainingTime(dueDate, startDate);
return `${date} (${remainingTime})`;
@@ -36,15 +41,19 @@ export default {
return __('Milestone');
},
milestoneLink() {
- return this.issue.milestone.webPath || this.issue.milestone.webUrl;
+ return this.milestone.webPath || this.milestone.webUrl;
},
dueDate() {
- return this.issue.dueDate && dateInWords(newDateAsLocaleTime(this.issue.dueDate), true);
+ return this.issue.dueDate || this.issue.widgets?.find(isStartAndDueDateWidget)?.dueDate;
+ },
+ dueDateText() {
+ return this.dueDate && dateInWords(newDateAsLocaleTime(this.dueDate), true);
+ },
+ isClosed() {
+ return this.issue.state === STATUS_CLOSED || this.issue.state === STATE_CLOSED;
},
showDueDateInRed() {
- return (
- isInPast(newDateAsLocaleTime(this.issue.dueDate)) && this.issue.state !== STATUS_CLOSED
- );
+ return isInPast(newDateAsLocaleTime(this.dueDate)) && !this.isClosed;
},
timeEstimate() {
return this.issue.humanTimeEstimate || this.issue.timeStats?.humanTimeEstimate;
@@ -57,11 +66,14 @@ export default {
if (dueDate && isInPast(due)) {
return __('Past due');
- } else if (dueDate && isToday(due)) {
+ }
+ if (dueDate && isToday(due)) {
return __('Today');
- } else if (startDate && isInFuture(start)) {
+ }
+ if (startDate && isInFuture(start)) {
return __('Upcoming');
- } else if (dueDate) {
+ }
+ if (dueDate) {
return getTimeRemainingInWords(due);
}
return '';
@@ -73,7 +85,7 @@ export default {
<template>
<span>
<span
- v-if="issue.milestone"
+ v-if="milestone"
class="issuable-milestone gl-mr-3 gl-text-truncate gl-max-w-26 gl-display-inline-block gl-vertical-align-bottom"
data-testid="issuable-milestone"
>
@@ -84,11 +96,11 @@ export default {
class="gl-font-sm gl-text-gray-500!"
>
<gl-icon name="clock" :size="12" />
- {{ issue.milestone.title }}
+ {{ milestone.title }}
</gl-link>
</span>
<span
- v-if="issue.dueDate"
+ v-if="dueDate"
v-gl-tooltip
class="issuable-due-date gl-mr-3"
:class="{ 'gl-text-red-500': showDueDateInRed }"
@@ -96,7 +108,7 @@ export default {
data-testid="issuable-due-date"
>
<gl-icon name="calendar" :size="12" />
- {{ dueDate }}
+ {{ dueDateText }}
</span>
<span
v-if="timeEstimate"
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 c50b48ca0d8..3d8ed3af816 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -99,8 +99,6 @@ import {
import eventHub from '../eventhub';
import reorderIssuesMutation from '../queries/reorder_issues.mutation.graphql';
import searchLabelsQuery from '../queries/search_labels.query.graphql';
-import searchMilestonesQuery from '../queries/search_milestones.query.graphql';
-import searchUsersQuery from '../queries/search_users.query.graphql';
import setSortPreferenceMutation from '../queries/set_sort_preference.mutation.graphql';
import {
convertToApiParams,
@@ -204,11 +202,6 @@ export default {
required: false,
default: () => [],
},
- eeIsOkrsEnabled: {
- type: Boolean,
- required: false,
- default: false,
- },
},
data() {
return {
@@ -411,9 +404,10 @@ export default {
title: TOKEN_TITLE_MILESTONE,
icon: 'clock',
token: MilestoneToken,
- fetchMilestones: this.fetchMilestones,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-milestone`,
shouldSkipSort: true,
+ fullPath: this.fullPath,
+ isProject: this.isProject,
},
{
type: TOKEN_TYPE_LABEL,
@@ -640,32 +634,13 @@ export default {
fetchLatestLabels(search) {
return this.fetchLabelsWithFetchPolicy(search, fetchPolicies.NETWORK_ONLY);
},
- fetchMilestones(search) {
- return this.$apollo
- .query({
- query: searchMilestonesQuery,
- variables: { fullPath: this.fullPath, search, isProject: this.isProject },
- })
- .then(({ data }) => data[this.namespace]?.milestones.nodes);
- },
fetchUsers(search) {
- if (gon.features?.newGraphqlUsersAutocomplete) {
- return this.$apollo
- .query({
- query: usersAutocompleteQuery,
- variables: { fullPath: this.fullPath, search, isProject: this.isProject },
- })
- .then(({ data }) => data[this.namespace]?.autocompleteUsers);
- }
-
return this.$apollo
.query({
- query: searchUsersQuery,
+ query: usersAutocompleteQuery,
variables: { fullPath: this.fullPath, search, isProject: this.isProject },
})
- .then(({ data }) =>
- data[this.namespace]?.[`${this.namespace}Members`].nodes.map((member) => member.user),
- );
+ .then(({ data }) => data[this.namespace]?.autocompleteUsers);
},
getExportCsvPathWithQuery() {
return `${this.exportCsvPath}${window.location.search}`;
@@ -966,7 +941,6 @@ export default {
v-if="hasAnyIssues"
:namespace="fullPath"
recent-searches-storage-key="issues"
- :search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
:has-scoped-labels-feature="hasScopedLabelsFeature"
:initial-filter-value="filterTokens"
@@ -1037,14 +1011,11 @@ export default {
>
{{ $options.i18n.editIssues }}
</gl-button>
- <gl-button
- v-if="showNewIssueLink && !eeIsOkrsEnabled"
- :href="newIssuePath"
- variant="confirm"
- >
- {{ $options.i18n.newIssueLabel }}
- </gl-button>
- <slot name="new-objective-button"></slot>
+ <slot name="new-issuable-button">
+ <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+ {{ $options.i18n.newIssueLabel }}
+ </gl-button>
+ </slot>
<new-resource-dropdown
v-if="showNewIssueDropdown"
:query="$options.searchProjectsQuery"
@@ -1059,7 +1030,7 @@ export default {
no-caret
:toggle-text="$options.i18n.actionsLabel"
text-sr-only
- data-qa-selector="issues_list_more_actions_dropdown"
+ data-testid="issues-list-more-actions-dropdown"
>
<csv-import-export-buttons
v-if="showCsvButtons"
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 85e300b6474..682c7629962 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -121,7 +121,6 @@ export const i18n = {
reorderError: __('An error occurred while reordering issues.'),
deleteError: __('An error occurred while deleting an issuable.'),
rssLabel: __('Subscribe to RSS feed'),
- searchPlaceholder: __('Search or filter results...'),
upvotes: __('Upvotes'),
titles: __('Titles'),
descriptions: __('Descriptions'),
diff --git a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
index 3b49c0efb14..f3173f0e33a 100644
--- a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
@@ -11,6 +11,7 @@ fragment IssueFragment on Issue {
moved
state
title
+ titleHtml
updatedAt
closedAt
upvotes
diff --git a/app/assets/javascripts/issues/list/queries/search_milestones.query.graphql b/app/assets/javascripts/issues/list/queries/search_milestones.query.graphql
index 040240cde99..941e71b7ca7 100644
--- a/app/assets/javascripts/issues/list/queries/search_milestones.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/search_milestones.query.graphql
@@ -1,6 +1,11 @@
#import "./milestone.fragment.graphql"
-query searchMilestones($fullPath: ID!, $search: String, $isProject: Boolean = false) {
+query searchMilestones(
+ $fullPath: ID!
+ $search: String
+ $isProject: Boolean = false
+ $state: MilestoneStateEnum
+) {
group(fullPath: $fullPath) @skip(if: $isProject) {
id
milestones(
@@ -8,7 +13,7 @@ query searchMilestones($fullPath: ID!, $search: String, $isProject: Boolean = fa
includeAncestors: true
includeDescendants: true
sort: EXPIRED_LAST_DUE_DATE_ASC
- state: active
+ state: $state
) {
nodes {
...Milestone
@@ -21,7 +26,7 @@ query searchMilestones($fullPath: ID!, $search: String, $isProject: Boolean = fa
searchTitle: $search
includeAncestors: true
sort: EXPIRED_LAST_DUE_DATE_ASC
- state: active
+ state: $state
) {
nodes {
...Milestone
diff --git a/app/assets/javascripts/issues/list/queries/search_users.query.graphql b/app/assets/javascripts/issues/list/queries/search_users.query.graphql
deleted file mode 100644
index 6a1967a8875..00000000000
--- a/app/assets/javascripts/issues/list/queries/search_users.query.graphql
+++ /dev/null
@@ -1,29 +0,0 @@
-#import "./user.fragment.graphql"
-
-query searchUsers($fullPath: ID!, $search: String, $isProject: Boolean = false) {
- group(fullPath: $fullPath) @skip(if: $isProject) {
- id
- groupMembers(search: $search, relations: [DIRECT, INHERITED, SHARED_FROM_GROUPS]) {
- nodes {
- id
- user {
- ...User
- }
- }
- }
- }
- project(fullPath: $fullPath) @include(if: $isProject) {
- id
- projectMembers(
- search: $search
- relations: [DIRECT, INHERITED, INVITED_GROUPS, SHARED_INTO_ANCESTORS]
- ) {
- nodes {
- id
- user {
- ...User
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/issues/list/queries/user.fragment.graphql b/app/assets/javascripts/issues/list/queries/user.fragment.graphql
deleted file mode 100644
index 3e5bc0f7b93..00000000000
--- a/app/assets/javascripts/issues/list/queries/user.fragment.graphql
+++ /dev/null
@@ -1,6 +0,0 @@
-fragment User on User {
- id
- avatarUrl
- name
- username
-}
diff --git a/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue
index d819a371c69..5e81f7ad4f6 100644
--- a/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue
+++ b/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue
@@ -4,7 +4,6 @@ import { GlLink, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { sprintf, __, n__ } from '~/locale';
import RelatedIssuableItem from '~/issuable/components/related_issuable_item.vue';
-import { parseIssuableData } from '~/issues/show/utils/parse_data';
export default {
name: 'RelatedMergeRequests',
@@ -19,6 +18,11 @@ export default {
type: String,
required: true,
},
+ hasClosingMergeRequest: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
projectNamespace: {
type: String,
required: true,
@@ -48,9 +52,6 @@ export default {
this.setInitialState({ apiEndpoint: this.endpoint });
this.fetchMergeRequests();
},
- created() {
- this.hasClosingMergeRequest = parseIssuableData().hasClosingMergeRequest;
- },
methods: {
...mapActions(['setInitialState', 'fetchMergeRequests']),
getAssignees(mr) {
diff --git a/app/assets/javascripts/issues/related_merge_requests/index.js b/app/assets/javascripts/issues/related_merge_requests/index.js
index 196084093c8..413b48b9720 100644
--- a/app/assets/javascripts/issues/related_merge_requests/index.js
+++ b/app/assets/javascripts/issues/related_merge_requests/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import RelatedMergeRequests from './components/related_merge_requests.vue';
import createStore from './store';
@@ -9,7 +10,7 @@ export function initRelatedMergeRequests() {
return undefined;
}
- const { endpoint, projectPath, projectNamespace } = el.dataset;
+ const { endpoint, hasClosingMergeRequest, projectPath, projectNamespace } = el.dataset;
return new Vue({
el,
@@ -17,7 +18,12 @@ export function initRelatedMergeRequests() {
store: createStore(),
render: (createElement) =>
createElement(RelatedMergeRequests, {
- props: { endpoint, projectNamespace, projectPath },
+ props: {
+ endpoint,
+ hasClosingMergeRequest: parseBoolean(hasClosingMergeRequest),
+ projectNamespace,
+ projectPath,
+ },
}),
});
}
diff --git a/app/assets/javascripts/service_desk/components/empty_state_with_any_issues.vue b/app/assets/javascripts/issues/service_desk/components/empty_state_with_any_issues.vue
index a15c8ee2e9f..ab9e70ae223 100644
--- a/app/assets/javascripts/service_desk/components/empty_state_with_any_issues.vue
+++ b/app/assets/javascripts/issues/service_desk/components/empty_state_with_any_issues.vue
@@ -38,7 +38,8 @@ export default {
description: noSearchResultsDescription,
svgHeight: 150,
};
- } else if (this.isOpenTab) {
+ }
+ if (this.isOpenTab) {
return { title: noOpenIssuesTitle, description: infoBannerUserNote };
}
diff --git a/app/assets/javascripts/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..9dbed2c2579 100644
--- a/app/assets/javascripts/service_desk/components/empty_state_without_any_issues.vue
+++ b/app/assets/javascripts/issues/service_desk/components/empty_state_without_any_issues.vue
diff --git a/app/assets/javascripts/service_desk/components/info_banner.vue b/app/assets/javascripts/issues/service_desk/components/info_banner.vue
index 5667ee2f31d..5667ee2f31d 100644
--- a/app/assets/javascripts/service_desk/components/info_banner.vue
+++ b/app/assets/javascripts/issues/service_desk/components/info_banner.vue
diff --git a/app/assets/javascripts/service_desk/components/service_desk_list_app.vue b/app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue
index 56cd21d7ea9..4b59672428b 100644
--- a/app/assets/javascripts/service_desk/components/service_desk_list_app.vue
+++ b/app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue
@@ -5,7 +5,8 @@ import { isEmpty } from 'lodash';
import { fetchPolicies } from '~/lib/graphql';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import axios from '~/lib/utils/axios_utils';
-import { getParameterByName } from '~/lib/utils/url_utility';
+import { getParameterByName, joinPaths } from '~/lib/utils/url_utility';
+import { scrollUp } from '~/lib/utils/scroll_utils';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { DEFAULT_PAGE_SIZE, issuableListTabs } from '~/vue_shared/issuable/list/constants';
@@ -15,6 +16,8 @@ import {
getInitialPageParams,
getFilterTokens,
isSortKey,
+ getSortOptions,
+ getSortKey,
} from '~/issues/list/utils';
import {
OPERATORS_IS_NOT,
@@ -31,19 +34,24 @@ import {
PARAM_SORT,
CREATED_DESC,
UPDATED_DESC,
+ RELATIVE_POSITION_ASC,
urlSortParams,
} from '~/issues/list/constants';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { createAlert, VARIANT_INFO } from '~/alert';
+import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import searchProjectMembers from '~/graphql_shared/queries/project_user_members_search.query.graphql';
-import getServiceDeskIssuesQuery from 'ee_else_ce/service_desk/queries/get_service_desk_issues.query.graphql';
-import getServiceDeskIssuesCounts from 'ee_else_ce/service_desk/queries/get_service_desk_issues_counts.query.graphql';
+import getServiceDeskIssuesQuery from 'ee_else_ce/issues/service_desk/queries/get_service_desk_issues.query.graphql';
+import getServiceDeskIssuesCounts from 'ee_else_ce/issues/service_desk/queries/get_service_desk_issues_counts.query.graphql';
import searchProjectLabelsQuery from '../queries/search_project_labels.query.graphql';
import searchProjectMilestonesQuery from '../queries/search_project_milestones.query.graphql';
+import setSortingPreferenceMutation from '../queries/set_sorting_preference.mutation.graphql';
+import reorderServiceDeskIssuesMutation from '../queries/reorder_service_desk_issues.mutation.graphql';
import {
errorFetchingCounts,
errorFetchingIssues,
- searchPlaceholder,
+ issueRepositioningMessage,
+ reorderError,
SERVICE_DESK_BOT_USERNAME,
STATUS_OPEN,
STATUS_CLOSED,
@@ -68,7 +76,8 @@ export default {
i18n: {
errorFetchingCounts,
errorFetchingIssues,
- searchPlaceholder,
+ issueRepositioningMessage,
+ reorderError,
},
issuableListTabs,
components: {
@@ -81,6 +90,7 @@ export default {
inject: [
'releasesPath',
'autocompleteAwardEmojisPath',
+ 'hasBlockedIssuesFeature',
'hasIterationsFeature',
'hasIssueWeightsFeature',
'hasIssuableHealthStatusFeature',
@@ -92,6 +102,7 @@ export default {
'isServiceDeskSupported',
'hasAnyIssues',
'initialSort',
+ 'isIssueRepositioningDisabled',
],
props: {
eeSearchTokens: {
@@ -104,14 +115,13 @@ export default {
return {
serviceDeskIssues: [],
serviceDeskIssuesCounts: {},
- sortOptions: [],
filterTokens: [],
pageInfo: {},
pageParams: {},
sortKey: CREATED_DESC,
state: STATUS_OPEN,
pageSize: DEFAULT_PAGE_SIZE,
- issuesError: null,
+ issuesError: '',
};
},
apollo: {
@@ -167,7 +177,6 @@ export default {
return {
fullPath: this.fullPath,
iid: isIidSearch ? this.searchQuery.slice(1) : undefined,
- isProject: this.isProject,
isSignedIn: this.isSignedIn,
authorUsername: SERVICE_DESK_BOT_USERNAME,
sort: this.sortKey,
@@ -180,6 +189,13 @@ export default {
shouldSkipQuery() {
return !this.hasAnyIssues || isEmpty(this.pageParams);
},
+ sortOptions() {
+ return getSortOptions({
+ hasBlockedIssuesFeature: this.hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature: this.hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature: this.hasIssueWeightsFeature,
+ });
+ },
tabCounts() {
const { openedIssues, closedIssues, allIssues } = this.serviceDeskIssuesCounts;
return {
@@ -188,8 +204,20 @@ export default {
[STATUS_ALL]: allIssues?.count,
};
},
+ currentTabCount() {
+ return this.tabCounts[this.state] ?? 0;
+ },
+ showPaginationControls() {
+ return (
+ this.serviceDeskIssues.length > 0 &&
+ (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage)
+ );
+ },
+ showPageSizeControls() {
+ return this.currentTabCount > DEFAULT_PAGE_SIZE;
+ },
isLoading() {
- return this.$apollo.queries.serviceDeskIssues.loading;
+ return this.$apollo.loading;
},
isOpenTab() {
return this.state === STATUS_OPEN;
@@ -205,11 +233,14 @@ export default {
page_before: this.pageParams.beforeCursor ?? undefined,
};
},
+ hasAnyServiceDeskIssue() {
+ return this.hasSearch || Boolean(this.tabCounts.all);
+ },
isInfoBannerVisible() {
- return this.isServiceDeskSupported && this.hasAnyServiceDeskIssues;
+ return this.isServiceDeskSupported && this.hasAnyServiceDeskIssue;
},
- hasAnyServiceDeskIssues() {
- return this.hasSearch || Boolean(this.tabCounts.all);
+ canShowIssuesList() {
+ return this.isLoading || this.issuesError.length || this.hasAnyServiceDeskIssue;
},
hasOrFeature() {
return this.glFeatures.orIssuableQueries;
@@ -296,6 +327,9 @@ export default {
return tokens;
},
+ isManualOrdering() {
+ return this.sortKey === RELATIVE_POSITION_ASC;
+ },
},
watch: {
$route(newValue, oldValue) {
@@ -383,15 +417,120 @@ export default {
this.$router.push({ query: this.urlParams });
},
+ handleDismissAlert() {
+ this.issuesError = '';
+ },
+ handleNextPage() {
+ this.pageParams = {
+ afterCursor: this.pageInfo.endCursor,
+ firstPageSize: this.pageSize,
+ };
+ scrollUp();
+
+ this.$router.push({ query: this.urlParams });
+ },
+ handlePreviousPage() {
+ this.pageParams = {
+ beforeCursor: this.pageInfo.startCursor,
+ lastPageSize: this.pageSize,
+ };
+ scrollUp();
+
+ this.$router.push({ query: this.urlParams });
+ },
+ handlePageSizeChange(newPageSize) {
+ const pageParam = getParameterByName(PARAM_LAST_PAGE_SIZE) ? 'lastPageSize' : 'firstPageSize';
+ this.pageParams[pageParam] = newPageSize;
+ this.pageSize = newPageSize;
+ scrollUp();
+
+ this.$router.push({ query: this.urlParams });
+ },
+ handleSort(sortKey) {
+ if (this.sortKey === sortKey) {
+ return;
+ }
+
+ if (this.isIssueRepositioningDisabled && sortKey === RELATIVE_POSITION_ASC) {
+ this.showIssueRepositioningMessage();
+ return;
+ }
+
+ this.sortKey = sortKey;
+ this.pageParams = getInitialPageParams(this.pageSize);
+
+ if (this.isSignedIn) {
+ this.saveSortPreference(sortKey);
+ }
+
+ this.$router.push({ query: this.urlParams });
+ },
+ saveSortPreference(sortKey) {
+ this.$apollo
+ .mutate({
+ mutation: setSortingPreferenceMutation,
+ variables: { input: { issuesSort: sortKey } },
+ })
+ .then(({ data }) => {
+ if (data.userPreferencesUpdate.errors.length) {
+ throw new Error(data.userPreferencesUpdate.errors);
+ }
+ })
+ .catch((error) => {
+ Sentry.captureException(error);
+ });
+ },
+ handleReorder({ newIndex, oldIndex }) {
+ const issueToMove = this.serviceDeskIssues[oldIndex];
+ const isDragDropDownwards = newIndex > oldIndex;
+ const isMovingToBeginning = newIndex === 0;
+ const isMovingToEnd = newIndex === this.serviceDeskIssues.length - 1;
+
+ let moveBeforeId;
+ let moveAfterId;
+
+ if (isDragDropDownwards) {
+ const afterIndex = isMovingToEnd ? newIndex : newIndex + 1;
+ moveBeforeId = this.serviceDeskIssues[newIndex].id;
+ moveAfterId = this.serviceDeskIssues[afterIndex].id;
+ } else {
+ const beforeIndex = isMovingToBeginning ? newIndex : newIndex - 1;
+ moveBeforeId = this.serviceDeskIssues[beforeIndex].id;
+ moveAfterId = this.serviceDeskIssues[newIndex].id;
+ }
+
+ return axios
+ .put(joinPaths(issueToMove.webPath, 'reorder'), {
+ move_before_id: isMovingToBeginning ? null : getIdFromGraphQLId(moveBeforeId),
+ move_after_id: isMovingToEnd ? null : getIdFromGraphQLId(moveAfterId),
+ })
+ .then(() => {
+ const serializedVariables = JSON.stringify(this.queryVariables);
+ return this.$apollo.mutate({
+ mutation: reorderServiceDeskIssuesMutation,
+ variables: { oldIndex, newIndex, namespace: this.namespace, serializedVariables },
+ });
+ })
+ .catch((error) => {
+ this.issuesError = this.$options.i18n.reorderError;
+ Sentry.captureException(error);
+ });
+ },
updateData(sortValue) {
const firstPageSize = getParameterByName(PARAM_FIRST_PAGE_SIZE);
const lastPageSize = getParameterByName(PARAM_LAST_PAGE_SIZE);
const state = getParameterByName(PARAM_STATE);
const defaultSortKey = state === STATUS_CLOSED ? UPDATED_DESC : CREATED_DESC;
+ const dashboardSortKey = getSortKey(sortValue);
const graphQLSortKey = isSortKey(sortValue?.toUpperCase()) && sortValue.toUpperCase();
- const sortKey = graphQLSortKey || defaultSortKey;
+ let sortKey = dashboardSortKey || graphQLSortKey || defaultSortKey;
+
+ if (this.isIssueRepositioningDisabled && sortKey === RELATIVE_POSITION_ASC) {
+ this.showIssueRepositioningMessage();
+ sortKey = defaultSortKey;
+ }
this.filterTokens = getFilterTokens(window.location.search);
@@ -405,6 +544,12 @@ export default {
this.sortKey = sortKey;
this.state = state || STATUS_OPEN;
},
+ showIssueRepositioningMessage() {
+ createAlert({
+ message: this.$options.i18n.issueRepositioningMessage,
+ variant: VARIANT_INFO,
+ });
+ },
},
};
</script>
@@ -413,25 +558,36 @@ export default {
<section>
<info-banner v-if="isInfoBannerVisible" />
<issuable-list
- v-if="isLoading || hasAnyServiceDeskIssues"
+ v-if="canShowIssuesList"
namespace="service-desk"
recent-searches-storage-key="service-desk-issues"
:error="issuesError"
- :search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
:issuables-loading="isLoading"
:initial-filter-value="filterTokens"
:show-filtered-search-friendly-text="hasOrFeature"
+ :show-pagination-controls="showPaginationControls"
+ :show-page-size-change-controls="showPageSizeControls"
:sort-options="sortOptions"
:initial-sort-by="sortKey"
+ :is-manual-ordering="isManualOrdering"
:issuables="serviceDeskIssues"
:tabs="$options.issuableListTabs"
:tab-counts="tabCounts"
:current-tab="state"
:default-page-size="pageSize"
+ :has-next-page="pageInfo.hasNextPage"
+ :has-previous-page="pageInfo.hasPreviousPage"
sync-filter-and-sort
+ use-keyset-pagination
@click-tab="handleClickTab"
+ @dismiss-alert="handleDismissAlert"
@filter="handleFilter"
+ @sort="handleSort"
+ @reorder="handleReorder"
+ @next-page="handleNextPage"
+ @previous-page="handlePreviousPage"
+ @page-size-change="handlePageSizeChange"
>
<template #empty-state>
<empty-state-with-any-issues :has-search="hasSearch" :is-open-tab="isOpenTab" />
diff --git a/app/assets/javascripts/service_desk/constants.js b/app/assets/javascripts/issues/service_desk/constants.js
index a83c0d9ca57..e498a4f39a1 100644
--- a/app/assets/javascripts/service_desk/constants.js
+++ b/app/assets/javascripts/issues/service_desk/constants.js
@@ -235,7 +235,10 @@ export const noSearchResultsDescription = __(
'To widen your search, change or remove filters above',
);
export const noSearchResultsTitle = __('Sorry, your filter produced no results');
-export const searchPlaceholder = __('Search or filter results...');
+export const issueRepositioningMessage = __(
+ 'Issues are being rebalanced at the moment, so manual reordering is disabled.',
+);
+export const reorderError = __('An error occurred while reordering issues.');
export const infoBannerTitle = s__(
'ServiceDesk|Use Service Desk to connect with your users and offer customer support through email right inside GitLab',
);
diff --git a/app/assets/javascripts/service_desk/graphql.js b/app/assets/javascripts/issues/service_desk/graphql.js
index e01973f1e8a..e01973f1e8a 100644
--- a/app/assets/javascripts/service_desk/graphql.js
+++ b/app/assets/javascripts/issues/service_desk/graphql.js
diff --git a/app/assets/javascripts/service_desk/index.js b/app/assets/javascripts/issues/service_desk/index.js
index afb2d0e8de3..579cf343477 100644
--- a/app/assets/javascripts/service_desk/index.js
+++ b/app/assets/javascripts/issues/service_desk/index.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import { parseBoolean } from '~/lib/utils/common_utils';
-import ServiceDeskListApp from 'ee_else_ce/service_desk/components/service_desk_list_app.vue';
+import ServiceDeskListApp from 'ee_else_ce/issues/service_desk/components/service_desk_list_app.vue';
import { gqlClient } from './graphql';
export async function mountServiceDeskListApp() {
@@ -15,6 +15,7 @@ export async function mountServiceDeskListApp() {
const {
projectDataReleasesPath,
projectDataAutocompleteAwardEmojisPath,
+ projectDataHasBlockedIssuesFeature,
projectDataHasIterationsFeature,
projectDataHasIssueWeightsFeature,
projectDataHasIssuableHealthStatusFeature,
@@ -26,6 +27,7 @@ export async function mountServiceDeskListApp() {
projectDataSignInPath,
projectDataHasAnyIssues,
projectDataInitialSort,
+ projectDataIsIssueRepositioningDisabled,
serviceDeskEmailAddress,
canAdminIssues,
canEditProjectSettings,
@@ -53,6 +55,7 @@ export async function mountServiceDeskListApp() {
provide: {
releasesPath: projectDataReleasesPath,
autocompleteAwardEmojisPath: projectDataAutocompleteAwardEmojisPath,
+ hasBlockedIssuesFeature: parseBoolean(projectDataHasBlockedIssuesFeature),
hasIterationsFeature: parseBoolean(projectDataHasIterationsFeature),
hasIssueWeightsFeature: parseBoolean(projectDataHasIssueWeightsFeature),
hasIssuableHealthStatusFeature: parseBoolean(projectDataHasIssuableHealthStatusFeature),
@@ -72,6 +75,7 @@ export async function mountServiceDeskListApp() {
signInPath: projectDataSignInPath,
hasAnyIssues: parseBoolean(projectDataHasAnyIssues),
initialSort: projectDataInitialSort,
+ isIssueRepositioningDisabled: parseBoolean(projectDataIsIssueRepositioningDisabled),
},
render: (createComponent) => createComponent(ServiceDeskListApp),
});
diff --git a/app/assets/javascripts/service_desk/queries/get_service_desk_issues.query.graphql b/app/assets/javascripts/issues/service_desk/queries/get_service_desk_issues.query.graphql
index c678b8dd8ab..d8cd28f5cf1 100644
--- a/app/assets/javascripts/service_desk/queries/get_service_desk_issues.query.graphql
+++ b/app/assets/javascripts/issues/service_desk/queries/get_service_desk_issues.query.graphql
@@ -3,7 +3,6 @@
query getServiceDeskIssues(
$hideUsers: Boolean = false
- $isProject: Boolean = false
$isSignedIn: Boolean = false
$fullPath: ID!
$iid: String
@@ -22,8 +21,6 @@ query getServiceDeskIssues(
$releaseTag: [String!]
$releaseTagWildcardId: ReleaseTagWildcardId
$types: [IssueType!]
- $crmContactId: String
- $crmOrganizationId: String
$not: NegatedIssueFilterInput
$or: UnionedIssueFilterInput
$beforeCursor: String
@@ -31,7 +28,7 @@ query getServiceDeskIssues(
$firstPageSize: Int
$lastPageSize: Int
) {
- project(fullPath: $fullPath) @include(if: $isProject) @persist {
+ project(fullPath: $fullPath) @persist {
id
issues(
iid: $iid
@@ -50,8 +47,6 @@ query getServiceDeskIssues(
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
- crmContactId: $crmContactId
- crmOrganizationId: $crmOrganizationId
not: $not
or: $or
before: $beforeCursor
diff --git a/app/assets/javascripts/service_desk/queries/get_service_desk_issues_counts.query.graphql b/app/assets/javascripts/issues/service_desk/queries/get_service_desk_issues_counts.query.graphql
index c2ba397d76f..008cde60b74 100644
--- a/app/assets/javascripts/service_desk/queries/get_service_desk_issues_counts.query.graphql
+++ b/app/assets/javascripts/issues/service_desk/queries/get_service_desk_issues_counts.query.graphql
@@ -1,5 +1,4 @@
query getServiceDeskIssuesCount(
- $isProject: Boolean = false
$fullPath: ID!
$iid: String
$search: String
@@ -14,12 +13,10 @@ query getServiceDeskIssuesCount(
$releaseTag: [String!]
$releaseTagWildcardId: ReleaseTagWildcardId
$types: [IssueType!]
- $crmContactId: String
- $crmOrganizationId: String
$not: NegatedIssueFilterInput
$or: UnionedIssueFilterInput
) {
- project(fullPath: $fullPath) @include(if: $isProject) {
+ project(fullPath: $fullPath) {
id
openedIssues: issues(
state: opened
@@ -36,8 +33,6 @@ query getServiceDeskIssuesCount(
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
- crmContactId: $crmContactId
- crmOrganizationId: $crmOrganizationId
not: $not
or: $or
) {
@@ -58,8 +53,6 @@ query getServiceDeskIssuesCount(
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
- crmContactId: $crmContactId
- crmOrganizationId: $crmOrganizationId
not: $not
or: $or
) {
@@ -80,8 +73,6 @@ query getServiceDeskIssuesCount(
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
- crmContactId: $crmContactId
- crmOrganizationId: $crmOrganizationId
not: $not
or: $or
) {
diff --git a/app/assets/javascripts/service_desk/queries/issue.fragment.graphql b/app/assets/javascripts/issues/service_desk/queries/issue.fragment.graphql
index 3b49c0efb14..f72663ae5f6 100644
--- a/app/assets/javascripts/service_desk/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/issues/service_desk/queries/issue.fragment.graphql
@@ -36,6 +36,7 @@ fragment IssueFragment on Issue {
username
webUrl
}
+ externalAuthor
labels {
nodes {
__persist
diff --git a/app/assets/javascripts/service_desk/queries/label.fragment.graphql b/app/assets/javascripts/issues/service_desk/queries/label.fragment.graphql
index bb1d8f1ac9b..bb1d8f1ac9b 100644
--- a/app/assets/javascripts/service_desk/queries/label.fragment.graphql
+++ b/app/assets/javascripts/issues/service_desk/queries/label.fragment.graphql
diff --git a/app/assets/javascripts/service_desk/queries/milestone.fragment.graphql b/app/assets/javascripts/issues/service_desk/queries/milestone.fragment.graphql
index 3cdf69bf585..3cdf69bf585 100644
--- a/app/assets/javascripts/service_desk/queries/milestone.fragment.graphql
+++ b/app/assets/javascripts/issues/service_desk/queries/milestone.fragment.graphql
diff --git a/app/assets/javascripts/issues/service_desk/queries/reorder_service_desk_issues.mutation.graphql b/app/assets/javascripts/issues/service_desk/queries/reorder_service_desk_issues.mutation.graphql
new file mode 100644
index 00000000000..2da7850d77d
--- /dev/null
+++ b/app/assets/javascripts/issues/service_desk/queries/reorder_service_desk_issues.mutation.graphql
@@ -0,0 +1,13 @@
+mutation reorderServiceDeskIssues(
+ $oldIndex: Int
+ $newIndex: Int
+ $namespace: String
+ $serializedVariables: String
+) {
+ reorderIssues(
+ oldIndex: $oldIndex
+ newIndex: $newIndex
+ namespace: $namespace
+ serializedVariables: $serializedVariables
+ ) @client
+}
diff --git a/app/assets/javascripts/service_desk/queries/search_project_labels.query.graphql b/app/assets/javascripts/issues/service_desk/queries/search_project_labels.query.graphql
index 89ce14134b4..89ce14134b4 100644
--- a/app/assets/javascripts/service_desk/queries/search_project_labels.query.graphql
+++ b/app/assets/javascripts/issues/service_desk/queries/search_project_labels.query.graphql
diff --git a/app/assets/javascripts/service_desk/queries/search_project_milestones.query.graphql b/app/assets/javascripts/issues/service_desk/queries/search_project_milestones.query.graphql
index f34166be87d..f34166be87d 100644
--- a/app/assets/javascripts/service_desk/queries/search_project_milestones.query.graphql
+++ b/app/assets/javascripts/issues/service_desk/queries/search_project_milestones.query.graphql
diff --git a/app/assets/javascripts/issues/service_desk/queries/set_sorting_preference.mutation.graphql b/app/assets/javascripts/issues/service_desk/queries/set_sorting_preference.mutation.graphql
new file mode 100644
index 00000000000..b01ae3863cd
--- /dev/null
+++ b/app/assets/javascripts/issues/service_desk/queries/set_sorting_preference.mutation.graphql
@@ -0,0 +1,5 @@
+mutation setSortingPreference($input: UserPreferencesUpdateInput!) {
+ userPreferencesUpdate(input: $input) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/service_desk/search_tokens.js b/app/assets/javascripts/issues/service_desk/search_tokens.js
index 72750f518e4..72750f518e4 100644
--- a/app/assets/javascripts/service_desk/search_tokens.js
+++ b/app/assets/javascripts/issues/service_desk/search_tokens.js
diff --git a/app/assets/javascripts/service_desk/utils.js b/app/assets/javascripts/issues/service_desk/utils.js
index 86f76da3880..86f76da3880 100644
--- a/app/assets/javascripts/service_desk/utils.js
+++ b/app/assets/javascripts/issues/service_desk/utils.js
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index 26c3db647a3..d59692d2a28 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -1,49 +1,36 @@
<script>
-import { GlIcon, GlBadge, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
import Visibility from 'visibilityjs';
import { createAlert } from '~/alert';
-import {
- issuableStatusText,
- STATUS_CLOSED,
- TYPE_EPIC,
- TYPE_INCIDENT,
- TYPE_ISSUE,
- WORKSPACE_PROJECT,
-} from '~/issues/constants';
+import { TYPE_EPIC, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
+import updateDescription from '~/issues/show/utils/update_description';
+import { sanitize } from '~/lib/dompurify';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
+import { containsSensitiveToken, confirmSensitiveAction, i18n } from '~/lib/utils/secret_detection';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
-import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
-import { containsSensitiveToken, confirmSensitiveAction, i18n } from '~/lib/utils/secret_detection';
import { ISSUE_TYPE_PATH, INCIDENT_TYPE_PATH, POLLING_DELAY } from '../constants';
import eventHub from '../event_hub';
import getIssueStateQuery from '../queries/get_issue_state.query.graphql';
import Service from '../services/index';
-import Store from '../stores';
import DescriptionComponent from './description.vue';
import EditedComponent from './edited.vue';
import FormComponent from './form.vue';
import HeaderActions from './header_actions.vue';
import IssueHeader from './issue_header.vue';
import PinnedLinks from './pinned_links.vue';
+import StickyHeader from './sticky_header.vue';
import TitleComponent from './title.vue';
export default {
- WORKSPACE_PROJECT,
components: {
- GlIcon,
- GlBadge,
- GlIntersectionObserver,
HeaderActions,
IssueHeader,
TitleComponent,
EditedComponent,
FormComponent,
PinnedLinks,
- ConfidentialityBadge,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
+ StickyHeader,
},
props: {
author: {
@@ -234,21 +221,26 @@ export default {
},
},
data() {
- const store = new Store({
- titleHtml: this.initialTitleHtml,
- titleText: this.initialTitleText,
- descriptionHtml: this.initialDescriptionHtml,
- descriptionText: this.initialDescriptionText,
- updatedAt: this.updatedAt,
- updatedByName: this.updatedByName,
- updatedByPath: this.updatedByPath,
- taskCompletionStatus: this.initialTaskCompletionStatus,
- lock_version: this.lockVersion,
- });
-
return {
- store,
- state: store.state,
+ formState: {
+ title: '',
+ description: '',
+ lockedWarningVisible: false,
+ updateLoading: false,
+ lock_version: 0,
+ issuableTemplates: {},
+ },
+ state: {
+ titleHtml: this.initialTitleHtml,
+ titleText: this.initialTitleText,
+ descriptionHtml: this.initialDescriptionHtml,
+ descriptionText: this.initialDescriptionText,
+ updatedAt: this.updatedAt,
+ updatedByName: this.updatedByName,
+ updatedByPath: this.updatedByPath,
+ taskCompletionStatus: this.initialTaskCompletionStatus,
+ lock_version: this.lockVersion,
+ },
showForm: false,
templatesRequested: false,
isStickyHeaderShowing: false,
@@ -264,17 +256,9 @@ export default {
headerClasses() {
return this.issuableType === TYPE_INCIDENT ? 'gl-mb-3' : 'gl-mb-6';
},
- issuableTemplates() {
- return this.store.formState.issuableTemplates;
- },
- formState() {
- return this.store.formState;
- },
issueChanged() {
const {
- store: {
- formState: { description, title },
- },
+ formState: { description, title },
initialDescriptionText,
initialTitleText,
} = this;
@@ -292,26 +276,13 @@ export default {
defaultErrorMessage() {
return sprintf(__('Error updating %{issuableType}'), { issuableType: this.issuableType });
},
- isClosed() {
- return this.issuableStatus === STATUS_CLOSED;
- },
+
pinnedLinkClasses() {
return this.showTitleBorder
? 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6'
: '';
},
- statusIcon() {
- if (this.issuableType === TYPE_EPIC) {
- return this.isClosed ? 'epic-closed' : 'epic';
- }
- return this.isClosed ? 'issue-closed' : 'issues';
- },
- statusVariant() {
- return this.isClosed ? 'info' : 'success';
- },
- statusText() {
- return issuableStatusText[this.issuableStatus];
- },
+
shouldShowStickyHeader() {
return [TYPE_INCIDENT, TYPE_ISSUE, TYPE_EPIC].includes(this.issuableType);
},
@@ -322,7 +293,7 @@ export default {
this.poll = new Poll({
resource: this.service,
method: 'getData',
- successCallback: (res) => this.store.updateState(res.data),
+ successCallback: (res) => this.updateState(res.data),
errorCallback(err) {
throw new Error(err);
},
@@ -360,23 +331,37 @@ export default {
}
return undefined;
},
+ updateState(data) {
+ const stateShouldUpdate =
+ this.state.titleText !== data.title_text ||
+ this.state.descriptionText !== data.description_text;
- updateStoreState() {
+ if (stateShouldUpdate) {
+ this.formState.lockedWarningVisible = true;
+ }
+
+ Object.assign(this.state, convertObjectPropsToCamelCase(data));
+ // find if there is an open details node inside of the issue description.
+ const descriptionSection = document.body.querySelector(
+ '.detail-page-description.content-block',
+ );
+ const details =
+ descriptionSection != null && descriptionSection.getElementsByTagName('details');
+
+ this.state.descriptionHtml = updateDescription(sanitize(data.description), details);
+ this.state.titleHtml = sanitize(data.title);
+ this.state.lock_version = data.lock_version;
+ },
+ refetchData() {
return this.service
.getData()
.then((res) => res.data)
- .then((data) => {
- this.store.updateState(data);
- })
- .catch(() => {
- createAlert({
- message: this.defaultErrorMessage,
- });
- });
+ .then(this.updateState)
+ .catch(() => createAlert({ message: this.defaultErrorMessage }));
},
setFormState(state) {
- this.store.setFormState(state);
+ this.formState = { ...this.formState, ...state };
},
updateFormState(templates = {}) {
@@ -416,7 +401,7 @@ export default {
this.templatesRequested = true;
this.requestTemplatesAndShowForm();
} else {
- this.updateAndShowForm(this.issuableTemplates);
+ this.updateAndShowForm(this.formState.issuableTemplates);
}
},
@@ -427,10 +412,7 @@ export default {
async updateIssuable() {
this.setFormState({ updateLoading: true });
- const {
- store: { formState },
- issueState,
- } = this;
+ const { formState, issueState } = this;
const issuablePayload = issueState.isDirty
? { ...formState, issue_type: issueState.issueType }
: formState;
@@ -464,7 +446,7 @@ export default {
visitUrl(URI);
}
})
- .then(this.updateStoreState)
+ .then(this.refetchData)
.then(() => {
eventHub.$emit('close.form');
})
@@ -518,7 +500,7 @@ export default {
this.poll.enable();
this.poll.makeDelayedRequest(POLLING_DELAY);
- this.updateStoreState();
+ this.refetchData();
},
},
};
@@ -531,7 +513,7 @@ export default {
:endpoint="endpoint"
:form-state="formState"
:initial-description-text="initialDescriptionText"
- :issuable-templates="issuableTemplates"
+ :issuable-templates="formState.issuableTemplates"
:markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath"
:project-path="projectPath"
@@ -559,61 +541,19 @@ export default {
</template>
</title-component>
- <gl-intersection-observer
+ <sticky-header
v-if="shouldShowStickyHeader"
- @appear="hideStickyHeader"
- @disappear="showStickyHeader"
- >
- <transition name="issuable-header-slide">
- <div
- v-if="isStickyHeaderShowing"
- class="issue-sticky-header gl-fixed gl-z-index-3 gl-bg-white gl-border-1 gl-border-b-solid gl-border-b-gray-100 gl-py-3"
- data-testid="issue-sticky-header"
- >
- <div
- class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-mx-auto gl-px-5"
- >
- <gl-badge :variant="statusVariant" class="gl-mr-2">
- <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"
- data-testid="confidential"
- :workspace-type="$options.WORKSPACE_PROJECT"
- :issuable-type="issuableType"
- />
- <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
- 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="state.titleText"
- >
- {{ state.titleText }}
- </a>
- </div>
- </div>
- </transition>
- </gl-intersection-observer>
+ :is-confidential="isConfidential"
+ :is-hidden="isHidden"
+ :is-locked="isLocked"
+ :issuable-status="issuableStatus"
+ :issuable-type="issuableType"
+ :show="isStickyHeaderShowing"
+ :title="state.titleText"
+ :title-html="state.titleHtml"
+ @hide="hideStickyHeader"
+ @show="showStickyHeader"
+ />
<slot name="header">
<issue-header
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 90f01603f96..acbba216601 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -307,7 +307,8 @@ export default {
);
taskListItems?.forEach((item) => {
- const dropdown = this.createTaskListItemActions({ canUpdate: this.canUpdate });
+ const provide = { canUpdate: this.canUpdate, issuableType: this.issuableType };
+ const dropdown = this.createTaskListItemActions(provide);
this.insertNextToTaskListItemText(dropdown, item);
this.addPointerEventListeners(item, '.task-list-item-actions');
this.hasTaskListItemActions = true;
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 1ade5e654e9..81e5c30a264 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -79,62 +79,25 @@ export default {
GlTooltip: GlTooltipDirective,
},
mixins: [trackingMixin, glFeatureFlagMixin()],
- inject: {
- canCreateIssue: {
- default: false,
- },
- canDestroyIssue: {
- default: false,
- },
- canPromoteToEpic: {
- default: false,
- },
- canReopenIssue: {
- default: false,
- },
- canReportSpam: {
- default: false,
- },
- canUpdateIssue: {
- default: false,
- },
- iid: {
- default: '',
- },
- issuableId: {
- default: '',
- },
- isIssueAuthor: {
- default: false,
- },
- issuePath: {
- default: '',
- },
- issueType: {
- default: TYPE_ISSUE,
- },
- newIssuePath: {
- default: '',
- },
- projectPath: {
- default: '',
- },
- submitAsSpamPath: {
- default: '',
- },
- reportedUserId: {
- default: '',
- },
- reportedFromUrl: {
- default: '',
- },
- issuableEmailAddress: {
- default: '',
- },
- fullPath: {
- default: '',
- },
- },
+ inject: [
+ 'canCreateIssue',
+ 'canDestroyIssue',
+ 'canPromoteToEpic',
+ 'canReopenIssue',
+ 'canReportSpam',
+ 'canUpdateIssue',
+ 'iid',
+ 'isIssueAuthor',
+ 'issuePath',
+ 'issueType',
+ 'newIssuePath',
+ 'projectPath',
+ 'submitAsSpamPath',
+ 'reportedUserId',
+ 'reportedFromUrl',
+ 'issuableEmailAddress',
+ 'fullPath',
+ ],
data() {
return {
isReportAbuseDrawerOpen: false,
@@ -256,7 +219,7 @@ export default {
mutation: updateIssueMutation,
variables: {
input: {
- iid: this.iid.toString(),
+ iid: String(this.iid),
projectPath: this.projectPath,
stateEvent: this.isClosed ? ISSUE_STATE_EVENT_REOPEN : ISSUE_STATE_EVENT_CLOSE,
},
@@ -501,7 +464,7 @@ export default {
>{{ copyMailAddressText }}</gl-dropdown-item
>
</template>
- <gl-dropdown-divider v-if="showToggleIssueStateButton || canDestroyIssue || canReportSpam" />
+ <gl-dropdown-divider v-if="canDestroyIssue || canReportSpam || !isIssueAuthor" />
<gl-dropdown-item
v-if="canReportSpam"
:href="submitAsSpamPath"
diff --git a/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue b/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
index ac64c35bf15..ab1bb9253f4 100644
--- a/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
@@ -107,10 +107,7 @@ export default {
</script>
<template>
- <div
- class="create-timeline-event gl-relative gl-display-flex gl-align-items-start"
- :class="{ 'timeline-entry-vertical-line': hasTimelineEvents }"
- >
+ <div class="create-timeline-event gl-relative gl-display-flex gl-align-items-start">
<div
v-if="hasTimelineEvents"
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-align-self-start gl-bg-white gl-text-gray-200 gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-full gl-mt-2 gl-w-8 gl-h-8 gl-flex-shrink-0 gl-p-3 gl-z-index-1"
diff --git a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
index 4ec64ef838d..2909a4d2666 100644
--- a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
@@ -43,7 +43,7 @@ export default {
variables() {
return {
fullPath: this.fullPath,
- iid: this.iid,
+ iid: String(this.iid),
};
},
update(data) {
diff --git a/app/assets/javascripts/issues/show/components/sticky_header.vue b/app/assets/javascripts/issues/show/components/sticky_header.vue
new file mode 100644
index 00000000000..bcf10ee92bb
--- /dev/null
+++ b/app/assets/javascripts/issues/show/components/sticky_header.vue
@@ -0,0 +1,130 @@
+<script>
+import { GlBadge, GlIcon, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
+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 {
+ WORKSPACE_PROJECT,
+ components: {
+ ConfidentialityBadge,
+ GlBadge,
+ GlIcon,
+ GlIntersectionObserver,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ SafeHtml,
+ },
+ props: {
+ isConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isHidden: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isLocked: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ issuableStatus: {
+ type: String,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: true,
+ },
+ show: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ titleHtml: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ isClosed() {
+ return this.issuableStatus === STATUS_CLOSED;
+ },
+ statusIcon() {
+ if (this.issuableType === TYPE_EPIC) {
+ return this.isClosed ? 'epic-closed' : 'epic';
+ }
+ return this.isClosed ? 'issue-closed' : 'issues';
+ },
+ statusText() {
+ return issuableStatusText[this.issuableStatus];
+ },
+ statusVariant() {
+ return this.isClosed ? 'info' : 'success';
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-intersection-observer @appear="$emit('hide')" @disappear="$emit('show')">
+ <transition name="issuable-header-slide">
+ <div
+ v-if="show"
+ class="issue-sticky-header gl-fixed gl-z-index-3 gl-bg-white gl-border-1 gl-border-b-solid gl-border-b-gray-100 gl-py-3"
+ data-testid="issue-sticky-header"
+ >
+ <div
+ class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-gap-2 gl-mx-auto gl-px-5"
+ >
+ <gl-badge :variant="statusVariant">
+ <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"
+ 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"
+ >
+ </a>
+ </div>
+ </div>
+ </transition>
+ </gl-intersection-observer>
+</template>
diff --git a/app/assets/javascripts/issues/show/components/task_list_item_actions.vue b/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
index 64b916caddb..55e2e857050 100644
--- a/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
+++ b/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
@@ -1,5 +1,6 @@
<script>
import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
+import { TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
import { __, s__ } from '~/locale';
import eventHub from '../event_hub';
@@ -13,7 +14,12 @@ export default {
GlDisclosureDropdown,
GlDisclosureDropdownItem,
},
- inject: ['canUpdate'],
+ inject: ['canUpdate', 'issuableType'],
+ computed: {
+ showConvertToTaskItem() {
+ return [TYPE_INCIDENT, TYPE_ISSUE].includes(this.issuableType);
+ },
+ },
methods: {
convertToTask() {
eventHub.$emit('convert-task-list-item', this.$el.closest('li').dataset.sourcepos);
@@ -37,12 +43,17 @@ export default {
text-sr-only
toggle-class="task-list-item-actions gl-opacity-0 gl-p-2! "
>
- <gl-disclosure-dropdown-item class="gl-ml-2!" @action="convertToTask">
+ <gl-disclosure-dropdown-item
+ v-if="showConvertToTaskItem"
+ class="gl-ml-2!"
+ data-testid="convert"
+ @action="convertToTask"
+ >
<template #list-item>
{{ $options.i18n.convertToTask }}
</template>
</gl-disclosure-dropdown-item>
- <gl-disclosure-dropdown-item class="gl-ml-2!" @action="deleteTaskListItem">
+ <gl-disclosure-dropdown-item class="gl-ml-2!" data-testid="delete" @action="deleteTaskListItem">
<template #list-item>
<span class="gl-text-red-500!">{{ $options.i18n.delete }}</span>
</template>
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index a27f86bd9c3..b94f88f690e 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -6,13 +6,15 @@ import { apolloProvider } from '~/graphql_shared/issuable_client';
import { TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import { scrollToTargetOnResize } from '~/lib/utils/resize_observer';
+import initLinkedResources from '~/linked_resources';
import IssueApp from './components/app.vue';
-import HeaderActions from './components/header_actions.vue';
+import DescriptionComponent from './components/description.vue';
import IncidentTabs from './components/incidents/incident_tabs.vue';
import SentryErrorStackTrace from './components/sentry_error_stack_trace.vue';
import { issueState } from './constants';
import getIssueStateQuery from './queries/get_issue_state.query.graphql';
import createRouter from './components/incidents/router';
+import { parseIssuableData } from './utils/parse_data';
const bootstrapApollo = (state = {}) => {
return apolloProvider.clients.defaultClient.cache.writeQuery({
@@ -23,14 +25,15 @@ const bootstrapApollo = (state = {}) => {
});
};
-export function initIncidentApp(issueData = {}, store) {
+export function initIssuableApp(store) {
const el = document.getElementById('js-issuable-app');
if (!el) {
return undefined;
}
- bootstrapApollo({ ...issueState, issueType: TYPE_INCIDENT });
+ const issuableData = parseIssuableData(el);
+ const headerActionsData = convertObjectPropsToCamelCase(JSON.parse(el.dataset.headerActionsData));
const {
authorId,
@@ -38,137 +41,72 @@ export function initIncidentApp(issueData = {}, store) {
authorUsername,
authorWebUrl,
canCreateIncident,
- canUpdate,
- canUpdateTimelineEvent,
+ fullPath,
iid,
issuableId,
+ issueType,
+ hasIterationsFeature,
+ // for issue
+ registerPath,
+ signInPath,
+ // for incident
+ canUpdate,
+ canUpdateTimelineEvent,
currentPath,
currentTab,
- projectNamespace,
- projectPath,
- projectId,
hasLinkedAlerts,
+ projectId,
slaFeatureAvailable,
uploadMetricsFeatureAvailable,
- } = issueData;
- const headerActionsData = convertObjectPropsToCamelCase(JSON.parse(el.dataset.headerActionsData));
-
- const fullPath = `${projectNamespace}/${projectPath}`;
- const router = createRouter(currentPath, currentTab);
-
- return new Vue({
- el,
- name: 'DescriptionRoot',
- apolloProvider,
- store,
- router,
- provide: {
- issueType: TYPE_INCIDENT,
- canCreateIncident,
- canUpdateTimelineEvent,
- canUpdate,
- fullPath,
- iid,
- issuableId,
- projectId,
- hasLinkedAlerts: parseBoolean(hasLinkedAlerts),
- slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
- uploadMetricsFeatureAvailable: parseBoolean(uploadMetricsFeatureAvailable),
- contentEditorOnIssues: gon.features.contentEditorOnIssues,
- // for HeaderActions component
- canCreateIssue: parseBoolean(headerActionsData.canCreateIncident),
- canDestroyIssue: parseBoolean(headerActionsData.canDestroyIssue),
- canPromoteToEpic: parseBoolean(headerActionsData.canPromoteToEpic),
- canReopenIssue: parseBoolean(headerActionsData.canReopenIssue),
- canReportSpam: parseBoolean(headerActionsData.canReportSpam),
- canUpdateIssue: parseBoolean(headerActionsData.canUpdateIssue),
- isIssueAuthor: parseBoolean(headerActionsData.isIssueAuthor),
- issuePath: headerActionsData.issuePath,
- newIssuePath: headerActionsData.newIssuePath,
- projectPath: headerActionsData.projectPath,
- reportAbusePath: headerActionsData.reportAbusePath,
- reportedUserId: headerActionsData.reportedUserId,
- reportedFromUrl: headerActionsData.reportedFromUrl,
- submitAsSpamPath: headerActionsData.submitAsSpamPath,
- issuableEmailAddress: headerActionsData.issuableEmailAddress,
- },
- computed: {
- ...mapGetters(['getNoteableData']),
- },
- render(createElement) {
- return createElement(IssueApp, {
- props: {
- ...issueData,
- author: {
- id: authorId,
- name: authorName,
- username: authorUsername,
- webUrl: authorWebUrl,
- },
- issueId: Number(issuableId),
- issuableStatus: this.getNoteableData?.state,
- issuableType: TYPE_INCIDENT,
- descriptionComponent: IncidentTabs,
- showTitleBorder: false,
- isConfidential: this.getNoteableData?.confidential,
- },
- });
- },
- });
-}
-
-export function initIssueApp(issueData, store) {
- const el = document.getElementById('js-issuable-app');
+ } = issuableData;
- if (!el) {
- return undefined;
- }
+ const issueProvideData = { registerPath, signInPath };
+ const incidentProvideData = {
+ canUpdate,
+ canUpdateTimelineEvent,
+ hasLinkedAlerts: parseBoolean(hasLinkedAlerts),
+ projectId,
+ slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
+ uploadMetricsFeatureAvailable: parseBoolean(uploadMetricsFeatureAvailable),
+ };
- const { fullPath, registerPath, signInPath } = el.dataset;
- const headerActionsData = convertObjectPropsToCamelCase(JSON.parse(el.dataset.headerActionsData));
+ bootstrapApollo({ ...issueState, issueType });
scrollToTargetOnResize();
- bootstrapApollo({ ...issueState, issueType: TYPE_ISSUE });
-
- const {
- authorId,
- authorName,
- authorUsername,
- authorWebUrl,
- canCreateIncident,
- hasIssueWeightsFeature,
- hasIterationsFeature,
- ...issueProps
- } = issueData;
+ if (issueType === TYPE_INCIDENT) {
+ initLinkedResources();
+ }
return new Vue({
el,
name: 'DescriptionRoot',
apolloProvider,
store,
+ router: issueType === TYPE_INCIDENT ? createRouter(currentPath, currentTab) : undefined,
provide: {
canCreateIncident,
fullPath,
- registerPath,
- signInPath,
- hasIssueWeightsFeature,
+ iid,
+ issuableId,
+ issueType,
hasIterationsFeature,
+ ...(issueType === TYPE_ISSUE && issueProvideData),
+ ...(issueType === TYPE_INCIDENT && incidentProvideData),
// for HeaderActions component
- canCreateIssue: parseBoolean(headerActionsData.canCreateIssue),
+ canCreateIssue:
+ issueType === TYPE_INCIDENT
+ ? parseBoolean(headerActionsData.canCreateIncident)
+ : parseBoolean(headerActionsData.canCreateIssue),
canDestroyIssue: parseBoolean(headerActionsData.canDestroyIssue),
canPromoteToEpic: parseBoolean(headerActionsData.canPromoteToEpic),
canReopenIssue: parseBoolean(headerActionsData.canReopenIssue),
canReportSpam: parseBoolean(headerActionsData.canReportSpam),
canUpdateIssue: parseBoolean(headerActionsData.canUpdateIssue),
- iid: headerActionsData.iid,
- issuableId: headerActionsData.issuableId,
isIssueAuthor: parseBoolean(headerActionsData.isIssueAuthor),
issuePath: headerActionsData.issuePath,
- issueType: headerActionsData.issueType,
newIssuePath: headerActionsData.newIssuePath,
projectPath: headerActionsData.projectPath,
- projectId: headerActionsData.projectId,
reportAbusePath: headerActionsData.reportAbusePath,
reportedUserId: headerActionsData.reportedUserId,
reportedFromUrl: headerActionsData.reportedFromUrl,
@@ -181,67 +119,27 @@ export function initIssueApp(issueData, store) {
render(createElement) {
return createElement(IssueApp, {
props: {
- ...issueProps,
+ ...issuableData,
author: {
id: authorId,
name: authorName,
username: authorUsername,
webUrl: authorWebUrl,
},
+ descriptionComponent: issueType === TYPE_INCIDENT ? IncidentTabs : DescriptionComponent,
isConfidential: this.getNoteableData?.confidential,
isLocked: this.getNoteableData?.discussion_locked,
issuableStatus: this.getNoteableData?.state,
+ issuableType: issueType,
issueId: this.getNoteableData?.id,
issueIid: this.getNoteableData?.iid,
+ showTitleBorder: issueType !== TYPE_INCIDENT,
},
});
},
});
}
-export function initHeaderActions(store, type = '') {
- const el = document.querySelector('.js-issue-header-actions');
-
- if (!el) {
- return undefined;
- }
-
- bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
-
- const canCreate =
- type === TYPE_INCIDENT ? el.dataset.canCreateIncident : el.dataset.canCreateIssue;
-
- return new Vue({
- el,
- name: 'HeaderActionsRoot',
- apolloProvider,
- store,
- provide: {
- canCreateIssue: parseBoolean(canCreate),
- canDestroyIssue: parseBoolean(el.dataset.canDestroyIssue),
- canPromoteToEpic: parseBoolean(el.dataset.canPromoteToEpic),
- canReopenIssue: parseBoolean(el.dataset.canReopenIssue),
- canReportSpam: parseBoolean(el.dataset.canReportSpam),
- canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue),
- iid: el.dataset.iid,
- issuableId: el.dataset.issuableId,
- isIssueAuthor: parseBoolean(el.dataset.isIssueAuthor),
- issuePath: el.dataset.issuePath,
- issueType: el.dataset.issueType,
- newIssuePath: el.dataset.newIssuePath,
- projectPath: el.dataset.projectPath,
- projectId: el.dataset.projectId,
- reportAbusePath: el.dataset.reportAbusePath,
- reportedUserId: parseInt(el.dataset.reportedUserId, 10),
- reportedFromUrl: el.dataset.reportedFromUrl,
- submitAsSpamPath: el.dataset.submitAsSpamPath,
- issuableEmailAddress: el.dataset.issuableEmailAddress,
- fullPath: el.dataset.projectPath,
- },
- render: (createElement) => createElement(HeaderActions),
- });
-}
-
export function initSentryErrorStackTrace() {
const el = document.querySelector('#js-sentry-error-stack-trace');
diff --git a/app/assets/javascripts/issues/show/stores/index.js b/app/assets/javascripts/issues/show/stores/index.js
deleted file mode 100644
index a50913d3455..00000000000
--- a/app/assets/javascripts/issues/show/stores/index.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import { sanitize } from '~/lib/dompurify';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import updateDescription from '../utils/update_description';
-
-export default class Store {
- constructor(initialState) {
- this.state = initialState;
- this.formState = {
- title: '',
- description: '',
- lockedWarningVisible: false,
- updateLoading: false,
- lock_version: 0,
- issuableTemplates: {},
- };
- }
-
- updateState(data) {
- if (this.stateShouldUpdate(data)) {
- this.formState.lockedWarningVisible = true;
- }
-
- Object.assign(this.state, convertObjectPropsToCamelCase(data));
- // find if there is an open details node inside of the issue description.
- const descriptionSection = document.body.querySelector(
- '.detail-page-description.content-block',
- );
- const details =
- descriptionSection != null && descriptionSection.getElementsByTagName('details');
-
- this.state.descriptionHtml = updateDescription(sanitize(data.description), details);
- this.state.titleHtml = sanitize(data.title);
- this.state.lock_version = data.lock_version;
- }
-
- stateShouldUpdate(data) {
- return (
- this.state.titleText !== data.title_text ||
- this.state.descriptionText !== data.description_text
- );
- }
-
- setFormState(state) {
- this.formState = Object.assign(this.formState, state);
- }
-}
diff --git a/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue b/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue
index 46c27c33f56..1d926c0d0c5 100644
--- a/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue
+++ b/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue
@@ -193,6 +193,7 @@ export default {
>
<source-branch-dropdown
id="source-branch-select"
+ :key="selectedProject.id"
:selected-project="selectedProject"
:selected-branch-name="selectedSourceBranchName"
@change="onSourceBranchSelect"
diff --git a/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue b/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue
index dd9afb01590..52a12cc7771 100644
--- a/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue
+++ b/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue
@@ -98,13 +98,13 @@ export default {
:loading="initialProjectsLoading"
:searchable="true"
:searching="projectsLoading"
+ fluid-width
@search="onSearch"
@select="onProjectSelect"
>
<template #list-item="{ item: project }">
<gl-avatar-labeled
v-if="project"
- class="gl-text-truncate"
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
:size="32"
:src="project.avatarUrl"
diff --git a/app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue b/app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue
index dac807dceb0..3f9dd4eb6c6 100644
--- a/app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue
+++ b/app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue
@@ -2,6 +2,8 @@
import { GlCollapsibleListbox } from '@gitlab/ui';
import { debounce } from 'lodash';
import { __ } from '~/locale';
+import { logError } from '~/lib/logger';
+
import { BRANCHES_PER_PAGE } from '../constants';
import getProjectQuery from '../graphql/queries/get_project.query.graphql';
@@ -26,6 +28,7 @@ export default {
return {
initialSourceBranchNamesLoading: false,
sourceBranchNamesLoading: false,
+ sourceBranchNamesLoadingMore: false,
sourceBranchNames: [],
};
},
@@ -36,6 +39,11 @@ export default {
hasSelectedSourceBranch() {
return Boolean(this.selectedBranchName);
},
+ hasMoreBranches() {
+ return (
+ this.sourceBranchNames.length > 0 && this.sourceBranchNames.length % BRANCHES_PER_PAGE === 0
+ );
+ },
branchDropdownText() {
return this.selectedBranchName || __('Select a branch');
},
@@ -59,45 +67,63 @@ export default {
onSearch: debounce(function debouncedSearch(branchSearchQuery) {
this.onSourceBranchSearchQuery(branchSearchQuery);
}, 250),
- onSourceBranchSearchQuery(branchSearchQuery) {
+ async onSourceBranchSearchQuery(branchSearchQuery) {
this.branchSearchQuery = branchSearchQuery;
- this.fetchSourceBranchNames({
+ this.sourceBranchNamesLoading = true;
+
+ await this.fetchSourceBranchNames({
+ projectPath: this.selectedProject.fullPath,
+ searchPattern: this.branchSearchQuery,
+ });
+ this.sourceBranchNamesLoading = false;
+ },
+ async onBottomReached() {
+ this.sourceBranchNamesLoadingMore = true;
+
+ await this.fetchSourceBranchNames({
projectPath: this.selectedProject.fullPath,
searchPattern: this.branchSearchQuery,
+ append: true,
});
+
+ this.sourceBranchNamesLoadingMore = false;
},
onError({ message } = {}) {
this.$emit('error', { message });
},
- async fetchSourceBranchNames({ projectPath, searchPattern } = {}) {
- this.sourceBranchNamesLoading = true;
+ async fetchSourceBranchNames({ projectPath, searchPattern, append = false } = {}) {
try {
const { data } = await this.$apollo.query({
query: getProjectQuery,
variables: {
projectPath,
branchNamesLimit: this.$options.BRANCHES_PER_PAGE,
- branchNamesOffset: 0,
+ branchNamesOffset: append ? this.sourceBranchNames.length : 0,
branchNamesSearchPattern: searchPattern ? `*${searchPattern}*` : '*',
},
});
const { branchNames, rootRef } = data?.project.repository || {};
- this.sourceBranchNames =
- branchNames.map((value) => {
+ const branchNameItems =
+ branchNames?.map((value) => {
return { text: value, value };
}) || [];
- // Use root ref as the default selection
- if (rootRef && !this.hasSelectedSourceBranch) {
- this.onSourceBranchSelect(rootRef);
+ if (append) {
+ this.sourceBranchNames.push(...branchNameItems);
+ } else {
+ this.sourceBranchNames = branchNameItems;
+
+ // Use root ref as the default selection
+ if (rootRef && !this.hasSelectedSourceBranch) {
+ this.onSourceBranchSelect(rootRef);
+ }
}
} catch (err) {
+ logError(err);
this.onError({
message: __('Something went wrong while fetching source branches.'),
});
- } finally {
- this.sourceBranchNamesLoading = false;
}
},
},
@@ -107,12 +133,17 @@ export default {
<template>
<gl-collapsible-listbox
:class="{ 'gl-font-monospace': hasSelectedSourceBranch }"
+ :selected="selectedBranchName"
:disabled="!hasSelectedProject"
:items="sourceBranchNames"
:loading="initialSourceBranchNamesLoading"
:searchable="true"
:searching="sourceBranchNamesLoading"
:toggle-text="branchDropdownText"
+ fluid-width
+ :infinite-scroll="hasMoreBranches"
+ :infinite-scroll-loading="sourceBranchNamesLoadingMore"
+ @bottom-reached="onBottomReached"
@search="onSearch"
@select="onSourceBranchSelect"
/>
diff --git a/app/assets/javascripts/jira_connect/branches/index.js b/app/assets/javascripts/jira_connect/branches/index.js
index a9a56a6362e..893b9dfa1c7 100644
--- a/app/assets/javascripts/jira_connect/branches/index.js
+++ b/app/assets/javascripts/jira_connect/branches/index.js
@@ -19,6 +19,7 @@ export default function initJiraConnectBranches() {
return new Vue({
el,
+ name: 'JiraConnectNewBranchRoot',
apolloProvider,
provide: {
initialBranchName,
diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue
index f8ca62da1a5..d737916857b 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_form.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue
@@ -269,7 +269,7 @@ export default {
<gl-form-select
id="jira-project-select"
v-model="selectedProject"
- data-qa-selector="jira_project_dropdown"
+ data-testid="jira-project-dropdown"
class="mb-2"
:options="jiraProjects"
:state="selectState"
@@ -349,7 +349,7 @@ export default {
variant="confirm"
class="js-no-auto-disable"
:loading="isSubmitting"
- data-qa-selector="jira_issues_import_button"
+ data-testid="jira-issues-import-button"
>
{{ __('Continue') }}
</gl-button>
diff --git a/app/assets/javascripts/jobs/components/filtered_search/constants.js b/app/assets/javascripts/jobs/components/filtered_search/constants.js
deleted file mode 100644
index 0daba892375..00000000000
--- a/app/assets/javascripts/jobs/components/filtered_search/constants.js
+++ /dev/null
@@ -1,13 +0,0 @@
-export const jobStatusValues = [
- 'CANCELED',
- 'CREATED',
- 'FAILED',
- 'MANUAL',
- 'SUCCESS',
- 'PENDING',
- 'PREPARING',
- 'RUNNING',
- 'SCHEDULED',
- 'SKIPPED',
- 'WAITING_FOR_RESOURCE',
-];
diff --git a/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue b/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue
deleted file mode 100644
index 67cdca6aa0a..00000000000
--- a/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue
+++ /dev/null
@@ -1,64 +0,0 @@
-<script>
-import { GlFilteredSearch } from '@gitlab/ui';
-import {
- OPERATORS_IS,
- TOKEN_TITLE_STATUS,
- TOKEN_TYPE_STATUS,
-} from '~/vue_shared/components/filtered_search_bar/constants';
-import JobStatusToken from './tokens/job_status_token.vue';
-
-export default {
- components: {
- GlFilteredSearch,
- },
- props: {
- queryString: {
- type: Object,
- required: false,
- default: null,
- },
- },
- computed: {
- tokens() {
- return [
- {
- type: TOKEN_TYPE_STATUS,
- icon: 'status',
- title: TOKEN_TITLE_STATUS,
- unique: true,
- token: JobStatusToken,
- operators: OPERATORS_IS,
- },
- ];
- },
- filteredSearchValue() {
- if (this.queryString?.statuses) {
- return [
- {
- type: TOKEN_TYPE_STATUS,
- value: {
- data: this.queryString?.statuses,
- operator: '=',
- },
- },
- ];
- }
- return [];
- },
- },
- methods: {
- onSubmit(filters) {
- this.$emit('filterJobsBySearch', filters);
- },
- },
-};
-</script>
-
-<template>
- <gl-filtered-search
- :placeholder="s__('Jobs|Filter jobs')"
- :available-tokens="tokens"
- :value="filteredSearchValue"
- @submit="onSubmit"
- />
-</template>
diff --git a/app/assets/javascripts/jobs/components/filtered_search/utils.js b/app/assets/javascripts/jobs/components/filtered_search/utils.js
deleted file mode 100644
index 696cd8d4706..00000000000
--- a/app/assets/javascripts/jobs/components/filtered_search/utils.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { jobStatusValues } from './constants';
-
-// validates query string used for filtered search
-// on jobs table to ensure GraphQL query is called correctly
-export const validateQueryString = (queryStringObj) => {
- // currently only one token is supported `statuses`
- // this code will need to be expanded as more tokens
- // are introduced
-
- const filters = Object.keys(queryStringObj);
-
- if (filters.includes('statuses')) {
- const queryStringStatus = {
- statuses: queryStringObj.statuses.toUpperCase(),
- };
-
- const found = jobStatusValues.find((status) => status === queryStringStatus.statuses);
-
- if (found) {
- return queryStringStatus;
- }
-
- return null;
- }
-
- return null;
-};
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue b/app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue
deleted file mode 100644
index 7f25ca8a94d..00000000000
--- a/app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<script>
-import { GlLink } from '@gitlab/ui';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-
-export default {
- components: {
- ClipboardButton,
- GlLink,
- },
- props: {
- commit: {
- type: Object,
- required: true,
- },
- mergeRequest: {
- type: Object,
- required: false,
- default: null,
- },
- },
-};
-</script>
-<template>
- <div>
- <span class="gl-font-weight-bold">{{ __('Commit') }}</span>
-
- <gl-link :href="commit.commit_path" class="gl-text-blue-600!" data-testid="commit-sha">
- {{ commit.short_id }}
- </gl-link>
-
- <clipboard-button
- :text="commit.id"
- :title="__('Copy commit SHA')"
- category="tertiary"
- size="small"
- />
-
- <span v-if="mergeRequest">
- {{ __('in') }}
- <gl-link :href="mergeRequest.path" class="gl-text-blue-600!" data-testid="link-commit"
- >!{{ mergeRequest.iid }}</gl-link
- >
- </span>
-
- <p class="gl-mb-0">{{ commit.title }}</p>
- </div>
-</template>
diff --git a/app/assets/javascripts/jobs/constants.js b/app/assets/javascripts/jobs/constants.js
deleted file mode 100644
index 40b3de7edd9..00000000000
--- a/app/assets/javascripts/jobs/constants.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { __, s__ } from '~/locale';
-
-const cancel = __('Cancel');
-const moreInfo = __('More information');
-
-export const forwardDeploymentFailureModalId = 'forward-deployment-failure';
-
-export const JOB_SIDEBAR_COPY = {
- cancel,
- cancelJobButtonLabel: s__('Job|Cancel'),
- debug: __('Debug'),
- eraseLogButtonLabel: s__('Job|Erase job log and artifacts'),
- 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'),
- updateVariables: s__('Job|Update CI/CD variables'),
-};
-
-export const JOB_GRAPHQL_ERRORS = {
- jobMutationErrorText: __('There was an error running the job. Please try again.'),
- jobQueryErrorText: __('There was an error fetching the job.'),
-};
-
-export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = {
- cancel,
- info: s__(
- `Jobs|You're about to retry a job that failed because it attempted to deploy code that is older than the latest deployment.
- Retrying this job could result in overwriting the environment with the older source code.`,
- ),
- areYouSure: s__('Jobs|Are you sure you want to proceed?'),
- moreInfo,
- primaryText: __('Retry job'),
- title: s__('Jobs|Are you sure you want to retry this job?'),
-};
-
-export const SUCCESS_STATUS = 'SUCCESS';
-export const PASSED_STATUS = 'passed';
-export const MANUAL_STATUS = 'manual';
diff --git a/app/assets/javascripts/labels/labels_select.js b/app/assets/javascripts/labels/labels_select.js
index 587cc82f0fa..db1dbc72624 100644
--- a/app/assets/javascripts/labels/labels_select.js
+++ b/app/assets/javascripts/labels/labels_select.js
@@ -276,7 +276,8 @@ export default class LabelsSelect {
if (selected && selected.id === 0) {
this.selected = [];
return __('No label');
- } else if (isSelected) {
+ }
+ if (isSelected) {
this.selected.push(title);
} else if (!isSelected && title) {
const index = this.selected.indexOf(title);
@@ -285,7 +286,8 @@ export default class LabelsSelect {
if (selectedLabels.length === 1) {
return selectedLabels;
- } else if (selectedLabels.length) {
+ }
+ if (selectedLabels.length) {
return sprintf(__('%{firstLabel} +%{labelCount} more'), {
firstLabel: selectedLabels[0],
labelCount: selectedLabels.length - 1,
diff --git a/app/assets/javascripts/lib/swagger.js b/app/assets/javascripts/lib/swagger.js
index fcdab18c623..b0701054174 100644
--- a/app/assets/javascripts/lib/swagger.js
+++ b/app/assets/javascripts/lib/swagger.js
@@ -27,7 +27,7 @@ const renderSwaggerUI = (value) => {
spec,
dom_id: '#swagger-ui',
deepLinking: true,
- displayOperationId: true,
+ displayOperationId: Boolean(getParameterByName('displayOperationId')),
});
})
.catch((error) => {
diff --git a/app/assets/javascripts/lib/utils/array_utility.js b/app/assets/javascripts/lib/utils/array_utility.js
index 04f9cb1cdb5..9eddae860c2 100644
--- a/app/assets/javascripts/lib/utils/array_utility.js
+++ b/app/assets/javascripts/lib/utils/array_utility.js
@@ -28,3 +28,18 @@ export const swapArrayItems = (array, leftIndex = 0, rightIndex = 0) => {
export const getDuplicateItemsFromArray = (array) => [
...new Set(array.filter((value, index) => array.indexOf(value) !== index)),
];
+
+/**
+ * Toggles the presence of an item in a given array.
+ * Use pass by reference when toggling non-primivive types.
+ *
+ * @param {Array} array - The array to use
+ * @param {Any} item - The array item to toggle
+ * @returns {Array} new array with toggled item
+ */
+export const toggleArrayItem = (array, value) => {
+ if (array.includes(value)) {
+ return array.filter((item) => item !== value);
+ }
+ return [...array, value];
+};
diff --git a/app/assets/javascripts/lib/utils/breadcrumbs.js b/app/assets/javascripts/lib/utils/breadcrumbs.js
new file mode 100644
index 00000000000..e38094fc895
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/breadcrumbs.js
@@ -0,0 +1,28 @@
+import Vue from 'vue';
+
+// TODO: Review replacing this when a breadcrumbs ViewComponent has been created https://gitlab.com/gitlab-org/gitlab/-/issues/367326
+export const injectVueAppBreadcrumbs = (router, BreadcrumbsComponent, apolloProvider = null) => {
+ const breadcrumbEls = document.querySelectorAll('nav .js-breadcrumbs-list li');
+
+ if (breadcrumbEls.length < 1) {
+ return false;
+ }
+
+ const breadcrumbEl = breadcrumbEls[breadcrumbEls.length - 1];
+
+ const lastCrumb = breadcrumbEl.children[0];
+ const nestedBreadcrumbEl = document.createElement('div');
+
+ breadcrumbEl.replaceChild(nestedBreadcrumbEl, lastCrumb);
+
+ return new Vue({
+ el: nestedBreadcrumbEl,
+ router,
+ apolloProvider,
+ render(createElement) {
+ return createElement(BreadcrumbsComponent, {
+ class: breadcrumbEl.className,
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index cca4cf68f5e..7d16af003e4 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -86,7 +86,6 @@ export const handleLocationHash = () => {
const performanceBar = document.querySelector('#js-peek');
const topPadding = 8;
const diffFileHeader = document.querySelector('.js-file-title');
- const versionMenusContainer = document.querySelector('.mr-version-menus-container');
const fixedIssuableTitle = document.querySelector('.issue-sticky-header');
let adjustment = 0;
@@ -97,7 +96,6 @@ export const handleLocationHash = () => {
adjustment -= getElementOffsetHeight(fixedTopBar);
adjustment -= getElementOffsetHeight(performanceBar);
adjustment -= getElementOffsetHeight(diffFileHeader);
- adjustment -= getElementOffsetHeight(versionMenusContainer);
if (isInIssuePage()) {
adjustment -= getElementOffsetHeight(fixedIssuableTitle);
@@ -731,3 +729,11 @@ export const isCurrentUser = (userId) => {
return Number(userId) === currentUserId;
};
+
+/**
+ * Clones an object via JSON stringifying and re-parsing.
+ * This ensures object references are not persisted (e.g. unlike lodash cloneDeep)
+ */
+export const cloneWithoutReferences = (obj) => {
+ return JSON.parse(JSON.stringify(obj));
+};
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index aceae188b73..da5fb831ae5 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -3,15 +3,6 @@ export const DEFAULT_DEBOUNCE_AND_THROTTLE_MS = 250;
export const THOUSAND = 1000;
export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80;
export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12;
-
-export const DATETIME_RANGE_TYPES = {
- fixed: 'fixed',
- anchored: 'anchored',
- rolling: 'rolling',
- open: 'open',
- invalid: 'invalid',
-};
-
export const BV_SHOW_MODAL = 'bv::show::modal';
export const BV_HIDE_MODAL = 'bv::hide::modal';
export const BV_HIDE_TOOLTIP = 'bv::hide::tooltip';
diff --git a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
index d52672b9d08..4e0d19f2c2a 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
@@ -133,7 +133,8 @@ export const dayInQuarter = (date, quarter) => {
return quarter.reduce((acc, month) => {
if (dateValues.month > month.getMonth()) {
return acc + totalDaysInMonth(month);
- } else if (dateValues.month === month.getMonth()) {
+ }
+ if (dateValues.month === month.getMonth()) {
return acc + dateValues.date;
}
return acc + 0;
@@ -562,9 +563,11 @@ export const approximateDuration = (seconds = 0) => {
if (seconds < 30) {
return __('less than a minute');
- } else if (seconds < MINUTES_LIMIT) {
+ }
+ if (seconds < MINUTES_LIMIT) {
return n__('1 minute', '%d minutes', seconds < ONE_MINUTE_LIMIT ? 1 : minutes);
- } else if (seconds < HOURS_LIMIT) {
+ }
+ if (seconds < HOURS_LIMIT) {
return n__('about 1 hour', 'about %d hours', seconds < ONE_HOUR_LIMIT ? 1 : hours);
}
return n__('1 day', '%d days', seconds < ONE_DAY_LIMIT ? 1 : days);
diff --git a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
index b0264796d90..c4b8f95e99f 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
@@ -93,10 +93,12 @@ export const humanizeTimeInterval = (intervalInSeconds) => {
if (intervalInSeconds < 60 /* = 1 minute */) {
const seconds = Math.round(intervalInSeconds * 10) / 10;
return n__('%d second', '%d seconds', seconds);
- } else if (intervalInSeconds < 3600 /* = 1 hour */) {
+ }
+ if (intervalInSeconds < 3600 /* = 1 hour */) {
const minutes = Math.round(intervalInSeconds / 6) / 10;
return n__('%d minute', '%d minutes', minutes);
- } else if (intervalInSeconds < 86400 /* = 1 day */) {
+ }
+ if (intervalInSeconds < 86400 /* = 1 day */) {
const hours = Math.round(intervalInSeconds / 360) / 10;
return n__('%d hour', '%d hours', hours);
}
@@ -378,19 +380,24 @@ export const formatTimeAsSummary = ({ seconds, hours, days, minutes, weeks, mont
return sprintf(s__('ValueStreamAnalytics|%{value}M'), {
value: roundToNearestHalf(months),
});
- } else if (weeks) {
+ }
+ if (weeks) {
return sprintf(s__('ValueStreamAnalytics|%{value}w'), {
value: roundToNearestHalf(weeks),
});
- } else if (days) {
+ }
+ if (days) {
return sprintf(s__('ValueStreamAnalytics|%{value}d'), {
value: roundToNearestHalf(days),
});
- } else if (hours) {
+ }
+ if (hours) {
return sprintf(s__('ValueStreamAnalytics|%{value}h'), { value: hours });
- } else if (minutes) {
+ }
+ if (minutes) {
return sprintf(s__('ValueStreamAnalytics|%{value}m'), { value: minutes });
- } else if (seconds) {
+ }
+ if (seconds) {
return unescape(sanitize(s__('ValueStreamAnalytics|&lt;1m'), { ALLOWED_TAGS: [] }));
}
return '-';
@@ -441,11 +448,13 @@ export const humanTimeframe = (startDate, dueDate) => {
startDate: startDateInWords,
dueDate: dueDateInWords,
});
- } else if (startDate && !dueDate) {
+ }
+ if (startDate && !dueDate) {
return sprintf(__('%{startDate} – No due date'), {
startDate: dateInWords(start, true, false),
});
- } else if (!startDate && dueDate) {
+ }
+ if (!startDate && dueDate) {
return sprintf(__('No start date – %{dueDate}'), {
dueDate: dateInWords(due, true, false),
});
diff --git a/app/assets/javascripts/lib/utils/datetime_range.js b/app/assets/javascripts/lib/utils/datetime_range.js
index 548f5a438df..f0a03874949 100644
--- a/app/assets/javascripts/lib/utils/datetime_range.js
+++ b/app/assets/javascripts/lib/utils/datetime_range.js
@@ -1,27 +1,6 @@
-import { pick, omit, isEqual, isEmpty } from 'lodash';
import dateformat from '~/lib/dateformat';
-import { DATETIME_RANGE_TYPES } from './constants';
-import { secondsToMilliseconds } from './datetime_utility';
-const MINIMUM_DATE = new Date(0);
-
-const DEFAULT_DIRECTION = 'before';
-
-const durationToMillis = (duration) => {
- if (Object.entries(duration).length === 1 && Number.isFinite(duration.seconds)) {
- return secondsToMilliseconds(duration.seconds);
- }
- // eslint-disable-next-line @gitlab/require-i18n-strings
- throw new Error('Invalid duration: only `seconds` is supported');
-};
-
-const dateMinusDuration = (date, duration) => new Date(date.getTime() - durationToMillis(duration));
-
-const datePlusDuration = (date, duration) => new Date(date.getTime() + durationToMillis(duration));
-
-const isValidDuration = (duration) => Boolean(duration && Number.isFinite(duration.seconds));
-
-const isValidDateString = (dateString) => {
+export const isValidDateString = (dateString) => {
if (typeof dateString !== 'string' || !dateString.trim()) {
return false;
}
@@ -38,291 +17,3 @@ const isValidDateString = (dateString) => {
}
return !Number.isNaN(Date.parse(isoFormatted));
};
-
-const handleRangeDirection = ({ direction = DEFAULT_DIRECTION, anchorDate, minDate, maxDate }) => {
- let startDate;
- let endDate;
-
- if (direction === DEFAULT_DIRECTION) {
- startDate = minDate;
- endDate = anchorDate;
- } else {
- startDate = anchorDate;
- endDate = maxDate;
- }
-
- return {
- startDate,
- endDate,
- };
-};
-
-/**
- * Converts a fixed range to a fixed range
- * @param {Object} fixedRange - A range with fixed start and
- * end (e.g. "midnight January 1st 2020 to midday January31st 2020")
- */
-const convertFixedToFixed = ({ start, end }) => ({
- start,
- end,
-});
-
-/**
- * Converts an anchored range to a fixed range
- * @param {Object} anchoredRange - A duration of time
- * relative to a fixed point in time (e.g., "the 30 minutes
- * before midnight January 1st 2020", or "the 2 days
- * after midday on the 11th of May 2019")
- */
-const convertAnchoredToFixed = ({ anchor, duration, direction }) => {
- const anchorDate = new Date(anchor);
-
- const { startDate, endDate } = handleRangeDirection({
- minDate: dateMinusDuration(anchorDate, duration),
- maxDate: datePlusDuration(anchorDate, duration),
- direction,
- anchorDate,
- });
-
- return {
- start: startDate.toISOString(),
- end: endDate.toISOString(),
- };
-};
-
-/**
- * Converts a rolling change to a fixed range
- *
- * @param {Object} rollingRange - A time range relative to
- * now (e.g., "last 2 minutes", or "next 2 days")
- */
-const convertRollingToFixed = ({ duration, direction }) => {
- // Use Date.now internally for easier mocking in tests
- const now = new Date(Date.now());
-
- return convertAnchoredToFixed({
- duration,
- direction,
- anchor: now.toISOString(),
- });
-};
-
-/**
- * Converts an open range to a fixed range
- *
- * @param {Object} openRange - A time range relative
- * to an anchor (e.g., "before midnight on the 1st of
- * January 2020", or "after midday on the 11th of May 2019")
- */
-const convertOpenToFixed = ({ anchor, direction }) => {
- // Use Date.now internally for easier mocking in tests
- const now = new Date(Date.now());
-
- const { startDate, endDate } = handleRangeDirection({
- minDate: MINIMUM_DATE,
- maxDate: now,
- direction,
- anchorDate: new Date(anchor),
- });
-
- return {
- start: startDate.toISOString(),
- end: endDate.toISOString(),
- };
-};
-
-/**
- * Handles invalid date ranges
- */
-const handleInvalidRange = () => {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- throw new Error('The input range does not have the right format.');
-};
-
-const handlers = {
- invalid: handleInvalidRange,
- fixed: convertFixedToFixed,
- anchored: convertAnchoredToFixed,
- rolling: convertRollingToFixed,
- open: convertOpenToFixed,
-};
-
-/**
- * Validates and returns the type of range
- *
- * @param {Object} Date time range
- * @returns {String} `key` value for one of the handlers
- */
-export function getRangeType(range) {
- const { start, end, anchor, duration } = range;
-
- if ((start || end) && !anchor && !duration) {
- return isValidDateString(start) && isValidDateString(end)
- ? DATETIME_RANGE_TYPES.fixed
- : DATETIME_RANGE_TYPES.invalid;
- }
- if (anchor && duration) {
- return isValidDateString(anchor) && isValidDuration(duration)
- ? DATETIME_RANGE_TYPES.anchored
- : DATETIME_RANGE_TYPES.invalid;
- }
- if (duration && !anchor) {
- return isValidDuration(duration) ? DATETIME_RANGE_TYPES.rolling : DATETIME_RANGE_TYPES.invalid;
- }
- if (anchor && !duration) {
- return isValidDateString(anchor) ? DATETIME_RANGE_TYPES.open : DATETIME_RANGE_TYPES.invalid;
- }
- return DATETIME_RANGE_TYPES.invalid;
-}
-
-/**
- * convertToFixedRange Transforms a `range of time` into a `fixed range of time`.
- *
- * The following types of a `ranges of time` can be represented:
- *
- * Fixed Range: A range with fixed start and end (e.g. "midnight January 1st 2020 to midday January 31st 2020")
- * Anchored Range: A duration of time relative to a fixed point in time (e.g., "the 30 minutes before midnight January 1st 2020", or "the 2 days after midday on the 11th of May 2019")
- * Rolling Range: A time range relative to now (e.g., "last 2 minutes", or "next 2 days")
- * Open Range: A time range relative to an anchor (e.g., "before midnight on the 1st of January 2020", or "after midday on the 11th of May 2019")
- *
- * @param {Object} dateTimeRange - A Time Range representation
- * It contains the data needed to create a fixed time range plus
- * a label (recommended) to indicate the range that is covered.
- *
- * A definition via a TypeScript notation is presented below:
- *
- *
- * type Duration = { // A duration of time, always in seconds
- * seconds: number;
- * }
- *
- * type Direction = 'before' | 'after'; // Direction of time relative to an anchor
- *
- * type FixedRange = {
- * start: ISO8601;
- * end: ISO8601;
- * label: string;
- * }
- *
- * type AnchoredRange = {
- * anchor: ISO8601;
- * duration: Duration;
- * direction: Direction; // defaults to 'before'
- * label: string;
- * }
- *
- * type RollingRange = {
- * duration: Duration;
- * direction: Direction; // defaults to 'before'
- * label: string;
- * }
- *
- * type OpenRange = {
- * anchor: ISO8601;
- * direction: Direction; // defaults to 'before'
- * label: string;
- * }
- *
- * type DateTimeRange = FixedRange | AnchoredRange | RollingRange | OpenRange;
- *
- *
- * @returns {FixedRange} An object with a start and end in ISO8601 format.
- */
-export const convertToFixedRange = (dateTimeRange) =>
- handlers[getRangeType(dateTimeRange)](dateTimeRange);
-
-/**
- * Returns a copy of the object only with time range
- * properties relevant to time range calculation.
- *
- * Filtered properties are:
- * - 'start'
- * - 'end'
- * - 'anchor'
- * - 'duration'
- * - 'direction': if direction is already the default, its removed.
- *
- * @param {Object} timeRange - A time range object
- * @returns Copy of time range
- */
-const pruneTimeRange = (timeRange) => {
- const res = pick(timeRange, ['start', 'end', 'anchor', 'duration', 'direction']);
- if (res.direction === DEFAULT_DIRECTION) {
- return omit(res, 'direction');
- }
- return res;
-};
-
-/**
- * Returns true if the time ranges are equal according to
- * the time range calculation properties
- *
- * @param {Object} timeRange - A time range object
- * @param {Object} other - Time range object to compare with.
- * @returns true if the time ranges are equal, false otherwise
- */
-export const isEqualTimeRanges = (timeRange, other) => {
- const tr1 = pruneTimeRange(timeRange);
- const tr2 = pruneTimeRange(other);
- return isEqual(tr1, tr2);
-};
-
-/**
- * Searches for a time range in a array of time ranges using
- * only the properies relevant to time ranges calculation.
- *
- * @param {Object} timeRange - Time range to search (needle)
- * @param {Array} timeRanges - Array of time tanges (haystack)
- */
-export const findTimeRange = (timeRange, timeRanges) =>
- timeRanges.find((element) => isEqualTimeRanges(element, timeRange));
-
-// Time Ranges as URL Parameters Utils
-
-/**
- * List of possible time ranges parameters
- */
-export const timeRangeParamNames = ['start', 'end', 'anchor', 'duration_seconds', 'direction'];
-
-/**
- * Converts a valid time range to a flat key-value pairs object.
- *
- * Duration is flatted to avoid having nested objects.
- *
- * @param {Object} A time range
- * @returns key-value pairs object that can be used as parameters in a URL.
- */
-export const timeRangeToParams = (timeRange) => {
- let params = pruneTimeRange(timeRange);
- if (timeRange.duration) {
- const durationParms = {};
- Object.keys(timeRange.duration).forEach((key) => {
- durationParms[`duration_${key}`] = timeRange.duration[key].toString();
- });
- params = { ...durationParms, ...params };
- params = omit(params, 'duration');
- }
- return params;
-};
-
-/**
- * Converts a valid set of flat params to a time range object
- *
- * Parameters that are not part of time range object are ignored.
- *
- * @param {params} params - key-value pairs object.
- */
-export const timeRangeFromParams = (params) => {
- const timeRangeParams = pick(params, timeRangeParamNames);
- let range = Object.entries(timeRangeParams).reduce((acc, [key, val]) => {
- // unflatten duration
- if (key.startsWith('duration_')) {
- acc.duration = acc.duration || {};
- acc.duration[key.slice('duration_'.length)] = parseInt(val, 10);
- return acc;
- }
- return { [key]: val, ...acc };
- }, {});
- range = pruneTimeRange(range);
- return !isEmpty(range) ? range : null;
-};
diff --git a/app/assets/javascripts/lib/utils/grammar.js b/app/assets/javascripts/lib/utils/grammar.js
index 6d6361d19b6..64ba151d829 100644
--- a/app/assets/javascripts/lib/utils/grammar.js
+++ b/app/assets/javascripts/lib/utils/grammar.js
@@ -19,9 +19,11 @@ import { sprintf, s__ } from '~/locale';
export const toNounSeriesText = (items, { onlyCommas = false } = {}) => {
if (items.length === 0) {
return '';
- } else if (items.length === 1) {
+ }
+ if (items.length === 1) {
return sprintf(s__(`nounSeries|%{item}`), { item: items[0] }, false);
- } else if (items.length === 2 && !onlyCommas) {
+ }
+ if (items.length === 2 && !onlyCommas) {
return sprintf(
s__('nounSeries|%{firstItem} and %{lastItem}'),
{
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index 0e943cdb623..d17719c0bc0 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -84,9 +84,11 @@ export function numberToHumanSizeSplit(size, digits = 2) {
if (abs < BYTES_IN_KIB) {
return [size.toString(), BYTES_FORMAT_BYTES];
- } else if (abs < BYTES_IN_KIB ** 2) {
+ }
+ if (abs < BYTES_IN_KIB ** 2) {
return [bytesToKiB(size).toFixed(digits), BYTES_FORMAT_KIB];
- } else if (abs < BYTES_IN_KIB ** 3) {
+ }
+ if (abs < BYTES_IN_KIB ** 3) {
return [bytesToMiB(size).toFixed(digits), BYTES_FORMAT_MIB];
}
return [bytesToGiB(size).toFixed(digits), BYTES_FORMAT_GIB];
diff --git a/app/assets/javascripts/lib/utils/secret_detection.js b/app/assets/javascripts/lib/utils/secret_detection.js
index 8e673855631..49de7b3a081 100644
--- a/app/assets/javascripts/lib/utils/secret_detection.js
+++ b/app/assets/javascripts/lib/utils/secret_detection.js
@@ -24,6 +24,10 @@ export const containsSensitiveToken = (message) => {
name: 'Feed Token',
regex: 'feed_token=((glft-)?[0-9a-zA-Z_-]{20}|glft-[a-h0-9]+-[0-9]+_)',
},
+ {
+ name: 'GitLab OAuth Application Secret',
+ regex: `gloas-[0-9a-zA-Z_-]{64}`,
+ },
];
for (const rule of sensitiveDataPatterns) {
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index e6eb74834c0..d48e0217fd8 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -202,7 +202,8 @@ function moveCursor({
const startPosition = textArea.selectionStart - (tag.length - tag.indexOf(select));
const endPosition = startPosition + select.length;
return textArea.setSelectionRange(startPosition, endPosition);
- } else if (editor) {
+ }
+ if (editor) {
editor.selectWithinSelection(select, tag);
return;
}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 31e16f7b4db..638ee1f7e5a 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -178,18 +178,6 @@ export function capitalizeFirstCharacter(text) {
}
/**
- * Returns the first character capitalized
- *
- * If falsey, returns empty string.
- *
- * @param {String} text
- * @return {String}
- */
-export function getFirstCharacterCapitalized(text) {
- return text ? text.charAt(0).toUpperCase() : '';
-}
-
-/**
* Replaces all html tags from a string with the given replacement.
*
* @param {String} string
@@ -529,9 +517,11 @@ export const humanizeBranchValidationErrors = (invalidChars = []) => {
if (chars.length && !chars.includes(' ')) {
return sprintf(__("Can't contain %{chars}"), { chars: chars.join(', ') });
- } else if (chars.includes(' ') && chars.length <= 1) {
+ }
+ if (chars.includes(' ') && chars.length <= 1) {
return __("Can't contain spaces");
- } else if (chars.includes(' ') && chars.length > 1) {
+ }
+ if (chars.includes(' ') && chars.length > 1) {
return sprintf(__("Can't contain spaces, %{chars}"), {
chars: chars.filter((c) => c !== ' ').join(', '),
});
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 08c98298121..ea0520e3157 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,5 +1,3 @@
-import * as Sentry from '@sentry/browser';
-
export const DASH_SCOPE = '-';
export const PATH_SEPARATOR = '/';
@@ -705,11 +703,7 @@ export function visitUrl(destination, external = false) {
}
if (!isSafeURL(url)) {
- // For now log this to Sentry and do not block the execution.
- // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121551#note_1408873600
- // for more context. Once we're sure that it's not breaking functionality, we can use
- // a RangeError here (throw new RangeError('Only http and https protocols are allowed')).
- Sentry.captureException(new RangeError(`Only http and https protocols are allowed: ${url}`));
+ throw new RangeError(`Only http and https protocols are allowed: ${url}`);
}
if (external) {
diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js
index 38d2f3d7551..f1e87145406 100644
--- a/app/assets/javascripts/lib/utils/webpack.js
+++ b/app/assets/javascripts/lib/utils/webpack.js
@@ -5,10 +5,11 @@ import { joinPaths } from '~/lib/utils/url_utility';
* See https://gitlab.com/gitlab-org/gitlab/-/issues/321656 for a fix
*/
export function resetServiceWorkersPublicPath() {
+ // No-op if we're running Vite instead of Webpack
+ if (typeof __webpack_public_path__ === 'undefined') return; // eslint-disable-line camelcase
// __webpack_public_path__ is a global variable that can be used to adjust
// the webpack publicPath setting at runtime.
// see: https://webpack.js.org/guides/public-path/
const relativeRootPath = (gon && gon.relative_url_root) || '';
- const webpackAssetPath = joinPaths(relativeRootPath, '/assets/webpack/');
- __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase
+ __webpack_public_path__ = joinPaths(relativeRootPath, '/assets/webpack/'); // eslint-disable-line camelcase
}
diff --git a/app/assets/javascripts/locale/ensure_single_line.cjs b/app/assets/javascripts/locale/ensure_single_line.cjs
index f7790cadc48..abdd56c3589 100644
--- a/app/assets/javascripts/locale/ensure_single_line.cjs
+++ b/app/assets/javascripts/locale/ensure_single_line.cjs
@@ -1,15 +1,11 @@
const SPLIT_REGEX = /\s*[\r\n]+\s*/;
/**
- *
- * strips newlines from strings and replaces them with a single space
- *
+ * Strips newlines from strings and replaces them with a single space.
* @example
- *
* ensureSingleLine('foo \n bar') === 'foo bar'
- *
- * @param {String} str
- * @returns {String}
+ * @param {string} - str
+ * @returns {string}
*/
module.exports = function ensureSingleLine(str) {
// This guard makes the function significantly faster
diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js
index 600654794a5..48cf06e0793 100644
--- a/app/assets/javascripts/locale/index.js
+++ b/app/assets/javascripts/locale/index.js
@@ -19,22 +19,22 @@ if (hasTranslations) {
}
/**
- Translates `text`
- @param text The text to be translated
- @returns {String} The translated text
-*/
+ * Translates `text`.
+ * @param {string} text - The text to be translated
+ * @returns {string} The translated text
+ */
const gettext = (text) => locale.gettext(ensureSingleLine(text));
/**
- Translate the text with a number
- if the number is more than 1 it will use the `pluralText` translation.
- This method allows for contexts, see below re. contexts
-
- @param text Singular text to translate (eg. '%d day')
- @param pluralText Plural text to translate (eg. '%d days')
- @param count Number to decide which translation to use (eg. 2)
- @returns {String} Translated text with the number replaced (eg. '2 days')
-*/
+ * Translate the text with a number.
+ *
+ * If the number is more than 1 it will use the `pluralText` translation.
+ * This method allows for contexts, see below re. contexts
+ * @param {string} text - Singular text to translate (e.g. '%d day')
+ * @param {string} pluralText - Plural text to translate (e.g. '%d days')
+ * @param {number} count - Number to decide which translation to use (e.g. 2)
+ * @returns {string} Translated text with the number replaced (e.g. '2 days')
+ */
const ngettext = (text, pluralText, count) => {
const translated = locale
.ngettext(ensureSingleLine(text), ensureSingleLine(pluralText), count)
@@ -45,16 +45,16 @@ const ngettext = (text, pluralText, count) => {
};
/**
- Translate context based text
- Either pass in the context translation like `Context|Text to translate`
- or allow for dynamic text by doing passing in the context first & then the text to translate
-
- @param keyOrContext Can be either the key to translate including the context
- (eg. 'Context|Text') or just the context for the translation
- (eg. 'Context')
- @param key Is the dynamic variable you want to be translated
- @returns {String} Translated context based text
-*/
+ * Translate context based text.
+ * @example
+ * s__('Context|Text to translate');
+ * @example
+ * s__('Context', 'Text to translate');
+ * @param {string} keyOrContext - Context and a key to translation (e.g. 'Context|Text')
+ * or just a context (e.g. 'Context')
+ * @param {string} [key] - if `keyOrContext` is just a context, this is the key to translation
+ * @returns {string} Translated context based text
+ */
const pgettext = (keyOrContext, key) => {
const normalizedKey = ensureSingleLine(key ? `${keyOrContext}|${key}` : keyOrContext);
const translated = gettext(normalizedKey).split('|');
@@ -102,21 +102,19 @@ export const getPreferredLocales = () => {
};
/**
- Creates an instance of Intl.DateTimeFormat for the current locale.
-
- @param formatOptions for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
- @returns {Intl.DateTimeFormat}
-*/
+ * Creates an instance of Intl.DateTimeFormat for the current locale.
+ * @param {Intl.DateTimeFormatOptions} [formatOptions] - for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
+ * @returns {Intl.DateTimeFormat}
+ */
const createDateTimeFormat = (formatOptions) =>
Intl.DateTimeFormat(getPreferredLocales(), formatOptions);
/**
* Formats a number as a string using `toLocaleString`.
- *
- * @param {Number} value - number to be converted
- * @param {options?} options - options to be passed to
- * `toLocaleString` such as `unit` and `style`.
- * @param {langCode?} langCode - If set, forces a different
+ * @param {number} value - number to be converted
+ * @param {Intl.NumberFormatOptions} [options] - options to be passed to
+ * `toLocaleString`.
+ * @param {string|string[]} [langCode] - If set, forces a different
* language code from the one currently in the document.
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
*
diff --git a/app/assets/javascripts/locale/sprintf.js b/app/assets/javascripts/locale/sprintf.js
index 12df67670f9..7faf9390684 100644
--- a/app/assets/javascripts/locale/sprintf.js
+++ b/app/assets/javascripts/locale/sprintf.js
@@ -1,17 +1,15 @@
import { escape } from 'lodash';
/**
- Very limited implementation of sprintf supporting only named parameters.
-
- @param input (translated) text with parameters (e.g. '%{num_users} users use us')
- @param {Object} parameters object mapping parameter names to values (e.g. { num_users: 5 })
- @param {Boolean} escapeParameters whether parameter values should be escaped (see https://lodash.com/docs/4.17.15#escape)
- @returns {String} the text with parameters replaces (e.g. '5 users use us')
-
- @see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf
- @see https://gitlab.com/gitlab-org/gitlab-foss/issues/37992
-*/
-export default (input, parameters, escapeParameters = true) => {
+ * Very limited implementation of sprintf supporting only named parameters.
+ * @param {string} input - (translated) text with parameters (e.g. '%{num_users} users use us')
+ * @param {Object.<string, string|number>} [parameters] - object mapping parameter names to values (e.g. { num_users: 5 })
+ * @param {boolean} [escapeParameters=true] - whether parameter values should be escaped (see https://lodash.com/docs/4.17.15#escape)
+ * @returns {string} the text with parameters replaces (e.g. '5 users use us')
+ * @see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf
+ * @see https://gitlab.com/gitlab-org/gitlab-foss/issues/37992
+ */
+export default function sprintf(input, parameters, escapeParameters = true) {
let output = input;
output = output.replace(/%+/g, '%');
@@ -29,4 +27,4 @@ export default (input, parameters, escapeParameters = true) => {
}
return output;
-};
+}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index fd002e29afc..5bfdd174694 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -8,7 +8,6 @@ import './commons';
import './behaviors';
// lib/utils
-import applyGitLabUIConfig from '@gitlab/ui/dist/config';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { initRails } from '~/lib/utils/rails_ujs';
import * as popovers from '~/popovers';
@@ -44,8 +43,6 @@ import 'jh_else_ce/main_jh';
logHelloDeferred();
-applyGitLabUIConfig();
-
// expose jQuery as global (TODO: remove these)
window.jQuery = jQuery;
window.$ = jQuery;
diff --git a/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue b/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
index 1ae341820d1..2f9acc96923 100644
--- a/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
@@ -40,7 +40,7 @@ export default {
v-gl-tooltip.hover
:title="$options.title"
:aria-label="$options.title"
- data-qa-selector="approve_access_request_button"
+ data-testid="approve-access-request-button"
icon="check"
type="submit"
/>
diff --git a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
index caa292b37ce..1f134fe3f5d 100644
--- a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
@@ -71,7 +71,7 @@ export default {
:title="title"
:aria-label="title"
icon="remove"
- data-qa-selector="delete_member_button"
+ data-testid="delete-member-button"
@click="showRemoveMemberModal(modalData)"
/>
</template>
diff --git a/app/assets/javascripts/members/components/modals/remove_member_modal.vue b/app/assets/javascripts/members/components/modals/remove_member_modal.vue
index c7bd1525558..ecc769174f4 100644
--- a/app/assets/javascripts/members/components/modals/remove_member_modal.vue
+++ b/app/assets/javascripts/members/components/modals/remove_member_modal.vue
@@ -56,7 +56,8 @@ export default {
actionText() {
if (this.isAccessRequest) {
return __('Deny access request');
- } else if (this.isInvite) {
+ }
+ if (this.isInvite) {
return s__('Member|Revoke invite');
}
diff --git a/app/assets/javascripts/members/components/table/members_table_cell.vue b/app/assets/javascripts/members/components/table/members_table_cell.vue
index 407cbc55dd3..cac8c9fb4db 100644
--- a/app/assets/javascripts/members/components/table/members_table_cell.vue
+++ b/app/assets/javascripts/members/components/table/members_table_cell.vue
@@ -32,9 +32,11 @@ export default {
memberType() {
if (this.isGroup) {
return MEMBER_TYPES.group;
- } else if (this.isInvite) {
+ }
+ if (this.isInvite) {
return MEMBER_TYPES.invite;
- } else if (this.isAccessRequest) {
+ }
+ if (this.isAccessRequest) {
return MEMBER_TYPES.accessRequest;
}
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 09fe611262c..1bc67522e82 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -93,7 +93,11 @@ function mountPipelines() {
const { mrWidgetData } = gl;
const table = new Vue({
components: {
- CommitPipelinesTable: () => import('~/commit/pipelines/pipelines_table.vue'),
+ CommitPipelinesTable: () => {
+ return gon.features.mrPipelinesGraphql
+ ? import('~/ci/merge_requests/components/pipelines_table_wrapper.vue')
+ : import('~/commit/pipelines/legacy_pipelines_table_wrapper.vue');
+ },
},
apolloProvider,
provide: {
@@ -103,6 +107,8 @@ function mountPipelines() {
fullPath: pipelineTableViewEl.dataset.fullPath,
graphqlPath: pipelineTableViewEl.dataset.graphqlPath,
manualActionsLimit: 50,
+ mergeRequestId: mrWidgetData ? mrWidgetData.iid : null,
+ sourceProjectFullPath: mrWidgetData?.source_project_full_path || '',
withFailedJobsDetails: true,
},
render(createElement) {
@@ -573,10 +579,12 @@ export default class MergeRequestTabs {
expandViewContainer() {
this.contentWrapper.classList.remove('container-limited');
+ this.contentWrapper.classList.add('diffs-container-limited');
}
resetViewContainer() {
this.contentWrapper.classList.toggle('container-limited', this.isFixedLayoutPreferred);
+ this.contentWrapper.classList.remove('diffs-container-limited');
}
// Expand the issuable sidebar unless the user explicitly collapsed it
diff --git a/app/assets/javascripts/merge_requests/components/compare_app.vue b/app/assets/javascripts/merge_requests/components/compare_app.vue
index 8e02048f494..c7c16e91e4c 100644
--- a/app/assets/javascripts/merge_requests/components/compare_app.vue
+++ b/app/assets/javascripts/merge_requests/components/compare_app.vue
@@ -23,9 +23,6 @@ export default {
currentProject: {
default: () => ({}),
},
- currentBranch: {
- default: () => ({}),
- },
inputs: {
default: () => ({}),
},
@@ -35,8 +32,12 @@ export default {
toggleClass: {
default: () => ({}),
},
- branchQaSelector: {
- default: '',
+ },
+ props: {
+ currentBranch: {
+ type: Object,
+ required: false,
+ default: () => ({}),
},
},
data() {
@@ -57,6 +58,12 @@ export default {
return this.commitHtml || this.loading || !this.selectedBranch.value;
},
},
+ watch: {
+ currentBranch(newVal) {
+ this.selectedBranch = newVal;
+ this.fetchCommit();
+ },
+ },
mounted() {
this.fetchCommit();
},
@@ -67,6 +74,7 @@ export default {
selectBranch(branch) {
this.selectedBranch = branch;
this.fetchCommit();
+ this.$emit('select-branch', branch.value);
},
async fetchCommit() {
if (!this.selectedBranch.value) return;
@@ -108,7 +116,7 @@ export default {
:input-name="inputs.branch.name"
:default="currentBranch"
:toggle-class="toggleClass.branch"
- :qa-selector="branchQaSelector"
+ data-testid="compare-dropdown"
@selected="selectBranch"
/>
</div>
diff --git a/app/assets/javascripts/merge_requests/components/compare_dropdown.vue b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
index a5a4e683214..2855d704507 100644
--- a/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
+++ b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
@@ -46,11 +46,6 @@ export default {
required: false,
default: '',
},
- qaSelector: {
- type: String,
- required: false,
- default: null,
- },
},
data() {
return {
@@ -70,6 +65,12 @@ export default {
);
},
},
+ watch: {
+ default(newVal) {
+ this.current = newVal;
+ this.selected = newVal.value;
+ },
+ },
methods: {
async fetchData() {
if (!this.endpoint) return;
@@ -136,7 +137,7 @@ export default {
'gl-align-items-flex-start! gl-justify-content-start! mr-compare-dropdown',
toggleClass,
]"
- :data-qa-selector="qaSelector"
+ data-testid="source-branch-dropdown"
@shown="fetchData"
@search="searchData"
@select="selectItem"
diff --git a/app/assets/javascripts/issuable/components/issuable_header_warnings.vue b/app/assets/javascripts/merge_requests/components/header_metadata.vue
index a0854be099d..fce7ba385b4 100644
--- a/app/assets/javascripts/issuable/components/issuable_header_warnings.vue
+++ b/app/assets/javascripts/merge_requests/components/header_metadata.vue
@@ -2,16 +2,10 @@
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters } from 'vuex';
-import { sprintf, __ } from '~/locale';
-import { TYPE_ISSUE, TYPE_MERGE_REQUEST, WORKSPACE_PROJECT } from '~/issues/constants';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { __ } from '~/locale';
+import { TYPE_ISSUE, WORKSPACE_PROJECT } from '~/issues/constants';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
-const noteableTypeText = {
- issue: __('issue'),
- merge_request: __('merge request'),
-};
-
export default {
TYPE_ISSUE,
WORKSPACE_PROJECT,
@@ -22,7 +16,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagMixin()],
inject: ['hidden'],
computed: {
...mapGetters(['getNoteableData']),
@@ -32,26 +25,19 @@ export default {
isConfidential() {
return this.getNoteableData.confidential;
},
- isMergeRequest() {
- return this.getNoteableData.targetType === TYPE_MERGE_REQUEST;
- },
warningIconsMeta() {
return [
{
iconName: 'lock',
visible: this.isLocked,
dataTestId: 'locked',
- tooltip: sprintf(__('This %{issuable} is locked. Only project members can comment.'), {
- issuable: noteableTypeText[this.getNoteableData.targetType],
- }),
+ tooltip: __('This merge request is locked. Only project members can comment.'),
},
{
iconName: 'spam',
visible: this.hidden,
dataTestId: 'hidden',
- tooltip: sprintf(__('This %{issuable} is hidden because its author has been banned'), {
- issuable: noteableTypeText[this.getNoteableData.targetType],
- }),
+ tooltip: __('This merge request is hidden because its author has been banned'),
},
];
},
@@ -63,9 +49,9 @@ export default {
<div class="gl-display-inline-block">
<confidentiality-badge
v-if="isConfidential"
- data-testid="confidential"
- :workspace-type="$options.WORKSPACE_PROJECT"
+ class="gl-mr-3"
:issuable-type="$options.TYPE_ISSUE"
+ :workspace-type="$options.WORKSPACE_PROJECT"
/>
<template v-for="meta in warningIconsMeta">
<div
@@ -74,11 +60,7 @@ export default {
v-gl-tooltip.bottom
:data-testid="meta.dataTestId"
:title="meta.tooltip || null"
- :class="{
- 'gl-mr-3 gl-mt-2 gl-display-flex gl-justify-content-center gl-align-items-center': isMergeRequest,
- 'gl-display-inline-block': !isMergeRequest,
- }"
- class="issuable-warning-icon"
+ 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>
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
new file mode 100644
index 00000000000..3d5478757a8
--- /dev/null
+++ b/app/assets/javascripts/merge_requests/components/merge_request_status_badge.vue
@@ -0,0 +1,74 @@
+<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 3c3bee9b108..c1e88a901c4 100644
--- a/app/assets/javascripts/merge_requests/components/sticky_header.vue
+++ b/app/assets/javascripts/merge_requests/components/sticky_header.vue
@@ -7,13 +7,15 @@ import { TYPENAME_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { isLoggedIn } from '~/lib/utils/common_utils';
-import StatusBox from '~/issuable/components/status_box.vue';
+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 ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import titleSubscription from '../queries/title.subscription.graphql';
export default {
+ TYPE_MERGE_REQUEST,
apollo: {
$subscribe: {
title: {
@@ -41,8 +43,8 @@ export default {
GlLink,
GlSprintf,
GlBadge,
- StatusBox,
DiscussionCounter,
+ StatusBadge,
TodoWidget,
ClipboardButton,
},
@@ -115,7 +117,11 @@ export default {
:class="{ 'gl-max-w-container-xl': !isFluidLayout }"
>
<div class="gl-w-full gl-display-flex gl-align-items-baseline">
- <status-box :initial-state="getNoteableData.state" issuable-type="merge_request" />
+ <status-badge
+ class="gl-align-self-center gl-mr-3"
+ :issuable-type="$options.TYPE_MERGE_REQUEST"
+ :state="getNoteableData.state"
+ />
<a
v-safe-html:[$options.safeHtmlConfig]="titleHtml"
href="#top"
diff --git a/app/assets/javascripts/merge_requests/index.js b/app/assets/javascripts/merge_requests/index.js
new file mode 100644
index 00000000000..29218eb53e0
--- /dev/null
+++ b/app/assets/javascripts/merge_requests/index.js
@@ -0,0 +1,19 @@
+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/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue
index c8268b1a9ae..a7b753b7ca8 100644
--- a/app/assets/javascripts/notebook/cells/output/index.vue
+++ b/app/assets/javascripts/notebook/cells/output/index.vue
@@ -33,19 +33,26 @@ export default {
outputType(output) {
if (output.text) {
return 'text/plain';
- } else if (output.output_type === ERROR_OUTPUT_TYPE) {
+ }
+ if (output.output_type === ERROR_OUTPUT_TYPE) {
return 'error';
- } else if (output.data['image/png']) {
+ }
+ if (output.data['image/png']) {
return 'image/png';
- } else if (output.data['image/jpeg']) {
+ }
+ if (output.data['image/jpeg']) {
return 'image/jpeg';
- } else if (output.data['text/html']) {
+ }
+ if (output.data['text/html']) {
return 'text/html';
- } else if (output.data['text/latex']) {
+ }
+ if (output.data['text/latex']) {
return 'text/latex';
- } else if (output.data['image/svg+xml']) {
+ }
+ if (output.data['image/svg+xml']) {
return 'image/svg+xml';
- } else if (output.data[TEXT_MARKDOWN]) {
+ }
+ if (output.data[TEXT_MARKDOWN]) {
return TEXT_MARKDOWN;
}
@@ -63,21 +70,29 @@ export default {
getComponent(output) {
if (output.text) {
return CodeOutput;
- } else if (output.output_type === ERROR_OUTPUT_TYPE) {
+ }
+ if (output.output_type === ERROR_OUTPUT_TYPE) {
return ErrorOutput;
- } else if (output.data['image/png']) {
+ }
+ if (output.data['image/png']) {
return ImageOutput;
- } else if (output.data['image/jpeg']) {
+ }
+ if (output.data['image/jpeg']) {
return ImageOutput;
- } else if (isDataframe(output)) {
+ }
+ if (isDataframe(output)) {
return DataframeOutput;
- } else if (output.data['text/html']) {
+ }
+ if (output.data['text/html']) {
return HtmlOutput;
- } else if (output.data['text/latex']) {
+ }
+ if (output.data['text/latex']) {
return LatexOutput;
- } else if (output.data['image/svg+xml']) {
+ }
+ if (output.data['image/svg+xml']) {
return HtmlOutput;
- } else if (output.data[TEXT_MARKDOWN]) {
+ }
+ if (output.data[TEXT_MARKDOWN]) {
return MarkdownOutput;
}
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index a009f2975bb..144cfa4295b 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -5,7 +5,6 @@ import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { createAlert } from '~/alert';
-import { badgeState } from '~/issuable/components/status_box.vue';
import { STATUS_CLOSED, STATUS_MERGED, STATUS_OPEN, STATUS_REOPENED } from '~/issues/constants';
import { containsSensitiveToken, confirmSensitiveAction } from '~/lib/utils/secret_detection';
import {
@@ -14,6 +13,7 @@ import {
slugifyWithUnderscore,
} from '~/lib/utils/text_utility';
import { sprintf } from '~/locale';
+import { badgeState } from '~/merge_requests/components/merge_request_status_badge.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';
@@ -210,8 +210,6 @@ export default {
methods: {
...mapActions([
'saveNote',
- 'stopPolling',
- 'restartPolling',
'removePlaceholderNotes',
'closeIssuable',
'reopenIssuable',
@@ -253,7 +251,6 @@ export default {
}
this.note = ''; // Empty textarea while being requested. Repopulate in catch
- this.stopPolling();
this.isSubmitting = true;
@@ -264,7 +261,6 @@ export default {
this.saveNote(noteData)
.then(() => {
- this.restartPolling();
this.discard();
if (withIssueAction) {
@@ -381,7 +377,10 @@ export default {
@input="onInput"
/>
</comment-field-layout>
- <div class="note-form-actions">
+ <div
+ class="note-form-actions gl-font-size-0"
+ :class="{ 'gl-display-flex gl-gap-3': hasDrafts }"
+ >
<template v-if="hasDrafts">
<gl-button
:disabled="disableSubmitButton"
@@ -404,7 +403,7 @@ export default {
<gl-form-checkbox
v-if="canSetInternalNote"
v-model="noteIsInternal"
- class="gl-mb-2"
+ class="gl-mb-2 gl-flex-basis-full"
data-testid="internal-note-checkbox"
>
{{ $options.i18n.internal }}
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 7266cdb6405..90f7a6862f0 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -137,7 +137,8 @@ export default {
filterType(value) {
if (value === 0) {
return DISCUSSION_FILTER_TYPES.ALL;
- } else if (value === 1) {
+ }
+ if (value === 1) {
return DISCUSSION_FILTER_TYPES.COMMENTS;
}
return DISCUSSION_FILTER_TYPES.HISTORY;
diff --git a/app/assets/javascripts/notes/components/mr_discussion_filter.vue b/app/assets/javascripts/notes/components/mr_discussion_filter.vue
index 08d3670ae6a..c2ac95ca56e 100644
--- a/app/assets/javascripts/notes/components/mr_discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/mr_discussion_filter.vue
@@ -36,7 +36,8 @@ export default {
if (length === MR_FILTER_OPTIONS.length) {
return __('All activity');
- } else if (length > 1) {
+ }
+ if (length > 1) {
return `%{strongStart}${firstSelected.text}%{strongEnd} +${length - 1} more`;
}
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 8b43f068f11..363383fd7ad 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -160,12 +160,14 @@ export default {
filePath: this.diffFile.file_path,
refs: this.diffFile.diff_refs,
};
- } else if (this.note && this.note.position) {
+ }
+ if (this.note && this.note.position) {
return {
filePath: this.note.position.new_path,
refs: this.note.position,
};
- } else if (this.discussion && this.discussion.diff_file) {
+ }
+ if (this.discussion && this.discussion.diff_file) {
return {
filePath: this.discussion.diff_file.file_path,
refs: this.discussion.diff_file.diff_refs,
@@ -381,8 +383,8 @@ export default {
@handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
/>
</comment-field-layout>
- <div class="note-form-actions">
- <p v-if="showResolveDiscussionToggle">
+ <div class="note-form-actions gl-font-size-0">
+ <template v-if="showResolveDiscussionToggle">
<label>
<template v-if="discussionResolved">
<gl-form-checkbox v-model="isUnresolving" class="js-unresolve-checkbox">
@@ -395,7 +397,7 @@ export default {
</gl-form-checkbox>
</template>
</label>
- </p>
+ </template>
<template v-if="showBatchCommentsActions">
<div class="gl-display-flex gl-flex-wrap gl-mb-n3">
@@ -432,7 +434,7 @@ export default {
</div>
</template>
<template v-else>
- <div class="gl-display-sm-flex gl-flex-wrap">
+ <div class="gl-display-sm-flex gl-flex-wrap gl-font-size-0">
<gl-button
:disabled="isDisabled"
category="primary"
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 6fb958e810b..2524b9efdb6 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -169,7 +169,6 @@ export default {
});
},
beforeDestroy() {
- this.stopPolling();
window.removeEventListener('hashchange', this.handleHashChanged);
eventHub.$off('notesApp.updateIssuableConfidentiality', this.setConfidentiality);
},
@@ -182,7 +181,6 @@ export default {
'expandDiscussion',
'startTaskList',
'convertToDiscussion',
- 'stopPolling',
'setConfidentiality',
'fetchNotes',
]),
diff --git a/app/assets/javascripts/notes/components/sidebar_subscription.vue b/app/assets/javascripts/notes/components/sidebar_subscription.vue
index f60a17eb36b..c02c7a57dfa 100644
--- a/app/assets/javascripts/notes/components/sidebar_subscription.vue
+++ b/app/assets/javascripts/notes/components/sidebar_subscription.vue
@@ -3,7 +3,7 @@
import { mapActions } from 'vuex';
import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import { fetchPolicies } from '~/lib/graphql';
-import { confidentialityQueries } from '~/sidebar/constants';
+import { confidentialityQueries } from '~/sidebar/queries/constants';
import { defaultClient as gqlClient } from '~/graphql_shared/issuable_client';
export default {
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 0444eca9aa7..7eb01897296 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -1,5 +1,4 @@
import $ from 'jquery';
-import Visibility from 'visibilityjs';
import Vue from 'vue';
import actionCable from '~/actioncable_consumer';
import Api from '~/api';
@@ -14,8 +13,6 @@ import updateIssueLockMutation from '~/sidebar/queries/update_issue_lock.mutatio
import updateMergeRequestLockMutation from '~/sidebar/queries/update_merge_request_lock.mutation.graphql';
import loadAwardsHandler from '~/awards_handler';
import { isInViewport, scrollToElement, isInMRPage } from '~/lib/utils/common_utils';
-import Poll from '~/lib/utils/poll';
-import { create } from '~/lib/utils/recurrence';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import sidebarTimeTrackingEventHub from '~/sidebar/event_hub';
import TaskList from '~/task_list';
@@ -30,9 +27,6 @@ import * as constants from '../constants';
import * as types from './mutation_types';
import * as utils from './utils';
-const NOTES_POLLING_INTERVAL = 6000;
-let eTagPoll;
-
export const updateLockedAttribute = ({ commit, getters }, { locked, fullPath }) => {
const { iid, targetType } = getters.getNoteableData;
@@ -152,29 +146,25 @@ export const initPolling = ({ state, dispatch, getters, commit }) => {
dispatch('setLastFetchedAt', getters.getNotesDataByProp('lastFetchedAt'));
- if (gon.features?.actionCableNotes) {
- actionCable.subscriptions.create(
- {
- channel: 'Noteable::NotesChannel',
- project_id: state.notesData.projectId,
- group_id: state.notesData.groupId,
- noteable_type: state.notesData.noteableType,
- noteable_id: state.notesData.noteableId,
+ actionCable.subscriptions.create(
+ {
+ channel: 'Noteable::NotesChannel',
+ project_id: state.notesData.projectId,
+ group_id: state.notesData.groupId,
+ noteable_type: state.notesData.noteableType,
+ noteable_id: state.notesData.noteableId,
+ },
+ {
+ connected() {
+ dispatch('fetchUpdatedNotes');
},
- {
- connected() {
+ received(data) {
+ if (data.event === 'updated') {
dispatch('fetchUpdatedNotes');
- },
- received(data) {
- if (data.event === 'updated') {
- dispatch('fetchUpdatedNotes');
- }
- },
+ }
},
- );
- } else {
- dispatch('poll');
- }
+ },
+ );
commit(types.SET_IS_POLLING_INITIALIZED, true);
};
@@ -386,7 +376,8 @@ export const resolveDiscussion = ({ state, dispatch, getters }, { discussionId }
if (!discussion) {
return Promise.reject();
- } else if (isResolved) {
+ }
+ if (isResolved) {
return Promise.resolve();
}
@@ -515,8 +506,6 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
{"commands_changes":{},"valid":false,"errors":{"commands_only":["Commands applied"]}}
*/
if (hasQuickActions && message) {
- if (eTagPoll) eTagPoll.makeRequest();
-
// synchronizing the quick action with the sidebar widget
// this is a temporary solution until we have confidentiality real-time updates
if (
@@ -624,69 +613,7 @@ export const fetchUpdatedNotes = ({ commit, state, getters, dispatch }) => {
.then(({ data }) => {
pollSuccessCallBack(data, commit, state, getters, dispatch);
})
- .catch(() => {
- createAlert({
- message: __('Something went wrong while fetching latest comments.'),
- });
- });
-};
-
-export const poll = ({ commit, state, getters, dispatch }) => {
- const notePollOccurrenceTracking = create();
- let alert;
-
- notePollOccurrenceTracking.handle(1, () => {
- // Since polling halts internally after 1 failure, we manually try one more time
- setTimeout(() => eTagPoll.restart(), NOTES_POLLING_INTERVAL);
- });
- notePollOccurrenceTracking.handle(2, () => {
- // On the second failure in a row, show the alert and try one more time (hoping to succeed and clear the error)
- alert = createAlert({
- message: __('Something went wrong while fetching latest comments.'),
- });
- setTimeout(() => eTagPoll.restart(), NOTES_POLLING_INTERVAL);
- });
-
- eTagPoll = new Poll({
- resource: {
- poll: () => {
- const { endpoint, options } = getFetchDataParams(state);
- return axios.get(endpoint, options);
- },
- },
- method: 'poll',
- successCallback: ({ data }) => {
- pollSuccessCallBack(data, commit, state, getters, dispatch);
-
- if (notePollOccurrenceTracking.count) {
- notePollOccurrenceTracking.reset();
- }
- alert?.dismiss();
- },
- errorCallback: () => notePollOccurrenceTracking.occur(),
- });
-
- if (!Visibility.hidden()) {
- eTagPoll.makeDelayedRequest(2500);
- } else {
- eTagPoll.makeRequest();
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- eTagPoll.restart();
- } else {
- eTagPoll.stop();
- }
- });
-};
-
-export const stopPolling = () => {
- if (eTagPoll) eTagPoll.stop();
-};
-
-export const restartPolling = () => {
- if (eTagPoll) eTagPoll.restart();
+ .catch(() => {});
};
export const toggleAward = ({ commit, getters }, { awardName, noteId }) => {
@@ -766,7 +693,6 @@ export const submitSuggestion = (
dispatch('resolveDiscussion', { discussionId }).catch(() => {});
commit(types.SET_RESOLVING_DISCUSSION, true);
- dispatch('stopPolling');
return Api.applySuggestion(suggestionId, message)
.then(dispatchResolveDiscussion)
@@ -786,7 +712,6 @@ export const submitSuggestion = (
})
.finally(() => {
commit(types.SET_RESOLVING_DISCUSSION, false);
- dispatch('restartPolling');
});
};
@@ -801,7 +726,6 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { message, fl
commit(types.SET_APPLYING_BATCH_STATE, true);
commit(types.SET_RESOLVING_DISCUSSION, true);
- dispatch('stopPolling');
return Api.applySuggestionBatch(suggestionIds, message)
.then(() => Promise.all(resolveAllDiscussions()))
@@ -823,7 +747,6 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { message, fl
.finally(() => {
commit(types.SET_APPLYING_BATCH_STATE, false);
commit(types.SET_RESOLVING_DISCUSSION, false);
- dispatch('restartPolling');
});
};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 3fb9913bdcb..c43430639ad 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -1,8 +1,8 @@
import { flattenDeep, clone } from 'lodash';
import { match } from '~/diffs/utils/diff_file';
-import { badgeState } from '~/issuable/components/status_box.vue';
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 * as constants from '../constants';
import { collapseSystemNotes } from './collapse_utils';
diff --git a/app/assets/javascripts/observability/client.js b/app/assets/javascripts/observability/client.js
index c55600f3db2..718001e98fe 100644
--- a/app/assets/javascripts/observability/client.js
+++ b/app/assets/javascripts/observability/client.js
@@ -1,57 +1,62 @@
+import * as Sentry from '@sentry/browser';
import axios from '~/lib/utils/axios_utils';
-// import mockData from './mock_traces.json';
-
-function enableTraces() {
- // TODO remove mocks https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2271
- return new Promise((resolve) => {
- setTimeout(() => {
- resolve();
- }, 1000);
- });
-}
-function isTracingEnabled() {
- // TODO remove mocks https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2271
- return new Promise((resolve) => {
- setTimeout(() => {
- // Currently relying on manual provisioning, hence assuming tracing is enabled
- resolve(true);
- }, 1000);
- });
+function reportErrorAndThrow(e) {
+ Sentry.captureException(e);
+ throw e;
}
-
-function traceWithDuration(trace) {
- // aggregating duration on the client for now, but expecting to be coming from the backend
- // https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2274
- const duration = trace.spans[0].duration_nano;
- return {
- ...trace,
- duration: duration / 1000,
- };
+// Provisioning API spec: https://gitlab.com/gitlab-org/opstrace/opstrace/-/blob/main/provisioning-api/pkg/provisioningapi/routes.go#L59
+async function enableTraces(provisioningUrl) {
+ try {
+ // Note: axios.put(url, undefined, {withCredentials: true}) does not send cookies properly, so need to use the API below for the correct behaviour
+ return await axios(provisioningUrl, {
+ method: 'put',
+ withCredentials: true,
+ });
+ } catch (e) {
+ return reportErrorAndThrow(e);
+ }
}
-async function fetchTrace(tracingUrl, traceId) {
- if (!traceId) {
- throw new Error('traceId is required.');
+// Provisioning API spec: https://gitlab.com/gitlab-org/opstrace/opstrace/-/blob/main/provisioning-api/pkg/provisioningapi/routes.go#L37
+async function isTracingEnabled(provisioningUrl) {
+ try {
+ const { data } = await axios.get(provisioningUrl, { withCredentials: true });
+ if (data && data.status) {
+ // we currently ignore the 'status' payload and just check if the request was successful
+ // We might improve this as part of https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2315
+ return true;
+ }
+ } catch (e) {
+ if (e.response.status === 404) {
+ return false;
+ }
+ return reportErrorAndThrow(e);
}
+ return reportErrorAndThrow(new Error('Failed to check provisioning')); // eslint-disable-line @gitlab/require-i18n-strings
+}
- const { data } = await axios.get(tracingUrl, {
- withCredentials: true,
- params: {
- trace_id: traceId,
- },
- });
+async function fetchTrace(tracingUrl, traceId) {
+ try {
+ if (!traceId) {
+ throw new Error('traceId is required.');
+ }
+
+ const { data } = await axios.get(tracingUrl, {
+ withCredentials: true,
+ params: {
+ trace_id: traceId,
+ },
+ });
- // TODO: Improve local GDK dev experience with tracing https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2308
- // const data = mockData;
- // const trace = data.traces.find((t) => t.trace_id === traceId);
+ if (!Array.isArray(data.traces) || data.traces.length === 0) {
+ throw new Error('traces are missing/invalid in the response'); // eslint-disable-line @gitlab/require-i18n-strings
+ }
- if (!Array.isArray(data.traces) || data.traces.length === 0) {
- throw new Error('traces are missing/invalid in the response.'); // eslint-disable-line @gitlab/require-i18n-strings
+ return data.traces[0];
+ } catch (e) {
+ return reportErrorAndThrow(e);
}
-
- const trace = data.traces[0];
- return traceWithDuration(trace);
}
/**
@@ -164,18 +169,18 @@ function filterObjToQueryParams(filterObj) {
async function fetchTraces(tracingUrl, filters = {}) {
const filterParams = filterObjToQueryParams(filters);
- const { data } = await axios.get(tracingUrl, {
- withCredentials: true,
- params: filterParams,
- });
- // TODO: Improve local GDK dev experience with tracing https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2308
- // Uncomment the line below to test this locally
- // const data = mockData;
-
- if (!Array.isArray(data.traces)) {
- throw new Error('traces are missing/invalid in the response.'); // eslint-disable-line @gitlab/require-i18n-strings
+ try {
+ const { data } = await axios.get(tracingUrl, {
+ withCredentials: true,
+ params: filterParams,
+ });
+ 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;
+ } catch (e) {
+ return reportErrorAndThrow(e);
}
- return data.traces.map(traceWithDuration);
}
export function buildClient({ provisioningUrl, tracingUrl }) {
diff --git a/app/assets/javascripts/observability/mock_traces.json b/app/assets/javascripts/observability/mock_traces.json
index ee59258e591..cd7dfb40af6 100644
--- a/app/assets/javascripts/observability/mock_traces.json
+++ b/app/assets/javascripts/observability/mock_traces.json
@@ -1,348 +1,107 @@
{
- "project_id": "10141740",
+ "project_id": 123,
"traces": [
{
- "timestamp": "2023-07-18T10:31:23.661285Z",
- "trace_id": "08a1b018-e1b9-88b2-094b-ca5fd40783ad",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:23.661285Z",
- "span_id": "30A9220B254C42B1",
- "trace_id": "08a1b018-e1b9-88b2-094b-ca5fd40783ad",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 250,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:17.026724Z",
- "trace_id": "1c2099e0-6da8-d5fb-a91d-bdd5a5bea82c",
- "service_name": "my-service-name2",
- "operation": "Addition",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:17.026724Z",
- "span_id": "154925D3DA2C1307",
- "trace_id": "1c2099e0-6da8-d5fb-a91d-bdd5a5bea82c",
- "service_name": "my-service-name",
- "operation": "Addition",
- "duration_nano": 208,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:21.602132Z",
- "trace_id": "f4c2f964-afee-cc2e-bd1a-c654ff55db4e",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:21.602132Z",
- "span_id": "53A4AE94DFF72A28",
- "trace_id": "f4c2f964-afee-cc2e-bd1a-c654ff55db4e",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 5125,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:14.772009Z",
- "trace_id": "fa6302ad-7214-7c05-40f7-91195356774e",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:14.772009Z",
- "span_id": "5BB240D099656820",
- "trace_id": "fa6302ad-7214-7c05-40f7-91195356774e",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 1584,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:22.623552Z",
- "trace_id": "54021e57-a25c-c6fe-1f53-542bbdbcb16c",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:22.623552Z",
- "span_id": "C5AE65D0C26BF3FD",
- "trace_id": "54021e57-a25c-c6fe-1f53-542bbdbcb16c",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 750,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:21.602156Z",
- "trace_id": "34d455cc-e518-fb4e-513f-e88030d4ccc8",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:21.602156Z",
- "span_id": "5288B61252594EB2",
- "trace_id": "34d455cc-e518-fb4e-513f-e88030d4ccc8",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 750,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:20.567364Z",
- "trace_id": "3892a93a-f4eb-b416-372e-3c9237be97e3",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:20.567364Z",
- "span_id": "1D690E5094345C98",
- "trace_id": "3892a93a-f4eb-b416-372e-3c9237be97e3",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 958,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:23.661289Z",
- "trace_id": "9d0630d5-21b5-686f-57cb-d97c647fc31f",
- "service_name": "my-service-name",
- "operation": "Addition",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:23.661289Z",
- "span_id": "8F548EE08F9C2EAC",
- "trace_id": "9d0630d5-21b5-686f-57cb-d97c647fc31f",
- "service_name": "my-service-name",
- "operation": "Addition",
- "duration_nano": 167,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:14.77197Z",
- "trace_id": "f2470b0e-3bbb-8af2-68f8-c97343fba7ee",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:14.77197Z",
- "span_id": "6B5AB710CE8A4471",
- "trace_id": "f2470b0e-3bbb-8af2-68f8-c97343fba7ee",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 5583,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:17.026712Z",
- "trace_id": "f4e64ee0-ee32-0edb-c4d1-a15f4047bdc4",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:17.026712Z",
- "span_id": "199D402DE1A29F3F",
- "trace_id": "f4e64ee0-ee32-0edb-c4d1-a15f4047bdc4",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 6959,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:20.567337Z",
- "trace_id": "344b4db1-c890-514c-b94f-425fff3a795b",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:20.567337Z",
- "span_id": "CAC38748150E5A0C",
- "trace_id": "344b4db1-c890-514c-b94f-425fff3a795b",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 3917,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:22.623559Z",
- "trace_id": "40b933be-11d7-7b41-e63a-ff7e7c5d50ab",
- "service_name": "my-service-name",
- "operation": "Addition",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:22.623559Z",
- "span_id": "3485100A27958F59",
- "trace_id": "40b933be-11d7-7b41-e63a-ff7e7c5d50ab",
- "service_name": "my-service-name",
- "operation": "Addition",
- "duration_nano": 709,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:17.026723Z",
- "trace_id": "c3347c43-316f-2b08-0b46-7d142d6764b7",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:17.026723Z",
- "span_id": "1CF28C36AB7EB3F9",
- "trace_id": "c3347c43-316f-2b08-0b46-7d142d6764b7",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 208,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:23.661272Z",
- "trace_id": "762f9104-d3db-c762-d6d7-0476ca21249f",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:23.661272Z",
- "span_id": "83D8D6D2BD99A4D1",
- "trace_id": "762f9104-d3db-c762-d6d7-0476ca21249f",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 10000,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:22.623524Z",
- "trace_id": "b46ded15-f900-fba7-7396-a6b453221038",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:22.623524Z",
- "span_id": "EB84455AE35DEAD5",
- "trace_id": "b46ded15-f900-fba7-7396-a6b453221038",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 17666,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:21.60216Z",
- "trace_id": "8dcd7b90-7f94-6b19-4a8f-b681801568ba",
- "service_name": "my-service-name",
- "operation": "Addition",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:21.60216Z",
- "span_id": "A5C773414186949D",
- "trace_id": "8dcd7b90-7f94-6b19-4a8f-b681801568ba",
- "service_name": "my-service-name",
- "operation": "Addition",
- "duration_nano": 250,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:14.772014Z",
- "trace_id": "51e23c12-033d-a0db-d2b0-2f3f4e3d9fb3",
- "service_name": "my-service-name",
- "operation": "Addition",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:14.772014Z",
- "span_id": "3397060046FD4428",
- "trace_id": "51e23c12-033d-a0db-d2b0-2f3f4e3d9fb3",
- "service_name": "my-service-name",
- "operation": "Addition",
- "duration_nano": 291,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:20.567369Z",
- "trace_id": "d54b5015-3938-ddf8-3aa1-49882503233e",
- "service_name": "my-service-name",
- "operation": "Addition",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:20.567369Z",
- "span_id": "DAC36ACC2DBA8B11",
- "trace_id": "d54b5015-3938-ddf8-3aa1-49882503233e",
- "service_name": "my-service-name",
- "operation": "Addition",
- "duration_nano": 208,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
+ "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": 18
+ "totalTraces": 50
}
diff --git a/app/assets/javascripts/organizations/constants.js b/app/assets/javascripts/organizations/constants.js
new file mode 100644
index 00000000000..8ade37b169e
--- /dev/null
+++ b/app/assets/javascripts/organizations/constants.js
@@ -0,0 +1,4 @@
+export const RESOURCE_TYPE_GROUPS = 'groups';
+export const RESOURCE_TYPE_PROJECTS = 'projects';
+
+export const ORGANIZATION_ROOT_ROUTE_NAME = 'root';
diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
index 10471cc1fdd..dba738de5e1 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
+++ b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
@@ -13,9 +13,10 @@ import {
FILTERED_SEARCH_TERM,
TOKEN_EMPTY_SEARCH_TERM,
} from '~/vue_shared/components/filtered_search_bar/constants';
+import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '../../constants';
+import GroupsView from '../../shared/components/groups_view.vue';
+import ProjectsView from '../../shared/components/projects_view.vue';
import {
- DISPLAY_QUERY_GROUPS,
- DISPLAY_QUERY_PROJECTS,
DISPLAY_LISTBOX_ITEMS,
SORT_DIRECTION_ASC,
SORT_DIRECTION_DESC,
@@ -23,8 +24,6 @@ import {
SORT_ITEM_CREATED,
FILTERED_SEARCH_TERM_KEY,
} from '../constants';
-import GroupsPage from './groups_page.vue';
-import ProjectsPage from './projects_page.vue';
export default {
i18n: {
@@ -45,14 +44,14 @@ export default {
const { display } = this.$route.query;
switch (display) {
- case DISPLAY_QUERY_GROUPS:
- return GroupsPage;
+ case RESOURCE_TYPE_GROUPS:
+ return GroupsView;
- case DISPLAY_QUERY_PROJECTS:
- return ProjectsPage;
+ case RESOURCE_TYPE_PROJECTS:
+ return ProjectsView;
default:
- return GroupsPage;
+ return GroupsView;
}
},
activeSortItem() {
@@ -80,9 +79,9 @@ export default {
displayListboxSelected() {
const { display } = this.$route.query;
- return [DISPLAY_QUERY_GROUPS, DISPLAY_QUERY_PROJECTS].includes(display)
+ return [RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS].includes(display)
? display
- : DISPLAY_QUERY_GROUPS;
+ : RESOURCE_TYPE_GROUPS;
},
},
methods: {
diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue b/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue
deleted file mode 100644
index 20db38403f7..00000000000
--- a/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { createAlert } from '~/alert';
-import { s__ } from '~/locale';
-import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
-import groupsQuery from '../graphql/queries/groups.query.graphql';
-import { formatGroups } from '../utils';
-
-export default {
- i18n: {
- errorMessage: s__(
- 'Organization|An error occurred loading the groups. Please refresh the page to try again.',
- ),
- },
- components: { GlLoadingIcon, GroupsList },
- data() {
- return {
- groups: [],
- };
- },
- apollo: {
- groups: {
- query: groupsQuery,
- update(data) {
- return formatGroups(data.organization.groups.nodes);
- },
- error(error) {
- createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
- },
- },
- },
- computed: {
- isLoading() {
- return this.$apollo.queries.groups.loading;
- },
- },
-};
-</script>
-
-<template>
- <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
- <groups-list v-else :groups="groups" show-group-icon />
-</template>
diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue b/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue
deleted file mode 100644
index d6958ee996e..00000000000
--- a/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue
+++ /dev/null
@@ -1,46 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
-import { createAlert } from '~/alert';
-import projectsQuery from '../graphql/queries/projects.query.graphql';
-import { formatProjects } from '../utils';
-
-export default {
- i18n: {
- errorMessage: s__(
- 'Organization|An error occurred loading the projects. Please refresh the page to try again.',
- ),
- },
- components: {
- ProjectsList,
- GlLoadingIcon,
- },
- data() {
- return {
- projects: [],
- };
- },
- apollo: {
- projects: {
- query: projectsQuery,
- update(data) {
- return formatProjects(data.organization.projects.nodes);
- },
- error(error) {
- createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
- },
- },
- },
- computed: {
- isLoading() {
- return this.$apollo.queries.projects.loading;
- },
- },
-};
-</script>
-
-<template>
- <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
- <projects-list v-else :projects="projects" show-project-icon />
-</template>
diff --git a/app/assets/javascripts/organizations/groups_and_projects/constants.js b/app/assets/javascripts/organizations/groups_and_projects/constants.js
index 529caa666a0..d79b632f6fb 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/constants.js
+++ b/app/assets/javascripts/organizations/groups_and_projects/constants.js
@@ -3,8 +3,6 @@ import { __ } from '~/locale';
export const DISPLAY_QUERY_GROUPS = 'groups';
export const DISPLAY_QUERY_PROJECTS = 'projects';
-export const ORGANIZATION_ROOT_ROUTE_NAME = 'root';
-
export const FILTERED_SEARCH_TERM_KEY = 'search';
export const DISPLAY_LISTBOX_ITEMS = [
diff --git a/app/assets/javascripts/organizations/groups_and_projects/index.js b/app/assets/javascripts/organizations/groups_and_projects/index.js
index f3f15c635f1..3e05e4d0a4c 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/index.js
+++ b/app/assets/javascripts/organizations/groups_and_projects/index.js
@@ -2,9 +2,10 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import createDefaultClient from '~/lib/graphql';
-import resolvers from './graphql/resolvers';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { ORGANIZATION_ROOT_ROUTE_NAME } from '../constants';
+import resolvers from '../shared/graphql/resolvers';
import App from './components/app.vue';
-import { ORGANIZATION_ROOT_ROUTE_NAME } from './constants';
export const createRouter = () => {
const routes = [{ path: '/', name: ORGANIZATION_ROOT_ROUTE_NAME }];
@@ -23,6 +24,16 @@ export const initOrganizationsGroupsAndProjects = () => {
if (!el) return false;
+ const {
+ dataset: { appData },
+ } = el;
+ const {
+ projectsEmptyStateSvgPath,
+ groupsEmptyStateSvgPath,
+ newGroupPath,
+ newProjectPath,
+ } = convertObjectPropsToCamelCase(JSON.parse(appData));
+
Vue.use(VueRouter);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(resolvers),
@@ -34,6 +45,12 @@ export const initOrganizationsGroupsAndProjects = () => {
name: 'OrganizationsGroupsAndProjects',
apolloProvider,
router,
+ provide: {
+ projectsEmptyStateSvgPath,
+ groupsEmptyStateSvgPath,
+ newGroupPath,
+ newProjectPath,
+ },
render(createElement) {
return createElement(App);
},
diff --git a/app/assets/javascripts/organizations/mock_data.js b/app/assets/javascripts/organizations/mock_data.js
new file mode 100644
index 00000000000..17ab7bd1d34
--- /dev/null
+++ b/app/assets/javascripts/organizations/mock_data.js
@@ -0,0 +1,258 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+
+// This is temporary mock data that will be removed when completing the following:
+// 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 organizationProjects = {
+ nodes: [
+ {
+ id: 'gid://gitlab/Project/8',
+ nameWithNamespace: 'Twitter / Typeahead.Js',
+ webUrl: 'http://127.0.0.1:3000/twitter/Typeahead.Js',
+ topics: ['JavaScript', 'Vue.js'],
+ forksCount: 4,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'public',
+ openIssuesCount: 48,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:59" dir="auto">Optio et reprehenderit enim doloremque deserunt et commodi.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ isForked: true,
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/7',
+ nameWithNamespace: 'Flightjs / Flight',
+ webUrl: 'http://127.0.0.1:3000/flightjs/Flight',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'private',
+ openIssuesCount: 37,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:49" dir="auto">Dolor dicta rerum et ut eius voluptate earum qui.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ isForked: false,
+ accessLevel: {
+ integerValue: 20,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/6',
+ nameWithNamespace: 'Jashkenas / Underscore',
+ webUrl: 'http://127.0.0.1:3000/jashkenas/Underscore',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'private',
+ openIssuesCount: 34,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:52" dir="auto">Incidunt est aliquam autem nihil eveniet quis autem.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ isForked: false,
+ accessLevel: {
+ integerValue: 40,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/5',
+ nameWithNamespace: 'Commit451 / Lab Coat',
+ webUrl: 'http://127.0.0.1:3000/Commit451/lab-coat',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'internal',
+ openIssuesCount: 49,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:34" dir="auto">Sint eos dolorem impedit rerum et.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ isForked: false,
+ accessLevel: {
+ integerValue: 10,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/1',
+ nameWithNamespace: 'Toolbox / Gitlab Smoke Tests',
+ webUrl: 'http://127.0.0.1:3000/toolbox/gitlab-smoke-tests',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'internal',
+ openIssuesCount: 34,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:40" dir="auto">Veritatis error laboriosam libero autem.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ isForked: false,
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ ],
+};
+
+export const organizationGroups = {
+ nodes: [
+ {
+ id: 'gid://gitlab/Group/29',
+ fullName: 'Commit451',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/Commit451',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:52" dir="auto">Autem praesentium vel ut ratione itaque ullam culpa.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 0,
+ projectsCount: 3,
+ groupMembersCount: 2,
+ visibility: 'public',
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/33',
+ fullName: 'Flightjs',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/flightjs',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:60" dir="auto">Ipsa reiciendis deleniti officiis illum nostrum quo aliquam.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 4,
+ projectsCount: 3,
+ groupMembersCount: 1,
+ visibility: 'private',
+ accessLevel: {
+ integerValue: 20,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/24',
+ fullName: 'Gitlab Org',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/gitlab-org',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:64" dir="auto">Dolorem dolorem omnis impedit cupiditate pariatur officia velit.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 1,
+ projectsCount: 1,
+ groupMembersCount: 2,
+ visibility: 'internal',
+ accessLevel: {
+ integerValue: 10,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/27',
+ fullName: 'Gnuwget',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/gnuwgetf',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:47" dir="auto">Culpa soluta aut eius dolores est vel sapiente.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 4,
+ projectsCount: 2,
+ groupMembersCount: 3,
+ visibility: 'public',
+ accessLevel: {
+ integerValue: 40,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/31',
+ fullName: 'Jashkenas',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/jashkenas',
+ descriptionHtml: '<p data-sourcepos="1:1-1:25" dir="auto">Ut ut id aliquid nostrum.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 3,
+ projectsCount: 3,
+ groupMembersCount: 10,
+ visibility: 'private',
+ accessLevel: {
+ integerValue: 10,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/22',
+ fullName: 'Toolbox',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/toolbox',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:46" dir="auto">Quo voluptatem magnam facere voluptates alias.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 2,
+ projectsCount: 3,
+ groupMembersCount: 40,
+ visibility: 'internal',
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/35',
+ fullName: 'Twitter',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/twitter',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:40" dir="auto">Quae nulla consequatur assumenda id quo.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 20,
+ projectsCount: 30,
+ groupMembersCount: 100,
+ visibility: 'public',
+ accessLevel: {
+ integerValue: 40,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/73',
+ fullName: 'test',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/test',
+ descriptionHtml: '',
+ avatarUrl: null,
+ descendantGroupsCount: 1,
+ projectsCount: 1,
+ groupMembersCount: 1,
+ visibility: 'private',
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/74',
+ fullName: 'Twitter / test subgroup',
+ parent: {
+ id: 'gid://gitlab/Group/35',
+ },
+ webUrl: 'http://127.0.0.1:3000/groups/twitter/test-subgroup',
+ descriptionHtml: '',
+ avatarUrl: null,
+ descendantGroupsCount: 4,
+ projectsCount: 4,
+ groupMembersCount: 4,
+ visibility: 'internal',
+ accessLevel: {
+ integerValue: 20,
+ },
+ },
+ ],
+};
diff --git a/app/assets/javascripts/organizations/shared/components/groups_view.vue b/app/assets/javascripts/organizations/shared/components/groups_view.vue
new file mode 100644
index 00000000000..eaa3017ef97
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/components/groups_view.vue
@@ -0,0 +1,82 @@
+<script>
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { createAlert } from '~/alert';
+import { s__, __ } from '~/locale';
+import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
+import groupsQuery from '../graphql/queries/groups.query.graphql';
+import { formatGroups } from '../utils';
+
+export default {
+ i18n: {
+ errorMessage: s__(
+ 'Organization|An error occurred loading the groups. Please refresh the page to try again.',
+ ),
+ emptyState: {
+ title: s__("Organization|You don't have any groups yet."),
+ description: s__(
+ 'Organization|A group is a collection of several projects. If you organize your projects under a group, it works like a folder.',
+ ),
+ primaryButtonText: __('New group'),
+ },
+ },
+ components: { GlLoadingIcon, GlEmptyState, GroupsList },
+ inject: {
+ groupsEmptyStateSvgPath: {},
+ newGroupPath: {
+ default: null,
+ },
+ },
+ props: {
+ shouldShowEmptyStateButtons: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ groups: [],
+ };
+ },
+ apollo: {
+ groups: {
+ query: groupsQuery,
+ update(data) {
+ return formatGroups(data.organization.groups.nodes);
+ },
+ error(error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.groups.loading;
+ },
+ emptyStateProps() {
+ const baseProps = {
+ svgHeight: 144,
+ svgPath: this.groupsEmptyStateSvgPath,
+ title: this.$options.i18n.emptyState.title,
+ description: this.$options.i18n.emptyState.description,
+ };
+
+ if (this.shouldShowEmptyStateButtons && this.newGroupPath) {
+ return {
+ ...baseProps,
+ primaryButtonLink: this.newGroupPath,
+ primaryButtonText: this.$options.i18n.emptyState.primaryButtonText,
+ };
+ }
+
+ return baseProps;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
+ <groups-list v-else-if="groups.length" :groups="groups" show-group-icon />
+ <gl-empty-state v-else v-bind="emptyStateProps" />
+</template>
diff --git a/app/assets/javascripts/organizations/shared/components/projects_view.vue b/app/assets/javascripts/organizations/shared/components/projects_view.vue
new file mode 100644
index 00000000000..9bf4e597884
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/components/projects_view.vue
@@ -0,0 +1,86 @@
+<script>
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
+import { createAlert } from '~/alert';
+import projectsQuery from '../graphql/queries/projects.query.graphql';
+import { formatProjects } from '../utils';
+
+export default {
+ i18n: {
+ errorMessage: s__(
+ 'Organization|An error occurred loading the projects. Please refresh the page to try again.',
+ ),
+ emptyState: {
+ title: s__("Organization|You don't have any projects yet."),
+ description: s__(
+ 'GroupsEmptyState|Projects are where you can store your code, access issues, wiki, and other features of Gitlab.',
+ ),
+ primaryButtonText: __('New project'),
+ },
+ },
+ components: {
+ ProjectsList,
+ GlLoadingIcon,
+ GlEmptyState,
+ },
+ inject: {
+ projectsEmptyStateSvgPath: {},
+ newProjectPath: {
+ default: null,
+ },
+ },
+ props: {
+ shouldShowEmptyStateButtons: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ projects: [],
+ };
+ },
+ apollo: {
+ projects: {
+ query: projectsQuery,
+ update(data) {
+ return formatProjects(data.organization.projects.nodes);
+ },
+ error(error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.projects.loading;
+ },
+ emptyStateProps() {
+ const baseProps = {
+ svgHeight: 144,
+ svgPath: this.projectsEmptyStateSvgPath,
+ title: this.$options.i18n.emptyState.title,
+ description: this.$options.i18n.emptyState.description,
+ };
+
+ if (this.shouldShowEmptyStateButtons && this.newProjectPath) {
+ return {
+ ...baseProps,
+ primaryButtonLink: this.newProjectPath,
+ primaryButtonText: this.$options.i18n.emptyState.primaryButtonText,
+ };
+ }
+
+ return baseProps;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
+ <projects-list v-else-if="projects.length" :projects="projects" show-project-icon />
+ <gl-empty-state v-else v-bind="emptyStateProps" />
+</template>
diff --git a/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/groups.query.graphql b/app/assets/javascripts/organizations/shared/graphql/queries/groups.query.graphql
index 842c601e326..842c601e326 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/groups.query.graphql
+++ b/app/assets/javascripts/organizations/shared/graphql/queries/groups.query.graphql
diff --git a/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/projects.query.graphql b/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql
index 2a7971e1106..2a7971e1106 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/projects.query.graphql
+++ b/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql
diff --git a/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
index 8a375b28797..c78266b0476 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js
+++ b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
@@ -1,8 +1,4 @@
-import {
- organization,
- organizationProjects,
- organizationGroups,
-} from 'jest/organizations/groups_and_projects/mock_data';
+import { organization, organizationProjects, organizationGroups } from '../../mock_data';
export default {
Query: {
diff --git a/app/assets/javascripts/organizations/groups_and_projects/utils.js b/app/assets/javascripts/organizations/shared/utils.js
index d2a4e05e806..c1aafefc553 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/utils.js
+++ b/app/assets/javascripts/organizations/shared/utils.js
@@ -1,5 +1,5 @@
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/projects_list/constants';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
export const formatProjects = (projects) =>
projects.map(({ id, nameWithNamespace, accessLevel, webUrl, ...project }) => ({
@@ -13,11 +13,14 @@ export const formatProjects = (projects) =>
},
webUrl,
editPath: `${webUrl}/edit`,
- actions: [ACTION_EDIT, ACTION_DELETE],
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
}));
export const formatGroups = (groups) =>
- groups.map(({ id, ...group }) => ({
+ groups.map(({ id, webUrl, ...group }) => ({
...group,
id: getIdFromGraphQLId(id),
+ webUrl,
+ editPath: `${webUrl}/-/edit`,
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
}));
diff --git a/app/assets/javascripts/organizations/show/components/app.vue b/app/assets/javascripts/organizations/show/components/app.vue
new file mode 100644
index 00000000000..47264d80454
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/components/app.vue
@@ -0,0 +1,37 @@
+<script>
+import OrganizationAvatar from './organization_avatar.vue';
+import GroupsAndProjects from './groups_and_projects.vue';
+import AssociationCounts from './association_counts.vue';
+
+export default {
+ name: 'OrganizationShowApp',
+ components: { OrganizationAvatar, GroupsAndProjects, AssociationCounts },
+ props: {
+ organization: {
+ type: Object,
+ required: true,
+ },
+ groupsAndProjectsOrganizationPath: {
+ type: String,
+ required: true,
+ },
+ associationCounts: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-py-6">
+ <organization-avatar :organization="organization" />
+ <association-counts
+ :association-counts="associationCounts"
+ :groups-and-projects-organization-path="groupsAndProjectsOrganizationPath"
+ />
+ <groups-and-projects
+ :groups-and-projects-organization-path="groupsAndProjectsOrganizationPath"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/show/components/association_count_card.vue b/app/assets/javascripts/organizations/show/components/association_count_card.vue
new file mode 100644
index 00000000000..0567f43132f
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/components/association_count_card.vue
@@ -0,0 +1,54 @@
+<script>
+import { GlIcon, GlLink, GlCard } from '@gitlab/ui';
+import { numberToMetricPrefix } from '~/lib/utils/number_utils';
+import { __ } from '~/locale';
+
+export default {
+ name: 'AssociationCountCard',
+ components: { GlIcon, GlLink, GlCard },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ iconName: {
+ type: String,
+ required: true,
+ },
+ count: {
+ type: Number,
+ required: true,
+ },
+ linkHref: {
+ type: String,
+ required: true,
+ },
+ linkText: {
+ type: String,
+ required: false,
+ default: __('View all'),
+ },
+ },
+ computed: {
+ formattedCount() {
+ return numberToMetricPrefix(this.count, 0);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-card>
+ <div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
+ <div class="gl-display-flex gl-align-items-center gl-text-gray-700">
+ <gl-icon :name="iconName" />
+ <span class="gl-ml-2">{{ title }}</span>
+ </div>
+ <gl-link :href="linkHref">{{ linkText }}</gl-link>
+ </div>
+ <span
+ class="gl-font-size-h-display gl-font-weight-bold gl-line-height-ratio-1000 gl-mt-2 gl-display-block"
+ >{{ formattedCount }}</span
+ >
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/organizations/show/components/association_counts.vue b/app/assets/javascripts/organizations/show/components/association_counts.vue
new file mode 100644
index 00000000000..3b312924bd2
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/components/association_counts.vue
@@ -0,0 +1,71 @@
+<script>
+import { __, s__ } from '~/locale';
+import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '../../constants';
+import AssociationCountCard from './association_count_card.vue';
+
+export default {
+ name: 'AssociationCounts',
+ i18n: {
+ groups: __('Groups'),
+ projects: __('Projects'),
+ users: __('Users'),
+ viewAll: __('View all'),
+ manage: s__('Organization|Manage'),
+ },
+ components: { AssociationCountCard },
+ props: {
+ associationCounts: {
+ type: Object,
+ required: true,
+ },
+ groupsAndProjectsOrganizationPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ groupsLinkHref() {
+ return `${this.groupsAndProjectsOrganizationPath}?display=${RESOURCE_TYPE_GROUPS}`;
+ },
+ projectsLinkHref() {
+ return `${this.groupsAndProjectsOrganizationPath}?display=${RESOURCE_TYPE_PROJECTS}`;
+ },
+ associationCountCards() {
+ return [
+ {
+ title: this.$options.i18n.groups,
+ iconName: 'group',
+ count: this.associationCounts.groups,
+ linkHref: this.groupsLinkHref,
+ },
+ {
+ title: this.$options.i18n.projects,
+ iconName: 'project',
+ count: this.associationCounts.projects,
+ linkHref: this.projectsLinkHref,
+ },
+ {
+ title: this.$options.i18n.users,
+ iconName: 'users',
+ count: this.associationCounts.users,
+ linkText: this.$options.i18n.manage,
+ // TODO: update `linkHref` prop to point to users route
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/409313
+ linkHref: '/',
+ },
+ ];
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-grid gl-lg-grid-template-columns-4 gl-mt-5 gl-gap-5">
+ <association-count-card
+ v-for="props in associationCountCards"
+ :key="props.title"
+ v-bind="props"
+ class="gl-w-full"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/show/components/groups_and_projects.vue b/app/assets/javascripts/organizations/show/components/groups_and_projects.vue
new file mode 100644
index 00000000000..e8972f3b380
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/components/groups_and_projects.vue
@@ -0,0 +1,110 @@
+<script>
+import { GlCollapsibleListbox, GlLink } from '@gitlab/ui';
+import { isEqual } from 'lodash';
+import { s__, __ } from '~/locale';
+import GroupsView from '../../shared/components/groups_view.vue';
+import ProjectsView from '../../shared/components/projects_view.vue';
+import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '../../constants';
+import { FILTER_FREQUENTLY_VISITED } from '../constants';
+import { buildDisplayListboxItem } from '../utils';
+
+export default {
+ name: 'OrganizationFrontPageGroupsAndProjects',
+ i18n: {
+ displayListboxLabel: __('Display'),
+ viewAll: s__('Organization|View all'),
+ },
+ displayListboxLabelId: 'display-listbox-label',
+ components: { GlCollapsibleListbox, GlLink },
+ displayListboxItems: [
+ buildDisplayListboxItem({
+ filter: FILTER_FREQUENTLY_VISITED,
+ resourceType: RESOURCE_TYPE_PROJECTS,
+ text: s__('Organization|Frequently visited projects'),
+ }),
+ buildDisplayListboxItem({
+ filter: FILTER_FREQUENTLY_VISITED,
+ resourceType: RESOURCE_TYPE_GROUPS,
+ text: s__('Organization|Frequently visited groups'),
+ }),
+ ],
+ props: {
+ groupsAndProjectsOrganizationPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ displayListboxSelected() {
+ const { display } = this.$route.query;
+ const [{ value: fallbackSelected }] = this.$options.displayListboxItems;
+
+ return (
+ this.$options.displayListboxItems.find(({ value }) => value === display)?.value ||
+ fallbackSelected
+ );
+ },
+ resourceTypeSelected() {
+ return [RESOURCE_TYPE_PROJECTS, RESOURCE_TYPE_GROUPS].find((resourceType) =>
+ this.displayListboxSelected.endsWith(resourceType),
+ );
+ },
+ routerView() {
+ switch (this.resourceTypeSelected) {
+ case RESOURCE_TYPE_GROUPS:
+ return GroupsView;
+
+ case RESOURCE_TYPE_PROJECTS:
+ return ProjectsView;
+
+ default:
+ return ProjectsView;
+ }
+ },
+ groupsAndProjectsOrganizationPathWithQueryParam() {
+ return `${this.groupsAndProjectsOrganizationPath}?display=${this.resourceTypeSelected}`;
+ },
+ },
+ methods: {
+ pushQuery(query) {
+ const currentQuery = this.$route.query;
+
+ if (isEqual(currentQuery, query)) {
+ return;
+ }
+
+ this.$router.push({ query });
+ },
+ onDisplayListboxSelect(display) {
+ this.pushQuery({ display });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-mt-7">
+ <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
+ <div>
+ <label
+ :id="$options.displayListboxLabelId"
+ class="gl-display-block gl-mb-2"
+ data-testid="label"
+ >{{ $options.i18n.displayListboxLabel }}</label
+ >
+ <gl-collapsible-listbox
+ block
+ toggle-class="gl-w-30"
+ :selected="displayListboxSelected"
+ :items="$options.displayListboxItems"
+ :toggle-aria-labelled-by="$options.displayListboxLabelId"
+ @select="onDisplayListboxSelect"
+ />
+ </div>
+ <gl-link class="gl-mt-5" :href="groupsAndProjectsOrganizationPathWithQueryParam">{{
+ $options.i18n.viewAll
+ }}</gl-link>
+ </div>
+ <component :is="routerView" should-show-empty-state-buttons class="gl-mt-5" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/show/components/organization_avatar.vue b/app/assets/javascripts/organizations/show/components/organization_avatar.vue
new file mode 100644
index 00000000000..c57ee0ea5b5
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/components/organization_avatar.vue
@@ -0,0 +1,71 @@
+<script>
+import { GlAvatar, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import {
+ VISIBILITY_TYPE_ICON,
+ ORGANIZATION_VISIBILITY_TYPE,
+ VISIBILITY_LEVEL_PUBLIC_STRING,
+} from '~/visibility_level/constants';
+
+export default {
+ name: 'OrganizationAvatar',
+ AVATAR_SHAPE_OPTION_RECT,
+ i18n: {
+ copyButtonText: s__('Organization|Copy organization ID'),
+ orgId: s__('Organization|Org ID'),
+ },
+ components: { GlAvatar, GlIcon, ClipboardButton },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ organization: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ visibilityIcon() {
+ return VISIBILITY_TYPE_ICON[VISIBILITY_LEVEL_PUBLIC_STRING];
+ },
+ visibilityTooltip() {
+ return ORGANIZATION_VISIBILITY_TYPE[VISIBILITY_LEVEL_PUBLIC_STRING];
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-center">
+ <gl-avatar
+ :entity-id="organization.id"
+ :entity-name="organization.name"
+ :shape="$options.AVATAR_SHAPE_OPTION_RECT"
+ :size="64"
+ />
+ <div class="gl-ml-3">
+ <div class="gl-display-flex gl-align-items-center">
+ <h1 class="gl-m-0 gl-font-size-h1">{{ organization.name }}</h1>
+ <gl-icon
+ v-gl-tooltip="visibilityTooltip"
+ :name="visibilityIcon"
+ class="gl-text-secondary gl-ml-3"
+ />
+ </div>
+ <div class="gl-display-flex gl-align-items-center">
+ <span class="gl-text-secondary gl-font-sm"
+ >{{ $options.i18n.orgId }}: {{ organization.id }}</span
+ >
+ <clipboard-button
+ class="gl-ml-2"
+ category="tertiary"
+ size="small"
+ :title="$options.i18n.copyButtonText"
+ :text="organization.id.toString()"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/show/constants.js b/app/assets/javascripts/organizations/show/constants.js
new file mode 100644
index 00000000000..fe29af67f6b
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/constants.js
@@ -0,0 +1 @@
+export const FILTER_FREQUENTLY_VISITED = 'frequently_visited';
diff --git a/app/assets/javascripts/organizations/show/index.js b/app/assets/javascripts/organizations/show/index.js
new file mode 100644
index 00000000000..83a9c37e325
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/index.js
@@ -0,0 +1,63 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import VueApollo from 'vue-apollo';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import createDefaultClient from '~/lib/graphql';
+import { ORGANIZATION_ROOT_ROUTE_NAME } from '../constants';
+import resolvers from '../shared/graphql/resolvers';
+import App from './components/app.vue';
+
+export const createRouter = () => {
+ const routes = [{ path: '/', name: ORGANIZATION_ROOT_ROUTE_NAME }];
+
+ const router = new VueRouter({
+ routes,
+ base: '/',
+ mode: 'history',
+ });
+
+ return router;
+};
+
+export const initOrganizationsShow = () => {
+ const el = document.getElementById('js-organizations-show');
+
+ if (!el) return false;
+
+ const {
+ dataset: { appData },
+ } = el;
+ const {
+ organization,
+ groupsAndProjectsOrganizationPath,
+ projectsEmptyStateSvgPath,
+ groupsEmptyStateSvgPath,
+ newGroupPath,
+ newProjectPath,
+ associationCounts,
+ } = convertObjectPropsToCamelCase(JSON.parse(appData));
+
+ Vue.use(VueRouter);
+ const router = createRouter();
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
+
+ return new Vue({
+ el,
+ name: 'OrganizationShowRoot',
+ apolloProvider,
+ router,
+ provide: {
+ projectsEmptyStateSvgPath,
+ groupsEmptyStateSvgPath,
+ newGroupPath,
+ newProjectPath,
+ },
+ render(createElement) {
+ return createElement(App, {
+ props: { organization, groupsAndProjectsOrganizationPath, associationCounts },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/organizations/show/utils.js b/app/assets/javascripts/organizations/show/utils.js
new file mode 100644
index 00000000000..b4f935563aa
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/utils.js
@@ -0,0 +1,4 @@
+export const buildDisplayListboxItem = ({ filter, resourceType, text }) => ({
+ text,
+ value: `${filter}_${resourceType}`,
+});
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js
index afddf78203d..7040f42398e 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js
@@ -4,7 +4,7 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import PerformancePlugin from '~/performance/vue_performance_plugin';
import Translate from '~/vue_shared/translate';
import RegistryBreadcrumb from '~/packages_and_registries/shared/components/registry_breadcrumb.vue';
-import { renderBreadcrumb } from '~/packages_and_registries/shared/utils';
+import { injectVueAppBreadcrumbs } from '~/lib/utils/breadcrumbs';
import { apolloProvider } from './graphql/index';
import RegistryExplorer from './pages/index.vue';
import createRouter from './router';
@@ -88,7 +88,7 @@ export default () => {
});
return {
- attachBreadcrumb: renderBreadcrumb(router, apolloProvider, RegistryBreadcrumb),
+ attachBreadcrumb: () => injectVueAppBreadcrumbs(router, RegistryBreadcrumb, apolloProvider),
attachMainComponent,
};
};
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
index e18e6f7ed1a..6bb4a8797df 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
@@ -2,8 +2,8 @@
import {
GlAlert,
GlButton,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlFormGroup,
GlFormInputGroup,
GlSkeletonLoader,
@@ -18,6 +18,8 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
+import { getPageParams } from '~/packages_and_registries/dependency_proxy/utils';
+import { extractPageInfo } from '~/packages_and_registries/shared/utils';
import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql';
@@ -25,8 +27,8 @@ export default {
components: {
GlAlert,
GlButton,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlSkeletonLoader,
GlFormGroup,
GlFormInputGroup,
@@ -79,11 +81,15 @@ export default {
},
computed: {
queryVariables() {
- return { fullPath: this.groupPath, first: GRAPHQL_PAGE_SIZE };
+ return { fullPath: this.groupPath, first: GRAPHQL_PAGE_SIZE, ...this.pageParams };
},
pageInfo() {
return this.group.dependencyProxyManifests?.pageInfo;
},
+ pageParams() {
+ const pageInfo = extractPageInfo(this.$route.query);
+ return getPageParams(pageInfo);
+ },
manifests() {
return this.group.dependencyProxyManifests?.nodes ?? [];
},
@@ -123,25 +129,10 @@ export default {
},
methods: {
fetchNextPage() {
- this.fetchMore({
- first: GRAPHQL_PAGE_SIZE,
- after: this.pageInfo?.endCursor,
- });
+ this.$router.push({ query: { after: this.pageInfo?.endCursor } });
},
fetchPreviousPage() {
- this.fetchMore({
- first: null,
- last: GRAPHQL_PAGE_SIZE,
- before: this.pageInfo?.startCursor,
- });
- },
- fetchMore(variables) {
- this.$apollo.queries.group.fetchMore({
- variables: { ...this.queryVariables, ...variables },
- updateQuery(_, { fetchMoreResult }) {
- return fetchMoreResult;
- },
- });
+ this.$router.push({ query: { before: this.pageInfo?.startCursor } });
},
async submit() {
try {
@@ -165,20 +156,23 @@ export default {
</gl-alert>
<title-area :title="$options.i18n.pageTitle">
<template #right-actions>
- <gl-dropdown
+ <gl-disclosure-dropdown
v-if="showDeleteDropdown"
icon="ellipsis_v"
- text="More actions"
+ :toggle-text="__('More actions')"
:text-sr-only="true"
category="tertiary"
+ placement="right"
no-caret
>
- <gl-dropdown-item
- v-gl-modal-directive="$options.confirmClearCacheModal"
- variant="danger"
- >{{ $options.i18n.clearCache }}</gl-dropdown-item
- >
- </gl-dropdown>
+ <gl-disclosure-dropdown-item v-gl-modal-directive="$options.confirmClearCacheModal">
+ <template #list-item>
+ <span class="gl-text-red-500">
+ {{ $options.i18n.clearCache }}
+ </span>
+ </template>
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown>
<gl-button
v-if="canClearCache"
v-gl-tooltip="$options.i18n.settingsText"
@@ -198,14 +192,14 @@ export default {
<gl-form-input-group
id="proxy-url"
readonly
- :value="group.dependencyProxyImagePrefix"
+ :value="dependencyProxyImagePrefix"
select-on-click
class="gl-layout-w-limited"
data-testid="proxy-url"
>
<template #append>
<clipboard-button
- :text="group.dependencyProxyImagePrefix"
+ :text="dependencyProxyImagePrefix"
:title="$options.i18n.copyImagePrefixText"
/>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js
index 74444d2c7ec..c115898c75b 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
-import app from '~/packages_and_registries/dependency_proxy/app.vue';
import { apolloProvider } from '~/packages_and_registries/dependency_proxy/graphql';
import Translate from '~/vue_shared/translate';
+import createRouter from './router';
Vue.use(Translate);
@@ -11,10 +11,18 @@ export const initDependencyProxyApp = () => {
if (!el) {
return null;
}
- const { groupPath, groupId, noManifestsIllustration, canClearCache, settingsPath } = el.dataset;
+ const {
+ endpoint,
+ groupPath,
+ groupId,
+ noManifestsIllustration,
+ canClearCache,
+ settingsPath,
+ } = el.dataset;
return new Vue({
el,
apolloProvider,
+ router: createRouter(endpoint),
provide: {
groupPath,
groupId,
@@ -23,7 +31,7 @@ export const initDependencyProxyApp = () => {
settingsPath,
},
render(createElement) {
- return createElement(app);
+ return createElement('router-view');
},
});
};
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/router.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/router.js
new file mode 100644
index 00000000000..087d8c189c4
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/router.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import App from '~/packages_and_registries/dependency_proxy/app.vue';
+
+Vue.use(VueRouter);
+
+export default function createRouter(base) {
+ const routes = [{ path: '/', name: 'dependencyProxyApp', component: App }];
+ return new VueRouter({
+ mode: 'history',
+ base,
+ routes,
+ });
+}
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/utils.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/utils.js
new file mode 100644
index 00000000000..e6b97fac896
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/utils.js
@@ -0,0 +1,24 @@
+import { GRAPHQL_PAGE_SIZE } from './constants';
+
+const getNextPageParams = (cursor) => ({
+ after: cursor,
+ first: GRAPHQL_PAGE_SIZE,
+});
+
+const getPreviousPageParams = (cursor) => ({
+ first: null,
+ before: cursor,
+ last: GRAPHQL_PAGE_SIZE,
+});
+
+export const getPageParams = (pageInfo = {}) => {
+ if (pageInfo.before) {
+ return getPreviousPageParams(pageInfo.before);
+ }
+
+ if (pageInfo.after) {
+ return getNextPageParams(pageInfo.after);
+ }
+
+ return {};
+};
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/index.js b/app/assets/javascripts/packages_and_registries/harbor_registry/index.js
index 6185e4c7bc6..41a5a0e3797 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/index.js
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/index.js
@@ -4,7 +4,7 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import PerformancePlugin from '~/performance/vue_performance_plugin';
import Translate from '~/vue_shared/translate';
import RegistryBreadcrumb from '~/packages_and_registries/harbor_registry/components/harbor_registry_breadcrumb.vue';
-import { renderBreadcrumb } from '~/packages_and_registries/shared/utils';
+import { injectVueAppBreadcrumbs } from '~/lib/utils/breadcrumbs';
import createRouter from './router';
import HarborRegistryExplorer from './pages/index.vue';
@@ -79,7 +79,7 @@ export default (id) => {
};
return {
- attachBreadcrumb: renderBreadcrumb(router, null, RegistryBreadcrumb),
+ attachBreadcrumb: () => injectVueAppBreadcrumbs(router, RegistryBreadcrumb),
attachMainComponent,
};
};
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
index 0cf49b25bf2..1020cd0c533 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
@@ -6,9 +6,7 @@ import {
TOKEN_TITLE_TYPE,
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
-import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
-import UrlSync from '~/vue_shared/components/url_sync.vue';
-import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
+import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
import { LIST_KEY_CREATED_AT } from '~/packages_and_registries/package_registry/constants';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import PackageTypeToken from './tokens/package_type_token.vue';
@@ -24,7 +22,10 @@ export default {
operators: OPERATORS_IS,
},
],
- components: { RegistrySearch, UrlSync, LocalStorageSync },
+ components: {
+ LocalStorageSync,
+ PersistedSearch,
+ },
inject: ['isGroupPage'],
data() {
return {
@@ -40,17 +41,25 @@ export default {
sortableFields() {
return sortableFields(this.isGroupPage);
},
- parsedSorting() {
- const cleanOrderBy = this.sorting?.orderBy.replace('_at', '');
- return `${cleanOrderBy}_${this.sorting?.sort}`.toUpperCase();
- },
- parsedFilters() {
+ },
+ mounted() {
+ // local-storage-sync does not emit `input`
+ // event when key is not found, so set the
+ // flag if it hasn't been updated
+ this.$nextTick(() => {
+ if (!this.mountRegistrySearch) {
+ this.mountRegistrySearch = true;
+ }
+ });
+ },
+ methods: {
+ formatFilters(filters) {
const parsed = {
packageName: '',
packageType: undefined,
};
- return this.filters.reduce((acc, filter) => {
+ return filters.reduce((acc, filter) => {
if (filter.type === TOKEN_TYPE_TYPE && filter.value?.data) {
return {
...acc,
@@ -68,28 +77,17 @@ export default {
return acc;
}, parsed);
},
- },
- mounted() {
- const queryParams = getQueryParams(window.document.location.search);
- const { sorting, filters } = extractFilterAndSorting(queryParams);
- this.updateSorting(sorting);
- this.updateFilters(filters);
- this.mountRegistrySearch = true;
- this.emitUpdate();
- },
- methods: {
- updateFilters(newValue) {
- this.filters = newValue;
- },
updateSorting(newValue) {
this.sorting = { ...this.sorting, ...newValue };
},
- updateSortingAndEmitUpdate(newValue) {
+ updateSortingFromLocalStorage(newValue) {
this.updateSorting(newValue);
- this.emitUpdate();
+ this.mountRegistrySearch = true;
},
- emitUpdate() {
- this.$emit('update', { sort: this.parsedSorting, filters: this.parsedFilters });
+ emitUpdate(values) {
+ const { filters, sorting } = values;
+ this.updateSorting(sorting);
+ this.$emit('update', { ...values, filters: this.formatFilters(filters) });
},
},
};
@@ -99,22 +97,15 @@ export default {
<local-storage-sync
storage-key="package_registry_list_sorting"
:value="sorting"
- @input="updateSorting"
+ @input="updateSortingFromLocalStorage"
>
- <url-sync>
- <template #default="{ updateQuery }">
- <registry-search
- v-if="mountRegistrySearch"
- :filters="filters"
- :sorting="sorting"
- :tokens="$options.tokens"
- :sortable-fields="sortableFields"
- @sorting:changed="updateSortingAndEmitUpdate"
- @filter:changed="updateFilters"
- @filter:submit="emitUpdate"
- @query:changed="updateQuery"
- />
- </template>
- </url-sync>
+ <persisted-search
+ v-if="mountRegistrySearch"
+ :sortable-fields="sortableFields"
+ :default-order="sorting.orderBy"
+ :default-sort="sorting.sort"
+ :tokens="$options.tokens"
+ @update="emitUpdate"
+ />
</local-storage-sync>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
index a7831ef2588..b892305055c 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
@@ -48,10 +48,6 @@ export default {
required: false,
default: false,
},
- pageInfo: {
- type: Object,
- required: true,
- },
groupSettings: {
type: Object,
required: false,
@@ -179,11 +175,8 @@ export default {
:hidden-delete="!canDeletePackages"
:is-loading="isLoading"
:items="list"
- :pagination="pageInfo"
:title="listTitle"
@delete="setItemsToBeDeleted"
- @prev-page="$emit('prev-page')"
- @next-page="$emit('next-page')"
>
<template #default="{ selectItem, isSelected, item, first }">
<packages-list-row
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/index.js b/app/assets/javascripts/packages_and_registries/package_registry/index.js
index ae0f6d18d99..1a9b192e2c8 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/index.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/index.js
@@ -4,7 +4,7 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import { apolloProvider } from '~/packages_and_registries/package_registry/graphql/index';
import PackageRegistry from '~/packages_and_registries/package_registry/pages/index.vue';
import RegistryBreadcrumb from '~/packages_and_registries/shared/components/registry_breadcrumb.vue';
-import { renderBreadcrumb } from '~/packages_and_registries/shared/utils';
+import { injectVueAppBreadcrumbs } from '~/lib/utils/breadcrumbs';
import createRouter from './router';
Vue.use(Translate);
@@ -60,7 +60,7 @@ export default () => {
});
return {
- attachBreadcrumb: renderBreadcrumb(router, apolloProvider, RegistryBreadcrumb),
+ attachBreadcrumb: () => injectVueAppBreadcrumbs(router, RegistryBreadcrumb, apolloProvider),
attachMainComponent,
};
};
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
index 6de89748708..a187c7a70d2 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
@@ -18,6 +18,12 @@ import DeletePackages from '~/packages_and_registries/package_registry/component
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import PackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
+import PersistedPagination from '~/packages_and_registries/shared/components/persisted_pagination.vue';
+import {
+ getPageParams,
+ getNextPageParams,
+ getPreviousPageParams,
+} from '~/packages_and_registries/package_registry/utils';
export default {
components: {
@@ -28,6 +34,7 @@ export default {
PackageList,
PackageTitle,
PackageSearch,
+ PersistedPagination,
DeletePackages,
},
directives: {
@@ -39,7 +46,7 @@ export default {
packagesResource: {},
sort: '',
filters: {},
- mutationLoading: false,
+ isDeleteInProgress: false,
pageParams: {},
};
},
@@ -100,7 +107,7 @@ export default {
: this.$options.i18n.noResultsTitle;
},
isLoading() {
- return this.$apollo.queries.packagesResource.loading || this.mutationLoading;
+ return this.$apollo.queries.packagesResource.loading || this.isDeleteInProgress;
},
refetchQueriesData() {
return [
@@ -124,23 +131,16 @@ export default {
historyReplaceState(cleanUrl);
}
},
- handleSearchUpdate({ sort, filters }) {
- this.pageParams = {};
+ handleSearchUpdate({ sort, filters, pageInfo }) {
+ this.pageParams = getPageParams(pageInfo);
this.sort = sort;
this.filters = { ...filters };
},
fetchNextPage() {
- this.pageParams = {
- first: GRAPHQL_PAGE_SIZE,
- after: this.pageInfo?.endCursor,
- };
+ this.pageParams = getNextPageParams(this.pageInfo.endCursor);
},
fetchPreviousPage() {
- this.pageParams = {
- first: null,
- last: GRAPHQL_PAGE_SIZE,
- before: this.pageInfo?.startCursor,
- };
+ this.pageParams = getPreviousPageParams(this.pageInfo.startCursor);
},
},
i18n: {
@@ -176,17 +176,14 @@ export default {
<delete-packages
:refetch-queries="refetchQueriesData"
show-success-alert
- @start="mutationLoading = true"
- @end="mutationLoading = false"
+ @start="isDeleteInProgress = true"
+ @end="isDeleteInProgress = false"
>
<template #default="{ deletePackages }">
<package-list
:group-settings="groupSettings"
:list="packages.nodes"
:is-loading="isLoading"
- :page-info="pageInfo"
- @prev-page="fetchPreviousPage"
- @next-page="fetchNextPage"
@delete="deletePackages"
>
<template #empty-state>
@@ -210,5 +207,13 @@ export default {
</package-list>
</template>
</delete-packages>
+ <div v-if="!isDeleteInProgress" class="gl-display-flex gl-justify-content-center">
+ <persisted-pagination
+ class="gl-mt-3"
+ :pagination="pageInfo"
+ @prev="fetchPreviousPage"
+ @next="fetchNextPage"
+ />
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/utils.js b/app/assets/javascripts/packages_and_registries/package_registry/utils.js
index 4ff8edb8f66..35ff3d5ea63 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/utils.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/utils.js
@@ -1,6 +1,7 @@
import { capitalize } from 'lodash';
import { s__ } from '~/locale';
import {
+ GRAPHQL_PAGE_SIZE,
PACKAGE_TYPE_CONAN,
PACKAGE_TYPE_MAVEN,
PACKAGE_TYPE_NPM,
@@ -46,3 +47,26 @@ export const packageTypeToTrackCategory = (type) => `UI::${capitalize(type)}Pack
export const sortableFields = (isGroupPage) =>
SORT_FIELDS.filter((f) => f.orderBy !== LIST_KEY_PROJECT || isGroupPage);
+
+export const getNextPageParams = (cursor) => ({
+ after: cursor,
+ first: GRAPHQL_PAGE_SIZE,
+});
+
+export const getPreviousPageParams = (cursor) => ({
+ first: null,
+ before: cursor,
+ last: GRAPHQL_PAGE_SIZE,
+});
+
+export const getPageParams = (pageInfo = {}) => {
+ if (pageInfo.before) {
+ return getPreviousPageParams(pageInfo.before);
+ }
+
+ if (pageInfo.after) {
+ return getNextPageParams(pageInfo.after);
+ }
+
+ return {};
+};
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/cli_commands.vue b/app/assets/javascripts/packages_and_registries/shared/components/cli_commands.vue
index dc61f3c788c..e00681f4183 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/cli_commands.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/cli_commands.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import Tracking from '~/tracking';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
import {
@@ -16,8 +16,8 @@ const trackingLabel = 'quickstart_dropdown';
export default {
components: {
- GlDropdown,
CodeInstruction,
+ GlDisclosureDropdown,
},
mixins: [Tracking.mixin({ label: trackingLabel })],
props: {
@@ -47,14 +47,13 @@ export default {
};
</script>
<template>
- <gl-dropdown
- :text="$options.i18n.QUICK_START"
+ <gl-disclosure-dropdown
+ :toggle-text="$options.i18n.QUICK_START"
variant="confirm"
- right
+ placement="right"
@shown="track('click_dropdown')"
>
- <!-- This li is used as a container since gl-dropdown produces a root ul, this mimics the functionality exposed by b-dropdown-form -->
- <li role="presentation" class="px-2 py-1">
+ <div class="gl-px-3 gl-py-2">
<code-instruction
:label="$options.i18n.LOGIN_COMMAND_LABEL"
:instruction="dockerLoginCommand"
@@ -79,6 +78,6 @@ export default {
tracking-action="click_copy_push"
:tracking-label="$options.trackingLabel"
/>
- </li>
- </gl-dropdown>
+ </div>
+ </gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue b/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue
index 95343a3a09b..529086e7f8c 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue
@@ -87,6 +87,7 @@ export default {
sort: this.parsedSorting,
filters: this.filters,
pageInfo: this.pageInfo,
+ sorting: this.sorting,
});
},
},
diff --git a/app/assets/javascripts/packages_and_registries/shared/utils.js b/app/assets/javascripts/packages_and_registries/shared/utils.js
index bda0839092e..a19c8ed5866 100644
--- a/app/assets/javascripts/packages_and_registries/shared/utils.js
+++ b/app/assets/javascripts/packages_and_registries/shared/utils.js
@@ -1,4 +1,3 @@
-import Vue from 'vue';
import { queryToObject } from '~/lib/utils/url_utility';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -47,28 +46,3 @@ export const getCommitLink = ({ project_path: projectPath, pipeline = {} }, isGr
return `../commit/${pipeline.sha}`;
};
-
-export const renderBreadcrumb = (router, apolloProvider, RegistryBreadcrumb) => () => {
- const breadCrumbEls = document.querySelectorAll('nav .js-breadcrumbs-list li');
- const breadCrumbEl = breadCrumbEls[breadCrumbEls.length - 1];
- const lastCrumb = breadCrumbEl.children[0];
- const crumbs = [lastCrumb];
- const nestedBreadcrumbEl = document.createElement('div');
- breadCrumbEl.replaceChild(nestedBreadcrumbEl, lastCrumb);
- return new Vue({
- el: nestedBreadcrumbEl,
- router,
- apolloProvider,
- components: {
- RegistryBreadcrumb,
- },
- render(createElement) {
- return createElement('registry-breadcrumb', {
- class: breadCrumbEl.className,
- props: {
- crumbs,
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js
deleted file mode 100644
index 29e92a8abad..00000000000
--- a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import $ from 'jquery';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import { truncate } from '~/lib/utils/text_utility';
-
-const MAX_MESSAGE_LENGTH = 500;
-const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
-
-export default class AbuseReports {
- constructor() {
- $(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage);
- $(document)
- .off('click', MESSAGE_CELL_SELECTOR)
- .on('click', MESSAGE_CELL_SELECTOR, this.toggleMessageTruncation);
- }
-
- truncateLongMessage() {
- const $messageCellElement = $(this);
- const reportMessage = $messageCellElement.text();
- if (reportMessage.length > MAX_MESSAGE_LENGTH) {
- $messageCellElement.data('originalMessage', reportMessage);
- $messageCellElement.data('messageTruncated', 'true');
- $messageCellElement.text(truncate(reportMessage, MAX_MESSAGE_LENGTH));
- }
- }
-
- toggleMessageTruncation() {
- const $messageCellElement = $(this);
- const originalMessage = $messageCellElement.data('originalMessage');
- if (!originalMessage) return;
- if (parseBoolean($messageCellElement.data('messageTruncated'))) {
- $messageCellElement.data('messageTruncated', 'false');
- $messageCellElement.text(originalMessage);
- } else {
- $messageCellElement.data('messageTruncated', 'true');
- $messageCellElement.text(truncate(originalMessage, MAX_MESSAGE_LENGTH));
- }
- }
-}
diff --git a/app/assets/javascripts/pages/admin/abuse_reports/index.js b/app/assets/javascripts/pages/admin/abuse_reports/index.js
index 7634f131e4d..78fef1b5531 100644
--- a/app/assets/javascripts/pages/admin/abuse_reports/index.js
+++ b/app/assets/javascripts/pages/admin/abuse_reports/index.js
@@ -1,10 +1,3 @@
import { initAbuseReportsApp } from '~/admin/abuse_reports';
-import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
-import UsersSelect from '~/users_select';
-import AbuseReports from './abuse_reports';
-new AbuseReports(); /* eslint-disable-line no-new */
-new UsersSelect(); /* eslint-disable-line no-new */
-
-initDeprecatedRemoveRowBehavior();
initAbuseReportsApp();
diff --git a/app/assets/javascripts/pages/admin/application_settings/general/index.js b/app/assets/javascripts/pages/admin/application_settings/general/index.js
index 8a810ca649c..d0593c82ac1 100644
--- a/app/assets/javascripts/pages/admin/application_settings/general/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/general/index.js
@@ -1,3 +1,4 @@
+import { initSilentModeSettings } from '~/silent_mode_settings';
import initAccountAndLimitsSection from '../account_and_limits';
import initGitpod from '../gitpod';
import initSignupRestrictions from '../signup_restrictions';
@@ -6,4 +7,5 @@ import initSignupRestrictions from '../signup_restrictions';
initAccountAndLimitsSection();
initGitpod();
initSignupRestrictions();
+ initSilentModeSettings();
})();
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs_count.query.graphql b/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs_count.query.graphql
deleted file mode 100644
index 8c59230b2b8..00000000000
--- a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs_count.query.graphql
+++ /dev/null
@@ -1,5 +0,0 @@
-query getAllJobsCount($statuses: [CiJobStatus!]) {
- jobs(statuses: $statuses) {
- count
- }
-}
diff --git a/app/assets/javascripts/pages/admin/jobs/index/index.js b/app/assets/javascripts/pages/admin/jobs/index/index.js
index 9c2a255a1a3..52e4d5dd19f 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/index.js
+++ b/app/assets/javascripts/pages/admin/jobs/index/index.js
@@ -1,12 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import Translate from '~/vue_shared/translate';
import createDefaultClient from '~/lib/graphql';
-import { CANCEL_JOBS_MODAL_ID } from '../components/constants';
-import CancelJobsModal from '../components/cancel_jobs_modal.vue';
-import AdminJobsTableApp from '../components/table/admin_jobs_table_app.vue';
-import cacheConfig from '../components/table/graphql/cache_config';
+import AdminJobsTableApp from '~/ci/admin/jobs_table/admin_jobs_table_app.vue';
+import cacheConfig from '~/ci/admin/jobs_table/graphql/cache_config';
Vue.use(Translate);
Vue.use(VueApollo);
@@ -17,35 +14,7 @@ const apolloProvider = new VueApollo({
defaultClient: client,
});
-function initJobs() {
- const buttonId = 'js-stop-jobs-button';
- const cancelJobsButton = document.getElementById(buttonId);
- if (cancelJobsButton) {
- // eslint-disable-next-line no-new
- new Vue({
- el: `#js-${CANCEL_JOBS_MODAL_ID}`,
- components: {
- CancelJobsModal,
- },
- mounted() {
- cancelJobsButton.classList.remove('disabled');
- cancelJobsButton.addEventListener('click', () => {
- this.$root.$emit(BV_SHOW_MODAL, CANCEL_JOBS_MODAL_ID, `#${buttonId}`);
- });
- },
- render(createElement) {
- return createElement(CANCEL_JOBS_MODAL_ID, {
- props: {
- url: cancelJobsButton.dataset.url,
- modalId: CANCEL_JOBS_MODAL_ID,
- },
- });
- },
- });
- }
-}
-
-export function initAdminJobsApp() {
+const initAdminJobsApp = () => {
const containerEl = document.getElementById('admin-jobs-app');
if (!containerEl) return false;
@@ -64,10 +33,6 @@ export function initAdminJobsApp() {
return createElement(AdminJobsTableApp);
},
});
-}
+};
-if (gon.features.adminJobsVue) {
- initAdminJobsApp();
-} else {
- initJobs();
-}
+initAdminJobsApp();
diff --git a/app/assets/javascripts/pages/dashboard/groups/index/index.js b/app/assets/javascripts/pages/dashboard/groups/index/index.js
index c14848c4798..fbfc6c07904 100644
--- a/app/assets/javascripts/pages/dashboard/groups/index/index.js
+++ b/app/assets/javascripts/pages/dashboard/groups/index/index.js
@@ -1,3 +1,4 @@
+import EmptyState from '~/groups/components/empty_states/groups_dashboard_empty_state.vue';
import initGroupsList from '~/groups';
-initGroupsList();
+initGroupsList(EmptyState);
diff --git a/app/assets/javascripts/pages/explore/groups/index.js b/app/assets/javascripts/pages/explore/groups/index.js
index 9b60b1f51a8..c6a9cf50d9a 100644
--- a/app/assets/javascripts/pages/explore/groups/index.js
+++ b/app/assets/javascripts/pages/explore/groups/index.js
@@ -1,8 +1,9 @@
+import EmptyState from '~/groups/components/empty_states/groups_explore_empty_state.vue';
import initGroupsList from '~/groups';
import Landing from '~/groups/landing';
function exploreGroups() {
- initGroupsList();
+ initGroupsList(EmptyState);
const landingElement = document.querySelector('.js-explore-groups-landing');
if (!landingElement) return;
const exploreGroupsLanding = new Landing(
diff --git a/app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue b/app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue
index f0c4ecbe3eb..3a73d0c9f40 100644
--- a/app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue
+++ b/app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue
@@ -19,9 +19,14 @@ export default {
<template>
<bitbucket-status-table v-bind="$attrs">
<template #actions>
- <gl-button variant="info" class="gl-ml-3" data-method="post" :href="reconfigurePath">{{
- __('Reconfigure')
- }}</gl-button>
+ <gl-button
+ category="secondary"
+ variant="confirm"
+ class="gl-ml-3"
+ data-method="post"
+ :href="reconfigurePath"
+ >{{ __('Reconfigure') }}</gl-button
+ >
</template>
</bitbucket-status-table>
</template>
diff --git a/app/assets/javascripts/pages/organizations/organizations/show/index.js b/app/assets/javascripts/pages/organizations/organizations/show/index.js
new file mode 100644
index 00000000000..f9f9d2db692
--- /dev/null
+++ b/app/assets/javascripts/pages/organizations/organizations/show/index.js
@@ -0,0 +1,3 @@
+import { initOrganizationsShow } from '~/organizations/show';
+
+initOrganizationsShow();
diff --git a/app/assets/javascripts/pages/projects/incidents/show/index.js b/app/assets/javascripts/pages/projects/incidents/show/index.js
index a83c4f1c0d2..a2ff45b454a 100644
--- a/app/assets/javascripts/pages/projects/incidents/show/index.js
+++ b/app/assets/javascripts/pages/projects/incidents/show/index.js
@@ -1,7 +1,3 @@
import { initShow } from '~/issues';
-import initSidebarBundle from '~/sidebar/sidebar_bundle';
-import initWorkItemLinks from '~/work_items/components/work_item_links';
initShow();
-initSidebarBundle();
-initWorkItemLinks();
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 0844e322de2..ead15143072 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/index.js
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
@@ -1,5 +1,5 @@
import { initFilteredSearchServiceDesk } from '~/issues';
-import { mountServiceDeskListApp } from '~/service_desk';
+import { mountServiceDeskListApp } from '~/issues/service_desk';
initFilteredSearchServiceDesk();
diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js
index c92958cd8c7..a2ff45b454a 100644
--- a/app/assets/javascripts/pages/projects/issues/show/index.js
+++ b/app/assets/javascripts/pages/projects/issues/show/index.js
@@ -1,10 +1,3 @@
import { initShow } from '~/issues';
-import { store } from '~/notes/stores';
-import { initRelatedIssues } from '~/related_issues';
-import initSidebarBundle from '~/sidebar/sidebar_bundle';
-import initWorkItemLinks from '~/work_items/components/work_item_links';
initShow();
-initSidebarBundle(store);
-initRelatedIssues();
-initWorkItemLinks();
diff --git a/app/assets/javascripts/pages/projects/jobs/index/index.js b/app/assets/javascripts/pages/projects/jobs/index/index.js
index eb3a24f38a8..9b5ad804750 100644
--- a/app/assets/javascripts/pages/projects/jobs/index/index.js
+++ b/app/assets/javascripts/pages/projects/jobs/index/index.js
@@ -1,3 +1,3 @@
-import initJobsTable from '~/jobs/components/table';
+import initJobsPage from '~/ci/jobs_page';
-initJobsTable();
+initJobsPage();
diff --git a/app/assets/javascripts/pages/projects/jobs/show/index.js b/app/assets/javascripts/pages/projects/jobs/show/index.js
index 6fef057dee0..cd83f2b7b64 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 '~/jobs';
+import initJobDetails from '~/ci/job_details';
initJobDetails();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/branch_finder.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/branch_finder.js
new file mode 100644
index 00000000000..ee84f54978a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/branch_finder.js
@@ -0,0 +1 @@
+export const findTargetBranch = async () => {};
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 f71a1041068..d23a0615bb8 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
@@ -2,6 +2,8 @@ import Vue from 'vue';
import { mountMarkdownEditor } from 'ee_else_ce/vue_shared/components/markdown/mount_markdown_editor';
+import { findTargetBranch } from 'ee_else_ce/pages/projects/merge_requests/creations/new/branch_finder';
+
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import MergeRequest from '~/merge_request';
import CompareApp from '~/merge_requests/components/compare_app.vue';
@@ -13,14 +15,15 @@ if (mrNewCompareNode) {
const targetCompareEl = document.getElementById('js-target-project-dropdown');
const sourceCompareEl = document.getElementById('js-source-project-dropdown');
const compareEl = document.querySelector('.js-merge-request-new-compare');
+ const targetBranch = Vue.observable({ name: '' });
+ const currentSourceBranch = JSON.parse(sourceCompareEl.dataset.currentBranch);
// eslint-disable-next-line no-new
new Vue({
el: sourceCompareEl,
name: 'SourceCompareApp',
provide: {
currentProject: JSON.parse(sourceCompareEl.dataset.currentProject),
- currentBranch: JSON.parse(sourceCompareEl.dataset.currentBranch),
branchCommitPath: compareEl.dataset.sourceBranchUrl,
inputs: {
project: {
@@ -40,20 +43,35 @@ if (mrNewCompareNode) {
project: 'js-source-project',
branch: 'js-source-branch gl-font-monospace',
},
- branchQaSelector: 'source_branch_dropdown',
+ },
+ methods: {
+ async selectedBranch(branchName) {
+ const targetBranchName = await findTargetBranch(branchName);
+
+ if (targetBranchName) {
+ targetBranch.name = targetBranchName;
+ }
+ },
},
render(h) {
- return h(CompareApp);
+ return h(CompareApp, {
+ props: {
+ currentBranch: currentSourceBranch,
+ },
+ on: {
+ 'select-branch': this.selectedBranch,
+ },
+ });
},
});
+ const currentTargetBranch = JSON.parse(targetCompareEl.dataset.currentBranch);
// eslint-disable-next-line no-new
new Vue({
el: targetCompareEl,
name: 'TargetCompareApp',
provide: {
currentProject: JSON.parse(targetCompareEl.dataset.currentProject),
- currentBranch: JSON.parse(targetCompareEl.dataset.currentBranch),
projectsPath: targetCompareEl.dataset.targetProjectsPath,
branchCommitPath: compareEl.dataset.targetBranchUrl,
inputs: {
@@ -75,8 +93,17 @@ if (mrNewCompareNode) {
branch: 'js-target-branch gl-font-monospace',
},
},
+ computed: {
+ currentBranch() {
+ if (targetBranch.name) {
+ return { text: targetBranch.name, value: targetBranch.name };
+ }
+
+ return currentTargetBranch;
+ },
+ },
render(h) {
- return h(CompareApp);
+ return h(CompareApp, { props: { currentBranch: this.currentBranch } });
},
});
} else {
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 30734f0b698..2cdbf0fb830 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,7 +4,7 @@ import { s__ } from '~/locale';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import { initPipelineCountListener } from '~/commit/pipelines/utils';
import { initIssuableSidebar } from '~/issuable';
-import StatusBox from '~/issuable/components/status_box.vue';
+import MergeRequestStatusBadge from '~/merge_requests/components/merge_request_status_badge.vue';
import createDefaultClient from '~/lib/graphql';
import initSourcegraph from '~/sourcegraph';
import ZenMode from '~/zen_mode';
@@ -24,24 +24,24 @@ export default function initMergeRequestShow() {
initMrExperienceSurvey();
const el = document.querySelector('.js-mr-status-box');
- const { iid, issuableType, projectPath } = el.dataset;
- const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
- });
+ const { iid, issuableType, projectPath, state } = el.dataset;
+
// eslint-disable-next-line no-new
new Vue({
el,
name: 'IssuableStatusBoxRoot',
- apolloProvider,
+ apolloProvider: new VueApollo({
+ defaultClient: createDefaultClient(),
+ }),
provide: {
query: getStateQuery,
iid,
projectPath,
},
- render(h) {
- return h(StatusBox, {
+ render(createElement) {
+ return createElement(MergeRequestStatusBadge, {
props: {
- initialState: el.dataset.state,
+ 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 75e308e706f..f7b522f7c85 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/page.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/page.js
@@ -1,8 +1,8 @@
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 { initIssuableHeaderWarnings } from '~/issuable';
import { start as startCodeReviewMessaging } from '~/code_review/signals';
import diffsEventHub from '~/diffs/event_hub';
import store from '~/mr_notes/stores';
@@ -24,7 +24,7 @@ export function initMrPage() {
requestIdleCallback(() => {
initSidebarBundle(store);
- initIssuableHeaderWarnings(store);
+ mountHeaderMetadata(store);
const el = document.getElementById('js-merge-sticky-header');
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
index a51c2e9c47b..f48c38b776f 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
@@ -1,8 +1,3 @@
import initPipelineSchedulesFormApp from '~/ci/pipeline_schedules/mount_pipeline_schedules_form_app';
-import initForm from '../shared/init_form';
-if (gon.features?.pipelineSchedulesVue) {
- initPipelineSchedulesFormApp('#pipeline-schedules-form-edit', true);
-} else {
- initForm();
-}
+initPipelineSchedulesFormApp('#pipeline-schedules-form-edit', true);
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
index 4bdbb70d942..0eff9110412 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
@@ -1,75 +1,3 @@
-import Vue from 'vue';
-import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import initPipelineSchedulesApp from '~/ci/pipeline_schedules/mount_pipeline_schedules_app';
-import PipelineSchedulesTakeOwnershipModalLegacy from '~/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue';
-import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue';
-function initPipelineSchedulesCallout() {
- const el = document.getElementById('pipeline-schedules-callout');
-
- if (!el) {
- return;
- }
-
- const { docsUrl, illustrationUrl } = el.dataset;
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- name: 'PipelineSchedulesCalloutRoot',
- provide: {
- docsUrl,
- illustrationUrl,
- },
- render(createElement) {
- return createElement(PipelineSchedulesCallout);
- },
- });
-}
-
-// TODO: move take ownership feature into new Vue app
-// located in directory app/assets/javascripts/pipeline_schedules/components
-function initTakeownershipModal() {
- const modalId = 'pipeline-take-ownership-modal';
- const buttonSelector = 'js-take-ownership-button';
- const el = document.getElementById(modalId);
- const takeOwnershipButtons = document.querySelectorAll(`.${buttonSelector}`);
-
- if (!el) {
- return;
- }
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- data() {
- return {
- url: '',
- };
- },
- mounted() {
- takeOwnershipButtons.forEach((button) => {
- button.addEventListener('click', () => {
- const { url } = button.dataset;
-
- this.url = url;
- this.$root.$emit(BV_SHOW_MODAL, modalId, `.${buttonSelector}`);
- });
- });
- },
- render(createElement) {
- return createElement(PipelineSchedulesTakeOwnershipModalLegacy, {
- props: {
- ownershipUrl: this.url,
- },
- });
- },
- });
-}
-
-if (gon.features?.pipelineSchedulesVue) {
- initPipelineSchedulesApp();
-} else {
- initPipelineSchedulesCallout();
- initTakeownershipModal();
-}
+initPipelineSchedulesApp();
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
index d8ba7bbd752..672e3d014bd 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
@@ -1,8 +1,3 @@
import initPipelineSchedulesFormApp from '~/ci/pipeline_schedules/mount_pipeline_schedules_form_app';
-import initForm from '../shared/init_form';
-if (gon.features?.pipelineSchedulesVue) {
- initPipelineSchedulesFormApp('#pipeline-schedules-form-new');
-} else {
- initForm();
-}
+initPipelineSchedulesFormApp('#pipeline-schedules-form-new');
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 5f6a73782c3..642fd56eab1 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
@@ -69,7 +69,8 @@ export default {
formattedTime() {
if (this.randomHour > 12) {
return `${this.randomHour - 12}:00pm`;
- } else if (this.randomHour === 12) {
+ }
+ if (this.randomHour === 12) {
return `12:00pm`;
}
return `${this.randomHour}:00am`;
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
deleted file mode 100644
index b3ad50f395b..00000000000
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import Vue from 'vue';
-import { getCookie, setCookie, parseBoolean } from '~/lib/utils/common_utils';
-import Translate from '~/vue_shared/translate';
-
-Vue.use(Translate);
-
-const cookieKey = 'pipeline_schedules_callout_dismissed';
-
-export default {
- name: 'PipelineSchedulesCallout',
- components: {
- GlButton,
- },
- inject: ['docsUrl', 'illustrationUrl'],
- data() {
- return {
- calloutDismissed: parseBoolean(getCookie(cookieKey)),
- };
- },
- methods: {
- dismissCallout() {
- this.calloutDismissed = true;
- setCookie(cookieKey, this.calloutDismissed);
- },
- },
-};
-</script>
-<template>
- <div v-if="!calloutDismissed" class="pipeline-schedules-user-callout user-callout">
- <div class="bordered-box landing content-block gl-p-5!" data-testid="innerContent">
- <gl-button
- category="tertiary"
- icon="close"
- :aria-label="__('Dismiss')"
- class="gl-absolute gl-top-2 gl-right-2"
- @click="dismissCallout"
- />
- <div class="svg-content">
- <img :src="illustrationUrl" />
- </div>
- <div class="user-callout-copy">
- <h4>{{ __('Scheduling Pipelines') }}</h4>
- <p>
- {{
- __(`The pipelines schedule runs pipelines in the future,
-repeatedly, for specific branches or tags.
-Those scheduled pipelines will inherit limited project access based on their associated user.`)
- }}
- </p>
- <p>
- {{ __('Learn more in the') }}
- <a :href="docsUrl" target="_blank" rel="nofollow">
- {{ __('pipeline schedules documentation') }}</a
- >.
- <!-- oneline to prevent extra space before period -->
- </p>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
deleted file mode 100644
index 8440d0e77cd..00000000000
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import $ from 'jquery';
-import Vue from 'vue';
-import { __ } from '~/locale';
-import RefSelector from '~/ref/components/ref_selector.vue';
-import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
-import setupNativeFormVariableList from '~/ci/ci_variable_list/native_form_variable_list';
-import GlFieldErrors from '~/gl_field_errors';
-import Translate from '~/vue_shared/translate';
-import { initTimezoneDropdown } from '../../../profiles/init_timezone_dropdown';
-import IntervalPatternInput from './components/interval_pattern_input.vue';
-
-Vue.use(Translate);
-
-function initIntervalPatternInput() {
- const intervalPatternMount = document.getElementById('interval-pattern-input');
- const initialCronInterval = intervalPatternMount?.dataset?.initialInterval;
- const dailyLimit = intervalPatternMount.dataset?.dailyLimit;
-
- return new Vue({
- el: intervalPatternMount,
- components: {
- IntervalPatternInput,
- },
- render(createElement) {
- return createElement('interval-pattern-input', {
- props: {
- initialCronInterval,
- dailyLimit,
- },
- });
- },
- });
-}
-
-function getEnabledRefTypes() {
- return [REF_TYPE_BRANCHES, REF_TYPE_TAGS];
-}
-
-function initTargetRefDropdown() {
- const $refField = document.getElementById('schedule_ref');
- const el = document.querySelector('.js-target-ref-dropdown');
- const { projectId, defaultBranch } = el.dataset;
-
- if (!$refField.value) {
- $refField.value = defaultBranch;
- }
-
- const refDropdown = new Vue({
- el,
- render(h) {
- return h(RefSelector, {
- props: {
- enabledRefTypes: getEnabledRefTypes(),
- projectId,
- value: $refField.value,
- useSymbolicRefNames: true,
- translations: {
- dropdownHeader: __('Select target branch or tag'),
- },
- },
- class: 'gl-w-full',
- });
- },
- });
-
- refDropdown.$children[0].$on('input', (newRef) => {
- $refField.value = newRef;
- });
-
- return refDropdown;
-}
-
-export default () => {
- /* Most of the form is written in haml, but for fields with more complex behaviors,
- * you should mount individual Vue components here. If at some point components need
- * to share state, it may make sense to refactor the whole form to Vue */
-
- initIntervalPatternInput();
-
- // Initialize non-Vue JS components in the form
-
- const formElement = document.getElementById('new-pipeline-schedule-form');
-
- gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement);
-
- initTargetRefDropdown();
-
- setupNativeFormVariableList({
- container: $('.js-ci-variable-list-section'),
- formField: 'schedule',
- });
-};
-
-initTimezoneDropdown();
diff --git a/app/assets/javascripts/pages/projects/pipelines/index/index.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js
index 63b1f2bf975..a0ddf96ede2 100644
--- a/app/assets/javascripts/pages/projects/pipelines/index/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js
@@ -1,3 +1,3 @@
-import { initPipelinesIndex } from '~/pipelines/pipelines_index';
+import { initPipelinesIndex } from '~/ci/pipeline_details/pipelines_index';
initPipelinesIndex();
diff --git a/app/assets/javascripts/pages/projects/pipelines/show/index.js b/app/assets/javascripts/pages/projects/pipelines/show/index.js
index d3f46b7e025..225479c5194 100644
--- a/app/assets/javascripts/pages/projects/pipelines/show/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/show/index.js
@@ -1,4 +1,4 @@
-import initPipelineDetails from '~/pipelines/pipeline_details_bundle';
+import initPipelineDetails from '~/ci/pipeline_details/pipeline_details_bundle';
import initPipelines from '../init_pipelines';
initPipelines();
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 4a5d5580c08..dce40c1f322 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -12,7 +12,6 @@ import initSettingsPanels from '~/settings_panels';
import { initTokenAccess } from '~/token_access';
import { initCiSecureFiles } from '~/ci_secure_files';
import initDeployTokens from '~/deploy_tokens';
-import { initProjectRunners } from '~/ci/runner/project_runners';
import { initProjectRunnersRegistrationDropdown } from '~/ci/runner/project_runners/register';
import { initGeneralPipelinesOptions } from '~/ci_settings_general_pipeline';
@@ -45,7 +44,6 @@ initDeployFreeze();
initSettingsPipelinesTriggers();
initArtifactsSettings();
-initProjectRunners();
initProjectRunnersRegistrationDropdown();
initSharedRunnersToggle();
initRefSwitcherBadges();
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index c43a0eb597c..bee0731d711 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -1,13 +1,11 @@
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-
import initClustersDeprecationAlert from '~/projects/clusters_deprecation_alert';
import leaveByUrl from '~/namespaces/leave_by_url';
import initVueNotificationsDropdown from '~/notifications';
-import Star from '~/projects/star';
+import { initStarButton } from '~/projects/project_star_button';
import initTerraformNotification from '~/projects/terraform_notification';
import { initUploadFileTrigger } from '~/projects/upload_file';
import initReadMore from '~/read_more';
-
import initForksButton from '~/forks/init_forks_button';
// Project show page loads different overview content based on user preferences
@@ -46,7 +44,7 @@ initClustersDeprecationAlert();
initTerraformNotification();
initReadMore();
-new Star(); // eslint-disable-line no-new
+initStarButton();
if (document.querySelector('.js-autodevops-banner')) {
import(/* webpackChunkName: 'userCallOut' */ '~/user_callout')
diff --git a/app/assets/javascripts/pages/projects/tracing/index/index.js b/app/assets/javascripts/pages/projects/tracing/index/index.js
deleted file mode 100644
index 64ca303f8ba..00000000000
--- a/app/assets/javascripts/pages/projects/tracing/index/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import { initSimpleApp } from '~/helpers/init_simple_app_helper';
-import ListIndex from '~/tracing/list_index.vue';
-
-initSimpleApp('#js-tracing', ListIndex);
diff --git a/app/assets/javascripts/pages/projects/tracing/show/index.js b/app/assets/javascripts/pages/projects/tracing/show/index.js
deleted file mode 100644
index 107c004aa5f..00000000000
--- a/app/assets/javascripts/pages/projects/tracing/show/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import { initSimpleApp } from '~/helpers/init_simple_app_helper';
-import DetailsIndex from '~/tracing/details_index.vue';
-
-initSimpleApp('#js-tracing-details', DetailsIndex);
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index 5bef7e6e322..b53e2709f83 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -67,7 +67,8 @@ export default {
metricDetailsLabel() {
if (this.metricDetails.duration && this.metricDetails.calls) {
return `${this.metricDetails.duration} / ${this.metricDetails.calls}`;
- } else if (this.metricDetails.calls) {
+ }
+ if (this.metricDetails.calls) {
return this.metricDetails.calls;
}
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index e7f2662ae09..cea01852630 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -19,13 +19,11 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-namespace-storage-alert',
'.js-web-hook-disabled-callout',
'.js-merge-request-settings-callout',
- '.js-ultimate-feature-removal-banner',
'.js-geo-enable-hashed-storage-callout',
'.js-geo-migrate-hashed-storage-callout',
'.js-unlimited-members-during-trial-alert',
'.js-branch-rules-info-callout',
'.js-new-navigation-callout',
- '.js-code-suggestions-third-party-callout',
'.js-namespace-over-storage-users-combined-alert',
];
diff --git a/app/assets/javascripts/pipelines/mixins/stage_column_mixin.js b/app/assets/javascripts/pipelines/mixins/stage_column_mixin.js
deleted file mode 100644
index 578ff498358..00000000000
--- a/app/assets/javascripts/pipelines/mixins/stage_column_mixin.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export default {
- props: {
- hasUpstream: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- methods: {
- buildConnnectorClass(index) {
- return index === 0 && (!this.isFirstColumn || this.hasUpstream) ? 'left-connector' : '';
- },
- },
-};
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index e21f8557d68..947bf7acd5c 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -30,7 +30,6 @@ export default class Profile {
bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
- $('#user_email_opted_in').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
this.form.on('submit', this.onSubmitForm);
}
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
index 6ff9bd7390f..b7355b909a1 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
@@ -2,16 +2,13 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
-import {
- getQueryHeaders,
- toggleQueryPollingByVisibility,
-} from '~/pipelines/components/graph/utils';
-import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
-import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
-import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
+import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
+import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
+import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
+import PipelineMiniGraph from '~/ci/pipeline_mini_graph/pipeline_mini_graph.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
-import getPipelineStagesQuery from '~/pipelines/graphql/queries/get_pipeline_stages.query.graphql';
+import getLinkedPipelinesQuery from '~/ci/pipeline_details/graphql/queries/get_linked_pipelines.query.graphql';
+import getPipelineStagesQuery from '~/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stages.query.graphql';
import { formatStages } from '../utils';
import { COMMIT_BOX_POLL_INTERVAL } from '../constants';
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 71f53613a3b..ccecc914cf1 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
@@ -2,10 +2,7 @@
import { GlLoadingIcon, GlLink } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { createAlert } from '~/alert';
-import {
- getQueryHeaders,
- toggleQueryPollingByVisibility,
-} from '~/pipelines/components/graph/utils';
+import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
import getLatestPipelineStatusQuery from '../graphql/queries/get_latest_pipeline_status.query.graphql';
import { COMMIT_BOX_POLL_INTERVAL, PIPELINE_STATUS_FETCH_ERROR } from '../constants';
diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue
deleted file mode 100644
index 034bae3066d..00000000000
--- a/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue
+++ /dev/null
@@ -1,156 +0,0 @@
-<script>
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlDropdownSectionHeader } from '@gitlab/ui';
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import { s__ } from '~/locale';
-
-export default {
- components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
- },
- props: {
- refsProjectPath: {
- type: String,
- required: true,
- },
- revisionText: {
- type: String,
- required: true,
- },
- paramsName: {
- type: String,
- required: true,
- },
- paramsBranch: {
- type: String,
- required: false,
- default: null,
- },
- },
- data() {
- return {
- branches: [],
- tags: [],
- loading: true,
- searchTerm: '',
- selectedRevision: this.getDefaultBranch(),
- };
- },
- computed: {
- filteredBranches() {
- return this.branches.filter((branch) =>
- branch.toLowerCase().includes(this.searchTerm.toLowerCase()),
- );
- },
- hasFilteredBranches() {
- return this.filteredBranches.length;
- },
- filteredTags() {
- return this.tags.filter((tag) => tag.toLowerCase().includes(this.searchTerm.toLowerCase()));
- },
- hasFilteredTags() {
- return this.filteredTags.length;
- },
- },
- watch: {
- paramsBranch(newBranch) {
- this.setSelectedRevision(newBranch);
- },
- },
- mounted() {
- this.fetchBranchesAndTags();
- },
- methods: {
- fetchBranchesAndTags() {
- const endpoint = this.refsProjectPath;
-
- return axios
- .get(endpoint)
- .then(({ data }) => {
- this.branches = data.Branches || [];
- this.tags = data.Tags || [];
- })
- .catch(() => {
- createAlert({
- message: `${s__(
- 'CompareRevisions|There was an error while updating the branch/tag list. Please try again.',
- )}`,
- });
- })
- .finally(() => {
- this.loading = false;
- });
- },
- getDefaultBranch() {
- return this.paramsBranch || s__('CompareRevisions|Select branch/tag');
- },
- onClick(revision) {
- this.setSelectedRevision(revision);
- },
- onSearchEnter() {
- this.setSelectedRevision(this.searchTerm);
- },
- setSelectedRevision(revision) {
- this.selectedRevision = revision || s__('CompareRevisions|Select branch/tag');
- this.$emit('selectRevision', { direction: this.paramsName, revision });
- },
- },
-};
-</script>
-
-<template>
- <div class="form-group compare-form-group" :class="`js-compare-${paramsName}-dropdown`">
- <div class="input-group inline-input-group">
- <span class="input-group-prepend">
- <div class="input-group-text">
- {{ revisionText }}
- </div>
- </span>
- <input type="hidden" :name="paramsName" :value="selectedRevision" />
- <gl-dropdown
- class="gl-flex-grow-1 gl-flex-basis-0 gl-min-w-0 gl-font-monospace"
- toggle-class="form-control compare-dropdown-toggle gl-min-w-0 gl-rounded-top-left-none! gl-rounded-bottom-left-none!"
- :text="selectedRevision"
- header-text="Select Git revision"
- :loading="loading"
- >
- <template #header>
- <gl-search-box-by-type
- v-model.trim="searchTerm"
- :placeholder="s__('CompareRevisions|Filter by Git revision')"
- @keyup.enter="onSearchEnter"
- />
- </template>
- <gl-dropdown-section-header v-if="hasFilteredBranches">
- {{ s__('CompareRevisions|Branches') }}
- </gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="(branch, index) in filteredBranches"
- :key="`branch${index}`"
- is-check-item
- :is-checked="selectedRevision === branch"
- data-testid="branches-dropdown-item"
- @click="onClick(branch)"
- >
- {{ branch }}
- </gl-dropdown-item>
- <gl-dropdown-section-header v-if="hasFilteredTags">
- {{ s__('CompareRevisions|Tags') }}
- </gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="(tag, index) in filteredTags"
- :key="`tag${index}`"
- is-check-item
- :is-checked="selectedRevision === tag"
- data-testid="tags-dropdown-item"
- @click="onClick(tag)"
- >
- {{ tag }}
- </gl-dropdown-item>
- </gl-dropdown>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/constants.js b/app/assets/javascripts/projects/pipelines/charts/constants.js
index c13824a9952..dcec77ac6a4 100644
--- a/app/assets/javascripts/projects/pipelines/charts/constants.js
+++ b/app/assets/javascripts/projects/pipelines/charts/constants.js
@@ -21,5 +21,5 @@ export const LOAD_PIPELINES_FAILURE = 'load_analytics_failure';
export const UNSUPPORTED_DATA = 'unsupported_data';
export const SNOWPLOW_LABEL = 'redis_hll_counters.analytics.analytics_total_unique_counts_monthly';
-export const SNOWPLOW_SCHEMA = 'iglu:com.gitlab/gitlab_service_ping/jsonschema/1-0-0';
+export const SNOWPLOW_SCHEMA = 'iglu:com.gitlab/gitlab_service_ping/jsonschema/1-0-1';
export const SNOWPLOW_DATA_SOURCE = 'redis_hll';
diff --git a/app/assets/javascripts/projects/project_star_button.js b/app/assets/javascripts/projects/project_star_button.js
new file mode 100644
index 00000000000..06f982b500d
--- /dev/null
+++ b/app/assets/javascripts/projects/project_star_button.js
@@ -0,0 +1,46 @@
+import { createAlert } from '~/alert';
+import axios from '~/lib/utils/axios_utils';
+import { spriteIcon } from '~/lib/utils/common_utils';
+import { __, s__ } from '~/locale';
+
+export function initStarButton(containerSelector = '.project-home-panel') {
+ const container = document.querySelector(containerSelector);
+ const starToggle = container?.querySelector('.toggle-star');
+
+ if (!starToggle) {
+ return;
+ }
+
+ starToggle.addEventListener('click', function toggleStarClickCallback() {
+ const starSpan = starToggle.querySelector('span');
+ const starIcon = starToggle.querySelector('svg');
+ const iconClasses = Array.from(starIcon.classList.values());
+
+ axios
+ .post(starToggle.dataset.endpoint)
+ .then(({ data }) => {
+ const isStarred = starSpan.classList.contains('starred');
+ starToggle.parentNode.querySelector('.count').textContent = data.star_count;
+
+ if (isStarred) {
+ starSpan.classList.remove('starred');
+ starSpan.textContent = s__('StarProject|Star');
+ starIcon.remove();
+ // eslint-disable-next-line no-unsanitized/method
+ starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star-o', iconClasses));
+ } else {
+ starSpan.classList.add('starred');
+ starSpan.textContent = __('Unstar');
+ starIcon.remove();
+
+ // eslint-disable-next-line no-unsanitized/method
+ starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star', iconClasses));
+ }
+ })
+ .catch(() =>
+ createAlert({
+ message: __('Star toggle failed. Try again later.'),
+ }),
+ );
+ });
+}
diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js
deleted file mode 100644
index 75d72f719e5..00000000000
--- a/app/assets/javascripts/projects/settings/access_dropdown.js
+++ /dev/null
@@ -1,611 +0,0 @@
-/* eslint-disable no-underscore-dangle, class-methods-use-this */
-import { escape, find, countBy } from 'lodash';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { createAlert } from '~/alert';
-import { n__, s__, __, sprintf } from '~/locale';
-import { renderAvatar } from '~/helpers/avatar_helper';
-import { getUsers, getGroups, getDeployKeys } from './api/access_dropdown_api';
-import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from './constants';
-
-export default class AccessDropdown {
- constructor(options) {
- const { $dropdown, accessLevel, accessLevelsData, hasLicense = true } = options;
- this.options = options;
- this.hasLicense = hasLicense;
- this.groups = [];
- this.accessLevel = accessLevel;
- this.accessLevelsData = accessLevelsData.roles;
- this.$dropdown = $dropdown;
- this.$wrap = this.$dropdown.closest(`.${this.accessLevel}-container`);
- this.defaultLabel = this.$dropdown.data('defaultLabel');
-
- this.setSelectedItems([]);
- this.persistPreselectedItems();
-
- this.noOneObj = this.accessLevelsData.find((level) => level.id === ACCESS_LEVEL_NONE);
-
- this.initDropdown();
- }
-
- initDropdown() {
- const { onSelect, onHide } = this.options;
- initDeprecatedJQueryDropdown(this.$dropdown, {
- data: this.getData.bind(this),
- selectable: true,
- filterable: true,
- filterRemote: true,
- multiSelect: this.$dropdown.hasClass('js-multiselect'),
- renderRow: this.renderRow.bind(this),
- toggleLabel: this.toggleLabel.bind(this),
- hidden() {
- if (onHide) {
- onHide();
- }
- },
- clicked: (options) => {
- const { $el, e } = options;
- const item = options.selectedObj;
- const fossWithMergeAccess = !this.hasLicense && this.accessLevel === ACCESS_LEVELS.MERGE;
-
- e.preventDefault();
-
- if (fossWithMergeAccess) {
- // We're not multiselecting quite yet in "Merge" access dropdown, on FOSS:
- // remove all preselected items before selecting this item
- // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37499
- this.accessLevelsData.forEach((level) => {
- this.removeSelectedItem(level);
- });
- }
-
- if ($el.is('.is-active')) {
- if (this.noOneObj) {
- if (item.id === this.noOneObj.id && !fossWithMergeAccess) {
- // remove all others selected items
- this.accessLevelsData.forEach((level) => {
- if (level.id !== item.id) {
- this.removeSelectedItem(level);
- }
- });
-
- // remove selected item visually
- this.$wrap.find(`.item-${item.type}`).removeClass('is-active');
- } else {
- const $noOne = this.$wrap.find(
- `.is-active.item-${item.type}[data-role-id="${this.noOneObj.id}"]`,
- );
- if ($noOne.length) {
- $noOne.removeClass('is-active');
- this.removeSelectedItem(this.noOneObj);
- }
- }
- }
-
- // make element active right away
- $el.addClass(`is-active item-${item.type}`);
-
- // Add "No one"
- this.addSelectedItem(item);
- } else {
- this.removeSelectedItem(item);
- }
-
- if (onSelect) {
- onSelect(item, $el, this);
- }
- },
- });
-
- this.$dropdown.find('.dropdown-toggle-text').text(this.toggleLabel());
- }
-
- persistPreselectedItems() {
- const itemsToPreselect = this.$dropdown.data('preselectedItems');
-
- if (!itemsToPreselect || !itemsToPreselect.length) {
- return;
- }
-
- const persistedItems = itemsToPreselect.map((item) => {
- const persistedItem = { ...item };
- persistedItem.persisted = true;
- return persistedItem;
- });
-
- this.setSelectedItems(persistedItems);
- }
-
- setSelectedItems(items = []) {
- this.items = items;
- }
-
- getSelectedItems() {
- return this.items.filter((item) => !item._destroy);
- }
-
- getAllSelectedItems() {
- return this.items;
- }
-
- // Return dropdown as input data ready to submit
- getInputData() {
- const selectedItems = this.getAllSelectedItems();
-
- const accessLevels = selectedItems.map((item) => {
- const obj = {};
-
- if (typeof item.id !== 'undefined') {
- obj.id = item.id;
- }
-
- if (typeof item._destroy !== 'undefined') {
- obj._destroy = item._destroy;
- }
-
- if (item.type === LEVEL_TYPES.ROLE) {
- obj.access_level = item.access_level;
- } else if (item.type === LEVEL_TYPES.USER) {
- obj.user_id = item.user_id;
- } else if (item.type === LEVEL_TYPES.DEPLOY_KEY) {
- obj.deploy_key_id = item.deploy_key_id;
- } else if (item.type === LEVEL_TYPES.GROUP) {
- obj.group_id = item.group_id;
- }
-
- return obj;
- });
-
- return accessLevels;
- }
-
- addSelectedItem(selectedItem) {
- let itemToAdd = {};
-
- let index = -1;
- let alreadyAdded = false;
- const selectedItems = this.getAllSelectedItems();
-
- // Compare IDs based on selectedItem.type
- selectedItems.forEach((item, i) => {
- let comparator;
- switch (selectedItem.type) {
- case LEVEL_TYPES.ROLE:
- comparator = LEVEL_ID_PROP.ROLE;
- // If the item already exists, just use it
- if (item[comparator] === selectedItem.id) {
- alreadyAdded = true;
- }
- break;
- case LEVEL_TYPES.GROUP:
- comparator = LEVEL_ID_PROP.GROUP;
- break;
- case LEVEL_TYPES.DEPLOY_KEY:
- comparator = LEVEL_ID_PROP.DEPLOY_KEY;
- break;
- case LEVEL_TYPES.USER:
- comparator = LEVEL_ID_PROP.USER;
- break;
- default:
- break;
- }
-
- if (selectedItem.id === item[comparator]) {
- index = i;
- }
- });
-
- if (alreadyAdded) {
- return;
- }
-
- if (index !== -1 && selectedItems[index]._destroy) {
- delete selectedItems[index]._destroy;
- return;
- }
-
- itemToAdd.type = selectedItem.type;
-
- if (selectedItem.type === LEVEL_TYPES.USER) {
- itemToAdd = {
- user_id: selectedItem.id,
- name: selectedItem.name || '_name1',
- username: selectedItem.username || '_username1',
- avatar_url: selectedItem.avatar_url || '_avatar_url1',
- type: LEVEL_TYPES.USER,
- };
- } else if (selectedItem.type === LEVEL_TYPES.ROLE) {
- itemToAdd = {
- access_level: selectedItem.id,
- type: LEVEL_TYPES.ROLE,
- };
- } else if (selectedItem.type === LEVEL_TYPES.GROUP) {
- itemToAdd = {
- group_id: selectedItem.id,
- type: LEVEL_TYPES.GROUP,
- };
- } else if (selectedItem.type === LEVEL_TYPES.DEPLOY_KEY) {
- itemToAdd = {
- deploy_key_id: selectedItem.id,
- type: LEVEL_TYPES.DEPLOY_KEY,
- };
- }
-
- this.items.push(itemToAdd);
- }
-
- removeSelectedItem(itemToDelete) {
- let index = -1;
- const selectedItems = this.getAllSelectedItems();
-
- // To find itemToDelete on selectedItems, first we need the index
- selectedItems.every((item, i) => {
- if (item.type !== itemToDelete.type) {
- return true;
- }
-
- if (
- (item.type === LEVEL_TYPES.USER && item.user_id === itemToDelete.id) ||
- (item.type === LEVEL_TYPES.ROLE && item.access_level === itemToDelete.id) ||
- (item.type === LEVEL_TYPES.DEPLOY_KEY && item.deploy_key_id === itemToDelete.id) ||
- (item.type === LEVEL_TYPES.GROUP && item.group_id === itemToDelete.id)
- ) {
- index = i;
- }
-
- // Break once we have index set
- return !(index > -1);
- });
-
- // if ItemToDelete is not really selected do nothing
- if (index === -1) {
- return;
- }
-
- if (selectedItems[index].persisted) {
- // If we toggle an item that has been already marked with _destroy
- if (selectedItems[index]._destroy) {
- delete selectedItems[index]._destroy;
- } else {
- selectedItems[index]._destroy = '1';
- }
- } else {
- selectedItems.splice(index, 1);
- }
- }
-
- toggleLabel() {
- const currentItems = this.getSelectedItems();
- const $dropdownToggleText = this.$dropdown.find('.dropdown-toggle-text');
-
- if (currentItems.length === 0) {
- $dropdownToggleText.addClass('is-default');
- return this.defaultLabel;
- }
-
- $dropdownToggleText.removeClass('is-default');
-
- if (currentItems.length === 1 && currentItems[0].type === LEVEL_TYPES.ROLE) {
- const roleData = this.accessLevelsData.find(
- (data) => data.id === currentItems[0].access_level,
- );
- return roleData.text;
- }
-
- const labelPieces = [];
- const counts = countBy(currentItems, (item) => item.type);
-
- if (counts[LEVEL_TYPES.ROLE] > 0) {
- labelPieces.push(n__('1 role', '%d roles', counts[LEVEL_TYPES.ROLE]));
- }
-
- if (counts[LEVEL_TYPES.USER] > 0) {
- labelPieces.push(n__('1 user', '%d users', counts[LEVEL_TYPES.USER]));
- }
-
- if (counts[LEVEL_TYPES.DEPLOY_KEY] > 0) {
- labelPieces.push(n__('1 deploy key', '%d deploy keys', counts[LEVEL_TYPES.DEPLOY_KEY]));
- }
-
- if (counts[LEVEL_TYPES.GROUP] > 0) {
- labelPieces.push(n__('1 group', '%d groups', counts[LEVEL_TYPES.GROUP]));
- }
-
- return labelPieces.join(', ');
- }
-
- getData(query, callback) {
- if (this.hasLicense) {
- Promise.all([
- getDeployKeys(query),
- getUsers(query),
- this.groupsData ? Promise.resolve(this.groupsData) : getGroups(),
- ])
- .then(([deployKeysResponse, usersResponse, groupsResponse]) => {
- this.groupsData = groupsResponse;
- callback(
- this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data),
- );
- })
- .catch(() => {
- createAlert({ message: __('Failed to load groups, users and deploy keys.') });
- });
- } else {
- getDeployKeys(query)
- .then((deployKeysResponse) => callback(this.consolidateData(deployKeysResponse.data)))
- .catch(() => createAlert({ message: __('Failed to load deploy keys.') }));
- }
- }
-
- consolidateData(deployKeysResponse, usersResponse = [], groupsResponse = []) {
- let consolidatedData = [];
-
- // ID property is handled differently locally from the server
- //
- // For Groups
- // In dropdown: `id`
- // For submit: `group_id`
- //
- // For Roles
- // In dropdown: `id`
- // For submit: `access_level`
- //
- // For Users
- // In dropdown: `id`
- // For submit: `user_id`
- //
- // For Deploy Keys
- // In dropdown: `id`
- // For submit: `deploy_key_id`
-
- /*
- * Build roles
- */
- const roles = this.accessLevelsData.map((level) => {
- /* eslint-disable no-param-reassign */
- // This re-assignment is intentional as
- // level.type property is being used in removeSelectedItem()
- // for comparision, and accessLevelsData is provided by
- // gon.create_access_levels which doesn't have `type` included.
- // See this discussion https://gitlab.com/gitlab-org/gitlab/merge_requests/1629#note_31285823
- level.type = LEVEL_TYPES.ROLE;
- return level;
- });
-
- if (roles.length) {
- consolidatedData = consolidatedData.concat(
- [{ type: 'header', content: s__('AccessDropdown|Roles') }],
- roles,
- );
- }
-
- if (this.hasLicense) {
- const map = [];
- const selectedItems = this.getSelectedItems();
- /*
- * Build groups
- */
- const groups = groupsResponse.map((group) => ({
- ...group,
- type: LEVEL_TYPES.GROUP,
- }));
-
- /*
- * Build users
- */
- const users = selectedItems
- .filter((item) => item.type === LEVEL_TYPES.USER)
- .map((item) => {
- // Save identifiers for easy-checking more later
- map.push(LEVEL_TYPES.USER + item.user_id);
-
- return {
- id: item.user_id,
- name: item.name,
- username: item.username,
- avatar_url: item.avatar_url,
- type: LEVEL_TYPES.USER,
- };
- });
-
- // Has to be checked against server response
- // because the selected item can be in filter results
- if (gon.current_project_id) {
- usersResponse.forEach((response) => {
- // Add is it has not been added
- if (map.indexOf(LEVEL_TYPES.USER + response.id) === -1) {
- const user = { ...response };
- user.type = LEVEL_TYPES.USER;
- users.push(user);
- }
- });
- }
-
- if (groups.length) {
- if (roles.length) {
- consolidatedData = consolidatedData.concat([{ type: 'divider' }]);
- }
-
- consolidatedData = consolidatedData.concat(
- [{ type: 'header', content: s__('AccessDropdown|Groups') }],
- groups,
- );
- }
-
- if (users.length) {
- consolidatedData = consolidatedData.concat(
- [{ type: 'divider' }],
- [{ type: 'header', content: s__('AccessDropdown|Users') }],
- users,
- );
- }
- }
-
- const deployKeys = deployKeysResponse.map((response) => {
- const {
- id,
- fingerprint,
- fingerprint_sha256: fingerprintSha256,
- title,
- owner: { avatar_url, name, username },
- } = response;
-
- const availableFingerprint = fingerprintSha256 || fingerprint;
- const shortFingerprint = `(${availableFingerprint.substring(0, 14)}...)`;
-
- return {
- id,
- title: title.concat(' ', shortFingerprint),
- avatar_url,
- fullname: name,
- username,
- type: LEVEL_TYPES.DEPLOY_KEY,
- };
- });
-
- if (this.accessLevel === ACCESS_LEVELS.PUSH) {
- if (deployKeys.length) {
- consolidatedData = consolidatedData.concat(
- [{ type: 'divider' }],
- [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }],
- deployKeys,
- );
- }
- }
-
- if (this.accessLevel === ACCESS_LEVELS.CREATE && deployKeys.length) {
- consolidatedData = consolidatedData.concat(
- [{ type: 'divider' }],
- [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }],
- deployKeys,
- );
- }
-
- return consolidatedData;
- }
-
- renderRow(item) {
- let criteria = {};
- let groupRowEl;
-
- // Dectect if the current item is already saved so we can add
- // the `is-active` class so the item looks as marked
- switch (item.type) {
- case LEVEL_TYPES.USER:
- criteria = { user_id: item.id };
- break;
- case LEVEL_TYPES.ROLE:
- criteria = { access_level: item.id };
- break;
- case LEVEL_TYPES.DEPLOY_KEY:
- criteria = { deploy_key_id: item.id };
- break;
- case LEVEL_TYPES.GROUP:
- criteria = { group_id: item.id };
- break;
- default:
- break;
- }
-
- const isActive = find(this.getSelectedItems(), criteria) ? 'is-active' : '';
-
- switch (item.type) {
- case LEVEL_TYPES.USER:
- groupRowEl = this.userRowHtml(item, isActive);
- break;
- case LEVEL_TYPES.ROLE:
- groupRowEl = this.roleRowHtml(item, isActive);
- break;
- case LEVEL_TYPES.DEPLOY_KEY:
- groupRowEl =
- this.accessLevel === ACCESS_LEVELS.PUSH || this.accessLevel === ACCESS_LEVELS.CREATE
- ? this.deployKeyRowHtml(item, isActive)
- : '';
-
- break;
- case LEVEL_TYPES.GROUP:
- groupRowEl = this.groupRowHtml(item, isActive);
- break;
- default:
- groupRowEl = '';
- break;
- }
-
- return groupRowEl;
- }
-
- userRowHtml(user, isActive) {
- const isActiveClass = isActive || '';
- const avatarEl = renderAvatar(user, {
- sizeClass: 's32',
- });
-
- return `
- <li>
- <a href="#" class="${isActiveClass}">
- <div class="gl-avatar-labeled">
- ${avatarEl}
- <div>
- <strong class="dropdown-menu-user-full-name">${escape(user.name)}</strong>
- <span class="gl-avatar-labeled-sublabel dropdown-menu-user-username">@${
- user.username
- }</span>
- </div>
- </div>
- </a>
- </li>
- `;
- }
-
- deployKeyRowHtml(key, isActive) {
- const isActiveClass = isActive || '';
-
- return `
- <li>
- <a href="#" class="${isActiveClass}">
- <strong>${escape(key.title)}</strong>
- <p>
- ${sprintf(
- __('Owned by %{image_tag}'),
- {
- image_tag: `<img src="${key.avatar_url}" class="avatar avatar-inline s26" width="30">`,
- },
- false,
- )}
- <strong class="dropdown-menu-user-full-name gl-display-inline">${escape(
- key.fullname,
- )}</strong>
- <span class="dropdown-menu-user-username gl-display-inline">${key.username}</span>
- </p>
- </a>
- </li>
- `;
- }
-
- groupRowHtml(group, isActive) {
- const isActiveClass = isActive || '';
- const avatarEl = group.avatar_url
- ? `<img src="${group.avatar_url}" class="avatar avatar-inline" width="30">`
- : '';
-
- return `
- <li>
- <a href="#" class="${isActiveClass}">
- ${avatarEl}
- <span class="dropdown-menu-group-groupname">${group.name}</span>
- </a>
- </li>
- `;
- }
-
- roleRowHtml(role, isActive) {
- const isActiveClass = isActive || '';
-
- return `
- <li>
- <a href="#" class="${isActiveClass} item-${role.type}" data-role-id="${role.id}">
- ${escape(role.text)}
- </a>
- </li>
- `;
- }
-}
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 df99aac6b9e..b886bf43b57 100644
--- a/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
+++ b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
@@ -1,7 +1,9 @@
+import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
+import { ACCESS_LEVEL_DEVELOPER_INTEGER } from '~/access_level/constants';
-const USERS_PATH = '/-/autocomplete/users.json';
const GROUPS_PATH = '/-/autocomplete/project_groups.json';
+const USERS_PATH = '/-/autocomplete/users.json';
const DEPLOY_KEYS_PATH = '/-/autocomplete/deploy_keys_with_owners.json';
const buildUrl = (urlRoot, url) => {
@@ -26,10 +28,14 @@ export const getUsers = (query, states) => {
};
export const getGroups = () => {
- return axios.get(buildUrl(gon.relative_url_root || '', GROUPS_PATH), {
- params: {
- project_id: gon.current_project_id,
- },
+ 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;
});
};
diff --git a/app/assets/javascripts/projects/settings/components/access_dropdown.vue b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
index a2e4827cbfa..ca24e948f69 100644
--- a/app/assets/javascripts/projects/settings/components/access_dropdown.vue
+++ b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
@@ -12,13 +12,14 @@ import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } fro
import { createAlert } from '~/alert';
import { __, s__, n__ } from '~/locale';
import { getUsers, getGroups, getDeployKeys } from '../api/access_dropdown_api';
-import { LEVEL_TYPES, ACCESS_LEVELS } from '../constants';
+import { LEVEL_TYPES, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from '../constants';
export const i18n = {
- selectUsers: s__('ProtectedEnvironment|Select users'),
+ defaultLabel: s__('AccessDropdown|Select'),
rolesSectionHeader: s__('AccessDropdown|Roles'),
groupsSectionHeader: s__('AccessDropdown|Groups'),
usersSectionHeader: s__('AccessDropdown|Users'),
+ noRole: s__('AccessDropdown|No role'),
deployKeysSectionHeader: s__('AccessDropdown|Deploy Keys'),
ownedBy: __('Owned by %{image_tag}'),
};
@@ -51,7 +52,7 @@ export default {
label: {
type: String,
required: false,
- default: i18n.selectUsers,
+ default: i18n.defaultLabel,
},
disabled: {
type: Boolean,
@@ -68,6 +69,31 @@ export default {
required: false,
default: () => [],
},
+ toggleClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ searchEnabled: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ block: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ testId: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
+ showUsers: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -96,6 +122,9 @@ export default {
this.deployKeys.length
);
},
+ isAccessesLevelNoneSelected() {
+ return this.selected.role[0].id === ACCESS_LEVEL_NONE;
+ },
toggleLabel() {
const counts = Object.entries(this.selected).reduce((acc, [key, value]) => {
acc[key] = value.length;
@@ -115,7 +144,11 @@ export default {
const labelPieces = [];
if (counts[LEVEL_TYPES.ROLE] > 0) {
- labelPieces.push(n__('1 role', '%d roles', counts[LEVEL_TYPES.ROLE]));
+ if (this.isAccessesLevelNoneSelected) {
+ labelPieces.push(this.$options.i18n.noRole);
+ } else {
+ labelPieces.push(n__('1 role', '%d roles', counts[LEVEL_TYPES.ROLE]));
+ }
}
if (counts[LEVEL_TYPES.USER] > 0) {
@@ -132,8 +165,14 @@ export default {
return labelPieces.join(', ') || this.label;
},
- toggleClass() {
- return this.toggleLabel === this.label ? 'gl-text-gray-500!' : '';
+ fossWithMergeAccess() {
+ return !this.hasLicense && this.accessLevel === ACCESS_LEVELS.MERGE;
+ },
+ dropdownToggleClass() {
+ return {
+ 'gl-text-gray-500!': this.toggleLabel === this.label,
+ [this.toggleClass]: true,
+ };
},
selection() {
return [
@@ -180,7 +219,7 @@ export default {
);
},
focusInput() {
- this.$refs.search.focusInput();
+ this.$refs.search?.focusInput();
},
getData({ initial = false } = {}) {
this.initialLoading = initial;
@@ -190,10 +229,10 @@ export default {
Promise.all([
getDeployKeys(this.query),
getUsers(this.query),
- this.groups.length ? Promise.resolve({ data: this.groups }) : getGroups(),
+ this.groups.length ? Promise.resolve(this.groups) : getGroups(),
])
.then(([deployKeysResponse, usersResponse, groupsResponse]) => {
- this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data);
+ this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse);
this.setSelected({ initial });
})
.catch(() =>
@@ -224,13 +263,18 @@ export default {
if (this.hasLicense) {
this.groups = groupsResponse.map((group) => ({ ...group, type: LEVEL_TYPES.GROUP }));
- this.users = usersResponse.map(({ id, name, username, avatar_url }) => ({
- id,
- name,
- username,
- avatar_url,
- type: LEVEL_TYPES.USER,
- }));
+
+ // Has to be checked against server response
+ // because the selected item can be in filter results
+ if (this.showUsers) {
+ this.users = usersResponse.map(({ id, name, username, avatar_url }) => ({
+ id,
+ name,
+ username,
+ avatar_url,
+ type: LEVEL_TYPES.USER,
+ }));
+ }
}
this.deployKeys = deployKeysResponse.map((response) => {
@@ -328,14 +372,38 @@ export default {
return [...added, ...removed, ...preserved];
},
onItemClick(item) {
- this.toggleSelection(this.selected[item.type], item);
+ this.toggleSelection(item);
this.emitUpdate();
},
- toggleSelection(arr, item) {
- const itemIndex = arr.findIndex(({ id }) => id === item.id);
- if (itemIndex > -1) {
- arr.splice(itemIndex, 1);
- } else arr.push(item);
+ toggleSelection(item) {
+ if (item.id === ACCESS_LEVEL_NONE) {
+ this.selected[LEVEL_TYPES.ROLE] = [item];
+ return;
+ }
+
+ const itemSelected = this.isSelected(item);
+ if (itemSelected) {
+ this.selected[item.type] = this.selected[item.type].filter(({ id }) => id !== item.id);
+ return;
+ }
+
+ // We're not multiselecting quite yet in "Merge" access dropdown, on FOSS:
+ // remove all preselected items before selecting this item
+ // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37499
+ if (this.fossWithMergeAccess) this.clearSelection();
+ else if (item.type === LEVEL_TYPES.ROLE) this.unselectNone();
+
+ this.selected[item.type].push(item);
+ },
+ unselectNone() {
+ this.selected[LEVEL_TYPES.ROLE] = this.selected[LEVEL_TYPES.ROLE].filter(
+ ({ id }) => id !== ACCESS_LEVEL_NONE,
+ );
+ },
+ clearSelection() {
+ Object.values(LEVEL_TYPES).forEach((level) => {
+ this.selected[level] = [];
+ });
},
isSelected(item) {
return this.selected[item.type].some((selected) => selected.id === item.id);
@@ -346,6 +414,10 @@ export default {
onHide() {
this.$emit('hidden', this.selection);
},
+ onShown() {
+ this.$emit('shown');
+ this.focusInput();
+ },
},
};
</script>
@@ -354,13 +426,15 @@ export default {
<gl-dropdown
:disabled="disabled || initialLoading"
:text="toggleLabel"
- class="gl-min-w-20"
- :toggle-class="toggleClass"
+ :block="block"
+ class="gl-min-w-20 gl-p-0!"
+ :toggle-class="dropdownToggleClass"
aria-labelledby="allowed-users-label"
- @shown="focusInput"
+ :data-testid="testId"
+ @shown="onShown"
@hidden="onHide"
>
- <template #header>
+ <template v-if="searchEnabled" #header>
<gl-search-box-by-type ref="search" v-model.trim="query" :is-loading="loading" />
</template>
<template v-if="roles.length">
diff --git a/app/assets/javascripts/projects/settings/constants.js b/app/assets/javascripts/projects/settings/constants.js
index 595cbc9c991..37a9fe0c741 100644
--- a/app/assets/javascripts/projects/settings/constants.js
+++ b/app/assets/javascripts/projects/settings/constants.js
@@ -7,13 +7,6 @@ export const LEVEL_TYPES = {
GROUP: 'group',
};
-export const LEVEL_ID_PROP = {
- ROLE: 'access_level',
- USER: 'user_id',
- DEPLOY_KEY: 'deploy_key_id',
- GROUP: 'group_id',
-};
-
export const ACCESS_LEVELS = {
MERGE: 'merge_access_levels',
PUSH: 'push_access_levels',
diff --git a/app/assets/javascripts/projects/settings/init_access_dropdown.js b/app/assets/javascripts/projects/settings/init_access_dropdown.js
index 941efaef3bc..67afbee3854 100644
--- a/app/assets/javascripts/projects/settings/init_access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/init_access_dropdown.js
@@ -7,8 +7,8 @@ export const initAccessDropdown = (el, options) => {
return null;
}
- const { accessLevelsData, accessLevel } = options;
- const { label, disabled, preselectedItems } = el.dataset;
+ const { accessLevelsData, ...props } = options;
+ const { label, disabled, preselectedItems } = el.dataset || {};
let preselected = [];
try {
preselected = JSON.parse(preselectedItems);
@@ -18,20 +18,35 @@ export const initAccessDropdown = (el, options) => {
return new Vue({
el,
+ name: 'AccessDropdownRoot',
+ data() {
+ return { preselected };
+ },
+ methods: {
+ setPreselectedItems(items) {
+ this.preselected = items;
+ },
+ },
render(createElement) {
const vm = this;
return createElement(AccessDropdown, {
props: {
- accessLevel,
- accessLevelsData: accessLevelsData.roles,
- preselectedItems: preselected,
label,
disabled,
+ accessLevelsData: accessLevelsData.roles,
+ preselectedItems: this.preselected,
+ ...props,
},
on: {
select(selected) {
vm.$emit('select', selected);
},
+ shown() {
+ vm.$emit('shown');
+ },
+ hidden() {
+ vm.$emit('hidden');
+ },
},
});
},
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
index 4affcd926d4..09bc275cbd4 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
@@ -1,5 +1,14 @@
<script>
-import { GlButton, GlForm, GlFormGroup, GlFormInputGroup, GlFormInput } from '@gitlab/ui';
+import {
+ GlButton,
+ GlForm,
+ GlFormGroup,
+ GlFormInputGroup,
+ GlFormInput,
+ GlLink,
+ GlSprintf,
+} from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
import { isEmptyValue, hasMinimumLength, isIntegerGreaterThan, isEmail } from '~/lib/utils/forms';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
@@ -23,6 +32,9 @@ import {
} from '../custom_email_constants';
export default {
+ customEmailHelpUrl: helpPagePath('user/project/service_desk/configure.html', {
+ anchor: 'custom-email-address',
+ }),
components: {
ClipboardButton,
GlButton,
@@ -30,6 +42,8 @@ export default {
GlFormGroup,
GlFormInputGroup,
GlFormInput,
+ GlLink,
+ GlSprintf,
},
I18N_FORM_INTRODUCTION_PARAGRAPH,
I18N_FORM_CUSTOM_EMAIL_LABEL,
@@ -137,7 +151,19 @@ export default {
<template>
<div>
- <p>{{ $options.I18N_FORM_INTRODUCTION_PARAGRAPH }}</p>
+ <p>
+ <gl-sprintf :message="$options.I18N_FORM_INTRODUCTION_PARAGRAPH">
+ <template #link="{ content }">
+ <gl-link
+ :href="$options.customEmailHelpUrl"
+ class="gl-display-inline-block"
+ target="_blank"
+ >
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
<gl-form class="js-quick-submit" @submit.prevent="onSubmit">
<gl-form-group
:label="$options.I18N_FORM_FORWARDING_LABEL"
@@ -149,7 +175,6 @@ export default {
id="custom-email-form-forwarding"
ref="service-desk-incoming-email"
type="text"
- data-testid="custom-email-form-forwarding"
:aria-label="$options.I18N_FORM_FORWARDING_LABEL"
:value="incomingEmail"
:disabled="true"
@@ -167,7 +192,6 @@ export default {
<gl-form-group
:label="$options.I18N_FORM_CUSTOM_EMAIL_LABEL"
label-for="custom-email-form-custom-email"
- data-testid="form-group-custom-email"
:invalid-feedback="$options.I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL"
class="gl-mt-3"
:description="$options.I18N_FORM_CUSTOM_EMAIL_DESCRIPTION"
@@ -191,7 +215,6 @@ export default {
<gl-form-group
:label="$options.I18N_FORM_SMTP_ADDRESS_LABEL"
label-for="custom-email-form-smtp-address"
- data-testid="form-group-smtp-address"
:invalid-feedback="$options.I18N_FORM_INVALID_FEEDBACK_SMTP_ADDRESS"
class="gl-mt-3"
>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
index 7e040e6001a..03ba99bcf71 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
@@ -233,6 +233,7 @@ export default {
<gl-link
:href="$options.FEEDBACK_ISSUE_URL"
target="_blank"
+ data-testid="feedback-link"
class="gl-text-blue-600 font-size-inherit"
>{{ content }}
</gl-link>
diff --git a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
index cdf2e53982e..aafd77bd25e 100644
--- a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
+++ b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
@@ -17,7 +17,7 @@ export const I18N_TOAST_ENABLED = s__('ServiceDesk|Custom email enabled.');
export const I18N_TOAST_DISABLED = s__('ServiceDesk|Custom email disabled.');
export const I18N_FORM_INTRODUCTION_PARAGRAPH = s__(
- 'ServiceDesk|Connect a custom email address your customers can use to create Service Desk issues. Forward all emails from your custom email address to the Service Desk email address of this project. GitLab will send Service Desk emails from the custom address on your behalf using your SMTP credentials.',
+ 'ServiceDesk|Connect a custom email address your customers can use to create Service Desk issues. Forward all emails from your custom email address to the Service Desk email address of this project. GitLab will send Service Desk emails from the custom address on your behalf using your SMTP credentials. %{linkStart}Learn more about prerequisites and the verification process%{linkEnd}.',
);
export const I18N_FORM_FORWARDING_LABEL = s__(
'ServiceDesk|Service Desk email address to forward emails to',
diff --git a/app/assets/javascripts/projects/star.js b/app/assets/javascripts/projects/star.js
deleted file mode 100644
index f294811dfff..00000000000
--- a/app/assets/javascripts/projects/star.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import { spriteIcon } from '~/lib/utils/common_utils';
-import { __, s__ } from '~/locale';
-
-export default class Star {
- constructor(containerSelector = '.project-home-panel') {
- const container = document.querySelector(containerSelector);
- const starToggle = container.querySelector('.toggle-star');
- starToggle?.addEventListener('click', function toggleStarClickCallback() {
- const starSpan = starToggle.querySelector('span');
- const starIcon = starToggle.querySelector('svg');
- const iconClasses = Array.from(starIcon.classList.values());
-
- axios
- .post(starToggle.dataset.endpoint)
- .then(({ data }) => {
- const isStarred = starSpan.classList.contains('starred');
- starToggle.parentNode.querySelector('.count').textContent = data.star_count;
-
- if (isStarred) {
- starSpan.classList.remove('starred');
- starSpan.textContent = s__('StarProject|Star');
- starIcon.remove();
- // eslint-disable-next-line no-unsanitized/method
- starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star-o', iconClasses));
- } else {
- starSpan.classList.add('starred');
- starSpan.textContent = __('Unstar');
- starIcon.remove();
-
- // eslint-disable-next-line no-unsanitized/method
- starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star', iconClasses));
- }
- })
- .catch(() =>
- createAlert({
- message: __('Star toggle failed. Try again later.'),
- }),
- );
- });
- }
-}
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index a11201627a4..49c55efca7b 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -4,16 +4,15 @@ import { createAlert, VARIANT_SUCCESS } from '~/alert';
import AccessorUtilities from '~/lib/utils/accessor';
import axios from '~/lib/utils/axios_utils';
import { __, s__ } from '~/locale';
-import AccessDropdown from '~/projects/settings/access_dropdown';
import { initToggle } from '~/toggles';
import { expandSection } from '~/settings_panels';
import { scrollToElement } from '~/lib/utils/common_utils';
+import { initAccessDropdown } from '~/projects/settings/init_access_dropdown';
import {
BRANCH_RULES_ANCHOR,
PROTECTED_BRANCHES_ANCHOR,
IS_PROTECTED_BRANCH_CREATED,
ACCESS_LEVELS,
- LEVEL_TYPES,
} from './constants';
export default class ProtectedBranchCreate {
@@ -21,14 +20,17 @@ export default class ProtectedBranchCreate {
this.hasLicense = options.hasLicense;
this.$form = $('.js-new-protected-branch');
this.isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
- this.currentProjectUserDefaults = {};
- this.buildDropdowns();
-
this.forcePushToggle = initToggle(document.querySelector('.js-force-push-toggle'));
-
if (this.hasLicense) {
this.codeOwnerToggle = initToggle(document.querySelector('.js-code-owner-toggle'));
}
+
+ this.selectedItems = {
+ [ACCESS_LEVELS.PUSH]: [],
+ [ACCESS_LEVELS.MERGE]: [],
+ };
+ this.initDropdowns();
+
this.showSuccessAlertIfNeeded();
this.bindEvents();
}
@@ -37,29 +39,26 @@ export default class ProtectedBranchCreate {
this.$form.on('submit', this.onFormSubmit.bind(this));
}
- buildDropdowns() {
- const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge');
- const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push');
-
+ initDropdowns() {
// Cache callback
this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Merge dropdown
- this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({
- $dropdown: $allowedToMergeDropdown,
- accessLevelsData: gon.merge_access_levels,
- onSelect: this.onSelectCallback,
+ const allowedToMergeSelector = 'js-allowed-to-merge';
+ this[`${ACCESS_LEVELS.MERGE}_dropdown`] = this.buildDropdown({
+ selector: allowedToMergeSelector,
accessLevel: ACCESS_LEVELS.MERGE,
- hasLicense: this.hasLicense,
+ accessLevelsData: gon.merge_access_levels,
+ testId: 'allowed-to-merge-dropdown',
});
// Allowed to Push dropdown
- this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({
- $dropdown: $allowedToPushDropdown,
- accessLevelsData: gon.push_access_levels,
- onSelect: this.onSelectCallback,
+ const allowedToPushSelector = 'js-allowed-to-push';
+ this[`${ACCESS_LEVELS.PUSH}_dropdown`] = this.buildDropdown({
+ selector: allowedToPushSelector,
accessLevel: ACCESS_LEVELS.PUSH,
- hasLicense: this.hasLicense,
+ accessLevelsData: gon.push_access_levels,
+ testId: 'allowed-to-push-dropdown',
});
this.createItemDropdown = new CreateItemDropdown({
@@ -71,14 +70,40 @@ export default class ProtectedBranchCreate {
});
}
+ buildDropdown({ selector, accessLevel, accessLevelsData, testId }) {
+ const [el] = this.$form.find(`.${selector}`);
+ if (!el) return undefined;
+
+ const projectId = gon.current_project_id;
+ const dropdown = initAccessDropdown(el, {
+ toggleClass: `${selector} gl-form-input-lg`,
+ hasLicense: this.hasLicense,
+ searchEnabled: el.dataset.filter !== undefined,
+ showUsers: projectId !== undefined,
+ block: true,
+ accessLevel,
+ accessLevelsData,
+ testId,
+ });
+
+ dropdown.$on('select', (selected) => {
+ this.selectedItems[accessLevel] = selected;
+ this.onSelectCallback();
+ });
+
+ dropdown.$on('shown', () => {
+ this.createItemDropdown.close();
+ });
+
+ return dropdown;
+ }
+
// Enable submit button after selecting an option
onSelect() {
- const $allowedToMerge = this[`${ACCESS_LEVELS.MERGE}_dropdown`].getSelectedItems();
- const $allowedToPush = this[`${ACCESS_LEVELS.PUSH}_dropdown`].getSelectedItems();
const toggle = !(
this.$form.find('input[name="protected_branch[name]"]').val() &&
- $allowedToMerge.length &&
- $allowedToPush.length
+ this.selectedItems[ACCESS_LEVELS.MERGE].length &&
+ this.selectedItems[ACCESS_LEVELS.PUSH].length
);
this.$form.find('button[type="submit"]').attr('disabled', toggle);
@@ -137,32 +162,8 @@ export default class ProtectedBranchCreate {
},
};
- Object.keys(ACCESS_LEVELS).forEach((level) => {
- const accessLevel = ACCESS_LEVELS[level];
- const selectedItems = this[`${accessLevel}_dropdown`].getSelectedItems();
- const levelAttributes = [];
-
- selectedItems.forEach((item) => {
- if (item.type === LEVEL_TYPES.USER) {
- levelAttributes.push({
- user_id: item.user_id,
- });
- } else if (item.type === LEVEL_TYPES.ROLE) {
- levelAttributes.push({
- access_level: item.access_level,
- });
- } else if (item.type === LEVEL_TYPES.GROUP) {
- levelAttributes.push({
- group_id: item.group_id,
- });
- } else if (item.type === LEVEL_TYPES.DEPLOY_KEY) {
- levelAttributes.push({
- deploy_key_id: item.deploy_key_id,
- });
- }
- });
-
- formData.protected_branch[`${accessLevel}_attributes`] = levelAttributes;
+ Object.values(ACCESS_LEVELS).forEach((level) => {
+ formData.protected_branch[`${level}_attributes`] = this.selectedItems[level];
});
return formData;
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index b6c86750723..29034b3bc0e 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -2,28 +2,23 @@ import { find } from 'lodash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
-import AccessDropdown from '~/projects/settings/access_dropdown';
import { initToggle } from '~/toggles';
+import { initAccessDropdown } from '~/projects/settings/init_access_dropdown';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
export default class ProtectedBranchEdit {
constructor(options) {
this.hasLicense = options.hasLicense;
- this.$wraps = {};
this.hasChanges = false;
this.$wrap = options.$wrap;
- this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
- this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
- this.$wraps[ACCESS_LEVELS.MERGE] = this.$allowedToMergeDropdown.closest(
- `.${ACCESS_LEVELS.MERGE}-container`,
- );
- this.$wraps[ACCESS_LEVELS.PUSH] = this.$allowedToPushDropdown.closest(
- `.${ACCESS_LEVELS.PUSH}-container`,
- );
+ this.selectedItems = {
+ [ACCESS_LEVELS.PUSH]: [],
+ [ACCESS_LEVELS.MERGE]: [],
+ };
+ this.initDropdowns();
- this.buildDropdowns();
this.initToggles();
}
@@ -67,90 +62,96 @@ export default class ProtectedBranchEdit {
}
}
- updateProtectedBranch(formData, callback) {
- axios
- .patch(this.$wrap.data('url'), {
- protected_branch: formData,
- })
- .then(callback)
- .catch(() => {
- createAlert({ message: __('Failed to update branch!') });
- });
+ initDropdowns() {
+ // Allowed to Merge dropdown
+ this[`${ACCESS_LEVELS.MERGE}_dropdown`] = this.buildDropdown(
+ 'js-allowed-to-merge',
+ ACCESS_LEVELS.MERGE,
+ gon.merge_access_levels,
+ 'protected-branch-allowed-to-merge',
+ );
+
+ // Allowed to Push dropdown
+ this[`${ACCESS_LEVELS.PUSH}_dropdown`] = this.buildDropdown(
+ 'js-allowed-to-push',
+ ACCESS_LEVELS.PUSH,
+ gon.push_access_levels,
+ 'protected-branch-allowed-to-push',
+ );
}
- buildDropdowns() {
- // Allowed to merge dropdown
- this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({
- accessLevel: ACCESS_LEVELS.MERGE,
- accessLevelsData: gon.merge_access_levels,
- $dropdown: this.$allowedToMergeDropdown,
- onSelect: this.onSelectOption.bind(this),
- onHide: this.onDropdownHide.bind(this),
+ buildDropdown(selector, accessLevel, accessLevelsData, testId) {
+ const [el] = this.$wrap.find(`.${selector}`);
+ if (!el) return undefined;
+
+ const projectId = gon.current_project_id;
+ const dropdown = initAccessDropdown(el, {
+ toggleClass: selector,
hasLicense: this.hasLicense,
+ searchEnabled: el.dataset.filter !== undefined,
+ showUsers: projectId !== undefined,
+ block: true,
+ accessLevel,
+ accessLevelsData,
+ testId,
});
- // Allowed to push dropdown
- this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({
- accessLevel: ACCESS_LEVELS.PUSH,
- accessLevelsData: gon.push_access_levels,
- $dropdown: this.$allowedToPushDropdown,
- onSelect: this.onSelectOption.bind(this),
- onHide: this.onDropdownHide.bind(this),
- hasLicense: this.hasLicense,
+ dropdown.$on('select', (selected) => this.onSelectItems(accessLevel, selected));
+ dropdown.$on('hidden', () => this.onDropdownHide());
+
+ this.initSelectedItems(dropdown, accessLevel);
+ return dropdown;
+ }
+
+ initSelectedItems(dropdown, accessLevel) {
+ this.selectedItems[accessLevel] = dropdown.preselected.map((item) => {
+ if (item.type === LEVEL_TYPES.USER) return { id: item.id, user_id: item.user_id };
+ if (item.type === LEVEL_TYPES.ROLE) return { id: item.id, access_level: item.access_level };
+ if (item.type === LEVEL_TYPES.GROUP) return { id: item.id, group_id: item.group_id };
+ return { id: item.id, deploy_key_id: item.deploy_key_id };
});
}
- onSelectOption() {
+ onSelectItems(accessLevel, selected) {
+ this.selectedItems[accessLevel] = selected;
this.hasChanges = true;
}
onDropdownHide() {
- if (!this.hasChanges) {
- return;
- }
-
- this.hasChanges = true;
+ if (!this.hasChanges) return;
this.updatePermissions();
}
- updatePermissions() {
- const formData = Object.keys(ACCESS_LEVELS).reduce((acc, level) => {
- const accessLevelName = ACCESS_LEVELS[level];
- const inputData = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName);
- acc[`${accessLevelName}_attributes`] = inputData;
-
- return acc;
- }, {});
-
+ updateProtectedBranch(formData, callback) {
axios
.patch(this.$wrap.data('url'), {
protected_branch: formData,
})
- .then(({ data }) => {
- this.hasChanges = false;
-
- Object.keys(ACCESS_LEVELS).forEach((level) => {
- const accessLevelName = ACCESS_LEVELS[level];
-
- // The data coming from server will be the new persisted *state* for each dropdown
- this.setSelectedItemsToDropdown(data[accessLevelName], `${accessLevelName}_dropdown`);
- });
- this.$allowedToMergeDropdown.enable();
- this.$allowedToPushDropdown.enable();
- })
+ .then(callback)
.catch(() => {
- this.$allowedToMergeDropdown.enable();
- this.$allowedToPushDropdown.enable();
createAlert({ message: __('Failed to update branch!') });
});
}
- setSelectedItemsToDropdown(items = [], dropdownName) {
+ updatePermissions() {
+ const formData = Object.values(ACCESS_LEVELS).reduce((acc, level) => {
+ acc[`${level}_attributes`] = this.selectedItems[level];
+ return acc;
+ }, {});
+ this.updateProtectedBranch(formData, ({ data }) => {
+ this.hasChanges = false;
+ Object.values(ACCESS_LEVELS).forEach((level) => {
+ this.setSelectedItemsToDropdown(data[level], level);
+ });
+ });
+ }
+
+ setSelectedItemsToDropdown(items = [], accessLevel) {
const itemsToAdd = items.map((currentItem) => {
if (currentItem.user_id) {
// Do this only for users for now
// get the current data for selected items
- const selectedItems = this[dropdownName].getSelectedItems();
+ const selectedItems = this.selectedItems[accessLevel];
const currentSelectedItem = find(selectedItems, {
user_id: currentItem.user_id,
});
@@ -164,7 +165,8 @@ export default class ProtectedBranchEdit {
username: currentSelectedItem.username,
avatar_url: currentSelectedItem.avatar_url,
};
- } else if (currentItem.group_id) {
+ }
+ if (currentItem.group_id) {
return {
id: currentItem.id,
group_id: currentItem.group_id,
@@ -181,6 +183,7 @@ export default class ProtectedBranchEdit {
};
});
- this[dropdownName].setSelectedItems(itemsToAdd);
+ this.selectedItems[accessLevel] = itemsToAdd;
+ this[`${accessLevel}_dropdown`]?.setPreselectedItems(itemsToAdd);
}
}
diff --git a/app/assets/javascripts/protected_tags/constants.js b/app/assets/javascripts/protected_tags/constants.js
index 758b820c4c4..d332ba1635f 100644
--- a/app/assets/javascripts/protected_tags/constants.js
+++ b/app/assets/javascripts/protected_tags/constants.js
@@ -1,7 +1,3 @@
-import { s__ } from '~/locale';
-
-export const FAILED_TO_UPDATE_TAG_MESSAGE = s__('ProjectSettings|Failed to update tag!');
-
export const ACCESS_LEVELS = {
CREATE: 'create_access_levels',
};
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index 365b9a3b142..b5661af352c 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -3,8 +3,8 @@ import CreateItemDropdown from '~/create_item_dropdown';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { s__, __ } from '~/locale';
-import AccessDropdown from '~/projects/settings/access_dropdown';
-import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
+import { initAccessDropdown } from '~/projects/settings/init_access_dropdown';
+import { ACCESS_LEVELS } from './constants';
export default class ProtectedTagCreate {
constructor({ hasLicense }) {
@@ -12,6 +12,7 @@ export default class ProtectedTagCreate {
this.$form = $('.js-new-protected-tag');
this.buildDropdowns();
this.bindEvents();
+ this.selectedItems = [];
}
bindEvents() {
@@ -19,20 +20,9 @@ export default class ProtectedTagCreate {
}
buildDropdowns() {
- const $allowedToCreateDropdown = this.$form.find('.js-allowed-to-create');
-
// Cache callback
this.onSelectCallback = this.onSelect.bind(this);
- // Allowed to Create dropdown
- this.protectedTagAccessDropdown = new AccessDropdown({
- $dropdown: $allowedToCreateDropdown,
- accessLevelsData: gon.create_access_levels,
- onSelect: this.onSelectCallback,
- accessLevel: ACCESS_LEVELS.CREATE,
- hasLicense: this.hasLicense,
- });
-
// Protected tag dropdown
this.createItemDropdown = new CreateItemDropdown({
$dropdown: this.$form.find('.js-protected-tag-select'),
@@ -41,17 +31,36 @@ export default class ProtectedTagCreate {
onSelect: this.onSelectCallback,
getData: ProtectedTagCreate.getProtectedTags,
});
+
+ // Allowed to Create dropdown
+ const createTagSelector = 'js-allowed-to-create';
+ const [dropdownEl] = this.$form.find(`.${createTagSelector}`);
+ this.protectedTagAccessDropdown = initAccessDropdown(dropdownEl, {
+ toggleClass: createTagSelector,
+ hasLicense: this.hasLicense,
+ accessLevel: ACCESS_LEVELS.CREATE,
+ accessLevelsData: gon.create_access_levels,
+ searchEnabled: dropdownEl.dataset.filter !== undefined,
+ testId: 'allowed_to_create_dropdown',
+ });
+
+ this.protectedTagAccessDropdown.$on('select', (selected) => {
+ this.selectedItems = selected;
+ this.onSelectCallback();
+ });
+
+ this.protectedTagAccessDropdown.$on('shown', () => {
+ this.createItemDropdown.close();
+ });
}
// This will run after clicked callback
onSelect() {
// Enable submit button
const $tagInput = this.$form.find('input[name="protected_tag[name]"]');
- const $allowedToCreateInput = this.protectedTagAccessDropdown.getSelectedItems();
-
this.$form
.find('button[type="submit"]')
- .prop('disabled', !($tagInput.val() && $allowedToCreateInput.length));
+ .prop('disabled', !($tagInput.val() && this.selectedItems.length));
}
static getProtectedTags(term, callback) {
@@ -65,35 +74,7 @@ export default class ProtectedTagCreate {
name: this.$form.find('input[name="protected_tag[name]"]').val(),
},
};
-
- Object.keys(ACCESS_LEVELS).forEach((level) => {
- const accessLevel = ACCESS_LEVELS[level];
- const selectedItems = this.protectedTagAccessDropdown.getSelectedItems();
- const levelAttributes = [];
-
- selectedItems.forEach((item) => {
- if (item.type === LEVEL_TYPES.USER) {
- levelAttributes.push({
- user_id: item.user_id,
- });
- } else if (item.type === LEVEL_TYPES.ROLE) {
- levelAttributes.push({
- access_level: item.access_level,
- });
- } else if (item.type === LEVEL_TYPES.GROUP) {
- levelAttributes.push({
- group_id: item.group_id,
- });
- } else if (item.type === LEVEL_TYPES.DEPLOY_KEY) {
- levelAttributes.push({
- deploy_key_id: item.deploy_key_id,
- });
- }
- });
-
- formData.protected_tag[`${accessLevel}_attributes`] = levelAttributes;
- });
-
+ formData.protected_tag[`${ACCESS_LEVELS.CREATE}_attributes`] = this.selectedItems;
return formData;
}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js
deleted file mode 100644
index 4fa3ac3be4b..00000000000
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import { find } from 'lodash';
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import AccessDropdown from '~/projects/settings/access_dropdown';
-import { ACCESS_LEVELS, LEVEL_TYPES, FAILED_TO_UPDATE_TAG_MESSAGE } from './constants';
-
-export default class ProtectedTagEdit {
- constructor(options) {
- this.hasLicense = options.hasLicense;
- this.hasChanges = false;
- this.$wrap = options.$wrap;
- this.$allowedToCreateDropdownButton = this.$wrap.find('.js-allowed-to-create');
-
- this.$allowedToCreateDropdownContainer = this.$allowedToCreateDropdownButton.closest(
- '.create_access_levels-container',
- );
-
- this.buildDropdowns();
- }
-
- buildDropdowns() {
- // Allowed to create dropdown
- this.protectedTagAccessDropdown = new AccessDropdown({
- accessLevel: ACCESS_LEVELS.CREATE,
- accessLevelsData: gon.create_access_levels,
- $dropdown: this.$allowedToCreateDropdownButton,
- onSelect: this.onSelectOption.bind(this),
- onHide: this.onDropdownHide.bind(this),
- hasLicense: this.hasLicense,
- });
- }
-
- onSelectOption() {
- this.hasChanges = true;
- }
-
- onDropdownHide() {
- if (!this.hasChanges) {
- return;
- }
-
- this.hasChanges = true;
- this.updatePermissions();
- }
-
- updatePermissions() {
- const formData = Object.keys(ACCESS_LEVELS).reduce((acc, level) => {
- const accessLevelName = ACCESS_LEVELS[level];
- const inputData = this.protectedTagAccessDropdown.getInputData(accessLevelName);
- acc[`${accessLevelName}_attributes`] = inputData;
-
- return acc;
- }, {});
-
- axios
- .patch(this.$wrap.data('url'), {
- protected_tag: formData,
- })
- .then(({ data }) => {
- this.hasChanges = false;
-
- Object.keys(ACCESS_LEVELS).forEach((level) => {
- const accessLevelName = ACCESS_LEVELS[level];
-
- // The data coming from server will be the new persisted *state* for each dropdown
- this.setSelectedItemsToDropdown(data[accessLevelName], `${accessLevelName}_dropdown`);
- });
- })
- .catch(() => {
- window.scrollTo({ top: 0, behavior: 'smooth' });
- createAlert({
- message: FAILED_TO_UPDATE_TAG_MESSAGE,
- });
- });
- }
-
- setSelectedItemsToDropdown(items = []) {
- const itemsToAdd = items.map((currentItem) => {
- if (currentItem.user_id) {
- // Do this only for users for now
- // get the current data for selected items
- const selectedItems = this.protectedTagAccessDropdown.getSelectedItems();
- const currentSelectedItem = find(selectedItems, {
- user_id: currentItem.user_id,
- });
-
- return {
- id: currentItem.id,
- user_id: currentItem.user_id,
- type: LEVEL_TYPES.USER,
- persisted: true,
- name: currentSelectedItem.name,
- username: currentSelectedItem.username,
- avatar_url: currentSelectedItem.avatar_url,
- };
- } else if (currentItem.group_id) {
- return {
- id: currentItem.id,
- group_id: currentItem.group_id,
- type: LEVEL_TYPES.GROUP,
- persisted: true,
- };
- }
-
- return {
- id: currentItem.id,
- access_level: currentItem.access_level,
- type: LEVEL_TYPES.ROLE,
- persisted: true,
- };
- });
-
- this.protectedTagAccessDropdown.setSelectedItems(itemsToAdd);
- }
-}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.vue b/app/assets/javascripts/protected_tags/protected_tag_edit.vue
new file mode 100644
index 00000000000..82b2ecc5f5c
--- /dev/null
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.vue
@@ -0,0 +1,113 @@
+<script>
+import AccessDropdown from '~/projects/settings/components/access_dropdown.vue';
+import { createAlert } from '~/alert';
+import axios from '~/lib/utils/axios_utils';
+import { s__ } from '~/locale';
+import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
+
+export const i18n = {
+ failureMessage: s__('ProjectSettings|Failed to update tag!'),
+};
+
+export default {
+ i18n,
+ ACCESS_LEVELS,
+ name: 'ProtectedTagEdit',
+ components: {
+ AccessDropdown,
+ },
+ props: {
+ url: {
+ type: String,
+ required: true,
+ },
+ accessLevelsData: {
+ type: Array,
+ required: true,
+ },
+ hasLicense: {
+ required: false,
+ type: Boolean,
+ default: true,
+ },
+ preselectedItems: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ searchEnabled: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ selected: this.preselectedItems,
+ };
+ },
+ methods: {
+ hasChanges(permissions) {
+ return permissions.some(({ id, _destroy }) => id === undefined || _destroy);
+ },
+ updatePermissions(permissions) {
+ if (!this.hasChanges(permissions)) return;
+ axios
+ .patch(this.url, {
+ protected_tag: {
+ [`${ACCESS_LEVELS.CREATE}_attributes`]: permissions,
+ },
+ })
+ .then(this.setSelected)
+ .catch(() => {
+ createAlert({
+ message: i18n.failureMessage,
+ parent: this.parentContainer,
+ });
+ });
+ },
+ setSelected({ data }) {
+ if (!data) return;
+ this.selected = data[ACCESS_LEVELS.CREATE].map(
+ ({ id, user_id: userId, group_id: groupId, access_level: accessLevel }) => {
+ if (userId) {
+ return {
+ id,
+ user_id: userId,
+ type: LEVEL_TYPES.USER,
+ };
+ }
+
+ if (groupId) {
+ return {
+ id,
+ group_id: groupId,
+ type: LEVEL_TYPES.GROUP,
+ };
+ }
+
+ return {
+ id,
+ access_level: accessLevel,
+ type: LEVEL_TYPES.ROLE,
+ };
+ },
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <access-dropdown
+ toggle-class="js-allowed-to-create gl-max-w-34"
+ test-id="allowed_to_create_dropdown"
+ :has-license="hasLicense"
+ :access-level="$options.ACCESS_LEVELS.CREATE"
+ :access-levels-data="accessLevelsData"
+ :preselected-items="selected"
+ :search-enabled="searchEnabled"
+ :block="true"
+ @hidden="updatePermissions"
+ />
+</template>
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
index 8ceb970bf03..444d6e9cf76 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
@@ -1,21 +1,49 @@
-/* eslint-disable no-new */
-
-import $ from 'jquery';
-import ProtectedTagEdit from './protected_tag_edit';
+import Vue from 'vue';
+import * as Sentry from '@sentry/browser';
+import ProtectedTagEdit from './protected_tag_edit.vue';
export default class ProtectedTagEditList {
constructor(options) {
this.hasLicense = options.hasLicense;
- this.$wrap = $('.protected-tags-list');
this.initEditForm();
}
initEditForm() {
- this.$wrap.find('.js-protected-tag-edit-form').each((i, el) => {
- new ProtectedTagEdit({
- $wrap: $(el),
- hasLicense: this.hasLicense,
+ document
+ .querySelector('.protected-tags-list')
+ .querySelectorAll('.js-protected-tag-edit-form')
+ ?.forEach((el) => {
+ const accessDropdownEl = el.querySelector('.js-allowed-to-create');
+ this.initAccessDropdown(accessDropdownEl, {
+ url: el.dataset.url,
+ hasLicense: this.hasLicense,
+ accessLevelsData: gon.create_access_levels.roles,
+ });
});
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ initAccessDropdown(el, options) {
+ if (!el) return null;
+
+ let preselected = [];
+ try {
+ preselected = JSON.parse(el.dataset.preselectedItems);
+ } catch (e) {
+ Sentry.captureException(e);
+ }
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(ProtectedTagEdit, {
+ props: {
+ preselectedItems: preselected,
+ searchEnabled: el.dataset.filter !== undefined,
+ ...options,
+ },
+ });
+ },
});
}
}
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 b3033ddf3b6..d36b29f69a5 100644
--- a/app/assets/javascripts/related_issues/components/add_issuable_form.vue
+++ b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
@@ -115,7 +115,8 @@ export default {
addRelatedErrorMessage() {
if (this.itemAddFailureMessage) {
return this.itemAddFailureMessage;
- } else if (this.itemAddFailureType === itemAddFailureTypesMap.NOT_FOUND) {
+ }
+ if (this.itemAddFailureType === itemAddFailureTypesMap.NOT_FOUND) {
return addRelatedIssueErrorMap[this.issuableType];
}
// Only other failure is MAX_NUMBER_OF_CHILD_EPICS at the moment
diff --git a/app/assets/javascripts/related_issues/index.js b/app/assets/javascripts/related_issues/index.js
index 23620432feb..cc0dae355b6 100644
--- a/app/assets/javascripts/related_issues/index.js
+++ b/app/assets/javascripts/related_issues/index.js
@@ -1,10 +1,9 @@
import Vue from 'vue';
-import { TYPE_ISSUE } from '~/issues/constants';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import { parseBoolean } from '~/lib/utils/common_utils';
import RelatedIssuesRoot from './components/related_issues_root.vue';
-export function initRelatedIssues(issueType = TYPE_ISSUE) {
+export function initRelatedIssues() {
const el = document.querySelector('.js-related-issues-root');
if (!el) {
@@ -28,7 +27,7 @@ export function initRelatedIssues(issueType = TYPE_ISSUE) {
canAdmin: parseBoolean(el.dataset.canAddRelatedIssues),
helpPath: el.dataset.helpPath,
showCategorizedIssues: parseBoolean(el.dataset.showCategorizedIssues),
- issuableType: issueType,
+ issuableType: el.dataset.issuableType,
autoCompleteEpics: false,
},
}),
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 6f9f0a81dfd..6565c84fa11 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -7,11 +7,9 @@ import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constant
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { isLoggedIn, handleLocationHash } from '~/lib/utils/common_utils';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import { redirectTo, getLocationHash } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import WebIdeLink from 'ee_else_ce/vue_shared/components/web_ide_link.vue';
import CodeIntelligence from '~/code_navigation/components/app.vue';
import LineHighlighter from '~/blob/line_highlighter';
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
@@ -19,15 +17,7 @@ import { addBlameLink } from '~/blob/blob_blame_link';
import highlightMixin from '~/repository/mixins/highlight_mixin';
import projectInfoQuery from '../queries/project_info.query.graphql';
import getRefMixin from '../mixins/get_ref';
-import userInfoQuery from '../queries/user_info.query.graphql';
-import applicationInfoQuery from '../queries/application_info.query.graphql';
-import {
- DEFAULT_BLOB_INFO,
- TEXT_FILE_TYPE,
- LFS_STORAGE,
- LEGACY_FILE_TYPES,
- CODEOWNERS_FILE_NAME,
-} from '../constants';
+import { DEFAULT_BLOB_INFO, TEXT_FILE_TYPE, LFS_STORAGE, LEGACY_FILE_TYPES } from '../constants';
import BlobButtonGroup from './blob_button_group.vue';
import ForkSuggestion from './fork_suggestion.vue';
import { loadViewer } from './blob_viewers';
@@ -38,10 +28,8 @@ export default {
BlobButtonGroup,
BlobContent,
GlLoadingIcon,
- CodeownersValidation: () => import('ee_component/blob/components/codeowners_validation.vue'),
GlButton,
ForkSuggestion,
- WebIdeLink,
CodeIntelligence,
AiGenie: () => import('ee_component/ai/components/ai_genie.vue'),
},
@@ -68,18 +56,6 @@ export default {
this.userPermissions = project.userPermissions;
},
},
- gitpodEnabled: {
- query: applicationInfoQuery,
- error() {
- this.displayError();
- },
- },
- currentUser: {
- query: userInfoQuery,
- error() {
- this.displayError();
- },
- },
project: {
query: blobInfoQuery,
variables() {
@@ -94,32 +70,17 @@ export default {
return queryVariables;
},
result({ data }) {
- const blob = data.project?.repository?.blobs?.nodes[0] || {};
- this.initHighlightWorker(blob);
+ const repository = data.project?.repository || {};
+ this.blobInfo = repository.blobs?.nodes[0] || {};
+ this.isEmptyRepository = repository.empty;
+ this.projectId = data.project?.id;
- const urlHash = getLocationHash();
- const plain = this.$route?.query?.plain;
+ const usePlain = this.$route?.query?.plain === '1'; // When the 'plain' URL param is present, its value determines which viewer to render
+ const urlHash = getLocationHash(); // If there is a code line hash in the URL we render with the simple viewer
+ const useSimpleViewer = usePlain || urlHash?.startsWith('L') || !this.hasRichViewer;
- // When the 'plain' URL param is present, its value determines which viewer to render:
- // - when 0 and the rich viewer is available we render with it
- // - otherwise we render the simple viewer
- if (plain !== undefined) {
- if (plain === '0' && this.hasRichViewer) {
- this.switchViewer(RICH_BLOB_VIEWER);
- } else {
- this.switchViewer(SIMPLE_BLOB_VIEWER);
- }
- return;
- }
-
- // If there is a code line hash in the URL we render with the simple viewer
- if (urlHash && urlHash.startsWith('L')) {
- this.switchViewer(SIMPLE_BLOB_VIEWER);
- return;
- }
-
- // By default, if present, use the rich viewer to render
- this.switchViewer(this.hasRichViewer ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER);
+ this.initHighlightWorker(this.blobInfo);
+ this.switchViewer(useSimpleViewer ? SIMPLE_BLOB_VIEWER : RICH_BLOB_VIEWER); // By default, if present, use the rich viewer to render
},
error() {
this.displayError();
@@ -127,9 +88,7 @@ export default {
},
},
provide() {
- return {
- blobHash: uniqueId(),
- };
+ return { blobHash: uniqueId() };
},
props: {
path: {
@@ -156,11 +115,13 @@ export default {
isRenderingLegacyTextViewer: false,
activeViewerType: SIMPLE_BLOB_VIEWER,
project: DEFAULT_BLOB_INFO.project,
- gitpodEnabled: DEFAULT_BLOB_INFO.gitpodEnabled,
currentUser: DEFAULT_BLOB_INFO.currentUser,
useFallback: false,
pathLocks: DEFAULT_BLOB_INFO.pathLocks,
userPermissions: DEFAULT_BLOB_INFO.userPermissions,
+ blobInfo: {},
+ isEmptyRepository: false,
+ projectId: null,
};
},
computed: {
@@ -173,17 +134,9 @@ export default {
isBinaryFileType() {
return this.isBinary || this.blobInfo.simpleViewer?.fileType !== TEXT_FILE_TYPE;
},
- blobInfo() {
- const nodes = this.project?.repository?.blobs?.nodes || [];
-
- return nodes[0] || {};
- },
currentRef() {
return this.originalBranch || this.ref;
},
- isCodeownersFile() {
- return this.path.includes(CODEOWNERS_FILE_NAME);
- },
viewer() {
const { richViewer, simpleViewer } = this.blobInfo;
return this.activeViewerType === RICH_BLOB_VIEWER ? richViewer : simpleViewer;
@@ -249,22 +202,14 @@ export default {
isUsingLfs() {
return this.blobInfo.storedExternally && this.blobInfo.externalStorage === LFS_STORAGE;
},
- projectIdAsNumber() {
- return getIdFromGraphQLId(this.project?.id);
- },
},
watch: {
// Watch the URL 'plain' query value to know if the viewer needs changing.
- // This is the case when the user switches the viewer and then goes back
- // through the hystory.
+ // This is the case when the user switches the viewer and then goes back through the history
'$route.query.plain': {
handler(plainValue) {
- this.switchViewer(
- this.hasRichViewer && (plainValue === undefined || plainValue === '0')
- ? RICH_BLOB_VIEWER
- : SIMPLE_BLOB_VIEWER,
- plainValue !== undefined,
- );
+ const useSimpleViewer = plainValue === '1' || !this.hasRichViewer;
+ this.switchViewer(useSimpleViewer ? SIMPLE_BLOB_VIEWER : RICH_BLOB_VIEWER);
},
},
},
@@ -323,21 +268,11 @@ export default {
this.loadLegacyViewer();
}
},
- updateRouteQuery() {
- const plain = this.activeViewerType === SIMPLE_BLOB_VIEWER ? '1' : '0';
-
- if (this.$route?.query?.plain === plain) {
- return;
- }
-
- this.$router.push({
- path: this.$route.path,
- query: { ...this.$route.query, plain },
- });
- },
handleViewerChanged(newViewer) {
this.switchViewer(newViewer);
- this.updateRouteQuery();
+ const plain = newViewer === SIMPLE_BLOB_VIEWER ? '1' : '0';
+ if (this.$route?.query?.plain === plain) return;
+ this.$router.push({ path: this.$route.path, query: { ...this.$route.query, plain } });
},
editBlob(target) {
if (this.showForkSuggestion) {
@@ -370,31 +305,15 @@ export default {
:has-render-error="hasRenderError"
:show-path="false"
:override-copy="true"
+ :show-fork-suggestion="showForkSuggestion"
+ :project-path="projectPath"
+ :project-id="projectId"
@viewer-changed="handleViewerChanged"
@copy="onCopy"
+ @edit="editBlob"
+ @error="displayError"
>
<template #actions>
- <web-ide-link
- v-if="!blobInfo.archived"
- :show-edit-button="!isBinaryFileType"
- class="gl-mr-3"
- :edit-url="blobInfo.editBlobPath"
- :web-ide-url="blobInfo.ideEditPath"
- :needs-to-fork="showForkSuggestion"
- :show-pipeline-editor-button="Boolean(blobInfo.pipelineEditorPath)"
- :pipeline-editor-url="blobInfo.pipelineEditorPath"
- :gitpod-url="blobInfo.gitpodBlobUrl"
- :show-gitpod-button="gitpodEnabled"
- :gitpod-enabled="currentUser && currentUser.gitpodEnabled"
- :project-path="projectPath"
- :project-id="projectIdAsNumber"
- :user-preferences-gitpod-path="currentUser && currentUser.preferencesGitpodPath"
- :user-profile-enable-gitpod-path="currentUser && currentUser.profileEnableGitpodPath"
- is-blob
- disable-fork-modal
- @edit="editBlob"
- />
-
<blob-button-group
v-if="isLoggedIn && !blobInfo.archived"
:path="path"
@@ -403,7 +322,7 @@ export default {
:delete-path="blobInfo.webPath"
:can-push-code="userPermissions.pushCode"
:can-push-to-branch="blobInfo.canCurrentUserPushToBranch"
- :empty-repo="project.repository.empty"
+ :empty-repo="isEmptyRepository"
:project-path="projectPath"
:is-locked="Boolean(pathLockedByUser)"
:can-lock="canLock"
@@ -418,12 +337,6 @@ export default {
:fork-path="forkPath"
@cancel="setForkTarget(null)"
/>
- <codeowners-validation
- v-if="isCodeownersFile"
- :current-ref="currentRef"
- :project-path="projectPath"
- :file-path="path"
- />
<blob-content
v-if="!blobViewer"
class="js-syntax-highlight"
@@ -441,6 +354,8 @@ export default {
v-else
:blob="blobInfo"
:chunks="chunks"
+ :project-path="projectPath"
+ :current-ref="currentRef"
class="blob-viewer"
@error="onError"
/>
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index fa51ef30546..12edeeb0d2f 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -4,7 +4,7 @@ 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 CiIcon from '~/vue_shared/components/ci_icon.vue';
+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';
@@ -20,13 +20,13 @@ export default {
UserAvatarLink,
TimeagoTooltip,
ClipboardButton,
- CiIcon,
GlButton,
GlButtonGroup,
GlLink,
GlLoadingIcon,
UserAvatarImage,
SignatureBadge,
+ CiBadgeLink,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -191,18 +191,14 @@ export default {
>
<signature-badge v-if="commit.signature" :signature="commit.signature" />
<div v-if="commit.pipeline" class="ci-status-link">
- <gl-link
- v-gl-tooltip.left
- :href="commit.pipeline.detailedStatus.detailsPath"
- :title="statusTitle"
+ <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"
- >
- <ci-icon
- :status="commit.pipeline.detailedStatus"
- :size="24"
- :aria-label="statusTitle"
- />
- </gl-link>
+ />
</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">{{
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index c839d7a53cd..a76d822317a 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -123,7 +123,8 @@ export default {
path: joinPaths('/-/blob', this.escapedRef, this.path),
refType: this.refType,
});
- } else if (this.isFolder) {
+ }
+ if (this.isFolder) {
return buildURLwithRefType({
path: joinPaths('/-/tree', this.escapedRef, this.path),
refType: this.refType,
@@ -260,7 +261,9 @@ export default {
</gl-intersection-observer>
</td>
<td class="tree-time-ago text-right cursor-default gl-text-secondary">
- <timeago-tooltip v-if="commitData" :time="commitData.committedDate" />
+ <gl-intersection-observer @appear="rowAppeared" @disappear="rowDisappeared">
+ <timeago-tooltip v-if="commitData" :time="commitData.committedDate" />
+ </gl-intersection-observer>
<gl-skeleton-loader v-if="showSkeletonLoader" :lines="1" />
</td>
</tr>
diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js
index 3079ef0bfbb..c4d03120c2e 100644
--- a/app/assets/javascripts/repository/constants.js
+++ b/app/assets/javascripts/repository/constants.js
@@ -27,12 +27,6 @@ export const PDF_MAX_PAGE_LIMIT = 50;
export const ROW_APPEAR_DELAY = 150;
export const DEFAULT_BLOB_INFO = {
- gitpodEnabled: false,
- currentUser: {
- gitpodEnabled: false,
- preferencesGitpodPath: null,
- profileEnableGitpodPath: null,
- },
userPermissions: {
pushCode: false,
downloadCode: false,
@@ -116,5 +110,3 @@ export const POLLING_INTERVAL_BACKOFF = 2;
export const CONFLICTS_MODAL_ID = 'fork-sync-conflicts-modal';
export const FORK_UPDATED_EVENT = 'fork:updated';
-
-export const CODEOWNERS_FILE_NAME = 'CODEOWNERS';
diff --git a/app/assets/javascripts/rest_api.js b/app/assets/javascripts/rest_api.js
index 87996d0bb85..2b9ec7b63d7 100644
--- a/app/assets/javascripts/rest_api.js
+++ b/app/assets/javascripts/rest_api.js
@@ -8,6 +8,7 @@ export * from './api/tags_api';
export * from './api/alert_management_alerts_api';
export * from './api/harbor_registry';
export * from './api/environments_api';
+export * from './api/application_settings_api';
// Note: It's not possible to spy on methods imported from this file in
// Jest tests.
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 58e4553d00d..bc1f32930e7 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -64,15 +64,12 @@ Sidebar.prototype.addEventListeners = function () {
};
Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
- const $this = $(this);
-
- if ($this.hasClass('right-sidebar-merge-requests')) return;
-
+ const $toggleButtons = $('.js-sidebar-toggle');
const $collapseIcon = $('.js-sidebar-collapse');
const $expandIcon = $('.js-sidebar-expand');
const $toggleContainer = $('.js-sidebar-toggle-container');
const isExpanded = $toggleContainer.data('is-expanded');
- const tooltipLabel = isExpanded ? __('Collapse sidebar') : __('Expand sidebar');
+ const tooltipLabel = isExpanded ? __('Expand sidebar') : __('Collapse sidebar');
e.preventDefault();
if (isExpanded) {
@@ -93,10 +90,10 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
$('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
}
- $this.attr('data-original-title', tooltipLabel);
- $this.attr('title', tooltipLabel);
- fixTitle($this);
- hide($this);
+ $toggleButtons.attr('data-original-title', tooltipLabel);
+ $toggleButtons.attr('title', tooltipLabel);
+ fixTitle($toggleButtons);
+ hide($toggleButtons);
if (!triggered) {
setCookie('collapsed_gutter', $('.right-sidebar').hasClass('right-sidebar-collapsed'));
diff --git a/app/assets/javascripts/run_modules.js b/app/assets/javascripts/run_modules.js
new file mode 100644
index 00000000000..fabdff9bb76
--- /dev/null
+++ b/app/assets/javascripts/run_modules.js
@@ -0,0 +1,9 @@
+export const runModules = (modules, prefix) => {
+ document
+ .querySelector('meta[name="controller-path"]')
+ .content.split('/')
+ .forEach((part, index, arr) => {
+ const path = `${prefix}${[...arr.slice(0, index), part].join('/')}/index.js`;
+ modules[path]?.();
+ });
+};
diff --git a/app/assets/javascripts/search/index.js b/app/assets/javascripts/search/index.js
index f5684cebbf9..f83130213f2 100644
--- a/app/assets/javascripts/search/index.js
+++ b/app/assets/javascripts/search/index.js
@@ -10,12 +10,13 @@ import { initBlobRefSwitcher } from './under_topbar';
export const initSearchApp = () => {
syntaxHighlight(document.querySelectorAll('.js-search-results'));
const query = queryToObject(window.location.search, { gatherArrays: true });
- const { navigationJsonParsed: navigation } = sidebarInitState() || {};
+ const { navigationJsonParsed: navigation, searchType } = sidebarInitState() || {};
const store = createStore({
query,
navigation,
useSidebarNavigation: gon.use_new_navigation,
+ searchType,
});
initTopbar(store);
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index 9962f711892..532a66affd8 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -3,29 +3,46 @@
import { mapState, mapGetters } from 'vuex';
import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue';
import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue';
+import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue';
import SidebarPortal from '~/super_sidebar/components/sidebar_portal.vue';
+import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { SCOPE_ISSUES, SCOPE_MERGE_REQUESTS, SCOPE_BLOB, SCOPE_PROJECTS } from '../constants';
+import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
+import {
+ SCOPE_ISSUES,
+ SCOPE_MERGE_REQUESTS,
+ SCOPE_BLOB,
+ SCOPE_PROJECTS,
+ SCOPE_NOTES,
+ SCOPE_COMMITS,
+ SEARCH_TYPE_ADVANCED,
+} from '../constants';
import IssuesFilters from './issues_filters.vue';
import MergeRequestsFilters from './merge_requests_filters.vue';
import BlobsFilters from './blobs_filters.vue';
import ProjectsFilters from './projects_filters.vue';
+import NotesFilters from './notes_filters.vue';
+import CommitsFilters from './commits_filters.vue';
export default {
name: 'GlobalSearchSidebar',
components: {
IssuesFilters,
- ScopeLegacyNavigation,
- ScopeSidebarNavigation,
- SidebarPortal,
MergeRequestsFilters,
BlobsFilters,
ProjectsFilters,
+ NotesFilters,
+ ScopeLegacyNavigation,
+ ScopeSidebarNavigation,
+ SidebarPortal,
+ DomElementListener,
+ SmallScreenDrawerNavigation,
+ CommitsFilters,
},
mixins: [glFeatureFlagsMixin()],
computed: {
// useSidebarNavigation refers to whether the new left sidebar navigation is enabled
- ...mapState(['useSidebarNavigation']),
+ ...mapState(['useSidebarNavigation', 'searchType']),
...mapGetters(['currentScope']),
showIssuesFilters() {
return this.currentScope === SCOPE_ISSUES;
@@ -34,11 +51,25 @@ export default {
return this.currentScope === SCOPE_MERGE_REQUESTS;
},
showBlobFilters() {
- return this.currentScope === SCOPE_BLOB;
+ return this.currentScope === SCOPE_BLOB && this.searchType === SEARCH_TYPE_ADVANCED;
},
showProjectsFilters() {
- // for now the feature flag is here. Since we have only one filter in projects scope
- return this.currentScope === SCOPE_PROJECTS && this.glFeatures.searchProjectsHideArchived;
+ return this.currentScope === SCOPE_PROJECTS;
+ },
+ showNotesFilters() {
+ return (
+ this.currentScope === SCOPE_NOTES &&
+ this.searchType === SEARCH_TYPE_ADVANCED &&
+ 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
+ );
},
showScopeNavigation() {
// showScopeNavigation refers to whether the scope navigation should be shown
@@ -47,27 +78,49 @@ export default {
return Boolean(this.currentScope);
},
},
+ methods: {
+ toggleFiltersFromSidebar() {
+ toggleSuperSidebarCollapsed();
+ },
+ },
};
</script>
<template>
<section v-if="useSidebarNavigation">
+ <dom-element-listener selector="#js-open-mobile-filters" @click="toggleFiltersFromSidebar" />
<sidebar-portal>
<scope-sidebar-navigation />
<issues-filters v-if="showIssuesFilters" />
<merge-requests-filters v-if="showMergeRequestFilters" />
<blobs-filters v-if="showBlobFilters" />
<projects-filters v-if="showProjectsFilters" />
+ <notes-filters v-if="showNotesFilters" />
+ <commits-filters v-if="showCommitsFilters" />
</sidebar-portal>
</section>
+
<section
v-else-if="showScopeNavigation"
- class="search-sidebar gl-display-flex gl-flex-direction-column gl-md-mr-5 gl-mb-6 gl-mt-5"
+ class="gl-display-flex gl-flex-direction-column gl-lg-mr-0 gl-md-mr-5 gl-lg-mb-6 gl-lg-mt-5"
>
- <scope-legacy-navigation />
- <issues-filters v-if="showIssuesFilters" />
- <merge-requests-filters v-if="showMergeRequestFilters" />
- <blobs-filters v-if="showBlobFilters" />
- <projects-filters v-if="showProjectsFilters" />
+ <div class="search-sidebar gl-display-none gl-lg-display-block">
+ <scope-legacy-navigation />
+ <issues-filters v-if="showIssuesFilters" />
+ <merge-requests-filters v-if="showMergeRequestFilters" />
+ <blobs-filters v-if="showBlobFilters" />
+ <projects-filters v-if="showProjectsFilters" />
+ <notes-filters v-if="showNotesFilters" />
+ <commits-filters v-if="showCommitsFilters" />
+ </div>
+ <small-screen-drawer-navigation class="gl-lg-display-none">
+ <scope-legacy-navigation />
+ <issues-filters v-if="showIssuesFilters" />
+ <merge-requests-filters v-if="showMergeRequestFilters" />
+ <blobs-filters v-if="showBlobFilters" />
+ <projects-filters v-if="showProjectsFilters" />
+ <notes-filters v-if="showNotesFilters" />
+ <commits-filters v-if="showCommitsFilters" />
+ </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 77efbdd9e60..5cddf5e744f 100644
--- a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
+++ b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
@@ -7,6 +7,11 @@ export const TRACKING_LABEL_CHECKBOX = 'checkbox';
const scopes = {
PROJECTS: 'projects',
+ ISSUES: 'issues',
+ MERGE_REQUESTS: 'merge_requests',
+ NOTES: 'notes',
+ BLOBS: 'blobs',
+ COMMITS: 'commits',
};
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 1984e3a36c4..c31c46f2e6a 100644
--- a/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue
@@ -14,7 +14,7 @@ export default {
GlFormCheckbox,
},
computed: {
- ...mapState(['urlQuery']),
+ ...mapState(['urlQuery', 'useSidebarNavigation']),
selectedFilter: {
get() {
return [parseBoolean(this.urlQuery?.include_archived)];
@@ -41,7 +41,9 @@ export default {
<template>
<gl-form-checkbox-group v-model="selectedFilter">
- <h5>{{ $options.archivedFilterData.headerLabel }}</h5>
+ <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }">
+ {{ $options.archivedFilterData.headerLabel }}
+ </h5>
<gl-form-checkbox
class="gl-flex-grow-1 gl-display-inline-flex gl-justify-content-space-between gl-w-full"
:class="$options.LABEL_DEFAULT_CLASSES"
diff --git a/app/assets/javascripts/search/sidebar/components/blobs_filters.vue b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
index 5f4d6fbd56c..ac36ae6b366 100644
--- a/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
@@ -1,5 +1,10 @@
<script>
+// 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 } from '../constants';
import LanguageFilter from './language_filter/index.vue';
+import ArchivedFilter from './archived_filter/index.vue';
import FiltersTemplate from './filters_template.vue';
export default {
@@ -7,6 +12,21 @@ export default {
components: {
LanguageFilter,
FiltersTemplate,
+ ArchivedFilter,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ computed: {
+ ...mapGetters(['currentScope']),
+ ...mapState(['useSidebarNavigation', 'searchType']),
+ showArchivedFilter() {
+ return this.glFeatures.searchBlobsHideArchivedProjects;
+ },
+ showDivider() {
+ return !this.useSidebarNavigation && this.showArchivedFilter;
+ },
+ hrClasses() {
+ return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block'];
+ },
},
};
</script>
@@ -14,5 +34,7 @@ export default {
<template>
<filters-template>
<language-filter class="gl-mb-5" />
+ <hr v-if="showDivider" :class="hrClasses" />
+ <archived-filter v-if="showArchivedFilter" class="gl-mb-5" />
</filters-template>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/commits_filters.vue b/app/assets/javascripts/search/sidebar/components/commits_filters.vue
new file mode 100644
index 00000000000..4f9fdbe9551
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/commits_filters.vue
@@ -0,0 +1,18 @@
+<script>
+import ArchivedFilter from './archived_filter/index.vue';
+import FiltersTemplate from './filters_template.vue';
+
+export default {
+ name: 'CommitsFilters',
+ components: {
+ ArchivedFilter,
+ FiltersTemplate,
+ },
+};
+</script>
+
+<template>
+ <filters-template>
+ <archived-filter class="gl-mb-5" />
+ </filters-template>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/filters_template.vue b/app/assets/javascripts/search/sidebar/components/filters_template.vue
index 3dae05ccc69..0f68bf92048 100644
--- a/app/assets/javascripts/search/sidebar/components/filters_template.vue
+++ b/app/assets/javascripts/search/sidebar/components/filters_template.vue
@@ -21,9 +21,6 @@ export default {
computed: {
...mapState(['sidebarDirty', 'useSidebarNavigation']),
...mapGetters(['currentScope']),
- hrClasses() {
- return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block'];
- },
},
methods: {
...mapActions(['applyQuery', 'resetQuery']),
@@ -40,14 +37,15 @@ export default {
this.resetQuery();
},
},
+ HR_DEFAULT_CLASSES,
};
</script>
<template>
<gl-form class="issue-filters gl-px-5 gl-pt-0" @submit.prevent="applyQueryWithTracking">
- <hr v-if="!useSidebarNavigation" :class="hrClasses" />
+ <hr v-if="!useSidebarNavigation" :class="$options.HR_DEFAULT_CLASSES" />
<slot></slot>
- <hr v-if="!useSidebarNavigation" :class="hrClasses" />
+ <hr v-if="!useSidebarNavigation" :class="$options.HR_DEFAULT_CLASSES" />
<div class="gl-display-flex gl-align-items-center gl-mt-4">
<gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty">
{{ __('Apply') }}
diff --git a/app/assets/javascripts/search/sidebar/components/issues_filters.vue b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
index 919bd2b2e49..dbd52978163 100644
--- a/app/assets/javascripts/search/sidebar/components/issues_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
@@ -2,13 +2,15 @@
// 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 } from '../constants/index';
+import { HR_DEFAULT_CLASSES, SEARCH_TYPE_ADVANCED } from '../constants';
import { confidentialFilterData } from './confidentiality_filter/data';
import { statusFilterData } from './status_filter/data';
import ConfidentialityFilter from './confidentiality_filter/index.vue';
import { labelFilterData } from './label_filter/data';
+import { archivedFilterData } from './archived_filter/data';
import LabelFilter from './label_filter/index.vue';
import StatusFilter from './status_filter/index.vue';
+import ArchivedFilter from './archived_filter/index.vue';
import FiltersTemplate from './filters_template.vue';
@@ -19,11 +21,12 @@ export default {
ConfidentialityFilter,
LabelFilter,
FiltersTemplate,
+ ArchivedFilter,
},
mixins: [glFeatureFlagsMixin()],
computed: {
...mapGetters(['currentScope']),
- ...mapState(['useSidebarNavigation']),
+ ...mapState(['useSidebarNavigation', 'searchType']),
showConfidentialityFilter() {
return Object.values(confidentialFilterData.scopes).includes(this.currentScope);
},
@@ -33,7 +36,15 @@ export default {
showLabelFilter() {
return (
Object.values(labelFilterData.scopes).includes(this.currentScope) &&
- this.glFeatures.searchIssueLabelAggregation
+ this.glFeatures.searchIssueLabelAggregation &&
+ this.searchType === SEARCH_TYPE_ADVANCED
+ );
+ },
+ showArchivedFilter() {
+ return (
+ Object.values(archivedFilterData.scopes).includes(this.currentScope) &&
+ this.glFeatures.searchIssuesHideArchivedProjects &&
+ this.searchType === SEARCH_TYPE_ADVANCED
);
},
showDivider() {
@@ -52,6 +63,8 @@ export default {
<hr v-if="showConfidentialityFilter && showDivider" :class="hrClasses" />
<confidentiality-filter v-if="showConfidentialityFilter" class="gl-mb-5" />
<hr v-if="showLabelFilter && showDivider" :class="hrClasses" />
- <label-filter v-if="showLabelFilter" />
+ <label-filter v-if="showLabelFilter" class="gl-mb-5" />
+ <hr v-if="showArchivedFilter && showDivider" :class="hrClasses" />
+ <archived-filter v-if="showArchivedFilter" class="gl-mb-5" />
</filters-template>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
index ca1503d7c64..784207cc702 100644
--- a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
@@ -74,7 +74,7 @@ export default {
</script>
<template>
- <div v-if="hasBuckets" class="gl-my-0 language-filter-checkbox">
+ <div v-if="hasBuckets" class="language-filter-checkbox">
<h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }">
{{ $options.languageFilterData.header }}
</h5>
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 bc5b797dd56..2845eb2049b 100644
--- a/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
@@ -1,18 +1,49 @@
<script>
+// 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 { statusFilterData } from './status_filter/data';
import StatusFilter from './status_filter/index.vue';
import FiltersTemplate from './filters_template.vue';
+import { archivedFilterData } from './archived_filter/data';
+import ArchivedFilter from './archived_filter/index.vue';
export default {
name: 'MergeRequestsFilters',
components: {
StatusFilter,
FiltersTemplate,
+ ArchivedFilter,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ computed: {
+ ...mapGetters(['currentScope']),
+ ...mapState(['useSidebarNavigation', 'searchType']),
+ showArchivedFilter() {
+ return (
+ Object.values(archivedFilterData.scopes).includes(this.currentScope) &&
+ this.glFeatures.searchMergeRequestsHideArchivedProjects &&
+ this.searchType === SEARCH_TYPE_ADVANCED
+ );
+ },
+ showStatusFilter() {
+ return Object.values(statusFilterData.scopes).includes(this.currentScope);
+ },
+ showDivider() {
+ return !this.useSidebarNavigation;
+ },
+ hrClasses() {
+ return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block'];
+ },
},
};
</script>
<template>
<filters-template>
- <status-filter class="gl-mb-5" />
+ <status-filter v-if="showStatusFilter" class="gl-mb-5" />
+ <hr v-if="showArchivedFilter && showDivider" :class="hrClasses" />
+ <archived-filter v-if="showArchivedFilter" class="gl-mb-5" />
</filters-template>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/notes_filters.vue b/app/assets/javascripts/search/sidebar/components/notes_filters.vue
new file mode 100644
index 00000000000..3a9f582d554
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/notes_filters.vue
@@ -0,0 +1,18 @@
+<script>
+import ArchivedFilter from './archived_filter/index.vue';
+import FiltersTemplate from './filters_template.vue';
+
+export default {
+ name: 'NotesFilters',
+ components: {
+ ArchivedFilter,
+ FiltersTemplate,
+ },
+};
+</script>
+
+<template>
+ <filters-template>
+ <archived-filter class="gl-mb-5" />
+ </filters-template>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue b/app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue
index e8d5de4d769..a4c1119736f 100644
--- a/app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue
+++ b/app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue
@@ -57,7 +57,7 @@ export default {
</script>
<template>
- <nav data-testid="search-filter">
+ <nav data-testid="search-filter" class="gl-border-none">
<gl-nav vertical pills>
<gl-nav-item
v-for="(item, scope) in navigation"
@@ -81,6 +81,5 @@ export default {
</span>
</gl-nav-item>
</gl-nav>
- <hr class="gl-mt-5 gl-mx-5 gl-mb-0 gl-border-gray-100 gl-md-display-none" />
</nav>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/small_screen_drawer_navigation.vue b/app/assets/javascripts/search/sidebar/components/small_screen_drawer_navigation.vue
new file mode 100644
index 00000000000..e966b8d877e
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/small_screen_drawer_navigation.vue
@@ -0,0 +1,61 @@
+<script>
+import { GlDrawer } from '@gitlab/ui';
+import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
+import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
+import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
+import { s__ } from '~/locale';
+
+export default {
+ name: 'SmallScreenDrawerNavigation',
+ components: {
+ GlDrawer,
+ DomElementListener,
+ },
+ i18n: {
+ smallScreenFiltersDrawerHeader: s__('GlobalSearch|Filters'),
+ },
+ data() {
+ return {
+ openSmallScreenFilters: false,
+ };
+ },
+ computed: {
+ getDrawerHeaderHeight() {
+ if (!this.openSmallScreenFilters) return '0';
+ return getContentWrapperHeight();
+ },
+ },
+ methods: {
+ closeSmallScreenFilters() {
+ this.openSmallScreenFilters = false;
+ },
+ toggleSmallScreenFilters() {
+ this.openSmallScreenFilters = !this.openSmallScreenFilters;
+ },
+ },
+ DRAWER_Z_INDEX,
+};
+</script>
+<template>
+ <dom-element-listener selector="#js-open-mobile-filters" @click="toggleSmallScreenFilters">
+ <gl-drawer
+ :header-height="getDrawerHeaderHeight"
+ :z-index="$options.DRAWER_Z_INDEX"
+ variant="sidebar"
+ class="small-screen-drawer-navigation"
+ :open="openSmallScreenFilters"
+ @close="closeSmallScreenFilters"
+ >
+ <template #title>
+ <h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24">
+ {{ $options.i18n.smallScreenFiltersDrawerHeader }}
+ </h2>
+ </template>
+ <template #default>
+ <div>
+ <slot></slot>
+ </div>
+ </template>
+ </gl-drawer>
+ </dom-element-listener>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js
index 01d0aad206c..19df875c292 100644
--- a/app/assets/javascripts/search/sidebar/constants/index.js
+++ b/app/assets/javascripts/search/sidebar/constants/index.js
@@ -2,6 +2,8 @@ export const SCOPE_ISSUES = 'issues';
export const SCOPE_MERGE_REQUESTS = 'merge_requests';
export const SCOPE_BLOB = 'blobs';
export const SCOPE_PROJECTS = 'projects';
+export const SCOPE_NOTES = 'notes';
+export const SCOPE_COMMITS = 'commits';
export const LABEL_DEFAULT_CLASSES = [
'gl-display-flex',
'gl-flex-direction-row',
@@ -19,3 +21,7 @@ export const ONLY_SHOW_MD = ['gl-display-none', 'gl-md-display-block'];
export const TRACKING_ACTION_CLICK = 'search:filters:click';
export const TRACKING_LABEL_APPLY = 'Apply Filters';
export const TRACKING_LABEL_RESET = 'Reset Filters';
+
+export const SEARCH_TYPE_BASIC = 'basic';
+export const SEARCH_TYPE_ADVANCED = 'advanced';
+export const SEARCH_TYPE_ZOEKT = 'zoekt';
diff --git a/app/assets/javascripts/search/sidebar/index.js b/app/assets/javascripts/search/sidebar/index.js
index 415f6f7454c..3a699355dc9 100644
--- a/app/assets/javascripts/search/sidebar/index.js
+++ b/app/assets/javascripts/search/sidebar/index.js
@@ -8,9 +8,11 @@ export const sidebarInitState = () => {
const el = document.getElementById('js-search-sidebar');
if (!el) return {};
- const { navigationJson } = el.dataset;
+ const { navigationJson, searchType } = el.dataset;
+
const navigationJsonParsed = JSON.parse(navigationJson);
- return { navigationJsonParsed };
+
+ return { navigationJsonParsed, searchType };
};
export const initSidebar = (store) => {
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
index a68a0f75a2f..211bbaf82cd 100644
--- a/app/assets/javascripts/search/store/actions.js
+++ b/app/assets/javascripts/search/store/actions.js
@@ -138,7 +138,7 @@ export const setLabelFilterSearch = ({ commit }, { value }) => {
export const fetchSidebarCount = ({ commit, state }) => {
const promises = Object.values(state.navigation).map((navItem) => {
// active nav item has count already so we skip it
- if (!navItem.active) {
+ if (!navItem.active && navItem.count_link) {
return axios
.get(navItem.count_link)
.then(({ data: { count } }) => {
diff --git a/app/assets/javascripts/search/store/mutations.js b/app/assets/javascripts/search/store/mutations.js
index 65bb21f1b8a..b248681f053 100644
--- a/app/assets/javascripts/search/store/mutations.js
+++ b/app/assets/javascripts/search/store/mutations.js
@@ -33,7 +33,7 @@ export default {
state.frequentItems[key] = data;
},
[types.RECEIVE_NAVIGATION_COUNT](state, { key, count }) {
- const item = { ...state.navigation[key], count };
+ const item = { ...state.navigation[key], count, count_link: null };
state.navigation = { ...state.navigation, [key]: item };
},
[types.REQUEST_AGGREGATIONS](state) {
diff --git a/app/assets/javascripts/search/store/state.js b/app/assets/javascripts/search/store/state.js
index 5407b08fa83..b4cd2af65ba 100644
--- a/app/assets/javascripts/search/store/state.js
+++ b/app/assets/javascripts/search/store/state.js
@@ -1,7 +1,7 @@
import { cloneDeep } from 'lodash';
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants';
-const createState = ({ query, navigation, useSidebarNavigation }) => ({
+const createState = ({ query, navigation, useSidebarNavigation, searchType }) => ({
urlQuery: cloneDeep(query),
query,
groups: [],
@@ -21,6 +21,7 @@ const createState = ({ query, navigation, useSidebarNavigation }) => ({
data: [],
},
searchLabelString: '',
+ searchType,
});
export default createState;
diff --git a/app/assets/javascripts/search/store/utils.js b/app/assets/javascripts/search/store/utils.js
index 2f02ef3475c..b15f89fc6a2 100644
--- a/app/assets/javascripts/search/store/utils.js
+++ b/app/assets/javascripts/search/store/utils.js
@@ -68,7 +68,8 @@ export const setFrequentItemToLS = (key, data, itemData) => {
frequentItems.sort((a, b) => {
if (a.frequency > b.frequency) {
return -1;
- } else if (a.frequency < b.frequency) {
+ }
+ if (a.frequency < b.frequency) {
return 1;
}
return b.lastUsed - a.lastUsed;
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index c7d89113895..32d46a0d4af 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -1,13 +1,18 @@
<script>
import { GlTab, GlTabs, GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
+import Api from '~/api';
import { __, s__ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import SectionLayout from '~/vue_shared/security_configuration/components/section_layout.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
+import { SERVICE_PING_SECURITY_CONFIGURATION_THREAT_MANAGEMENT_VISIT } from '~/tracking/constants';
import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
-import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
+import {
+ AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
+ TAB_VULNERABILITY_MANAGEMENT_INDEX,
+} from './constants';
import FeatureCard from './feature_card.vue';
import TrainingProviderList from './training_provider_list.vue';
@@ -123,6 +128,11 @@ export default {
dismissAlert() {
this.errorMessage = '';
},
+ tabChange(value) {
+ if (value === TAB_VULNERABILITY_MANAGEMENT_INDEX) {
+ Api.trackRedisHllUserEvent(SERVICE_PING_SECURITY_CONFIGURATION_THREAT_MANAGEMENT_VISIT);
+ }
+ },
},
autoDevopsEnabledAlertStorageKey: AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
};
@@ -167,6 +177,7 @@ export default {
data-testid="security-configuration-container"
sync-active-tab-with-query-params
lazy
+ @input="tabChange"
>
<gl-tab
data-testid="security-testing-tab"
diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js
index b427820144d..da213b0ed43 100644
--- a/app/assets/javascripts/security_configuration/components/constants.js
+++ b/app/assets/javascripts/security_configuration/components/constants.js
@@ -1,5 +1,6 @@
import { helpPagePath } from '~/helpers/help_page_helper';
import { __, s__ } from '~/locale';
+import ContinuousVulnerabilityScan from '~/security_configuration/components/continuous_vulnerability_scan.vue';
import {
REPORT_TYPE_SAST,
@@ -210,6 +211,7 @@ export const securityFeatures = [
configurationHelpPath: DEPENDENCY_SCANNING_CONFIG_HELP_PATH,
type: REPORT_TYPE_DEPENDENCY_SCANNING,
anchor: 'dependency-scanning',
+ slotComponent: ContinuousVulnerabilityScan,
},
{
name: CONTAINER_SCANNING_NAME,
@@ -326,3 +328,5 @@ export const TEMP_PROVIDER_URLS = {
[__('Secure Code Warrior')]: 'https://www.securecodewarrior.com/',
SecureFlag: 'https://www.secureflag.com/',
};
+
+export const TAB_VULNERABILITY_MANAGEMENT_INDEX = 1;
diff --git a/app/assets/javascripts/security_configuration/components/continuous_vulnerability_scan.vue b/app/assets/javascripts/security_configuration/components/continuous_vulnerability_scan.vue
new file mode 100644
index 00000000000..61cbde2107c
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/continuous_vulnerability_scan.vue
@@ -0,0 +1,127 @@
+<script>
+import { GlBadge, GlIcon, GlToggle, GlLink, GlSprintf, GlAlert } from '@gitlab/ui';
+import ProjectSetContinuousVulnerabilityScanning from '~/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { __, s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+export default {
+ name: 'ContinuousVulnerabilityscan',
+ components: { GlBadge, GlIcon, GlToggle, GlLink, GlSprintf, GlAlert },
+ mixins: [glFeatureFlagsMixin()],
+ inject: ['continuousVulnerabilityScansEnabled', 'projectFullPath'],
+ i18n: {
+ badgeLabel: __('Experiment'),
+ title: s__('CVS|Continuous Vulnerability Scan'),
+ description: s__(
+ 'CVS|Detect vulnerabilities outside a pipeline as new data is added to the GitLab Advisory Database.',
+ ),
+ learnMore: __('Learn more'),
+ testingAgreementMessage: s__(
+ 'CVS|By enabling this feature, you accept the %{linkStart}Testing Terms of Use%{linkEnd}',
+ ),
+ },
+ props: {
+ feature: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ toggleValue: this.continuousVulnerabilityScansEnabled,
+ errorMessage: '',
+ isAlertDismissed: false,
+ };
+ },
+ computed: {
+ isFeatureConfigured() {
+ return this.feature.available && this.feature.configured;
+ },
+ shouldShowAlert() {
+ return this.errorMessage && !this.isAlertDismissed;
+ },
+ },
+ methods: {
+ reportError(error) {
+ this.errorMessage = error;
+ this.isAlertDismissed = false;
+ },
+ async toggleCVS(checked) {
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: ProjectSetContinuousVulnerabilityScanning,
+ variables: {
+ input: {
+ projectPath: this.projectFullPath,
+ enable: checked,
+ },
+ },
+ });
+
+ const { errors } = data.projectSetContinuousVulnerabilityScanning;
+
+ if (errors.length > 0) {
+ this.reportError(errors[0].message);
+ }
+ if (data.projectSetContinuousVulnerabilityScanning !== null) {
+ this.toggleValue = checked;
+ }
+ } catch (error) {
+ this.reportError(error);
+ }
+ },
+ },
+ CVSHelpPagePath: helpPagePath(
+ 'user/application_security/continuous_vulnerability_scanning/index',
+ ),
+ experimentHelpPagePath: helpPagePath('policy/experiment-beta-support', { anchor: 'experiment' }),
+};
+</script>
+
+<template>
+ <div v-if="glFeatures.dependencyScanningOnAdvisoryIngestion">
+ <h4 class="gl-font-base gl-m-0 gl-mt-6">
+ {{ $options.i18n.title }}
+ <gl-badge
+ ref="badge"
+ :href="$options.experimentHelpPagePath"
+ target="_blank"
+ size="sm"
+ variant="neutral"
+ class="gl-cursor-pointer"
+ >{{ $options.i18n.badgeLabel }}</gl-badge
+ >
+ </h4>
+ <gl-alert
+ v-if="shouldShowAlert"
+ class="gl-mb-5 gl-mt-2"
+ variant="danger"
+ @dismiss="isAlertDismissed = true"
+ >{{ errorMessage }}</gl-alert
+ >
+ <gl-toggle
+ class="gl-mt-5"
+ :disabled="!isFeatureConfigured"
+ :value="toggleValue"
+ :label="s__('CVS|Toggle CVS')"
+ label-position="hidden"
+ @change="toggleCVS"
+ />
+
+ <p class="gl-mb-0 gl-mt-5">
+ {{ $options.i18n.description }}
+ <gl-link :href="$options.CVSHelpPagePath" target="_blank">{{
+ $options.i18n.learnMore
+ }}</gl-link>
+ <br />
+ <gl-sprintf :message="$options.i18n.testingAgreementMessage">
+ <template #link="{ content }">
+ <gl-link href="https://about.gitlab.com/handbook/legal/testing-agreement" target="_blank">
+ {{ content }} <gl-icon name="external-link" />
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/security_configuration/components/feature_card.vue b/app/assets/javascripts/security_configuration/components/feature_card.vue
index a757657339b..7f0a049a6ad 100644
--- a/app/assets/javascripts/security_configuration/components/feature_card.vue
+++ b/app/assets/javascripts/security_configuration/components/feature_card.vue
@@ -73,6 +73,9 @@ export default {
hasSecondary() {
return Boolean(this.feature.secondary);
},
+ hasSlotComponent() {
+ return Boolean(this.feature.slotComponent);
+ },
// This condition is a temporary hack to not display any wrong information
// until this BE Bug is fixed: https://gitlab.com/gitlab-org/gitlab/-/issues/350307.
// More Information: https://gitlab.com/gitlab-org/gitlab/-/issues/350307#note_825447417
@@ -215,5 +218,9 @@ export default {
{{ $options.i18n.configurationGuide }}
</gl-button>
</div>
+
+ <div v-if="hasSlotComponent">
+ <component :is="feature.slotComponent" :feature="feature" />
+ </div>
</gl-card>
</template>
diff --git a/app/assets/javascripts/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql b/app/assets/javascripts/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql
new file mode 100644
index 00000000000..79f4316d106
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql
@@ -0,0 +1,8 @@
+mutation ProjectSetContinuousVulnerabilityScanning(
+ $input: ProjectSetContinuousVulnerabilityScanningInput!
+) {
+ projectSetContinuousVulnerabilityScanning(input: $input) {
+ continuousVulnerabilityScanningEnabled
+ errors
+ }
+}
diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js
index aa3c9c87622..4b498091134 100644
--- a/app/assets/javascripts/security_configuration/index.js
+++ b/app/assets/javascripts/security_configuration/index.js
@@ -26,6 +26,7 @@ export const initSecurityConfiguration = (el) => {
autoDevopsHelpPagePath,
autoDevopsPath,
vulnerabilityTrainingDocsPath,
+ continuousVulnerabilityScansEnabled,
} = el.dataset;
const { augmentedSecurityFeatures } = augmentFeatures(
@@ -43,6 +44,7 @@ export const initSecurityConfiguration = (el) => {
autoDevopsHelpPagePath,
autoDevopsPath,
vulnerabilityTrainingDocsPath,
+ continuousVulnerabilityScansEnabled,
},
render(createElement) {
return createElement(SecurityConfigurationApp, {
diff --git a/app/assets/javascripts/security_configuration/utils.js b/app/assets/javascripts/security_configuration/utils.js
index 72e6d870e13..7f0caf1af46 100644
--- a/app/assets/javascripts/security_configuration/utils.js
+++ b/app/assets/javascripts/security_configuration/utils.js
@@ -1,5 +1,6 @@
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { SCANNER_NAMES_MAP } from '~/security_configuration/components/constants';
+import { REPORT_TYPE_DAST } from '~/vue_shared/security_reports/constants';
/**
* This function takes in 3 arrays of objects, securityFeatures and features.
@@ -29,6 +30,10 @@ export const augmentFeatures = (securityFeatures, features = []) => {
augmented.secondary = { ...augmented.secondary, ...featuresByType[feature.secondary.type] };
}
+ if (augmented.type === REPORT_TYPE_DAST && !augmented.onDemandAvailable) {
+ delete augmented.badge;
+ }
+
if (augmented.badge && augmented.metaInfoPath) {
augmented.badge.badgeHref = augmented.metaInfoPath;
}
diff --git a/app/assets/javascripts/sentry/index.js b/app/assets/javascripts/sentry/index.js
index cf6a79fe939..940caea3322 100644
--- a/app/assets/javascripts/sentry/index.js
+++ b/app/assets/javascripts/sentry/index.js
@@ -1,35 +1,4 @@
import '../webpack';
+import { initSentry } from './init_sentry';
-import * as Sentry from 'sentrybrowser7';
-import SentryConfig from './sentry_config';
-
-const index = function index() {
- // Configuration for newer versions of Sentry SDK (v7)
- SentryConfig.init({
- dsn: gon.sentry_dsn,
- environment: gon.sentry_environment,
- currentUserId: gon.current_user_id,
- allowUrls:
- process.env.NODE_ENV === 'production'
- ? [gon.gitlab_url]
- : [gon.gitlab_url, 'webpack-internal://'],
- release: gon?.version,
- tags: {
- revision: gon?.revision,
- feature_category: gon?.feature_category,
- page: document?.body?.dataset?.page,
- },
- });
-};
-
-index();
-
-// The _Sentry object is globally exported so it can be used by
-// ./sentry_browser_wrapper.js
-// This hack allows us to load a single version of `@sentry/browser`
-// in the browser, see app/views/layouts/_head.html.haml to find how it is imported.
-
-// eslint-disable-next-line no-underscore-dangle
-window._Sentry = Sentry;
-
-export default index;
+initSentry();
diff --git a/app/assets/javascripts/sentry/init_sentry.js b/app/assets/javascripts/sentry/init_sentry.js
new file mode 100644
index 00000000000..dbd12dc36ce
--- /dev/null
+++ b/app/assets/javascripts/sentry/init_sentry.js
@@ -0,0 +1,77 @@
+import {
+ BrowserClient,
+ getCurrentHub,
+ defaultStackParser,
+ makeFetchTransport,
+ defaultIntegrations,
+
+ // exports
+ captureException,
+ captureMessage,
+ withScope,
+ SDK_VERSION,
+} from 'sentrybrowser';
+
+const initSentry = () => {
+ if (!gon?.sentry_dsn) {
+ return;
+ }
+
+ const hub = getCurrentHub();
+
+ const client = new BrowserClient({
+ // Sentry.init(...) options
+ dsn: gon.sentry_dsn,
+ release: gon.version,
+ allowUrls:
+ process.env.NODE_ENV === 'production'
+ ? [gon.gitlab_url]
+ : [gon.gitlab_url, 'webpack-internal://'],
+ environment: gon.sentry_environment,
+
+ // Browser tracing configuration
+ tracePropagationTargets: [/^\//], // only trace internal requests
+ tracesSampleRate: gon.sentry_clientside_traces_sample_rate || 0,
+
+ // This configuration imitates the Sentry.init() default configuration
+ // https://github.com/getsentry/sentry-javascript/blob/7.66.0/MIGRATION.md#explicit-client-options
+ transport: makeFetchTransport,
+ stackParser: defaultStackParser,
+ integrations: defaultIntegrations,
+ });
+
+ hub.bindClient(client);
+
+ hub.setTags({
+ revision: gon.revision,
+ feature_category: gon.feature_category,
+ page: document?.body?.dataset?.page,
+ });
+
+ if (gon.current_user_id) {
+ hub.setUser({
+ id: gon.current_user_id,
+ });
+ }
+
+ // The option `autoSessionTracking` is only avaialble on Sentry.init
+ // this manually starts a session in a similar way.
+ // See: https://github.com/getsentry/sentry-javascript/blob/7.66.0/packages/browser/src/sdk.ts#L204
+ hub.startSession({ ignoreDuration: true }); // `ignoreDuration` counts only the page view.
+ hub.captureSession();
+
+ // The _Sentry object is globally exported so it can be used by
+ // ./sentry_browser_wrapper.js
+ // This hack allows us to load a single version of `@sentry/browser`
+ // in the browser, see app/views/layouts/_head.html.haml to find how it is imported.
+
+ // eslint-disable-next-line no-underscore-dangle
+ window._Sentry = {
+ captureException,
+ captureMessage,
+ withScope,
+ SDK_VERSION, // used to verify compatibility with the Sentry instance
+ };
+};
+
+export { initSentry };
diff --git a/app/assets/javascripts/sentry/sentry_browser_wrapper.js b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
index 0382827f82c..fbfd5d4f458 100644
--- a/app/assets/javascripts/sentry/sentry_browser_wrapper.js
+++ b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
@@ -5,6 +5,8 @@
// This module wraps methods used by our production code.
// Each export is names as we cannot export the entire namespace from *.
+
+/** @type {import('@sentry/core').captureException} */
export const captureException = (...args) => {
// eslint-disable-next-line no-underscore-dangle
const Sentry = window._Sentry;
@@ -12,6 +14,7 @@ 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;
@@ -19,6 +22,7 @@ export const captureMessage = (...args) => {
Sentry?.captureMessage(...args);
};
+/** @type {import('@sentry/core').withScope} */
export const withScope = (...args) => {
// eslint-disable-next-line no-underscore-dangle
const Sentry = window._Sentry;
diff --git a/app/assets/javascripts/sentry/sentry_config.js b/app/assets/javascripts/sentry/sentry_config.js
deleted file mode 100644
index 80f087691f4..00000000000
--- a/app/assets/javascripts/sentry/sentry_config.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import * as Sentry from 'sentrybrowser7';
-
-const SentryConfig = {
- init(options = {}) {
- this.options = options;
-
- this.configure();
- if (this.options.currentUserId) this.setUser();
- },
-
- configure() {
- const { dsn, release, tags, allowUrls, environment } = this.options;
-
- Sentry.init({
- dsn,
- release,
- allowUrls,
- environment,
- });
-
- Sentry.setTags(tags);
- },
-
- setUser() {
- Sentry.setUser({
- id: this.options.currentUserId,
- });
- },
-};
-
-export default SentryConfig;
diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js
index fe5b21713a2..da948cc85b6 100644
--- a/app/assets/javascripts/settings_panels.js
+++ b/app/assets/javascripts/settings_panels.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { InternalEvents } from '~/tracking';
import { __ } from './locale';
/**
@@ -47,6 +48,15 @@ export function toggleSection($section) {
}
}
+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');
+ }
+ });
+}
+
export default function initSettingsPanels() {
$('.settings').each((i, elm) => {
const $section = $(elm);
@@ -64,4 +74,6 @@ export default function initSettingsPanels() {
}
}
});
+
+ initTrackProductAnalyticsExpanded();
}
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
index 319699b88f3..cf77a5ca82c 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
@@ -1,6 +1,6 @@
<script>
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { assigneesQueries } from '../../constants';
+import { assigneesQueries } from '../../queries/constants';
export default {
subscription: null,
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
index 577c01c50ff..8a912b00df1 100644
--- a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
@@ -84,7 +84,8 @@ export default {
if (mergeLength === this.users.length) {
return '';
- } else if (mergeLength > 0) {
+ }
+ if (mergeLength > 0) {
return sprintf(__('%{mergeLength}/%{usersLength} can merge'), {
mergeLength,
usersLength: this.users.length,
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
index ae81dcb95de..4ff12824008 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -6,7 +6,7 @@ import { TYPE_ALERT, TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import { __, n__ } from '~/locale';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { assigneesQueries } from '../../constants';
+import { assigneesQueries } from '../../queries/constants';
import SidebarEditableItem from '../sidebar_editable_item.vue';
import SidebarAssigneesRealtime from './assignees_realtime.vue';
import IssuableAssignees from './issuable_assignees.vue';
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
index b41d126be68..232cdcd2198 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
@@ -15,7 +15,7 @@ export default {
},
computed: {
triggerSource() {
- return `${this.issuableType}-assignee-dropdown`;
+ return `${this.issuableType}_assignee_dropdown`;
},
},
};
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 930e7ff12d9..ef7f12f273f 100644
--- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -1,4 +1,5 @@
<script>
+import { GlButton } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import { __, sprintf } from '~/locale';
@@ -9,6 +10,7 @@ const DEFAULT_RENDER_COUNT = 5;
export default {
components: {
+ GlButton,
AssigneeAvatarLink,
UserNameWithStatus,
},
@@ -97,10 +99,11 @@ export default {
</assignee-avatar-link>
</div>
</div>
- <div v-if="renderShowMoreSection" class="user-list-more gl-hover-text-blue-800">
- <button
- type="button"
- class="btn-link gl-button gl-reset-color!"
+ <div v-if="renderShowMoreSection" class="gl-hover-text-blue-800" data-testid="user-list-more">
+ <gl-button
+ category="tertiary"
+ size="small"
+ data-testid="user-list-more-button"
data-qa-selector="more_assignees_link"
@click="toggleShowLess"
>
@@ -108,7 +111,7 @@ export default {
{{ hiddenAssigneesLabel }}
</template>
<template v-else>{{ __('- show less') }}</template>
- </button>
+ </gl-button>
</div>
</div>
</template>
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 3038cec03eb..7a1853b1b46 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
@@ -1,9 +1,9 @@
<script>
import { GlSprintf, GlButton } from '@gitlab/ui';
import { createAlert } from '~/alert';
-import { TYPE_ISSUE } from '~/issues/constants';
+import { TYPE_ISSUE, TYPE_TEST_CASE, IssuableTypeText } from '~/issues/constants';
import { __, sprintf } from '~/locale';
-import { confidentialityQueries } from '../../constants';
+import { confidentialityQueries } from '../../queries/constants';
export default {
i18n: {
@@ -11,7 +11,7 @@ export default {
'You are going to turn on confidentiality. Only %{context} members with %{strongStart}%{permissions}%{strongEnd} can view or be notified about this %{issuableType}.',
),
confidentialityOffWarning: __(
- 'You are going to turn off the confidentiality. This means %{strongStart}everyone%{strongEnd} will be able to see and leave a comment on this %{issuableType}.',
+ 'You are going to turn off the confidentiality. This means %{strongStart}everyone%{strongEnd} will be able to see%{commentText} this %{issuableType}.',
),
},
components: {
@@ -56,11 +56,17 @@ export default {
isIssue() {
return this.issuableType === TYPE_ISSUE;
},
+ isTestCase() {
+ return this.issuableType === TYPE_TEST_CASE;
+ },
+ isIssueOrTestCase() {
+ return this.isIssue || this.isTestCase;
+ },
context() {
- return this.isIssue ? __('project') : __('group');
+ return this.isIssueOrTestCase ? __('project') : __('group');
},
workspacePath() {
- return this.isIssue
+ return this.isIssueOrTestCase
? {
projectPath: this.fullPath,
}
@@ -73,6 +79,12 @@ export default {
? __('at least the Reporter role, the author, and assignees')
: __('at least the Reporter role');
},
+ issuableTypeText() {
+ return IssuableTypeText[this.issuableType];
+ },
+ commentText() {
+ return this.isTestCase ? '' : __(' and leave a comment on');
+ },
},
methods: {
submitForm() {
@@ -108,7 +120,7 @@ export default {
message: sprintf(
__('Something went wrong while setting %{issuableType} confidentiality.'),
{
- issuableType: this.issuableType,
+ issuableType: this.issuableTypeText,
},
),
});
@@ -135,7 +147,8 @@ export default {
</strong>
</template>
<template #context>{{ context }}</template>
- <template #issuableType>{{ issuableType }}</template>
+ <template #commentText>{{ commentText }}</template>
+ <template #issuableType>{{ issuableTypeText }}</template>
</gl-sprintf>
</p>
<div class="sidebar-item-warning-message-actions">
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 9177baec246..295d37671cc 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
@@ -3,7 +3,8 @@ import produce from 'immer';
import Vue from 'vue';
import { createAlert } from '~/alert';
import { __, sprintf } from '~/locale';
-import { confidentialityQueries, Tracking } from '../../constants';
+import { Tracking } from '../../constants';
+import { confidentialityQueries } from '../../queries/constants';
import SidebarEditableItem from '../sidebar_editable_item.vue';
import SidebarConfidentialityContent from './sidebar_confidentiality_content.vue';
import SidebarConfidentialityForm from './sidebar_confidentiality_form.vue';
diff --git a/app/assets/javascripts/sidebar/components/copy/sidebar_reference_widget.vue b/app/assets/javascripts/sidebar/components/copy/sidebar_reference_widget.vue
index 3287539e502..7a488bb379f 100644
--- a/app/assets/javascripts/sidebar/components/copy/sidebar_reference_widget.vue
+++ b/app/assets/javascripts/sidebar/components/copy/sidebar_reference_widget.vue
@@ -1,6 +1,6 @@
<script>
import { __ } from '~/locale';
-import { referenceQueries } from '../../constants';
+import { referenceQueries } from '../../queries/constants';
import CopyableField from './copyable_field.vue';
export default {
diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
index 5a9545f3460..89bc4b126d6 100644
--- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
@@ -4,7 +4,8 @@ import { createAlert } from '~/alert';
import { TYPE_ISSUE } from '~/issues/constants';
import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale';
-import { dateFields, dateTypes, dueDateQueries, startDateQueries, Tracking } from '../../constants';
+import { dateFields, dateTypes, Tracking } from '../../constants';
+import { dueDateQueries, startDateQueries } from '../../queries/constants';
import SidebarEditableItem from '../sidebar_editable_item.vue';
import SidebarFormattedDate from './sidebar_formatted_date.vue';
import SidebarInheritDate from './sidebar_inherit_date.vue';
diff --git a/app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue b/app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue
index 6db332a82da..576043963de 100644
--- a/app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue
+++ b/app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue
@@ -3,11 +3,8 @@ import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { logError } from '~/lib/logger';
import EscalationStatus from 'ee_else_ce/sidebar/components/incidents/escalation_status.vue';
-import {
- escalationStatusQuery,
- escalationStatusMutation,
- INCIDENTS_I18N as i18n,
-} from '../../constants';
+import { INCIDENTS_I18N as i18n } from '../../constants';
+import { escalationStatusQuery, escalationStatusMutation } from '../../queries/constants';
import { getStatusLabel } from '../../utils';
import SidebarEditableItem from '../sidebar_editable_item.vue';
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/getters.js b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/getters.js
index 03ace6286e0..3ab7757d34d 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/getters.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/getters.js
@@ -19,7 +19,8 @@ export const dropdownButtonText = (state, getters) => {
if (!selectedLabels.length) {
return state.dropdownButtonText || __('Label');
- } else if (selectedLabels.length > 1) {
+ }
+ if (selectedLabels.length > 1) {
return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
firstLabelName: selectedLabels[0].title,
remainingLabelCount: selectedLabels.length - 1,
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue
index 53582aacabd..a513c247be7 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue
@@ -101,7 +101,8 @@ export default {
buttonText() {
if (!this.localSelectedLabels.length) {
return this.dropdownButtonText || __('Label');
- } else if (this.localSelectedLabels.length > 1) {
+ }
+ if (this.localSelectedLabels.length > 1) {
return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
firstLabelName: this.localSelectedLabels[0].title,
remainingLabelCount: this.localSelectedLabels.length - 1,
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
index 45778640957..93e3cfba309 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
@@ -1,4 +1,5 @@
<script>
+import { get } from 'lodash';
import {
GlAlert,
GlTooltipDirective,
@@ -11,8 +12,7 @@ import produce from 'immer';
import { createAlert } from '~/alert';
import { WORKSPACE_GROUP } from '~/issues/constants';
import { __ } from '~/locale';
-import { workspaceLabelsQueries } from '../../../constants';
-import createLabelMutation from './graphql/create_label.mutation.graphql';
+import { workspaceLabelsQueries, workspaceCreateLabelMutation } from '../../../queries/constants';
import { DEFAULT_LABEL_COLOR } from './constants';
const errorMessage = __('Error creating label.');
@@ -68,13 +68,19 @@ export default {
return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] }));
},
mutationVariables() {
- const attributePath = this.labelCreateType === WORKSPACE_GROUP ? 'groupPath' : 'projectPath';
-
- return {
+ const variables = {
title: this.labelTitle,
color: this.selectedColor,
- [attributePath]: this.attrWorkspacePath,
};
+
+ if (this.labelCreateType) {
+ const attributePath =
+ this.labelCreateType === WORKSPACE_GROUP ? 'groupPath' : 'projectPath';
+
+ return { ...variables, [attributePath]: this.attrWorkspacePath };
+ }
+
+ return variables;
},
},
methods: {
@@ -88,7 +94,7 @@ export default {
this.selectedColor = this.getColorCode(color);
},
updateLabelsInCache(store, label) {
- const { query } = workspaceLabelsQueries[this.workspaceType];
+ const { query, dataPath } = workspaceLabelsQueries[this.workspaceType];
const sourceData = store.readQuery({
query,
@@ -97,7 +103,7 @@ export default {
const collator = new Intl.Collator('en');
const data = produce(sourceData, (draftData) => {
- const { nodes } = draftData.workspace.labels;
+ const { nodes } = get(draftData, dataPath);
nodes.push(label);
nodes.sort((a, b) => collator.compare(a.title, b.title));
});
@@ -114,7 +120,7 @@ export default {
const {
data: { labelCreate },
} = await this.$apollo.mutate({
- mutation: createLabelMutation,
+ mutation: workspaceCreateLabelMutation[this.workspaceType],
variables: this.mutationVariables,
update: (
store,
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue
index 19fe78aca87..fc8834a97d4 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue
@@ -4,7 +4,7 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { createAlert } from '~/alert';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
-import { workspaceLabelsQueries } from '../../../constants';
+import { workspaceLabelsQueries } from '../../../queries/constants';
import LabelItem from './label_item.vue';
export default {
@@ -135,6 +135,16 @@ export default {
this.handleLabelClick(this.visibleLabels[0]);
}
},
+ handleFocus(event, index) {
+ if (index === 0 && event.target.classList.contains('is-focused')) {
+ event.target.classList.remove('is-focused');
+
+ // Focus next element (if available) as the first item was already focused.
+ if (event.target.parentNode?.nextElementSibling?.querySelector('button')) {
+ event.target.parentNode.nextElementSibling.querySelector('button').focus();
+ }
+ }
+ },
},
};
</script>
@@ -157,6 +167,7 @@ export default {
:active="shouldHighlightFirstItem && index === 0"
active-class="is-focused"
data-testid="labels-list"
+ @focus.native.capture="handleFocus($event, index)"
@click.native.capture.stop="handleLabelClick(label)"
>
<label-item :label="label" />
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_footer.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_footer.vue
index e67e704ffb8..d6b43698766 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_footer.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_footer.vue
@@ -13,7 +13,13 @@ export default {
},
footerManageLabelTitle: {
type: String,
- required: true,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ showManageLabelsItem() {
+ return this.footerManageLabelTitle && this.labelsManagePath;
},
},
};
@@ -28,7 +34,12 @@ export default {
>
{{ footerCreateLabelTitle }}
</gl-dropdown-item>
- <gl-dropdown-item :href="labelsManagePath" @click.capture.native.stop>
+ <gl-dropdown-item
+ v-if="showManageLabelsItem"
+ data-testid="manage-labels-button"
+ :href="labelsManagePath"
+ @click.capture.native.stop
+ >
{{ footerManageLabelTitle }}
</gl-dropdown-item>
</div>
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 74c3f08a47b..f9a9cc316c1 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
@@ -7,7 +7,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { TYPE_EPIC, TYPE_ISSUE, TYPE_MERGE_REQUEST, TYPE_TEST_CASE } from '~/issues/constants';
import { __ } from '~/locale';
-import { issuableLabelsQueries } from '../../../constants';
+import { issuableLabelsQueries } from '../../../queries/constants';
import SidebarEditableItem from '../../sidebar_editable_item.vue';
import { DEBOUNCE_DROPDOWN_DELAY, VARIANT_SIDEBAR } from './constants';
import DropdownContents from './dropdown_contents.vue';
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 7b288e15a3e..99d36a61632 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -138,13 +138,10 @@ export default {
</a>
</div>
</div>
- <div v-if="hasMoreParticipants" class="participants-more hide-collapsed">
- <gl-button
- variant="link"
- button-text-classes="gl-text-secondary"
- @click="toggleMoreParticipants"
- >{{ toggleLabel }}</gl-button
- >
+ <div v-if="hasMoreParticipants" class="hide-collapsed">
+ <gl-button category="tertiary" size="small" @click="toggleMoreParticipants">{{
+ toggleLabel
+ }}</gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
index b0556e22a8d..b764d660d63 100644
--- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
+++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
@@ -1,6 +1,6 @@
<script>
import { __ } from '~/locale';
-import { participantsQueries } from '../../constants';
+import { participantsQueries } from '../../queries/constants';
import Participants from './participants.vue';
export default {
diff --git a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue
index 88a74784dd2..415c40b4779 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue
@@ -52,7 +52,8 @@ export default {
if (mergeLength === this.users.length) {
return '';
- } else if (mergeLength > 0) {
+ }
+ if (mergeLength > 0) {
return sprintf(__('%{mergeLength}/%{usersLength} can merge'), {
mergeLength,
usersLength: this.users.length,
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue
index 50b4284cde0..c9450244b40 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue
@@ -20,13 +20,13 @@ import {
defaultEpicSort,
dropdowni18nText,
epicIidPattern,
- issuableAttributesQueries,
IssuableAttributeState,
IssuableAttributeType,
IssuableAttributeTypeKeyMap,
LocalizedIssuableAttributeType,
noAttributeId,
} from 'ee_else_ce/sidebar/constants';
+import { issuableAttributesQueries } from 'ee_else_ce/sidebar/queries/constants';
import { createAlert } from '~/alert';
import { PathIdSeparator } from '~/related_issues/constants';
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index 4721c6fee61..7fde43a360d 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -11,10 +11,10 @@ import {
dropdowni18nText,
LocalizedIssuableAttributeType,
IssuableAttributeTypeKeyMap,
- issuableAttributesQueries,
IssuableAttributeType,
Tracking,
} from 'ee_else_ce/sidebar/constants';
+import { issuableAttributesQueries } from 'ee_else_ce/sidebar/queries/constants';
import SidebarDropdown from './sidebar_dropdown.vue';
import SidebarEditableItem from './sidebar_editable_item.vue';
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 d6e1847aecb..568962cddc7 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
@@ -13,7 +13,8 @@ import { isLoggedIn } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import toast from '~/vue_shared/plugins/global_toast';
-import { subscribedQueries, Tracking } from '../../constants';
+import { Tracking } from '../../constants';
+import { subscribedQueries } from '../../queries/constants';
import SidebarEditableItem from '../sidebar_editable_item.vue';
const ICON_ON = 'notifications';
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
index 465f971717f..ac05ae3896b 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -42,11 +42,14 @@ export default {
divClass() {
if (this.showComparisonState) {
return 'compare';
- } else if (this.showEstimateOnlyState) {
+ }
+ if (this.showEstimateOnlyState) {
return 'estimate-only';
- } else if (this.showSpentOnlyState) {
+ }
+ if (this.showSpentOnlyState) {
return 'spend-only';
- } else if (this.showNoTimeTrackingState) {
+ }
+ if (this.showNoTimeTrackingState) {
return 'no-tracking';
}
@@ -55,9 +58,11 @@ export default {
spanClass() {
if (this.showComparisonState) {
return '';
- } else if (this.showEstimateOnlyState || this.showSpentOnlyState) {
+ }
+ if (this.showEstimateOnlyState || this.showSpentOnlyState) {
return 'bold';
- } else if (this.showNoTimeTrackingState) {
+ }
+ if (this.showNoTimeTrackingState) {
return 'no-value collapse-truncated-title gl-pt-2 gl-px-3 gl-font-sm';
}
@@ -66,11 +71,14 @@ export default {
text() {
if (this.showComparisonState) {
return `${this.timeSpentHumanReadable} / ${this.timeEstimateHumanReadable}`;
- } else if (this.showEstimateOnlyState) {
+ }
+ if (this.showEstimateOnlyState) {
return `-- / ${this.timeEstimateHumanReadable}`;
- } else if (this.showSpentOnlyState) {
+ }
+ if (this.showSpentOnlyState) {
return `${this.timeSpentHumanReadable} / --`;
- } else if (this.showNoTimeTrackingState) {
+ }
+ if (this.showNoTimeTrackingState) {
return __('None');
}
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
index 70d8024f46a..9bd4c7f5c68 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
@@ -7,7 +7,7 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ISSUE } from '~/issues/constants';
import { formatDate, parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import { __, s__ } from '~/locale';
-import { timelogQueries } from '../../constants';
+import { timelogQueries } from '../../queries/constants';
import deleteTimelogMutation from '../../queries/delete_timelog.mutation.graphql';
const TIME_DATE_FORMAT = 'mmmm d, yyyy, HH:MM ("UTC:" o)';
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index 1d427a871e1..aff592d48e0 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -12,7 +12,8 @@ import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { s__, __ } from '~/locale';
-import { HOW_TO_TRACK_TIME, timeTrackingQueries } from '../../constants';
+import { HOW_TO_TRACK_TIME } from '../../constants';
+import { timeTrackingQueries } from '../../queries/constants';
import eventHub from '../../event_hub';
import TimeTrackingCollapsedState from './collapsed_state.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue';
@@ -122,9 +123,11 @@ export default {
// 3. issuableIid and fullPath are not provided
if (!this.issuableType || !timeTrackingQueries[this.issuableType]) {
return true;
- } else if (this.initialTimeTracking) {
+ }
+ if (this.initialTimeTracking) {
return true;
- } else if (!this.issuableIid || !this.fullPath) {
+ }
+ if (!this.issuableIid || !this.fullPath) {
return true;
}
return false;
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 551d306a9c4..1099dcb832f 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
@@ -6,7 +6,8 @@ import { TYPE_MERGE_REQUEST } from '~/issues/constants';
import { __, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import Tracking from '~/tracking';
-import { todoQueries, TodoMutationTypes, todoMutations } from '../../constants';
+import { TodoMutationTypes } from '../../constants';
+import { todoQueries, todoMutations } from '../../queries/constants';
import { todoLabel } from '../../utils';
import TodoButton from './todo_button.vue';
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index 0f82182c6e2..f13f613733b 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -1,173 +1,10 @@
import { invert } from 'lodash';
import { s__, __, sprintf } from '~/locale';
-import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
-import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
-import userSearchWithMRPermissionsQuery from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
-import {
- TYPE_ALERT,
- TYPE_EPIC,
- TYPE_ISSUE,
- TYPE_MERGE_REQUEST,
- TYPE_TEST_CASE,
- WORKSPACE_GROUP,
- WORKSPACE_PROJECT,
-} from '~/issues/constants';
-import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
-import issuableDatesUpdatedSubscription from '../graphql_shared/subscriptions/work_item_dates.subscription.graphql';
-import updateTestCaseLabelsMutation from './components/labels/labels_select_widget/graphql/update_test_case_labels.mutation.graphql';
-import epicLabelsQuery from './components/labels/labels_select_widget/graphql/epic_labels.query.graphql';
-import updateEpicLabelsMutation from './components/labels/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
-import groupLabelsQuery from './components/labels/labels_select_widget/graphql/group_labels.query.graphql';
-import issueLabelsQuery from './components/labels/labels_select_widget/graphql/issue_labels.query.graphql';
-import mergeRequestLabelsQuery from './components/labels/labels_select_widget/graphql/merge_request_labels.query.graphql';
-import projectLabelsQuery from './components/labels/labels_select_widget/graphql/project_labels.query.graphql';
-import epicConfidentialQuery from './queries/epic_confidential.query.graphql';
-import epicDueDateQuery from './queries/epic_due_date.query.graphql';
-import epicParticipantsQuery from './queries/epic_participants.query.graphql';
-import epicReferenceQuery from './queries/epic_reference.query.graphql';
-import epicStartDateQuery from './queries/epic_start_date.query.graphql';
-import epicSubscribedQuery from './queries/epic_subscribed.query.graphql';
-import epicTodoQuery from './queries/epic_todo.query.graphql';
-import issuableAssigneesSubscription from './queries/issuable_assignees.subscription.graphql';
-import issueConfidentialQuery from './queries/issue_confidential.query.graphql';
-import issueDueDateQuery from './queries/issue_due_date.query.graphql';
-import issueReferenceQuery from './queries/issue_reference.query.graphql';
-import issueSubscribedQuery from './queries/issue_subscribed.query.graphql';
-import issueTimeTrackingQuery from './queries/issue_time_tracking.query.graphql';
-import issueTodoQuery from './queries/issue_todo.query.graphql';
-import mergeRequestMilestone from './queries/merge_request_milestone.query.graphql';
-import mergeRequestReferenceQuery from './queries/merge_request_reference.query.graphql';
-import mergeRequestSubscribed from './queries/merge_request_subscribed.query.graphql';
-import mergeRequestTimeTrackingQuery from './queries/merge_request_time_tracking.query.graphql';
-import mergeRequestTodoQuery from './queries/merge_request_todo.query.graphql';
-import todoCreateMutation from './queries/todo_create.mutation.graphql';
-import todoMarkDoneMutation from './queries/todo_mark_done.mutation.graphql';
-import updateEpicConfidentialMutation from './queries/update_epic_confidential.mutation.graphql';
-import updateEpicDueDateMutation from './queries/update_epic_due_date.mutation.graphql';
-import updateEpicStartDateMutation from './queries/update_epic_start_date.mutation.graphql';
-import updateEpicSubscriptionMutation from './queries/update_epic_subscription.mutation.graphql';
-import updateIssueConfidentialMutation from './queries/update_issue_confidential.mutation.graphql';
-import updateIssueDueDateMutation from './queries/update_issue_due_date.mutation.graphql';
-import updateIssueSubscriptionMutation from './queries/update_issue_subscription.mutation.graphql';
-import mergeRequestMilestoneMutation from './queries/update_merge_request_milestone.mutation.graphql';
-import updateMergeRequestLabelsMutation from './queries/update_merge_request_labels.mutation.graphql';
-import updateMergeRequestSubscriptionMutation from './queries/update_merge_request_subscription.mutation.graphql';
-import getAlertAssignees from './queries/get_alert_assignees.query.graphql';
-import getIssueAssignees from './queries/get_issue_assignees.query.graphql';
-import issueParticipantsQuery from './queries/get_issue_participants.query.graphql';
-import getIssueTimelogsQuery from './queries/get_issue_timelogs.query.graphql';
-import getMergeRequestAssignees from './queries/get_mr_assignees.query.graphql';
-import getMergeRequestParticipants from './queries/get_mr_participants.query.graphql';
-import getMrTimelogsQuery from './queries/get_mr_timelogs.query.graphql';
-import updateIssueAssigneesMutation from './queries/update_issue_assignees.mutation.graphql';
-import updateMergeRequestAssigneesMutation from './queries/update_mr_assignees.mutation.graphql';
-import getEscalationStatusQuery from './queries/escalation_status.query.graphql';
-import updateEscalationStatusMutation from './queries/update_escalation_status.mutation.graphql';
-import groupMilestonesQuery from './queries/group_milestones.query.graphql';
-import projectIssueMilestoneMutation from './queries/project_issue_milestone.mutation.graphql';
-import projectIssueMilestoneQuery from './queries/project_issue_milestone.query.graphql';
-import projectMilestonesQuery from './queries/project_milestones.query.graphql';
export const defaultEpicSort = 'TITLE_ASC';
export const epicIidPattern = /^&(?<iid>\d+)$/;
-export const assigneesQueries = {
- [TYPE_ISSUE]: {
- query: getIssueAssignees,
- subscription: issuableAssigneesSubscription,
- mutation: updateIssueAssigneesMutation,
- },
- [TYPE_MERGE_REQUEST]: {
- query: getMergeRequestAssignees,
- mutation: updateMergeRequestAssigneesMutation,
- },
- [TYPE_ALERT]: {
- query: getAlertAssignees,
- mutation: updateAlertAssigneesMutation,
- },
-};
-
-export const participantsQueries = {
- [TYPE_ISSUE]: {
- query: issueParticipantsQuery,
- },
- [TYPE_MERGE_REQUEST]: {
- query: getMergeRequestParticipants,
- },
- [TYPE_EPIC]: {
- query: epicParticipantsQuery,
- },
- [TYPE_ALERT]: {
- query: '',
- skipQuery: true,
- },
-};
-
-export const userSearchQueries = {
- [TYPE_ISSUE]: {
- query: userSearchQuery,
- },
- [TYPE_MERGE_REQUEST]: {
- query: userSearchWithMRPermissionsQuery,
- },
-};
-
-export const confidentialityQueries = {
- [TYPE_ISSUE]: {
- query: issueConfidentialQuery,
- mutation: updateIssueConfidentialMutation,
- },
- [TYPE_EPIC]: {
- query: epicConfidentialQuery,
- mutation: updateEpicConfidentialMutation,
- },
-};
-
-export const referenceQueries = {
- [TYPE_ISSUE]: {
- query: issueReferenceQuery,
- },
- [TYPE_MERGE_REQUEST]: {
- query: mergeRequestReferenceQuery,
- },
- [TYPE_EPIC]: {
- query: epicReferenceQuery,
- },
-};
-
-export const workspaceLabelsQueries = {
- [WORKSPACE_PROJECT]: {
- query: projectLabelsQuery,
- },
- [WORKSPACE_GROUP]: {
- query: groupLabelsQuery,
- },
-};
-
-export const issuableLabelsQueries = {
- [TYPE_ISSUE]: {
- issuableQuery: issueLabelsQuery,
- mutation: updateIssueLabelsMutation,
- mutationName: 'updateIssue',
- },
- [TYPE_MERGE_REQUEST]: {
- issuableQuery: mergeRequestLabelsQuery,
- mutation: updateMergeRequestLabelsMutation,
- mutationName: 'mergeRequestSetLabels',
- },
- [TYPE_EPIC]: {
- issuableQuery: epicLabelsQuery,
- mutation: updateEpicLabelsMutation,
- mutationName: 'updateEpic',
- },
- [TYPE_TEST_CASE]: {
- issuableQuery: issueLabelsQuery,
- mutation: updateTestCaseLabelsMutation,
- mutationName: 'updateTestCaseLabels',
- },
-};
-
export const dateTypes = {
start: 'startDate',
due: 'dueDate',
@@ -186,91 +23,13 @@ export const dateFields = {
},
};
-export const subscribedQueries = {
- [TYPE_ISSUE]: {
- query: issueSubscribedQuery,
- mutation: updateIssueSubscriptionMutation,
- },
- [TYPE_EPIC]: {
- query: epicSubscribedQuery,
- mutation: updateEpicSubscriptionMutation,
- },
- [TYPE_MERGE_REQUEST]: {
- query: mergeRequestSubscribed,
- mutation: updateMergeRequestSubscriptionMutation,
- },
-};
-
export const Tracking = {
editEvent: 'click_edit_button',
rightSidebarLabel: 'right_sidebar',
};
-export const timeTrackingQueries = {
- [TYPE_ISSUE]: {
- query: issueTimeTrackingQuery,
- },
- [TYPE_MERGE_REQUEST]: {
- query: mergeRequestTimeTrackingQuery,
- },
-};
-
-export const dueDateQueries = {
- [TYPE_ISSUE]: {
- query: issueDueDateQuery,
- mutation: updateIssueDueDateMutation,
- subscription: issuableDatesUpdatedSubscription,
- },
- [TYPE_EPIC]: {
- query: epicDueDateQuery,
- mutation: updateEpicDueDateMutation,
- },
-};
-
-export const startDateQueries = {
- [TYPE_EPIC]: {
- query: epicStartDateQuery,
- mutation: updateEpicStartDateMutation,
- },
-};
-
-export const timelogQueries = {
- [TYPE_ISSUE]: {
- query: getIssueTimelogsQuery,
- },
- [TYPE_MERGE_REQUEST]: {
- query: getMrTimelogsQuery,
- },
-};
-
export const noAttributeId = null;
-export const issuableMilestoneQueries = {
- [TYPE_ISSUE]: {
- query: projectIssueMilestoneQuery,
- mutation: projectIssueMilestoneMutation,
- },
- [TYPE_MERGE_REQUEST]: {
- query: mergeRequestMilestone,
- mutation: mergeRequestMilestoneMutation,
- },
-};
-
-export const milestonesQueries = {
- [TYPE_ISSUE]: {
- query: {
- [WORKSPACE_GROUP]: groupMilestonesQuery,
- [WORKSPACE_PROJECT]: projectMilestonesQuery,
- },
- },
- [TYPE_MERGE_REQUEST]: {
- query: {
- [WORKSPACE_GROUP]: groupMilestonesQuery,
- [WORKSPACE_PROJECT]: projectMilestonesQuery,
- },
- },
-};
-
export const IssuableAttributeType = {
Milestone: 'milestone',
};
@@ -285,35 +44,11 @@ export const IssuableAttributeState = {
[IssuableAttributeType.Milestone]: 'active',
};
-export const issuableAttributesQueries = {
- [IssuableAttributeType.Milestone]: {
- current: issuableMilestoneQueries,
- list: milestonesQueries,
- },
-};
-
-export const todoQueries = {
- [TYPE_EPIC]: {
- query: epicTodoQuery,
- },
- [TYPE_ISSUE]: {
- query: issueTodoQuery,
- },
- [TYPE_MERGE_REQUEST]: {
- query: mergeRequestTodoQuery,
- },
-};
-
export const TodoMutationTypes = {
Create: 'create',
MarkDone: 'mark-done',
};
-export const todoMutations = {
- [TodoMutationTypes.Create]: todoCreateMutation,
- [TodoMutationTypes.MarkDone]: todoMarkDoneMutation,
-};
-
export function dropdowni18nText(issuableAttribute, issuableType) {
return {
noAttribute: sprintf(s__('DropdownWidget|No %{issuableAttribute}'), {
@@ -362,9 +97,6 @@ export function dropdowni18nText(issuableAttribute, issuableType) {
};
}
-export const escalationStatusQuery = getEscalationStatusQuery;
-export const escalationStatusMutation = updateEscalationStatusMutation;
-
export const HOW_TO_TRACK_TIME = __('How to track time');
export const statusDropdownOptions = [
diff --git a/app/assets/javascripts/sidebar/queries/constants.js b/app/assets/javascripts/sidebar/queries/constants.js
new file mode 100644
index 00000000000..0844abc4599
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/constants.js
@@ -0,0 +1,291 @@
+import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
+import userAutocompleteQuery from '~/graphql_shared/queries/project_autocomplete_users.query.graphql';
+import userAutocompleteWithMRPermissionsQuery from '~/graphql_shared/queries/project_autocomplete_users_with_mr_permissions.query.graphql';
+import issuableDatesUpdatedSubscription from '~/graphql_shared/subscriptions/work_item_dates.subscription.graphql';
+import {
+ TYPE_ALERT,
+ TYPE_EPIC,
+ TYPE_ISSUE,
+ TYPE_MERGE_REQUEST,
+ TYPE_TEST_CASE,
+ WORKSPACE_GROUP,
+ WORKSPACE_PROJECT,
+} from '~/issues/constants';
+import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
+import abuseReportLabelsQuery from '~/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql';
+import createAbuseReportLabelMutation from '~/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql';
+import createGroupOrProjectLabelMutation from '../components/labels/labels_select_widget/graphql/create_label.mutation.graphql';
+import updateTestCaseLabelsMutation from '../components/labels/labels_select_widget/graphql/update_test_case_labels.mutation.graphql';
+import epicLabelsQuery from '../components/labels/labels_select_widget/graphql/epic_labels.query.graphql';
+import updateEpicLabelsMutation from '../components/labels/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
+import groupLabelsQuery from '../components/labels/labels_select_widget/graphql/group_labels.query.graphql';
+import issueLabelsQuery from '../components/labels/labels_select_widget/graphql/issue_labels.query.graphql';
+import mergeRequestLabelsQuery from '../components/labels/labels_select_widget/graphql/merge_request_labels.query.graphql';
+import projectLabelsQuery from '../components/labels/labels_select_widget/graphql/project_labels.query.graphql';
+import { IssuableAttributeType, TodoMutationTypes } from '../constants';
+import epicConfidentialQuery from './epic_confidential.query.graphql';
+import epicDueDateQuery from './epic_due_date.query.graphql';
+import epicParticipantsQuery from './epic_participants.query.graphql';
+import epicReferenceQuery from './epic_reference.query.graphql';
+import epicStartDateQuery from './epic_start_date.query.graphql';
+import epicSubscribedQuery from './epic_subscribed.query.graphql';
+import epicTodoQuery from './epic_todo.query.graphql';
+import issuableAssigneesSubscription from './issuable_assignees.subscription.graphql';
+import issueConfidentialQuery from './issue_confidential.query.graphql';
+import issueDueDateQuery from './issue_due_date.query.graphql';
+import issueReferenceQuery from './issue_reference.query.graphql';
+import issueSubscribedQuery from './issue_subscribed.query.graphql';
+import issueTimeTrackingQuery from './issue_time_tracking.query.graphql';
+import issueTodoQuery from './issue_todo.query.graphql';
+import mergeRequestMilestone from './merge_request_milestone.query.graphql';
+import mergeRequestReferenceQuery from './merge_request_reference.query.graphql';
+import mergeRequestSubscribed from './merge_request_subscribed.query.graphql';
+import mergeRequestTimeTrackingQuery from './merge_request_time_tracking.query.graphql';
+import mergeRequestTodoQuery from './merge_request_todo.query.graphql';
+import todoCreateMutation from './todo_create.mutation.graphql';
+import todoMarkDoneMutation from './todo_mark_done.mutation.graphql';
+import updateEpicConfidentialMutation from './update_epic_confidential.mutation.graphql';
+import updateEpicDueDateMutation from './update_epic_due_date.mutation.graphql';
+import updateEpicStartDateMutation from './update_epic_start_date.mutation.graphql';
+import updateEpicSubscriptionMutation from './update_epic_subscription.mutation.graphql';
+import updateIssueConfidentialMutation from './update_issue_confidential.mutation.graphql';
+import updateIssueDueDateMutation from './update_issue_due_date.mutation.graphql';
+import updateIssueSubscriptionMutation from './update_issue_subscription.mutation.graphql';
+import mergeRequestMilestoneMutation from './update_merge_request_milestone.mutation.graphql';
+import updateMergeRequestLabelsMutation from './update_merge_request_labels.mutation.graphql';
+import updateMergeRequestSubscriptionMutation from './update_merge_request_subscription.mutation.graphql';
+import getAlertAssignees from './get_alert_assignees.query.graphql';
+import getIssueAssignees from './get_issue_assignees.query.graphql';
+import issueParticipantsQuery from './get_issue_participants.query.graphql';
+import getIssueTimelogsQuery from './get_issue_timelogs.query.graphql';
+import getMergeRequestAssignees from './get_mr_assignees.query.graphql';
+import getMergeRequestParticipants from './get_mr_participants.query.graphql';
+import getMrTimelogsQuery from './get_mr_timelogs.query.graphql';
+import updateIssueAssigneesMutation from './update_issue_assignees.mutation.graphql';
+import updateMergeRequestAssigneesMutation from './update_mr_assignees.mutation.graphql';
+import getEscalationStatusQuery from './escalation_status.query.graphql';
+import updateEscalationStatusMutation from './update_escalation_status.mutation.graphql';
+import groupMilestonesQuery from './group_milestones.query.graphql';
+import projectIssueMilestoneMutation from './project_issue_milestone.mutation.graphql';
+import projectIssueMilestoneQuery from './project_issue_milestone.query.graphql';
+import projectMilestonesQuery from './project_milestones.query.graphql';
+import testCaseConfidentialQuery from './test_case_confidential.query.graphql';
+import updateTestCaseConfidentialMutation from './update_test_case_confidential.mutation.graphql';
+
+export const assigneesQueries = {
+ [TYPE_ISSUE]: {
+ query: getIssueAssignees,
+ subscription: issuableAssigneesSubscription,
+ mutation: updateIssueAssigneesMutation,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: getMergeRequestAssignees,
+ mutation: updateMergeRequestAssigneesMutation,
+ },
+ [TYPE_ALERT]: {
+ query: getAlertAssignees,
+ mutation: updateAlertAssigneesMutation,
+ },
+};
+
+export const participantsQueries = {
+ [TYPE_ISSUE]: {
+ query: issueParticipantsQuery,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: getMergeRequestParticipants,
+ },
+ [TYPE_EPIC]: {
+ query: epicParticipantsQuery,
+ },
+ [TYPE_ALERT]: {
+ query: '',
+ skipQuery: true,
+ },
+};
+
+export const userSearchQueries = {
+ [TYPE_ISSUE]: {
+ query: userAutocompleteQuery,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: userAutocompleteWithMRPermissionsQuery,
+ },
+};
+
+export const confidentialityQueries = {
+ [TYPE_ISSUE]: {
+ query: issueConfidentialQuery,
+ mutation: updateIssueConfidentialMutation,
+ },
+ [TYPE_EPIC]: {
+ query: epicConfidentialQuery,
+ mutation: updateEpicConfidentialMutation,
+ },
+ [TYPE_TEST_CASE]: {
+ query: testCaseConfidentialQuery,
+ mutation: updateTestCaseConfidentialMutation,
+ },
+};
+
+export const referenceQueries = {
+ [TYPE_ISSUE]: {
+ query: issueReferenceQuery,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: mergeRequestReferenceQuery,
+ },
+ [TYPE_EPIC]: {
+ query: epicReferenceQuery,
+ },
+};
+
+export const workspaceLabelsQueries = {
+ [WORKSPACE_PROJECT]: {
+ query: projectLabelsQuery,
+ dataPath: 'workspace.labels',
+ },
+ [WORKSPACE_GROUP]: {
+ query: groupLabelsQuery,
+ dataPath: 'workspace.labels',
+ },
+ abuseReport: {
+ query: abuseReportLabelsQuery,
+ dataPath: 'labels',
+ },
+};
+
+export const workspaceCreateLabelMutation = {
+ [WORKSPACE_PROJECT]: createGroupOrProjectLabelMutation,
+ [WORKSPACE_GROUP]: createGroupOrProjectLabelMutation,
+ abuseReport: createAbuseReportLabelMutation,
+};
+
+export const issuableLabelsQueries = {
+ [TYPE_ISSUE]: {
+ issuableQuery: issueLabelsQuery,
+ mutation: updateIssueLabelsMutation,
+ mutationName: 'updateIssue',
+ },
+ [TYPE_MERGE_REQUEST]: {
+ issuableQuery: mergeRequestLabelsQuery,
+ mutation: updateMergeRequestLabelsMutation,
+ mutationName: 'mergeRequestSetLabels',
+ },
+ [TYPE_EPIC]: {
+ issuableQuery: epicLabelsQuery,
+ mutation: updateEpicLabelsMutation,
+ mutationName: 'updateEpic',
+ },
+ [TYPE_TEST_CASE]: {
+ issuableQuery: issueLabelsQuery,
+ mutation: updateTestCaseLabelsMutation,
+ mutationName: 'updateTestCaseLabels',
+ },
+};
+
+export const subscribedQueries = {
+ [TYPE_ISSUE]: {
+ query: issueSubscribedQuery,
+ mutation: updateIssueSubscriptionMutation,
+ },
+ [TYPE_EPIC]: {
+ query: epicSubscribedQuery,
+ mutation: updateEpicSubscriptionMutation,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: mergeRequestSubscribed,
+ mutation: updateMergeRequestSubscriptionMutation,
+ },
+};
+
+export const timeTrackingQueries = {
+ [TYPE_ISSUE]: {
+ query: issueTimeTrackingQuery,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: mergeRequestTimeTrackingQuery,
+ },
+};
+
+export const dueDateQueries = {
+ [TYPE_ISSUE]: {
+ query: issueDueDateQuery,
+ mutation: updateIssueDueDateMutation,
+ subscription: issuableDatesUpdatedSubscription,
+ },
+ [TYPE_EPIC]: {
+ query: epicDueDateQuery,
+ mutation: updateEpicDueDateMutation,
+ },
+};
+
+export const startDateQueries = {
+ [TYPE_EPIC]: {
+ query: epicStartDateQuery,
+ mutation: updateEpicStartDateMutation,
+ },
+};
+
+export const timelogQueries = {
+ [TYPE_ISSUE]: {
+ query: getIssueTimelogsQuery,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: getMrTimelogsQuery,
+ },
+};
+
+export const issuableMilestoneQueries = {
+ [TYPE_ISSUE]: {
+ query: projectIssueMilestoneQuery,
+ mutation: projectIssueMilestoneMutation,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: mergeRequestMilestone,
+ mutation: mergeRequestMilestoneMutation,
+ },
+};
+
+export const milestonesQueries = {
+ [TYPE_ISSUE]: {
+ query: {
+ [WORKSPACE_GROUP]: groupMilestonesQuery,
+ [WORKSPACE_PROJECT]: projectMilestonesQuery,
+ },
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: {
+ [WORKSPACE_GROUP]: groupMilestonesQuery,
+ [WORKSPACE_PROJECT]: projectMilestonesQuery,
+ },
+ },
+};
+
+export const issuableAttributesQueries = {
+ [IssuableAttributeType.Milestone]: {
+ current: issuableMilestoneQueries,
+ list: milestonesQueries,
+ },
+};
+
+export const todoQueries = {
+ [TYPE_EPIC]: {
+ query: epicTodoQuery,
+ },
+ [TYPE_ISSUE]: {
+ query: issueTodoQuery,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: mergeRequestTodoQuery,
+ },
+};
+
+export const todoMutations = {
+ [TodoMutationTypes.Create]: todoCreateMutation,
+ [TodoMutationTypes.MarkDone]: todoMarkDoneMutation,
+};
+
+export const escalationStatusQuery = getEscalationStatusQuery;
+
+export const escalationStatusMutation = updateEscalationStatusMutation;
diff --git a/app/assets/javascripts/sidebar/queries/test_case_confidential.query.graphql b/app/assets/javascripts/sidebar/queries/test_case_confidential.query.graphql
new file mode 100644
index 00000000000..d8959b5ce3f
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/test_case_confidential.query.graphql
@@ -0,0 +1,9 @@
+query testCaseConfidential($fullPath: ID!, $iid: String) {
+ workspace: project(fullPath: $fullPath) {
+ id
+ issuable: issue(iid: $iid) {
+ id
+ confidential
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/update_test_case_confidential.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_test_case_confidential.mutation.graphql
new file mode 100644
index 00000000000..4094907cb95
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/update_test_case_confidential.mutation.graphql
@@ -0,0 +1,9 @@
+mutation updateTestCaseConfidential($input: IssueSetConfidentialInput!) {
+ issuableSetConfidential: issueSetConfidential(input: $input) {
+ issuable: issue {
+ id
+ confidential
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/silent_mode_settings/components/app.vue b/app/assets/javascripts/silent_mode_settings/components/app.vue
new file mode 100644
index 00000000000..2dd0449448c
--- /dev/null
+++ b/app/assets/javascripts/silent_mode_settings/components/app.vue
@@ -0,0 +1,70 @@
+<script>
+import { GlToggle, GlBadge } from '@gitlab/ui';
+import { updateApplicationSettings } from '~/rest_api';
+import { createAlert } from '~/alert';
+import toast from '~/vue_shared/plugins/global_toast';
+import { sprintf, __, s__ } from '~/locale';
+
+export default {
+ name: 'SilentModeSettingsApp',
+ i18n: {
+ toggleLabel: s__('SilentMode|Enable silent mode'),
+ saveSuccess: s__('SilentMode|Silent mode %{status}'),
+ saveError: s__('SilentMode|There was an error updating the Silent Mode Settings.'),
+ enabled: __('enabled'),
+ disabled: __('disabled'),
+ experiment: __('Experiment'),
+ },
+ components: {
+ GlToggle,
+ GlBadge,
+ },
+ props: {
+ isSilentModeEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isLoading: false,
+ silentModeEnabled: this.isSilentModeEnabled,
+ };
+ },
+ methods: {
+ updateSilentModeSettings() {
+ this.isLoading = true;
+
+ updateApplicationSettings({
+ silent_mode_enabled: this.silentModeEnabled,
+ })
+ .then(() => {
+ const status = this.silentModeEnabled
+ ? this.$options.i18n.enabled
+ : this.$options.i18n.disabled;
+ toast(sprintf(this.$options.i18n.saveSuccess, { status }));
+ })
+ .catch(() => {
+ createAlert({ message: this.$options.i18n.saveError });
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ },
+ },
+};
+</script>
+<template>
+ <gl-toggle
+ v-model="silentModeEnabled"
+ label-id="silent-mode-toggle"
+ :label="$options.i18n.toggleLabel"
+ :is-loading="isLoading"
+ @change="updateSilentModeSettings"
+ >
+ <template #label
+ >{{ $options.i18n.toggleLabel }} <gl-badge>{{ $options.i18n.experiment }}</gl-badge></template
+ >
+ </gl-toggle>
+</template>
diff --git a/app/assets/javascripts/silent_mode_settings/index.js b/app/assets/javascripts/silent_mode_settings/index.js
new file mode 100644
index 00000000000..b18f9c02964
--- /dev/null
+++ b/app/assets/javascripts/silent_mode_settings/index.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import SilentModeSettingsApp from './components/app.vue';
+
+Vue.use(Translate);
+
+export const initSilentModeSettings = () => {
+ const el = document.getElementById('js-silent-mode-settings');
+
+ if (!el) {
+ return false;
+ }
+
+ const { silentModeEnabled } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(SilentModeSettingsApp, {
+ props: {
+ isSilentModeEnabled: parseBoolean(silentModeEnabled),
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/snippets/components/embed_dropdown.vue b/app/assets/javascripts/snippets/components/embed_dropdown.vue
index 0fdbc89a038..17312c2373b 100644
--- a/app/assets/javascripts/snippets/components/embed_dropdown.vue
+++ b/app/assets/javascripts/snippets/components/embed_dropdown.vue
@@ -1,12 +1,5 @@
<script>
-import {
- GlButton,
- GlDropdown,
- GlDropdownSectionHeader,
- GlDropdownText,
- GlFormInputGroup,
- GlTooltipDirective,
-} from '@gitlab/ui';
+import { GlButton, GlDisclosureDropdown, GlFormInputGroup, GlTooltipDirective } from '@gitlab/ui';
import { escape as esc } from 'lodash';
import { __ } from '~/locale';
@@ -17,9 +10,7 @@ const MSG_COPY = __('Copy');
export default {
components: {
GlButton,
- GlDropdown,
- GlDropdownSectionHeader,
- GlDropdownText,
+ GlDisclosureDropdown,
GlFormInputGroup,
},
directives: {
@@ -45,22 +36,16 @@ export default {
};
</script>
<template>
- <gl-dropdown
- right
- :text="$options.MSG_EMBED"
- menu-class="gl-px-1! gl-pb-5! gl-dropdown-menu-wide"
+ <gl-disclosure-dropdown
+ :auto-close="false"
+ fluid-width
+ placement="right"
+ :toggle-text="$options.MSG_EMBED"
>
<template v-for="{ name, value } in sections">
- <gl-dropdown-section-header :key="`header_${name}`" data-testid="header">{{
- name
- }}</gl-dropdown-section-header>
- <gl-dropdown-text
- :key="`input_${name}`"
- tag="div"
- class="gl-dropdown-text-py-0 gl-dropdown-text-block"
- data-testid="input"
- >
- <gl-form-input-group :value="value" readonly select-on-click :label="name">
+ <div :key="name" :data-testid="`section-${name}`" class="gl-px-4 gl-py-2">
+ <h5 class="gl-font-sm gl-mt-1 gl-mb-2" data-testid="header">{{ name }}</h5>
+ <gl-form-input-group class="gl-w-31" :value="value" readonly select-on-click :label="name">
<template #append>
<gl-button
v-gl-tooltip.hover
@@ -73,7 +58,7 @@ export default {
/>
</template>
</gl-form-input-group>
- </gl-dropdown-text>
+ </div>
</template>
- </gl-dropdown>
+ </gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/snippets/utils/blob.js b/app/assets/javascripts/snippets/utils/blob.js
index a228d6111ce..97f21654aae 100644
--- a/app/assets/javascripts/snippets/utils/blob.js
+++ b/app/assets/javascripts/snippets/utils/blob.js
@@ -34,7 +34,8 @@ const diff = ({ content, path }, origBlob) => {
content,
filePath: path,
};
- } else if (origBlob.path !== path || origBlob.content !== content) {
+ }
+ if (origBlob.path !== path || origBlob.content !== content) {
return {
action: origBlob.path === path ? SNIPPET_BLOB_ACTION_UPDATE : SNIPPET_BLOB_ACTION_MOVE,
previousPath: origBlob.path,
diff --git a/app/assets/javascripts/super_sidebar/components/brand_logo.vue b/app/assets/javascripts/super_sidebar/components/brand_logo.vue
index 1589f4978e1..02cf36fb053 100644
--- a/app/assets/javascripts/super_sidebar/components/brand_logo.vue
+++ b/app/assets/javascripts/super_sidebar/components/brand_logo.vue
@@ -26,20 +26,28 @@ export default {
<template>
<a
- v-gl-tooltip:super-sidebar.hover.noninteractive.bottom.ds500="$options.i18n.homepage"
+ v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.homepage"
class="brand-logo"
:href="rootPath"
- :title="$options.i18n.homepage"
data-track-action="click_link"
data-track-label="gitlab_logo_link"
data-track-property="nav_core_menu"
>
+ <span class="gl-sr-only">{{ $options.i18n.homepage }}</span>
+ <!-- eslint-disable @gitlab/vue-require-i18n-attribute-strings -->
<img
v-if="logoUrl"
+ alt=""
data-testid="brand-header-custom-logo"
:src="logoUrl"
class="gl-h-6 gl-max-w-full"
/>
- <span v-else v-safe-html="$options.logo" data-testid="brand-header-default-logo"></span>
+ <!-- eslint-enable @gitlab/vue-require-i18n-attribute-strings -->
+ <span
+ v-else
+ v-safe-html="$options.logo"
+ aria-hidden
+ data-testid="brand-header-default-logo"
+ ></span>
</a>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/context_header.vue b/app/assets/javascripts/super_sidebar/components/context_header.vue
deleted file mode 100644
index 11b9840a409..00000000000
--- a/app/assets/javascripts/super_sidebar/components/context_header.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-<script>
-import { GlTruncate, GlAvatar, GlIcon } from '@gitlab/ui';
-
-export default {
- components: {
- GlTruncate,
- GlAvatar,
- GlIcon,
- },
- props: {
- /*
- * Contains metadata about the current view, e.g. `id`, `title` and `avatar`
- */
- context: {
- type: Object,
- required: true,
- },
- tag: {
- type: String,
- required: false,
- default: 'div',
- },
- },
- computed: {
- avatarShape() {
- return this.context.avatar_shape || 'rect';
- },
- },
-};
-</script>
-
-<template>
- <component
- :is="tag"
- class="border-top border-bottom gl-border-gray-a-08! gl-display-flex gl-align-items-center gl-gap-3 gl-font-weight-bold gl-w-full gl-h-8 gl-px-4 gl-flex-shrink-0"
- >
- <span
- v-if="context.icon"
- class="gl-avatar avatar-container gl-bg-t-gray-a-08 icon-avatar rect-avatar s24"
- >
- <gl-icon class="gl-text-gray-700" :name="context.icon" :size="16" />
- </span>
- <gl-avatar
- v-else
- :size="24"
- :shape="avatarShape"
- :entity-name="context.title"
- :entity-id="context.id"
- :src="context.avatar"
- />
- <div class="gl-flex-grow-1 gl-overflow-auto gl-text-gray-900">
- <gl-truncate :text="context.title" />
- </div>
- <slot name="end"></slot>
- </component>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/components/context_switcher.vue b/app/assets/javascripts/super_sidebar/components/context_switcher.vue
deleted file mode 100644
index d4aa11b6e04..00000000000
--- a/app/assets/javascripts/super_sidebar/components/context_switcher.vue
+++ /dev/null
@@ -1,209 +0,0 @@
-<script>
-import * as Sentry from '@sentry/browser';
-import { GlDisclosureDropdown, GlSearchBoxByType, GlLoadingIcon, GlAlert } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import searchUserProjectsAndGroups from '../graphql/queries/search_user_groups_and_projects.query.graphql';
-import { trackContextAccess, formatContextSwitcherItems } from '../utils';
-import NavItem from './nav_item.vue';
-import ProjectsList from './projects_list.vue';
-import GroupsList from './groups_list.vue';
-import ContextSwitcherToggle from './context_switcher_toggle.vue';
-
-export default {
- i18n: {
- contextNavigation: s__('Navigation|Context navigation'),
- switchTo: s__('Navigation|Switch context'),
- searchPlaceholder: s__('Navigation|Search your projects or groups'),
- searchingLabel: s__('Navigation|Retrieving search results'),
- searchError: s__('Navigation|There was an error fetching search results.'),
- },
- apollo: {
- groupsAndProjects: {
- query: searchUserProjectsAndGroups,
- manual: true,
- variables() {
- return {
- username: this.username,
- search: this.searchString,
- };
- },
- result(response) {
- this.hasError = false;
- try {
- const {
- data: {
- projects: { nodes: projects },
- user: {
- groups: { nodes: groups },
- },
- },
- } = response;
-
- this.projects = formatContextSwitcherItems(projects);
- this.groups = formatContextSwitcherItems(groups);
- } catch (e) {
- this.handleError(e);
- }
- },
- error(e) {
- this.handleError(e);
- },
- skip() {
- return !this.searchString;
- },
- },
- },
- components: {
- GlDisclosureDropdown,
- ContextSwitcherToggle,
- GlSearchBoxByType,
- GlLoadingIcon,
- GlAlert,
- NavItem,
- ProjectsList,
- GroupsList,
- },
- inject: ['contextSwitcherLinks'],
- props: {
- username: {
- type: String,
- required: true,
- },
- projectsPath: {
- type: String,
- required: true,
- },
- groupsPath: {
- type: String,
- required: true,
- },
- currentContext: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- contextHeader: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- searchString: '',
- projects: [],
- groups: [],
- hasError: false,
- isOpen: false,
- };
- },
- computed: {
- isSearch() {
- return Boolean(this.searchString);
- },
- isSearching() {
- return this.$apollo.queries.groupsAndProjects.loading;
- },
- },
- watch: {
- isOpen(isOpen) {
- this.$emit('toggle', isOpen);
-
- if (isOpen) {
- this.focusInput();
- }
- },
- },
- created() {
- if (this.currentContext.namespace) {
- trackContextAccess(this.username, this.currentContext);
- }
- },
- methods: {
- close() {
- this.$refs['disclosure-dropdown'].close();
- },
- focusInput() {
- this.$refs['search-box'].focusInput();
- },
- handleError(e) {
- Sentry.captureException(e);
- this.hasError = true;
- },
- onDisclosureDropdownShown() {
- this.isOpen = true;
- },
- onDisclosureDropdownHidden() {
- this.isOpen = false;
- },
- },
- DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
-};
-</script>
-
-<template>
- <gl-disclosure-dropdown
- ref="disclosure-dropdown"
- class="context-switcher gl-w-full"
- placement="center"
- @shown="onDisclosureDropdownShown"
- @hidden="onDisclosureDropdownHidden"
- >
- <template #toggle>
- <context-switcher-toggle :context="contextHeader" :expanded="isOpen" />
- </template>
- <div aria-hidden="true" class="gl-font-sm gl-font-weight-bold gl-px-4 gl-pt-3 gl-pb-4">
- {{ $options.i18n.switchTo }}
- </div>
- <div class="gl-p-1 gl-border-t gl-border-b gl-border-gray-50 gl-bg-white">
- <gl-search-box-by-type
- ref="search-box"
- v-model="searchString"
- class="context-switcher-search-box"
- :placeholder="$options.i18n.searchPlaceholder"
- :debounce="$options.DEFAULT_DEBOUNCE_AND_THROTTLE_MS"
- borderless
- />
- </div>
- <gl-loading-icon
- v-if="isSearching"
- class="gl-mt-5"
- size="md"
- :label="$options.i18n.searchingLabel"
- />
- <gl-alert v-else-if="hasError" variant="danger" :dismissible="false" class="gl-m-2">
- {{ $options.i18n.searchError }}
- </gl-alert>
- <nav v-else :aria-label="$options.i18n.contextNavigation" data-testid="context-navigation">
- <ul class="gl-p-0 gl-m-0 gl-list-style-none">
- <li v-if="!isSearch">
- <ul
- :aria-label="$options.i18n.switchTo"
- class="gl-border-b gl-border-gray-50 gl-px-0 gl-py-2"
- >
- <nav-item
- v-for="item in contextSwitcherLinks"
- :key="item.link"
- :item="item"
- :link-classes="{ [item.link_classes]: item.link_classes }"
- is-subitem
- />
- </ul>
- </li>
- <projects-list
- :username="username"
- :view-all-link="projectsPath"
- :is-search="isSearch"
- :search-results="projects"
- />
- <groups-list
- class="gl-border-t gl-border-gray-50"
- :username="username"
- :view-all-link="groupsPath"
- :is-search="isSearch"
- :search-results="groups"
- />
- </ul>
- </nav>
- </gl-disclosure-dropdown>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue b/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue
deleted file mode 100644
index faa7eba6470..00000000000
--- a/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-import ContextHeader from './context_header.vue';
-
-export default {
- components: {
- GlIcon,
- ContextHeader,
- },
- props: {
- /*
- * Contains metadata about the current view, e.g. `id`, `title` and `avatar`
- */
- context: {
- type: Object,
- required: true,
- },
- expanded: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- collapseIcon() {
- return this.expanded ? 'chevron-up' : 'chevron-down';
- },
- },
-};
-</script>
-
-<template>
- <context-header
- :context="context"
- tag="button"
- type="button"
- class="context-switcher-toggle gl-p-0 gl-bg-transparent gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-border-0 gl-box-shadow-none gl-text-left"
- data-testid="context-switcher"
- >
- <template #end>
- <gl-icon class="gl-text-gray-400" :name="collapseIcon" />
- </template>
- </context-header>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/components/create_menu.vue b/app/assets/javascripts/super_sidebar/components/create_menu.vue
index 3645606515f..d1e96479631 100644
--- a/app/assets/javascripts/super_sidebar/components/create_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/create_menu.vue
@@ -1,7 +1,7 @@
<script>
import {
GlDisclosureDropdown,
- GlTooltip,
+ GlTooltipDirective,
GlDisclosureDropdownGroup,
GlDisclosureDropdownItem,
} from '@gitlab/ui';
@@ -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 = -147;
+const DROPDOWN_X_OFFSET_BASE = -179;
const DROPDOWN_X_OFFSET_IMPERSONATING = DROPDOWN_X_OFFSET_BASE + IMPERSONATING_OFFSET;
export default {
@@ -22,9 +22,11 @@ export default {
GlDisclosureDropdown,
GlDisclosureDropdownGroup,
GlDisclosureDropdownItem,
- GlTooltip,
InviteMembersTrigger,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
i18n: {
createNew: __('Create new...'),
},
@@ -59,45 +61,35 @@ export default {
</script>
<template>
- <div>
- <gl-disclosure-dropdown
- category="tertiary"
- icon="plus"
- no-caret
- text-sr-only
- :toggle-text="$options.i18n.createNew"
- :toggle-id="$options.toggleId"
- :dropdown-offset="dropdownOffset"
- data-qa-selector="new_menu_toggle"
- data-testid="new-menu-toggle"
- @shown="dropdownOpen = true"
- @hidden="dropdownOpen = false"
- >
- <gl-disclosure-dropdown-group
- v-for="(group, index) in groups"
- :key="group.name"
- :bordered="index !== 0"
- :group="group"
- >
- <template v-for="groupItem in group.items">
- <invite-members-trigger
- v-if="isInvitedMembers(groupItem)"
- :key="`${groupItem.text}-trigger`"
- trigger-source="top-nav"
- :trigger-element="$options.TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN"
- />
- <gl-disclosure-dropdown-item v-else :key="groupItem.text" :item="groupItem" />
- </template>
- </gl-disclosure-dropdown-group>
- </gl-disclosure-dropdown>
- <gl-tooltip
- v-if="!dropdownOpen"
- :target="`#${$options.toggleId}`"
- placement="bottom"
- container="#super-sidebar"
- noninteractive
+ <gl-disclosure-dropdown
+ v-gl-tooltip:super-sidebar.hover.bottom="dropdownOpen ? '' : $options.i18n.createNew"
+ category="tertiary"
+ icon="plus"
+ no-caret
+ text-sr-only
+ :toggle-text="$options.i18n.createNew"
+ :toggle-id="$options.toggleId"
+ :dropdown-offset="dropdownOffset"
+ data-qa-selector="new_menu_toggle"
+ data-testid="new-menu-toggle"
+ @shown="dropdownOpen = true"
+ @hidden="dropdownOpen = false"
+ >
+ <gl-disclosure-dropdown-group
+ v-for="(group, index) in groups"
+ :key="group.name"
+ :bordered="index !== 0"
+ :group="group"
>
- {{ $options.i18n.createNew }}
- </gl-tooltip>
- </div>
+ <template v-for="groupItem in group.items">
+ <invite-members-trigger
+ v-if="isInvitedMembers(groupItem)"
+ :key="`${groupItem.text}-trigger`"
+ trigger-source="top_nav"
+ :trigger-element="$options.TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN"
+ />
+ <gl-disclosure-dropdown-item v-else :key="groupItem.text" :item="groupItem" />
+ </template>
+ </gl-disclosure-dropdown-group>
+ </gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/flyout_menu.vue b/app/assets/javascripts/super_sidebar/components/flyout_menu.vue
index fa7960da2f4..e73b9b275ee 100644
--- a/app/assets/javascripts/super_sidebar/components/flyout_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/flyout_menu.vue
@@ -2,6 +2,23 @@
import { computePosition, autoUpdate, offset, flip, shift } from '@floating-ui/dom';
import NavItem from './nav_item.vue';
+// Flyout menus are shown when the MenuSection's title is hovered with the mouse.
+// Their position is dynamically calculated with floating-ui.
+//
+// Since flyout menus show all NavItems of a section, they can be very long and
+// a user might want to move their mouse diagonally from the section title down
+// to last nav item in the flyout. But this mouse movement over other sections
+// would loose hover and close the flyout, opening another section's flyout.
+// To avoid this annoyance, our flyouts come with a "diagonal tolerance". This
+// is an area between the current mouse position and the top- and bottom-left
+// corner of the flyout itself. While the mouse stays within this area and
+// reaches the flyout before a timer expires, the native browser hover stays
+// within the component.
+// This is done with an transparent SVG positioned left of the flyout menu,
+// overlapping the sidebar. The SVG itself ignores pointer events but its two
+// triangles, one above the section title, one below, do listen to events,
+// keeping hover.
+
export default {
name: 'FlyoutMenu',
components: { NavItem },
@@ -15,13 +32,45 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ currentMouseX: 0,
+ flyoutX: 0,
+ flyoutY: 0,
+ flyoutHeight: 0,
+ hoverTimeoutId: null,
+ showSVG: true,
+ targetRect: null,
+ };
+ },
cleanupFunction: undefined,
+ computed: {
+ topSVGPoints() {
+ const x = (this.currentMouseX / this.targetRect.width) * 100;
+ let y = ((this.targetRect.top - this.flyoutY) / this.flyoutHeight) * 100;
+ y += 1; // overlap title to not loose hover
+
+ return `${x}, ${y} 100, 0 100, ${y}`;
+ },
+ bottomSVGPoints() {
+ const x = (this.currentMouseX / this.targetRect.width) * 100;
+ let y = ((this.targetRect.bottom - this.flyoutY) / this.flyoutHeight) * 100;
+ y -= 1; // overlap title to not loose hover
+
+ return `${x}, ${y} 100, ${y} 100, 100`;
+ },
+ },
+ created() {
+ const target = document.querySelector(`#${this.targetId}`);
+ target.addEventListener('mousemove', this.onMouseMove);
+ },
mounted() {
const target = document.querySelector(`#${this.targetId}`);
const flyout = document.querySelector(`#${this.targetId}-flyout`);
+ const sidebar = document.querySelector('#super-sidebar');
- function updatePosition() {
- return computePosition(target, flyout, {
+ const updatePosition = () =>
+ computePosition(target, flyout, {
middleware: [offset({ alignmentAxis: -12 }), flip(), shift()],
placement: 'right-start',
strategy: 'fixed',
@@ -30,13 +79,46 @@ export default {
left: `${x}px`,
top: `${y}px`,
});
+ this.flyoutX = x;
+ this.flyoutY = y;
+ this.flyoutHeight = flyout.clientHeight;
+
+ // Flyout coordinates are relative to the sidebar which can be
+ // shifted down by the performance-bar etc.
+ // Adjust viewport coordinates from getBoundingClientRect:
+ const targetRect = target.getBoundingClientRect();
+ const sidebarRect = sidebar.getBoundingClientRect();
+ this.targetRect = {
+ top: targetRect.top - sidebarRect.top,
+ bottom: targetRect.bottom - sidebarRect.top,
+ width: targetRect.width,
+ };
});
- }
this.$options.cleanupFunction = autoUpdate(target, flyout, updatePosition);
},
beforeUnmount() {
this.$options.cleanupFunction();
+ clearTimeout(this.hoverTimeoutId);
+ },
+ beforeDestroy() {
+ const target = document.querySelector(`#${this.targetId}`);
+ target.removeEventListener('mousemove', this.onMouseMove);
+ },
+ methods: {
+ startHoverTimeout() {
+ this.hoverTimeoutId = setTimeout(() => {
+ this.showSVG = false;
+ this.$emit('mouseleave');
+ }, 1000);
+ },
+ stopHoverTimeout() {
+ clearTimeout(this.hoverTimeoutId);
+ },
+ onMouseMove(e) {
+ // add some wiggle room to the left of mouse cursor
+ this.currentMouseX = Math.max(0, e.clientX - 5);
+ },
},
};
</script>
@@ -49,8 +131,8 @@ export default {
@mouseleave="$emit('mouseleave')"
>
<ul
- v-if="items.length > 0"
class="gl-min-w-20 gl-max-w-34 gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-100 gl-shadow-md gl-bg-white gl-p-2 gl-pb-1 gl-list-style-none"
+ @mouseenter="showSVG = false"
>
<nav-item
v-for="item of items"
@@ -61,5 +143,44 @@ export default {
@pin-remove="(itemId) => $emit('pin-remove', itemId)"
/>
</ul>
+ <svg
+ v-if="targetRect && showSVG"
+ :width="flyoutX"
+ :height="flyoutHeight"
+ viewBox="0 0 100 100"
+ preserveAspectRatio="none"
+ :style="{
+ top: flyoutY + 'px',
+ }"
+ >
+ <polygon
+ ref="topSVG"
+ :points="topSVGPoints"
+ fill="transparent"
+ @mouseenter="startHoverTimeout"
+ @mouseleave="stopHoverTimeout"
+ />
+ <polygon
+ ref="bottomSVG"
+ :points="bottomSVGPoints"
+ fill="transparent"
+ @mouseenter="startHoverTimeout"
+ @mouseleave="stopHoverTimeout"
+ />
+ </svg>
</div>
</template>
+
+<style scoped>
+svg {
+ pointer-events: none;
+
+ position: fixed;
+ right: 0;
+}
+
+svg polygon,
+svg rect {
+ pointer-events: auto;
+}
+</style>
diff --git a/app/assets/javascripts/super_sidebar/components/frequent_items_list.vue b/app/assets/javascripts/super_sidebar/components/frequent_items_list.vue
deleted file mode 100644
index fe1a907bd91..00000000000
--- a/app/assets/javascripts/super_sidebar/components/frequent_items_list.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-<script>
-import { GlButton, GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
-import {
- getItemsFromLocalStorage,
- removeItemFromLocalStorage,
- formatContextSwitcherItems,
-} from '../utils';
-import ItemsList from './items_list.vue';
-
-export default {
- components: {
- GlButton,
- ItemsList,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- title: {
- type: String,
- required: true,
- },
- pristineText: {
- type: String,
- required: true,
- },
- storageKey: {
- type: String,
- required: true,
- },
- maxItems: {
- type: Number,
- required: true,
- },
- },
- data() {
- return {
- cachedFrequentItems: [],
- };
- },
- computed: {
- isEmpty() {
- return !this.cachedFrequentItems.length;
- },
- },
- created() {
- this.cachedFrequentItems = formatContextSwitcherItems(
- getItemsFromLocalStorage({
- storageKey: this.storageKey,
- maxItems: this.maxItems,
- }),
- );
- },
- methods: {
- handleItemRemove(item) {
- removeItemFromLocalStorage({
- storageKey: this.storageKey,
- item,
- });
-
- this.cachedFrequentItems = this.cachedFrequentItems.filter((i) => i.id !== item.id);
- },
- },
- i18n: {
- removeItem: __('Remove'),
- },
-};
-</script>
-
-<template>
- <li class="gl-py-3">
- <div
- data-testid="list-title"
- aria-hidden="true"
- class="gl-display-flex gl-align-items-center gl-text-transform-uppercase gl-text-secondary gl-font-weight-semibold gl-font-xs gl-line-height-12 gl-letter-spacing-06em gl-my-3"
- >
- <span class="gl-flex-grow-1 gl-px-3">{{ title }}</span>
- </div>
- <div
- v-if="isEmpty"
- data-testid="empty-text"
- class="gl-text-gray-500 gl-font-sm gl-my-3 gl-mx-3"
- >
- {{ pristineText }}
- </div>
- <items-list :aria-label="title" :items="cachedFrequentItems">
- <template #actions="{ item }">
- <gl-button
- v-gl-tooltip.right.viewport
- size="small"
- category="tertiary"
- icon="dash"
- class="show-on-focus-or-hover--target"
- :aria-label="$options.i18n.removeItem"
- :title="$options.i18n.removeItem"
- data-testid="item-remove"
- @click.stop.prevent="handleItemRemove(item)"
- />
- </template>
- <template #view-all-items>
- <slot name="view-all-items"></slot>
- </template>
- </items-list>
- </li>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
index bd79962f1a1..b85b163cea9 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
@@ -5,6 +5,7 @@ import { GlDisclosureDropdownGroup, GlLoadingIcon } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import Tracking from '~/tracking';
import { getFormattedItem } from '../utils';
import {
@@ -18,6 +19,8 @@ import {
PATH_GROUP_TITLE,
GROUP_TITLES,
MAX_ROWS,
+ TRACKING_ACTIVATE_COMMAND_PALETTE,
+ TRACKING_HANDLE_LABEL_MAP,
} from './constants';
import SearchItem from './search_item.vue';
import { commandMapper, linksReducer, autocompleteQuery, fileMapper } from './utils';
@@ -29,6 +32,7 @@ export default {
GlLoadingIcon,
SearchItem,
},
+ mixins: [Tracking.mixin()],
inject: [
'commandPaletteCommands',
'commandPaletteLinks',
@@ -134,10 +138,15 @@ export default {
immediate: true,
},
handle: {
- handler() {
- this.debouncedSearch();
+ handler(value, oldValue) {
+ // Do not run search immediately on component creation
+ if (oldValue !== undefined) this.debouncedSearch();
+
+ // Track immediately on component creation
+ const label = TRACKING_HANDLE_LABEL_MAP[value] ?? 'unknown';
+ this.track(TRACKING_ACTIVATE_COMMAND_PALETTE, { label });
},
- immediate: false,
+ immediate: true,
},
},
updated() {
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
index a43e621da44..f6f4e36e43a 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
@@ -6,6 +6,16 @@ export const PROJECT_HANDLE = ':';
export const ISSUE_HANDLE = '#';
export const PATH_HANDLE = '/';
+export const TRACKING_ACTIVATE_COMMAND_PALETTE = 'activate_command_palette';
+export const TRACKING_CLICK_COMMAND_PALETTE_ITEM = 'click_command_palette_item';
+export const TRACKING_HANDLE_LABEL_MAP = {
+ [COMMAND_HANDLE]: 'command',
+ [USER_HANDLE]: 'user',
+ [PROJECT_HANDLE]: 'project',
+ [PATH_HANDLE]: 'path',
+ // No ISSUE_HANDLE. See https://gitlab.com/gitlab-org/gitlab/-/issues/417434.
+};
+
export const COMMON_HANDLES = [COMMAND_HANDLE, USER_HANDLE, PROJECT_HANDLE];
export const SEARCH_OR_COMMAND_MODE_PLACEHOLDER = sprintf(
s__(
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
index 347a8ffb0b4..32abbbfd3c2 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
@@ -1,6 +1,11 @@
import { isNil, omitBy } from 'lodash';
import { objectToQuery, joinPaths } from '~/lib/utils/url_utility';
-import { SEARCH_SCOPE, GLOBAL_COMMANDS_GROUP_TITLE } from './constants';
+import { TRACKING_UNKNOWN_ID } from '~/super_sidebar/constants';
+import {
+ SEARCH_SCOPE,
+ GLOBAL_COMMANDS_GROUP_TITLE,
+ TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+} from './constants';
export const commandMapper = ({ name, items }) => {
// TODO: we filter out invite_members for now, because it is complicated to add the invite members modal here
@@ -12,18 +17,34 @@ export const commandMapper = ({ name, items }) => {
};
export const linksReducer = (acc, menuItem) => {
+ const trackingAttrs = ({ id, title }) => {
+ return {
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': id || TRACKING_UNKNOWN_ID,
+ ...(id
+ ? {}
+ : {
+ 'data-track-extra': JSON.stringify({ title }),
+ }),
+ },
+ };
+ };
+
acc.push({
text: menuItem.title,
keywords: menuItem.title,
icon: menuItem.icon,
href: menuItem.link,
+ ...trackingAttrs(menuItem),
});
if (menuItem.items?.length) {
- const items = menuItem.items.map(({ title, link }) => ({
- keywords: title,
- text: [menuItem.title, title].join(' > '),
- href: link,
+ const items = menuItem.items.map((item) => ({
+ keywords: item.title,
+ text: [menuItem.title, item.title].join(' > '),
+ href: item.link,
icon: menuItem.icon,
+ ...trackingAttrs(item),
}));
/* eslint-disable-next-line no-param-reassign */
@@ -37,6 +58,10 @@ export const fileMapper = (projectBlobPath, file) => {
icon: 'doc-code',
text: file,
href: joinPaths(projectBlobPath, file),
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': 'file',
+ },
};
};
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue
index 382d844ceee..ddadd6856ca 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue
@@ -2,6 +2,8 @@
import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlIcon } from '@gitlab/ui';
import { truncateNamespace } from '~/lib/utils/text_utility';
import { getItemsFromLocalStorage, removeItemFromLocalStorage } from '~/super_sidebar/utils';
+import { TRACKING_UNKNOWN_PANEL } from '~/super_sidebar/constants';
+import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from '../command_palette/constants';
import FrequentItem from './frequent_item.vue';
export default {
@@ -65,6 +67,12 @@ export default {
// validator, and the href field ensures it renders a link.
text: item.name,
href: item.webUrl,
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': item.id,
+ 'data-track-property': TRACKING_UNKNOWN_PANEL,
+ 'data-track-extra': JSON.stringify({ title: item.name }),
+ },
},
forRenderer: {
id: item.id,
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
index b64f3ac52b2..4cfc329f8b8 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
@@ -18,14 +18,12 @@ import { sprintf } from '~/locale';
import { ARROW_DOWN_KEY, ARROW_UP_KEY, END_KEY, HOME_KEY, ESC_KEY } from '~/lib/utils/keys';
import {
MIN_SEARCH_TERM,
- SEARCH_GITLAB,
SEARCH_DESCRIBED_BY_WITH_RESULTS,
SEARCH_DESCRIBED_BY_DEFAULT,
SEARCH_DESCRIBED_BY_UPDATED,
SEARCH_RESULTS_LOADING,
SEARCH_RESULTS_SCOPE,
} from '~/vue_shared/global_search/constants';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { darkModeEnabled } from '~/lib/utils/color_utils';
import {
SEARCH_INPUT_DESCRIPTION,
@@ -52,10 +50,10 @@ export default {
name: 'GlobalSearchModal',
SEARCH_MODAL_ID,
i18n: {
- SEARCH_GITLAB,
SEARCH_DESCRIBED_BY_WITH_RESULTS,
SEARCH_DESCRIBED_BY_DEFAULT,
SEARCH_DESCRIBED_BY_UPDATED,
+ SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
SEARCH_RESULTS_LOADING,
SEARCH_RESULTS_SCOPE,
MIN_SEARCH_TERM,
@@ -72,7 +70,6 @@ export default {
CommandPaletteItems,
FakeSearchInput,
},
- mixins: [glFeatureFlagMixin()],
data() {
return {
nextFocusedItemIndex: null,
@@ -89,9 +86,6 @@ export default {
this.setSearch(value);
},
},
- searchPlaceholder() {
- return this.glFeatures?.commandPalette ? SEARCH_OR_COMMAND_MODE_PLACEHOLDER : SEARCH_GITLAB;
- },
showDefaultItems() {
return !this.searchText;
},
@@ -146,9 +140,8 @@ export default {
},
isCommandMode() {
return (
- this.glFeatures?.commandPalette &&
- (COMMON_HANDLES.includes(this.searchTextFirstChar) ||
- (this.searchContext.project && this.searchTextFirstChar === PATH_HANDLE))
+ COMMON_HANDLES.includes(this.searchTextFirstChar) ||
+ (this.searchContext?.project && this.searchTextFirstChar === PATH_HANDLE)
);
},
commandPaletteQuery() {
@@ -294,7 +287,7 @@ export default {
>
<form
role="search"
- :aria-label="searchPlaceholder"
+ :aria-label="$options.i18n.SEARCH_OR_COMMAND_MODE_PLACEHOLDER"
class="gl-relative gl-rounded-base gl-w-full gl-pb-0"
>
<div class="gl-relative gl-bg-white gl-border-b gl-mb-n1 gl-p-3">
@@ -305,7 +298,7 @@ export default {
role="searchbox"
data-testid="global-search-input"
autocomplete="off"
- :placeholder="searchPlaceholder"
+ :placeholder="$options.i18n.SEARCH_OR_COMMAND_MODE_PLACEHOLDER"
:aria-describedby="$options.SEARCH_INPUT_DESCRIPTION"
borderless
@input="getAutocompleteOptions"
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue
index 9a375837102..9167be5c1cc 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue
@@ -1,6 +1,8 @@
<script>
import { GlDisclosureDropdownGroup } from '@gitlab/ui';
import { PLACES } from '~/vue_shared/global_search/constants';
+import { TRACKING_UNKNOWN_ID, TRACKING_UNKNOWN_PANEL } from '~/super_sidebar/constants';
+import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from '../command_palette/constants';
export default {
name: 'DefaultPlaces',
@@ -18,7 +20,23 @@ export default {
group() {
return {
name: this.$options.i18n.PLACES,
- items: this.contextSwitcherLinks.map(({ title, link }) => ({ text: title, href: link })),
+ items: this.contextSwitcherLinks.map(({ title, link }) => ({
+ text: title,
+ href: link,
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ // The label and property are hard-coded as unknown for now for
+ // parity with the existing corresponding context switcher items.
+ // Once the context switcher is removed, these can be changed.
+ 'data-track-label': TRACKING_UNKNOWN_ID,
+ 'data-track-property': TRACKING_UNKNOWN_PANEL,
+ 'data-track-extra': JSON.stringify({ title }),
+
+ // QA attributes
+ 'data-testid': 'places-item-link',
+ 'data-qa-places-item': title,
+ },
+ })),
};
},
},
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js b/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js
index 6871dabc9a1..79be56f1427 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js
@@ -14,6 +14,7 @@ import {
SEARCH_RESULTS_ORDER,
} from '~/vue_shared/global_search/constants';
import { getFormattedItem } from '../utils';
+import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from '../command_palette/constants';
import {
ICON_GROUP,
@@ -172,6 +173,10 @@ export const scopedSearchOptions = (state, getters) => {
scopeCategory: PROJECTS_CATEGORY,
icon: ICON_PROJECT,
href: getters.projectUrl,
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': 'scoped_in_project',
+ },
});
}
@@ -182,6 +187,10 @@ export const scopedSearchOptions = (state, getters) => {
scopeCategory: GROUPS_CATEGORY,
icon: state.searchContext.group?.full_name?.includes('/') ? ICON_SUBGROUP : ICON_GROUP,
href: getters.groupUrl,
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': 'scoped_in_group',
+ },
});
}
@@ -189,6 +198,10 @@ export const scopedSearchOptions = (state, getters) => {
text: 'scoped-in-all',
description: MSG_IN_ALL_GITLAB,
href: getters.allUrl,
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': 'scoped_in_all',
+ },
});
return items;
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/utils.js b/app/assets/javascripts/super_sidebar/components/global_search/utils.js
index 11d1fa1ab95..2c369cbdf5f 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/utils.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/utils.js
@@ -1,5 +1,5 @@
import { pickBy } from 'lodash';
-import { truncateNamespace } from '~/lib/utils/text_utility';
+import { slugify, truncateNamespace } from '~/lib/utils/text_utility';
import {
GROUPS_CATEGORY,
PROJECTS_CATEGORY,
@@ -7,6 +7,7 @@ import {
ISSUES_CATEGORY,
RECENT_EPICS_CATEGORY,
} from '~/vue_shared/global_search/constants';
+import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from './command_palette/constants';
import { LARGE_AVATAR_PX, SMALL_AVATAR_PX } from './constants';
const getTruncatedNamespace = (string) => {
@@ -61,6 +62,15 @@ export const getFormattedItem = (item, searchContext) => {
const avatarSize = getAvatarSize(category);
const entityId = getEntityId(item, searchContext);
const entityName = getEntityName(item, searchContext);
+ const trackingLabel = slugify(category ?? '');
+ const trackingAttrs = trackingLabel
+ ? {
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': slugify(category, '_'),
+ },
+ }
+ : {};
return pickBy(
{
@@ -75,6 +85,7 @@ export const getFormattedItem = (item, searchContext) => {
namespace,
entity_id: entityId,
entity_name: entityName,
+ ...trackingAttrs,
},
(val) => val !== undefined,
);
diff --git a/app/assets/javascripts/super_sidebar/components/groups_list.vue b/app/assets/javascripts/super_sidebar/components/groups_list.vue
deleted file mode 100644
index 48becacebb7..00000000000
--- a/app/assets/javascripts/super_sidebar/components/groups_list.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-<script>
-import { s__ } from '~/locale';
-import { MAX_FREQUENT_GROUPS_COUNT } from '../constants';
-import FrequentItemsList from './frequent_items_list.vue';
-import SearchResults from './search_results.vue';
-import NavItem from './nav_item.vue';
-
-export default {
- MAX_FREQUENT_GROUPS_COUNT,
- components: {
- FrequentItemsList,
- SearchResults,
- NavItem,
- },
- props: {
- username: {
- type: String,
- required: true,
- },
- viewAllLink: {
- type: String,
- required: true,
- },
- isSearch: {
- type: Boolean,
- required: false,
- default: false,
- },
- searchResults: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- computed: {
- storageKey() {
- return `${this.username}/frequent-groups`;
- },
- viewAllProps() {
- return {
- item: {
- link: this.viewAllLink,
- title: s__('Navigation|View all your groups'),
- icon: 'group',
- },
- linkClasses: { 'dashboard-shortcuts-groups': true },
- };
- },
- },
- i18n: {
- title: s__('Navigation|Frequently visited groups'),
- searchTitle: s__('Navigation|Groups'),
- pristineText: s__('Navigation|Groups you visit often will appear here.'),
- noResultsText: s__('Navigation|No group matches found'),
- },
-};
-</script>
-
-<template>
- <search-results
- v-if="isSearch"
- :title="$options.i18n.searchTitle"
- :no-results-text="$options.i18n.noResultsText"
- :search-results="searchResults"
- >
- <template #view-all-items>
- <nav-item v-bind="viewAllProps" is-subitem />
- </template>
- </search-results>
- <frequent-items-list
- v-else
- :title="$options.i18n.title"
- :storage-key="storageKey"
- :max-items="$options.MAX_FREQUENT_GROUPS_COUNT"
- :pristine-text="$options.i18n.pristineText"
- >
- <template #view-all-items>
- <nav-item v-bind="viewAllProps" is-subitem />
- </template>
- </frequent-items-list>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/components/items_list.vue b/app/assets/javascripts/super_sidebar/components/items_list.vue
deleted file mode 100644
index 1bad13f91e8..00000000000
--- a/app/assets/javascripts/super_sidebar/components/items_list.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<script>
-import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
-import NavItem from './nav_item.vue';
-
-export default {
- components: {
- ProjectAvatar,
- NavItem,
- },
- props: {
- items: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
-};
-</script>
-
-<template>
- <ul class="gl-p-0 gl-list-style-none">
- <nav-item
- v-for="item in items"
- :key="item.id"
- :item="item"
- is-subitem
- class="show-on-focus-or-hover--context"
- >
- <template #icon>
- <project-avatar
- :project-id="item.id"
- :project-name="item.title"
- :project-avatar-url="item.avatar"
- :size="24"
- aria-hidden="true"
- />
- </template>
- <template #actions>
- <slot name="actions" :item="item"></slot>
- </template>
- </nav-item>
- <slot name="view-all-items"></slot>
- </ul>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/components/menu_section.vue b/app/assets/javascripts/super_sidebar/components/menu_section.vue
index d2d45ca7b6e..6b5002e1aa8 100644
--- a/app/assets/javascripts/super_sidebar/components/menu_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/menu_section.vue
@@ -79,15 +79,26 @@ export default {
isExpanded(newIsExpanded) {
this.$emit('collapse-toggle', newIsExpanded);
this.keepFlyoutClosed = !this.newIsExpanded;
+ if (!newIsExpanded) {
+ this.isMouseOverFlyout = false;
+ }
},
},
methods: {
handlePointerover(e) {
+ if (!this.hasFlyout) return;
+
this.isMouseOverSection = e.pointerType === 'mouse';
},
handlePointerleave() {
- this.isMouseOverSection = false;
+ if (!this.hasFlyout) return;
+
this.keepFlyoutClosed = false;
+ // delay state change. otherwise the flyout menu gets removed before it
+ // has a chance to emit its mouseover event.
+ setTimeout(() => {
+ this.isMouseOverSection = false;
+ }, 5);
},
},
};
@@ -129,8 +140,7 @@ export default {
</button>
<flyout-menu
- v-if="hasFlyout"
- v-show="isMouseOver && !isExpanded && !keepFlyoutClosed"
+ v-if="hasFlyout && isMouseOver && !isExpanded && !keepFlyoutClosed && item.items.length > 0"
:target-id="`menu-section-button-${itemId}`"
:items="item.items"
@mouseover="isMouseOverFlyout = true"
diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue
index 36803a885e7..5e0f8fffb0e 100644
--- a/app/assets/javascripts/super_sidebar/components/nav_item.vue
+++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue
@@ -1,6 +1,6 @@
<script>
-import { GlButton, GlIcon, GlBadge, GlTooltipDirective } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { GlAvatar, GlButton, GlIcon, GlBadge, GlTooltipDirective } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
import {
CLICK_MENU_ITEM_ACTION,
CLICK_PINNED_MENU_ITEM_ACTION,
@@ -12,11 +12,14 @@ import NavItemRouterLink from './nav_item_router_link.vue';
export default {
i18n: {
+ pin: s__('Navigation|Pin %{title}'),
pinItem: s__('Navigation|Pin item'),
+ unpin: s__('Navigation|Unpin %{title}'),
unpinItem: s__('Navigation|Unpin item'),
},
name: 'NavItem',
components: {
+ GlAvatar,
GlButton,
GlIcon,
GlBadge,
@@ -62,6 +65,12 @@ export default {
default: false,
},
},
+ data() {
+ return {
+ isMouseIn: false,
+ canClickPinButton: false,
+ };
+ },
computed: {
pillData() {
return this.item.pill_count;
@@ -96,12 +105,27 @@ export default {
...extraData,
};
},
+ /**
+ * Some QA specs rely on a stable "Project overview"/"Group overview" nav
+ * item data-qa-submenu-item attribute value.
+ *
+ * This computed ensures that those particular nav items use the `id` of
+ * the item rather than its title for that QA attribute.
+ *
+ * In future, probably all nav items should do this, for consistency.
+ * See https://gitlab.com/gitlab-org/gitlab/-/issues/422925.
+ */
+ qaSubMenuItem() {
+ const { id } = this.item;
+ if (id === 'project_overview' || id === 'group_overview') return id.replace(/_/g, '-');
+ return this.item.title;
+ },
linkProps() {
return {
...this.$attrs,
...this.trackingProps,
item: this.item,
- 'data-qa-submenu-item': this.item.title,
+ 'data-qa-submenu-item': this.qaSubMenuItem,
'data-method': this.item.data_method ?? null,
};
},
@@ -118,26 +142,73 @@ export default {
navItemLinkComponent() {
return this.item.to ? NavItemRouterLink : NavItemLink;
},
+ hasAvatar() {
+ return Boolean(this.item.entity_id);
+ },
+ avatarShape() {
+ return this.item.avatar_shape || 'rect';
+ },
+ pinAriaLabel() {
+ return sprintf(this.$options.i18n.pin, {
+ title: this.item.title,
+ });
+ },
+ unpinAriaLabel() {
+ return sprintf(this.$options.i18n.unpin, {
+ title: this.item.title,
+ });
+ },
+ activeIndicatorStyle() {
+ const style = {
+ width: '3px',
+ borderRadius: '3px',
+ marginRight: '1px',
+ };
+
+ // The active indicator is too close to the avatar for items with one, so shift
+ // it left by 1px.
+ //
+ // The indicator is absolutely positioned using rem units. This tweak for this
+ // edge case is in pixel units, so that it does not scale with root font size.
+ if (this.hasAvatar) style.transform = 'translateX(-1px)';
+
+ return style;
+ },
+ },
+ mounted() {
+ if (this.item.is_active) {
+ this.$el.scrollIntoView(false);
+ }
+ },
+ methods: {
+ togglePointerEvents() {
+ this.canClickPinButton = this.isMouseIn;
+ },
},
};
</script>
<template>
- <li>
+ <li
+ class="gl-relative show-on-focus-or-hover--context hide-on-focus-or-hover--context transition-opacity-on-hover--context"
+ data-testid="nav-item"
+ @mouseenter="isMouseIn = true"
+ @mouseleave="isMouseIn = false"
+ >
<component
:is="navItemLinkComponent"
#default="{ isActive }"
v-bind="linkProps"
- class="nav-item-link gl-relative gl-display-flex gl-align-items-center gl-min-h-7 gl-gap-3 gl-mb-1 gl-py-2 gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-focus--focus show-on-focus-or-hover--context"
+ class="gl-relative gl-display-flex gl-align-items-center gl-min-h-7 gl-gap-3 gl-mb-1 gl-py-2 gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-focus--focus show-on-focus-or-hover--control hide-on-focus-or-hover--control"
:class="computedLinkClasses"
- data-qa-selector="nav_item_link"
data-testid="nav-item-link"
+ data-qa-selector="nav_item_link"
>
<div
:class="[isActive ? 'gl-opacity-10' : 'gl-opacity-0']"
class="active-indicator gl-bg-blue-500 gl-absolute gl-left-2 gl-top-2 gl-bottom-2 gl-transition-slow"
aria-hidden="true"
- style="width: 3px; border-radius: 3px; margin-right: 1px"
+ :style="activeIndicatorStyle"
data-testid="active-indicator"
></div>
<div v-if="!isFlyout" class="gl-flex-shrink-0 gl-w-6 gl-display-flex">
@@ -148,6 +219,14 @@ export default {
name="grip"
class="gl-m-auto gl-text-gray-400 js-draggable-icon gl-cursor-grab show-on-focus-or-hover--target"
/>
+ <gl-avatar
+ v-else-if="hasAvatar"
+ :size="24"
+ :shape="avatarShape"
+ :entity-name="item.title"
+ :entity-id="item.entity_id"
+ :src="item.avatar"
+ />
</slot>
</div>
<div class="gl-flex-grow-1 gl-text-gray-900 gl-truncate-end">
@@ -157,36 +236,47 @@ export default {
</div>
</div>
<slot name="actions"></slot>
- <span v-if="hasPill || isPinnable" class="gl-text-right gl-relative">
+ <span v-if="hasPill || isPinnable" class="gl-text-right gl-relative gl-min-w-8">
<gl-badge
v-if="hasPill"
size="sm"
variant="neutral"
- :class="{ 'nav-item-badge gl-absolute gl-right-0 gl-top-2': isPinnable }"
+ class="gl-bg-t-gray-a-08!"
+ :class="{
+ 'hide-on-focus-or-hover--target transition-opacity-on-hover--target': isPinnable,
+ }"
>
{{ pillData }}
</gl-badge>
- <gl-button
- v-if="isPinnable && !isPinned"
- v-gl-tooltip.noninteractive.ds500.right.viewport="$options.i18n.pinItem"
- size="small"
- category="tertiary"
- icon="thumbtack"
- class="show-on-focus-or-hover--target"
- :aria-label="$options.i18n.pinItem"
- @click.prevent="$emit('pin-add', item.id)"
- />
- <gl-button
- v-else-if="isPinnable && isPinned"
- v-gl-tooltip.noninteractive.ds500.right.viewport="$options.i18n.unpinItem"
- size="small"
- category="tertiary"
- :aria-label="$options.i18n.unpinItem"
- icon="thumbtack-solid"
- class="show-on-focus-or-hover--target"
- @click.prevent="$emit('pin-remove', item.id)"
- />
</span>
</component>
+ <template v-if="isPinnable">
+ <gl-button
+ v-if="isPinned"
+ v-gl-tooltip.noninteractive.right.viewport="$options.i18n.unpinItem"
+ :aria-label="unpinAriaLabel"
+ category="tertiary"
+ class="show-on-focus-or-hover--target transition-opacity-on-hover--target always-animate gl-absolute gl-right-3 gl-top-2"
+ :class="{ 'gl-pointer-events-none': !canClickPinButton }"
+ data-testid="nav-item-unpin"
+ icon="thumbtack-solid"
+ size="small"
+ @click="$emit('pin-remove', item.id)"
+ @transitionend="togglePointerEvents"
+ />
+ <gl-button
+ v-else
+ v-gl-tooltip.noninteractive.right.viewport="$options.i18n.pinItem"
+ :aria-label="pinAriaLabel"
+ category="tertiary"
+ class="show-on-focus-or-hover--target transition-opacity-on-hover--target always-animate gl-absolute gl-right-3 gl-top-2"
+ :class="{ 'gl-pointer-events-none': !canClickPinButton }"
+ data-testid="nav-item-pin"
+ icon="thumbtack"
+ size="small"
+ @click="$emit('pin-add', item.id)"
+ @transitionend="togglePointerEvents"
+ />
+ </template>
</li>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/pinned_section.vue b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
index 1e2201fbdff..5da45b52bf4 100644
--- a/app/assets/javascripts/super_sidebar/components/pinned_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
@@ -6,6 +6,13 @@ import { SIDEBAR_PINS_EXPANDED_COOKIE, SIDEBAR_COOKIE_EXPIRATION } from '../cons
import MenuSection from './menu_section.vue';
import NavItem from './nav_item.vue';
+const AMBIGUOUS_SETTINGS = {
+ ci_cd: s__('Navigation|CI/CD settings'),
+ merge_request_settings: s__('Navigation|Merge requests settings'),
+ monitor: s__('Navigation|Monitor settings'),
+ repository: s__('Navigation|Repository settings'),
+};
+
export default {
i18n: {
pinned: s__('Navigation|Pinned'),
@@ -23,11 +30,6 @@ export default {
required: false,
default: () => [],
},
- separated: {
- type: Boolean,
- required: false,
- default: false,
- },
hasFlyout: {
type: Boolean,
required: false,
@@ -37,7 +39,7 @@ export default {
data() {
return {
expanded: getCookie(SIDEBAR_PINS_EXPANDED_COOKIE) !== 'false',
- draggableItems: this.items,
+ draggableItems: this.renameSettings(this.items),
};
},
computed: {
@@ -63,7 +65,7 @@ export default {
});
},
items(newItems) {
- this.draggableItems = newItems;
+ this.draggableItems = this.renameSettings(newItems);
},
},
methods: {
@@ -76,6 +78,15 @@ export default {
event.oldIndex < event.newIndex,
);
},
+ renameSettings(items) {
+ return items.map((i) => {
+ const title = AMBIGUOUS_SETTINGS[i.id] || i.title;
+ return { ...i, title };
+ });
+ },
+ onPinRemove(itemId) {
+ this.$emit('pin-remove', itemId);
+ },
},
};
</script>
@@ -84,10 +95,9 @@ export default {
<menu-section
:item="sectionItem"
:expanded="expanded"
- :separated="separated"
:has-flyout="hasFlyout"
@collapse-toggle="expanded = !expanded"
- @pin-remove="(itemId) => $emit('pin-remove', itemId)"
+ @pin-remove="onPinRemove"
>
<draggable
v-if="items.length > 0"
@@ -103,7 +113,7 @@ export default {
:key="item.id"
:item="item"
is-in-pinned-section
- @pin-remove="(itemId) => $emit('pin-remove', itemId)"
+ @pin-remove="onPinRemove"
/>
</draggable>
<li v-else class="gl-text-secondary gl-font-sm gl-py-3" style="margin-left: 2.5rem">
diff --git a/app/assets/javascripts/super_sidebar/components/projects_list.vue b/app/assets/javascripts/super_sidebar/components/projects_list.vue
deleted file mode 100644
index 8d1a5c825b5..00000000000
--- a/app/assets/javascripts/super_sidebar/components/projects_list.vue
+++ /dev/null
@@ -1,82 +0,0 @@
-<script>
-import { s__ } from '~/locale';
-import { MAX_FREQUENT_PROJECTS_COUNT } from '../constants';
-import FrequentItemsList from './frequent_items_list.vue';
-import SearchResults from './search_results.vue';
-import NavItem from './nav_item.vue';
-
-export default {
- MAX_FREQUENT_PROJECTS_COUNT,
- components: {
- FrequentItemsList,
- SearchResults,
- NavItem,
- },
- props: {
- username: {
- type: String,
- required: true,
- },
- viewAllLink: {
- type: String,
- required: true,
- },
- isSearch: {
- type: Boolean,
- required: false,
- default: false,
- },
- searchResults: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- computed: {
- storageKey() {
- return `${this.username}/frequent-projects`;
- },
- viewAllProps() {
- return {
- item: {
- link: this.viewAllLink,
- title: s__('Navigation|View all your projects'),
- icon: 'project',
- },
- linkClasses: { 'dashboard-shortcuts-projects': true },
- };
- },
- },
- i18n: {
- title: s__('Navigation|Frequently visited projects'),
- searchTitle: s__('Navigation|Projects'),
- pristineText: s__('Navigation|Projects you visit often will appear here.'),
- noResultsText: s__('Navigation|No project matches found'),
- },
-};
-</script>
-
-<template>
- <search-results
- v-if="isSearch"
- class="gl-border-t-0"
- :title="$options.i18n.searchTitle"
- :no-results-text="$options.i18n.noResultsText"
- :search-results="searchResults"
- >
- <template #view-all-items>
- <nav-item v-bind="viewAllProps" is-subitem />
- </template>
- </search-results>
- <frequent-items-list
- v-else
- :title="$options.i18n.title"
- :storage-key="storageKey"
- :max-items="$options.MAX_FREQUENT_PROJECTS_COUNT"
- :pristine-text="$options.i18n.pristineText"
- >
- <template #view-all-items>
- <nav-item v-bind="viewAllProps" is-subitem />
- </template>
- </frequent-items-list>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/components/search_results.vue b/app/assets/javascripts/super_sidebar/components/search_results.vue
deleted file mode 100644
index ff933f341af..00000000000
--- a/app/assets/javascripts/super_sidebar/components/search_results.vue
+++ /dev/null
@@ -1,99 +0,0 @@
-<script>
-import { GlCollapse, GlCollapseToggleDirective, GlIcon } from '@gitlab/ui';
-import uniqueId from 'lodash/uniqueId';
-import ItemsList from './items_list.vue';
-
-export default {
- components: {
- GlCollapse,
- GlIcon,
- ItemsList,
- },
- directives: {
- CollapseToggle: GlCollapseToggleDirective,
- },
- props: {
- title: {
- type: String,
- required: true,
- },
- noResultsText: {
- type: String,
- required: true,
- },
- searchResults: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- data() {
- return {
- expanded: true,
- };
- },
- computed: {
- isEmpty() {
- return !this.searchResults.length;
- },
- collapseIcon() {
- return this.expanded ? 'chevron-up' : 'chevron-down';
- },
- },
- created() {
- this.collapseId = uniqueId('expandable-section-');
- },
- buttonClasses: [
- // Reset user agent styles
- 'gl-appearance-none',
- 'gl-border-0',
- 'gl-bg-transparent',
- // Text styles
- 'gl-text-left',
- 'gl-text-transform-uppercase',
- 'gl-text-secondary',
- 'gl-font-weight-semibold',
- 'gl-font-xs',
- 'gl-line-height-12',
- 'gl-letter-spacing-06em',
- // Border
- 'gl-border-t',
- 'gl-border-gray-50',
- // Spacing
- 'gl-my-3',
- 'gl-pt-2',
- // Layout
- 'gl-display-flex',
- 'gl-justify-content-space-between',
- 'gl-align-items-center',
- ],
-};
-</script>
-
-<template>
- <li class="gl-border-t gl-border-gray-50">
- <button
- v-collapse-toggle="collapseId"
- :class="$options.buttonClasses"
- class="gl-mx-3"
- data-testid="search-results-toggle"
- >
- {{ title }}
- <gl-icon :name="collapseIcon" :size="16" />
- </button>
- <gl-collapse :id="collapseId" v-model="expanded">
- <div
- v-if="isEmpty"
- data-testid="empty-text"
- class="gl-text-gray-500 gl-font-sm gl-mb-3 gl-mx-4"
- >
- {{ noResultsText }}
- </div>
- <items-list :aria-label="title" :items="searchResults">
- <template #view-all-items>
- <slot name="view-all-items"></slot>
- </template>
- </items-list>
- </gl-collapse>
- </li>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_hover_peek_behavior.vue b/app/assets/javascripts/super_sidebar/components/sidebar_hover_peek_behavior.vue
new file mode 100644
index 00000000000..df432a1928a
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/sidebar_hover_peek_behavior.vue
@@ -0,0 +1,126 @@
+<script>
+import { getCssClassDimensions } from '~/lib/utils/css_utils';
+import Tracking from '~/tracking';
+import {
+ JS_TOGGLE_EXPAND_CLASS,
+ SUPER_SIDEBAR_PEEK_OPEN_DELAY,
+ SUPER_SIDEBAR_PEEK_CLOSE_DELAY,
+ SUPER_SIDEBAR_PEEK_STATE_CLOSED as STATE_CLOSED,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN as STATE_WILL_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_OPEN as STATE_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_CLOSE as STATE_WILL_CLOSE,
+} from '../constants';
+
+export default {
+ name: 'SidebarHoverPeek',
+ mixins: [Tracking.mixin()],
+ props: {
+ isMouseOverSidebar: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ created() {
+ // Nothing needs to observe these properties, so they are not reactive.
+ this.state = null;
+ this.openTimer = null;
+ this.closeTimer = null;
+ this.xSidebarEdge = null;
+ this.isMouseWithinSidebarArea = false;
+ },
+ async mounted() {
+ await this.$nextTick();
+ this.xSidebarEdge = getCssClassDimensions('super-sidebar').width;
+ document.addEventListener('mousemove', this.onMouseMove);
+ document.documentElement.addEventListener('mouseleave', this.onDocumentLeave);
+ document
+ .querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`)
+ .addEventListener('mouseenter', this.onMouseEnter);
+ document
+ .querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`)
+ .addEventListener('mouseleave', this.onMouseLeave);
+ this.changeState(STATE_CLOSED);
+ },
+ beforeDestroy() {
+ document.removeEventListener('mousemove', this.onMouseMove);
+ document.documentElement.removeEventListener('mouseleave', this.onDocumentLeave);
+ document
+ .querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`)
+ .removeEventListener('mouseenter', this.onMouseEnter);
+ document
+ .querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`)
+ .removeEventListener('mouseleave', this.onMouseLeave);
+ this.clearTimers();
+ },
+ methods: {
+ onMouseMove({ clientX }) {
+ if (clientX < this.xSidebarEdge) {
+ this.isMouseWithinSidebarArea = true;
+ } else {
+ this.isMouseWithinSidebarArea = false;
+ if (!this.isMouseOverSidebar && this.state === STATE_OPEN) {
+ this.willClose();
+ }
+ }
+ },
+ onDocumentLeave() {
+ this.isMouseWithinSidebarArea = false;
+ if (this.state === STATE_OPEN) {
+ this.willClose();
+ } else if (this.state === STATE_WILL_OPEN) {
+ this.close();
+ }
+ },
+ onMouseEnter() {
+ clearTimeout(this.closeTimer);
+ this.willOpen();
+ },
+ onMouseLeave() {
+ clearTimeout(this.openTimer);
+ if (this.isMouseWithinSidebarArea || this.isMouseOverSidebar) return;
+ this.willClose();
+ },
+ willClose() {
+ this.changeState(STATE_WILL_CLOSE);
+ this.closeTimer = setTimeout(this.close, SUPER_SIDEBAR_PEEK_CLOSE_DELAY);
+ },
+ willOpen() {
+ this.changeState(STATE_WILL_OPEN);
+ this.openTimer = setTimeout(this.open, SUPER_SIDEBAR_PEEK_OPEN_DELAY);
+ },
+ open() {
+ this.changeState(STATE_OPEN);
+ this.clearTimers();
+ this.track('nav_hover_peek', {
+ label: 'nav_sidebar_toggle',
+ property: 'nav_sidebar',
+ });
+ },
+ close() {
+ if (this.isMouseWithinSidebarArea) return;
+ this.changeState(STATE_CLOSED);
+ this.clearTimers();
+ },
+ clearTimers() {
+ clearTimeout(this.closeTimer);
+ clearTimeout(this.openTimer);
+ },
+ /**
+ * Switches to the new state, and emits a change event.
+ *
+ * If the given state is the current state, do nothing.
+ *
+ * @param {string} state The state to transition to.
+ */
+ changeState(state) {
+ if (this.state === state) return;
+ this.state = state;
+ this.$emit('change', state);
+ },
+ },
+ render() {
+ return null;
+ },
+};
+</script>
diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
index 821b9dbcb7b..02488e99c0e 100644
--- a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
@@ -2,7 +2,6 @@
import * as Sentry from '@sentry/browser';
import { GlBreakpointInstance, breakpoints } from '@gitlab/ui/dist/utils';
import axios from '~/lib/utils/axios_utils';
-import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { PANELS_WITH_PINS } from '../constants';
import NavItem from './nav_item.vue';
@@ -51,10 +50,6 @@ export default {
},
},
- i18n: {
- mainNavigation: s__('Navigation|Main navigation'),
- },
-
data() {
return {
showFlyoutMenus: false,
@@ -109,10 +104,8 @@ export default {
},
},
mounted() {
- if (this.glFeatures.superSidebarFlyoutMenus) {
- this.decideFlyoutState();
- window.addEventListener('resize', this.decideFlyoutState);
- }
+ this.decideFlyoutState();
+ window.addEventListener('resize', this.decideFlyoutState);
},
beforeDestroy() {
window.removeEventListener('resize', this.decideFlyoutState);
@@ -164,13 +157,12 @@ export default {
</script>
<template>
- <nav :aria-label="$options.i18n.mainNavigation" class="gl-p-2 gl-relative">
+ <div class="gl-p-2 gl-relative">
<ul v-if="hasStaticItems" class="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
v-if="supportsPins"
- separated
:items="pinnedItems"
:has-flyout="showFlyoutMenus"
@pin-remove="destroyPin"
@@ -203,5 +195,5 @@ export default {
/>
</template>
</ul>
- </nav>
+ </div>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue b/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue
index ec728b4af9e..a20e37b945a 100644
--- a/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue
+++ b/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue
@@ -1,12 +1,14 @@
<script>
import { getCssClassDimensions } from '~/lib/utils/css_utils';
import Tracking from '~/tracking';
-import { SUPER_SIDEBAR_PEEK_OPEN_DELAY, SUPER_SIDEBAR_PEEK_CLOSE_DELAY } from '../constants';
-
-export const STATE_CLOSED = 'closed';
-export const STATE_WILL_OPEN = 'will-open';
-export const STATE_OPEN = 'open';
-export const STATE_WILL_CLOSE = 'will-close';
+import {
+ SUPER_SIDEBAR_PEEK_OPEN_DELAY,
+ SUPER_SIDEBAR_PEEK_CLOSE_DELAY,
+ SUPER_SIDEBAR_PEEK_STATE_CLOSED as STATE_CLOSED,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN as STATE_WILL_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_OPEN as STATE_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_CLOSE as STATE_WILL_CLOSE,
+} from '../constants';
export default {
name: 'SidebarPeek',
diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
index 29a3147e949..fe3e4a8199e 100644
--- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
@@ -2,27 +2,31 @@
import { GlButton } from '@gitlab/ui';
import { Mousetrap } from '~/lib/mousetrap';
import { keysFor, TOGGLE_SUPER_SIDEBAR } from '~/behaviors/shortcuts/keybindings';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
-import { sidebarState } from '../constants';
+import {
+ sidebarState,
+ SUPER_SIDEBAR_PEEK_STATE_CLOSED as STATE_CLOSED,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN as STATE_WILL_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_OPEN as STATE_OPEN,
+} from '../constants';
import { isCollapsed, toggleSuperSidebarCollapsed } from '../super_sidebar_collapsed_state_manager';
+import { trackContextAccess } from '../utils';
import UserBar from './user_bar.vue';
import SidebarPortalTarget from './sidebar_portal_target.vue';
-import ContextHeader from './context_header.vue';
-import ContextSwitcher from './context_switcher.vue';
import HelpCenter from './help_center.vue';
import SidebarMenu from './sidebar_menu.vue';
-import SidebarPeekBehavior, { STATE_CLOSED, STATE_WILL_OPEN } from './sidebar_peek_behavior.vue';
+import SidebarPeekBehavior from './sidebar_peek_behavior.vue';
+import SidebarHoverPeekBehavior from './sidebar_hover_peek_behavior.vue';
export default {
components: {
GlButton,
UserBar,
- ContextHeader,
- ContextSwitcher,
HelpCenter,
SidebarMenu,
SidebarPeekBehavior,
+ SidebarHoverPeekBehavior,
SidebarPortalTarget,
TrialStatusWidget: () =>
import('ee_component/contextual_sidebar/components/trial_status_widget.vue'),
@@ -32,6 +36,7 @@ export default {
mixins: [Tracking.mixin()],
i18n: {
skipToMainContent: __('Skip to main content'),
+ primary: s__('Navigation|Primary'),
},
inject: ['showTrialStatusWidget'],
props: {
@@ -45,25 +50,34 @@ export default {
sidebarState,
showPeekHint: false,
isMouseover: false,
+ breakpoint: null,
};
},
computed: {
+ showOverlay() {
+ return this.sidebarState.isPeek || this.sidebarState.isHoverPeek;
+ },
menuItems() {
return this.sidebarData.current_menu_items || [];
},
peekClasses() {
return {
'super-sidebar-peek-hint': this.showPeekHint,
- 'super-sidebar-peek': this.sidebarState.isPeek,
+ 'super-sidebar-peek': this.showOverlay,
+ 'super-sidebar-has-peeked': this.sidebarState.hasPeeked,
};
},
},
- watch: {
- 'sidebarState.isCollapsed': function isCollapsedWatcher(newIsCollapsed) {
- if (newIsCollapsed && this.$refs['context-switcher']) {
- this.$refs['context-switcher'].close();
- }
- },
+ created() {
+ const {
+ is_logged_in: isLoggedIn,
+ current_context: currentContext,
+ username,
+ track_visits_path: trackVisitsPath,
+ } = this.sidebarData;
+ if (isLoggedIn && currentContext.namespace) {
+ trackContextAccess(username, currentContext, trackVisitsPath);
+ }
},
mounted() {
Mousetrap.bind(keysFor(TOGGLE_SUPER_SIDEBAR), this.toggleSidebar);
@@ -88,6 +102,7 @@ export default {
this.sidebarState.isCollapsed = true;
this.showPeekHint = false;
} else if (state === STATE_WILL_OPEN) {
+ this.sidebarState.hasPeeked = true;
this.sidebarState.isPeek = false;
this.sidebarState.isCollapsed = true;
this.showPeekHint = true;
@@ -97,8 +112,15 @@ export default {
this.showPeekHint = false;
}
},
- onContextSwitcherToggled(open) {
- this.sidebarState.contextSwitcherOpen = open;
+ onHoverPeekChange(state) {
+ if (state === STATE_OPEN) {
+ this.sidebarState.hasPeeked = true;
+ this.sidebarState.isHoverPeek = true;
+ this.sidebarState.isCollapsed = false;
+ } else if (state === STATE_CLOSED) {
+ this.sidebarState.isHoverPeek = false;
+ this.sidebarState.isCollapsed = true;
+ }
},
},
};
@@ -114,8 +136,9 @@ export default {
>
{{ $options.i18n.skipToMainContent }}
</gl-button>
- <aside
+ <nav
id="super-sidebar"
+ :aria-label="$options.i18n.primary"
class="super-sidebar"
:class="peekClasses"
data-testid="super-sidebar"
@@ -124,32 +147,23 @@ export default {
@mouseenter="isMouseover = true"
@mouseleave="isMouseover = false"
>
- <user-bar :has-collapse-button="!sidebarState.isPeek" :sidebar-data="sidebarData" />
+ <user-bar :has-collapse-button="!showOverlay" :sidebar-data="sidebarData" />
<div v-if="showTrialStatusWidget" class="gl-px-2 gl-py-2">
<trial-status-widget
- class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-mb-1 gl-px-3 gl-line-height-normal gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! nav-item-link gl-py-3"
+ class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-mb-1 gl-px-3 gl-line-height-normal gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-py-3"
/>
<trial-status-popover />
</div>
<div
class="contextual-nav gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-overflow-hidden"
>
- <div
- class="gl-flex-grow-1"
- :class="{ 'gl-overflow-auto': !sidebarState.contextSwitcherOpen }"
- data-testid="nav-container"
- >
- <context-switcher
- v-if="sidebarData.is_logged_in"
- ref="context-switcher"
- :username="sidebarData.username"
- :projects-path="sidebarData.projects_path"
- :groups-path="sidebarData.groups_path"
- :current-context="sidebarData.current_context"
- :context-header="sidebarData.current_context_header"
- @toggle="onContextSwitcherToggled"
- />
- <context-header v-else :context="sidebarData.current_context_header" />
+ <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"
+ >
+ {{ sidebarData.current_context_header }}
+ </h2>
+
<sidebar-menu
v-if="menuItems.length"
:items="menuItems"
@@ -164,7 +178,7 @@ export default {
<help-center :sidebar-data="sidebarData" />
</div>
</div>
- </aside>
+ </nav>
<a
v-for="shortcutLink in sidebarData.shortcut_links"
:key="shortcutLink.href"
@@ -176,13 +190,18 @@ export default {
</a>
<!--
- Only mount SidebarPeekBehavior if the sidebar is peekable, to avoid
+ Only mount peek behavior components if the sidebar is peekable, to avoid
setting up event listeners unnecessarily.
-->
<sidebar-peek-behavior
- v-if="sidebarState.isPeekable"
+ v-if="sidebarState.isPeekable && !sidebarState.isHoverPeek"
:is-mouse-over-sidebar="isMouseover"
@change="onPeekChange"
/>
+ <sidebar-hover-peek-behavior
+ v-if="sidebarState.isPeekable && !sidebarState.isPeek"
+ :is-mouse-over-sidebar="isMouseover"
+ @change="onHoverPeekChange"
+ />
</div>
</template>
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 7d5e87805d5..30ee18cc369 100644
--- a/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue
@@ -27,19 +27,18 @@ export default {
},
i18n: {
collapseSidebar: __('Hide sidebar'),
- expandSidebar: __('Show sidebar'),
- navigationSidebar: __('Navigation sidebar'),
+ expandSidebar: __('Keep sidebar visible'),
+ primaryNavigationSidebar: __('Primary navigation sidebar'),
},
data() {
return sidebarState;
},
computed: {
+ canOpen() {
+ return this.isCollapsed || this.isPeek || this.isHoverPeek;
+ },
tooltipTitle() {
- if (this.isPeek) return '';
-
- return this.isCollapsed
- ? this.$options.i18n.expandSidebar
- : this.$options.i18n.collapseSidebar;
+ return this.canOpen ? this.$options.i18n.expandSidebar : this.$options.i18n.collapseSidebar;
},
tooltip() {
return {
@@ -49,21 +48,21 @@ export default {
};
},
ariaExpanded() {
- return String(!this.isCollapsed);
+ return String(!this.canOpen);
},
},
methods: {
toggle() {
- this.track(this.isCollapsed ? 'nav_show' : 'nav_hide', {
+ this.track(this.canOpen ? 'nav_show' : 'nav_hide', {
label: 'nav_toggle',
property: 'nav_sidebar',
});
- toggleSuperSidebarCollapsed(!this.isCollapsed, true);
+ toggleSuperSidebarCollapsed(!this.canOpen, true);
this.focusOtherToggle();
},
focusOtherToggle() {
this.$nextTick(() => {
- const classSelector = this.isCollapsed ? JS_TOGGLE_EXPAND_CLASS : JS_TOGGLE_COLLAPSE_CLASS;
+ const classSelector = this.canOpen ? JS_TOGGLE_EXPAND_CLASS : JS_TOGGLE_COLLAPSE_CLASS;
const otherToggle = document.querySelector(`.${classSelector}`);
otherToggle?.focus();
});
@@ -74,13 +73,12 @@ export default {
<template>
<gl-button
- v-gl-tooltip.hover.noninteractive.ds500="tooltip"
+ v-gl-tooltip.hover="tooltip"
aria-controls="super-sidebar"
:aria-expanded="ariaExpanded"
- :aria-label="$options.i18n.navigationSidebar"
+ :aria-label="$options.i18n.primaryNavigationSidebar"
icon="sidebar"
category="tertiary"
- :disabled="isPeek"
@click="toggle"
/>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
index b76ef91b768..49aee4f3470 100644
--- a/app/assets/javascripts/super_sidebar/components/user_bar.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -1,5 +1,5 @@
<script>
-import { GlBadge, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
+import { GlBadge, GlButton, GlModalDirective, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import {
destroyUserCountsManager,
@@ -34,20 +34,19 @@ export default {
),
SuperSidebarToggle,
BrandLogo,
+ GlIcon,
},
i18n: {
- createNew: __('Create new...'),
- homepage: __('Homepage'),
issues: __('Issues'),
mergeRequests: __('Merge requests'),
- search: __('Search'),
searchKbdHelp: sprintf(
- s__('GlobalSearch|Search GitLab %{kbdOpen}/%{kbdClose}'),
+ s__('GlobalSearch|Type %{kbdOpen}/%{kbdClose} to search'),
{ kbdOpen: '<kbd>', kbdClose: '</kbd>' },
false,
),
todoList: __('To-Do list'),
stopImpersonating: __('Stop impersonating'),
+ searchBtnText: __('Search or go to…'),
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -103,8 +102,14 @@ export default {
</script>
<template>
- <div class="user-bar">
- <div class="gl-display-flex gl-align-items-center gl-px-3 gl-py-2">
+ <div
+ class="user-bar gl-display-flex gl-p-3 gl-gap-1"
+ :class="{ 'gl-flex-direction-column gl-gap-3': sidebarData.is_logged_in }"
+ >
+ <div
+ v-if="hasCollapseButton || sidebarData.is_logged_in"
+ class="gl-display-flex gl-align-items-center gl-gap-1"
+ >
<template v-if="sidebarData.is_logged_in">
<brand-logo :logo-url="sidebarData.logo_url" />
<gl-badge
@@ -112,7 +117,6 @@ export default {
variant="success"
:href="sidebarData.canary_toggle_com_url"
size="sm"
- class="gl-ml-2"
>
{{ $options.NEXT_LABEL }}
</gl-badge>
@@ -126,24 +130,16 @@ export default {
tooltip-container="super-sidebar"
data-testid="super-sidebar-collapse-button"
/>
- <create-menu v-if="sidebarData.is_logged_in" :groups="sidebarData.create_new_menu_groups" />
-
- <gl-button
- id="super-sidebar-search"
- v-gl-tooltip.bottom.hover.noninteractive.ds500.html="searchTooltip"
- v-gl-modal="$options.SEARCH_MODAL_ID"
- data-testid="super-sidebar-search-button"
- icon="search"
- :aria-label="$options.i18n.search"
- category="tertiary"
+ <create-menu
+ v-if="sidebarData.is_logged_in && sidebarData.create_new_menu_groups.length > 0"
+ :groups="sidebarData.create_new_menu_groups"
/>
- <search-modal @shown="hideSearchTooltip" @hidden="showSearchTooltip" />
<user-menu v-if="sidebarData.is_logged_in" :data="sidebarData" />
<gl-button
v-if="isImpersonating"
- v-gl-tooltip.noninteractive.ds500.bottom
+ v-gl-tooltip.bottom
:href="sidebarData.stop_impersonation_path"
:title="$options.i18n.stopImpersonating"
:aria-label="$options.i18n.stopImpersonating"
@@ -155,10 +151,10 @@ export default {
</div>
<div
v-if="sidebarData.is_logged_in"
- class="gl-display-flex gl-justify-content-space-between gl-px-3 gl-py-2 gl-gap-2"
+ class="gl-display-flex gl-justify-content-space-between gl-gap-2"
>
<counter
- v-gl-tooltip:super-sidebar.hover.noninteractive.ds500.bottom="$options.i18n.issues"
+ v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.issues"
class="gl-flex-basis-third dashboard-shortcuts-issues"
icon="issues"
:count="userCounts.assigned_issues"
@@ -176,9 +172,7 @@ export default {
@hidden="mrMenuShown = false"
>
<counter
- v-gl-tooltip:super-sidebar.hover.noninteractive.ds500.bottom="
- mrMenuShown ? '' : $options.i18n.mergeRequests
- "
+ v-gl-tooltip:super-sidebar.hover.bottom="mrMenuShown ? '' : $options.i18n.mergeRequests"
class="gl-w-full"
icon="merge-request-open"
:count="mergeRequestTotalCount"
@@ -190,7 +184,7 @@ export default {
/>
</merge-request-menu>
<counter
- v-gl-tooltip:super-sidebar.hover.noninteractive.ds500.bottom="$options.i18n.todoList"
+ v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.todoList"
class="gl-flex-basis-third shortcuts-todos js-todos-count"
icon="todo-done"
:count="userCounts.todos"
@@ -202,5 +196,16 @@ export default {
data-track-property="nav_core_menu"
/>
</div>
+ <button
+ id="super-sidebar-search"
+ v-gl-tooltip.bottom.hover.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"
+ >
+ <gl-icon name="search" />
+ {{ $options.i18n.searchBtnText }}
+ </button>
+ <search-modal @shown="hideSearchTooltip" @hidden="showSearchTooltip" />
</div>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue
index 869f07520a2..ed6c41e85c6 100644
--- a/app/assets/javascripts/super_sidebar/components/user_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue
@@ -19,7 +19,6 @@ const DROPDOWN_X_OFFSET_BASE = -211;
const DROPDOWN_X_OFFSET_IMPERSONATING = DROPDOWN_X_OFFSET_BASE + IMPERSONATING_OFFSET;
export default {
- feedbackUrl: 'https://gitlab.com/gitlab-org/gitlab/-/issues/409005',
i18n: {
newNavigation: {
sectionTitle: s__('NorthstarNavigation|Navigation redesign'),
@@ -31,7 +30,6 @@ export default {
buyPipelineMinutes: s__('CurrentUser|Buy Pipeline minutes'),
oneOfGroupsRunningOutOfPipelineMinutes: s__('CurrentUser|One of your groups is running out'),
gitlabNext: s__('CurrentUser|Switch to GitLab Next'),
- provideFeedback: s__('NorthstarNavigation|Provide feedback'),
startTrial: s__('CurrentUser|Start an Ultimate trial'),
signOut: __('Sign out'),
},
@@ -131,17 +129,6 @@ export default {
},
};
},
- feedbackItem() {
- return {
- text: this.$options.i18n.provideFeedback,
- href: this.$options.feedbackUrl,
- extraAttrs: {
- target: '_blank',
- ...USER_MENU_TRACKING_DEFAULTS,
- 'data-track-label': 'provide_nav_feedback',
- },
- };
- },
signOutGroup() {
return {
items: [
@@ -316,7 +303,6 @@ export default {
<span class="gl-font-sm">{{ $options.i18n.newNavigation.sectionTitle }}</span>
</template>
<new-nav-toggle :endpoint="toggleNewNavEndpoint" enabled new-navigation />
- <gl-disclosure-dropdown-item :item="feedbackItem" data-testid="feedback-item" />
</gl-disclosure-dropdown-group>
<gl-disclosure-dropdown-group
diff --git a/app/assets/javascripts/super_sidebar/components/user_name_group.vue b/app/assets/javascripts/super_sidebar/components/user_name_group.vue
index 13f19338610..3c8059387fa 100644
--- a/app/assets/javascripts/super_sidebar/components/user_name_group.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_name_group.vue
@@ -71,7 +71,7 @@ export default {
v-if="user.status.customized"
ref="statusTooltipTarget"
data-testid="user-menu-status"
- class="gl-display-flex gl-align-items-center gl-mt-2 gl-font-sm"
+ 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>
diff --git a/app/assets/javascripts/super_sidebar/constants.js b/app/assets/javascripts/super_sidebar/constants.js
index 757bf9c7459..77bd8b4a734 100644
--- a/app/assets/javascripts/super_sidebar/constants.js
+++ b/app/assets/javascripts/super_sidebar/constants.js
@@ -13,10 +13,12 @@ export const portalState = Vue.observable({
});
export const sidebarState = Vue.observable({
- contextSwitcherOpen: false,
isCollapsed: false,
+ hasPeeked: false,
isPeek: false,
isPeekable: false,
+ isHoverPeek: false,
+ wasHoverPeek: false,
});
export const helpCenterState = Vue.observable({
@@ -28,13 +30,17 @@ export const MAX_FREQUENT_GROUPS_COUNT = 3;
export const SUPER_SIDEBAR_PEEK_OPEN_DELAY = 200;
export const SUPER_SIDEBAR_PEEK_CLOSE_DELAY = 500;
+export const SUPER_SIDEBAR_PEEK_STATE_CLOSED = 'closed';
+export const SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN = 'will-open';
+export const SUPER_SIDEBAR_PEEK_STATE_OPEN = 'open';
+export const SUPER_SIDEBAR_PEEK_STATE_WILL_CLOSE = 'will-close';
export const TRACKING_UNKNOWN_ID = 'item_without_id';
export const TRACKING_UNKNOWN_PANEL = 'nav_panel_unknown';
export const CLICK_MENU_ITEM_ACTION = 'click_menu_item';
export const CLICK_PINNED_MENU_ITEM_ACTION = 'click_pinned_menu_item';
-export const PANELS_WITH_PINS = ['group', 'project'];
+export const PANELS_WITH_PINS = ['group', 'project', 'organization'];
export const USER_MENU_TRACKING_DEFAULTS = {
'data-track-property': 'nav_user_menu',
diff --git a/app/assets/javascripts/super_sidebar/graphql/queries/search_user_groups_and_projects.query.graphql b/app/assets/javascripts/super_sidebar/graphql/queries/search_user_groups_and_projects.query.graphql
deleted file mode 100644
index 4b1e65be3fa..00000000000
--- a/app/assets/javascripts/super_sidebar/graphql/queries/search_user_groups_and_projects.query.graphql
+++ /dev/null
@@ -1,24 +0,0 @@
-query searchUserProjectsAndGroups($username: String!, $search: String) {
- projects(search: $search, sort: "latest_activity_desc", membership: true, first: 20) {
- nodes {
- id
- name
- namespace: nameWithNamespace
- webUrl
- avatarUrl
- }
- }
-
- user(username: $username) {
- id
- groups(search: $search, first: 20) {
- nodes {
- id
- name
- namespace: fullPath
- webUrl
- avatarUrl
- }
- }
- }
-}
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
index 2b62e7a6ede..de16161efb5 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
@@ -1,6 +1,4 @@
import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import { initStatusTriggers } from '../header';
import { JS_TOGGLE_EXPAND_CLASS } from './constants';
@@ -12,12 +10,6 @@ import {
import SuperSidebar from './components/super_sidebar.vue';
import SuperSidebarToggle from './components/super_sidebar_toggle.vue';
-Vue.use(VueApollo);
-
-const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
-});
-
const getTrialStatusWidgetData = (sidebarData) => {
if (sidebarData.trial_status_widget_data_attrs && sidebarData.trial_status_popover_data_attrs) {
const {
@@ -97,7 +89,6 @@ export const initSuperSidebar = () => {
return new Vue({
el,
name: 'SuperSidebarRoot',
- apolloProvider,
provide: {
rootPath,
toggleNewNavEndpoint,
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js b/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js
index feb7e274b07..9ee78a657b6 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js
@@ -26,6 +26,9 @@ export const toggleSuperSidebarCollapsed = (collapsed, saveCookie) => {
sidebarState.isPeek = false;
sidebarState.isPeekable = collapsed;
+ sidebarState.hasPeeked = false;
+ sidebarState.isHoverPeek = false;
+ sidebarState.wasHoverPeek = false;
sidebarState.isCollapsed = collapsed;
if (saveCookie && isDesktopBreakpoint()) {
diff --git a/app/assets/javascripts/super_sidebar/utils.js b/app/assets/javascripts/super_sidebar/utils.js
index cbf93155fb6..97830a32d78 100644
--- a/app/assets/javascripts/super_sidebar/utils.js
+++ b/app/assets/javascripts/super_sidebar/utils.js
@@ -1,7 +1,7 @@
import * as Sentry from '@sentry/browser';
import AccessorUtilities from '~/lib/utils/accessor';
import { FREQUENT_ITEMS, FIFTEEN_MINUTES_IN_MS } from '~/frequent_items/constants';
-import { truncateNamespace } from '~/lib/utils/text_utility';
+import axios from '~/lib/utils/axios_utils';
/**
* This takes an array of project or groups that were stored in the local storage, to be shown in
@@ -18,7 +18,8 @@ const sortItemsByFrequencyAndLastAccess = (items) =>
// and then by lastAccessedOn with recent most first
if (itemA.frequency !== itemB.frequency) {
return itemB.frequency - itemA.frequency;
- } else if (itemA.lastAccessedOn !== itemB.lastAccessedOn) {
+ }
+ if (itemA.lastAccessedOn !== itemB.lastAccessedOn) {
return itemB.lastAccessedOn - itemA.lastAccessedOn;
}
@@ -36,11 +37,43 @@ export const getTopFrequentItems = (items, maxCount) => {
return frequentItems.slice(0, maxCount);
};
-const updateItemAccess = (contextItem, { lastAccessedOn, frequency = 0 } = {}) => {
+/**
+ * This tracks projects' and groups' visits in order to suggest a list of frequently visited
+ * entities to the user. Currently, this track visits in two ways:
+ * - The legacy approach uses a simple counting algorithm and stores the data in the local storage.
+ * - The above approach is being migrated to a backend-based one, where visits will be stored in the
+ * DB, and suggestions will be made through a smarter algorithm. When we are ready to transition
+ * to the newer approach, the legacy one will be cleaned up.
+ * @param {object} item The project/group item being tracked.
+ * @param {string} namespace A string indicating whether the tracked entity is a project or a group.
+ * @param {string} trackVisitsPath The API endpoint to track visits server-side.
+ * @returns {void}
+ */
+const updateItemAccess = (
+ contextItem,
+ { lastAccessedOn, frequency = 0 } = {},
+ namespace,
+ trackVisitsPath,
+) => {
const now = Date.now();
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) {
+ Sentry.captureException(e);
+ }
+ }
+
return {
...contextItem,
frequency: shouldUpdate ? frequency + 1 : frequency,
@@ -48,7 +81,7 @@ const updateItemAccess = (contextItem, { lastAccessedOn, frequency = 0 } = {}) =
};
};
-export const trackContextAccess = (username, context) => {
+export const trackContextAccess = (username, context, trackVisitsPath) => {
if (!AccessorUtilities.canUseLocalStorage()) {
return false;
}
@@ -61,9 +94,19 @@ export const trackContextAccess = (username, context) => {
);
if (existingItemIndex > -1) {
- storedItems[existingItemIndex] = updateItemAccess(context.item, storedItems[existingItemIndex]);
+ storedItems[existingItemIndex] = updateItemAccess(
+ context.item,
+ storedItems[existingItemIndex],
+ context.namespace,
+ trackVisitsPath,
+ );
} else {
- const newItem = updateItemAccess(context.item);
+ const newItem = updateItemAccess(
+ context.item,
+ storedItems[existingItemIndex],
+ context.namespace,
+ trackVisitsPath,
+ );
if (storedItems.length === FREQUENT_ITEMS.MAX_COUNT) {
sortItemsByFrequencyAndLastAccess(storedItems);
storedItems.pop();
@@ -74,15 +117,6 @@ export const trackContextAccess = (username, context) => {
return localStorage.setItem(storageKey, JSON.stringify(storedItems));
};
-export const formatContextSwitcherItems = (items) =>
- items.map(({ id, name: title, namespace, avatarUrl: avatar, webUrl: link }) => ({
- id,
- title,
- subtitle: truncateNamespace(namespace),
- avatar,
- link,
- }));
-
export const getItemsFromLocalStorage = ({ storageKey, maxItems }) => {
if (!AccessorUtilities.canUseLocalStorage()) {
return [];
diff --git a/app/assets/javascripts/time_tracking/components/queries/get_timelogs.query.graphql b/app/assets/javascripts/time_tracking/components/queries/get_timelogs.query.graphql
index 3ba0ab29530..0de76e25d01 100644
--- a/app/assets/javascripts/time_tracking/components/queries/get_timelogs.query.graphql
+++ b/app/assets/javascripts/time_tracking/components/queries/get_timelogs.query.graphql
@@ -1,8 +1,8 @@
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
query timeTrackingReport(
- $startDate: Time
- $endDate: Time
+ $startTime: Time
+ $endTime: Time
$projectId: ProjectID
$groupId: GroupID
$username: String
@@ -12,8 +12,8 @@ query timeTrackingReport(
$after: String
) {
timelogs(
- startDate: $startDate
- endDate: $endDate
+ startTime: $startTime
+ endTime: $endTime
projectId: $projectId
groupId: $groupId
username: $username
diff --git a/app/assets/javascripts/time_tracking/components/timelogs_app.vue b/app/assets/javascripts/time_tracking/components/timelogs_app.vue
index 2069e4a6722..7bb9b6c52a5 100644
--- a/app/assets/javascripts/time_tracking/components/timelogs_app.vue
+++ b/app/assets/javascripts/time_tracking/components/timelogs_app.vue
@@ -17,11 +17,11 @@ import TimelogsTable from './timelogs_table.vue';
const ENTRIES_PER_PAGE = 20;
// Define initial dates to current date and time
-const INITIAL_TO_DATE = new Date();
-const INITIAL_FROM_DATE = new Date();
+const INITIAL_TO_DATE_TIME = new Date(new Date().setHours(0, 0, 0, 0));
+const INITIAL_FROM_DATE_TIME = new Date(new Date().setHours(0, 0, 0, 0));
// Set the initial 'from' date to 30 days before the current date
-INITIAL_FROM_DATE.setDate(INITIAL_TO_DATE.getDate() - 30);
+INITIAL_FROM_DATE_TIME.setDate(INITIAL_TO_DATE_TIME.getDate() - 30);
export default {
components: {
@@ -45,8 +45,8 @@ export default {
projectId: null,
groupId: null,
username: null,
- timeSpentFrom: INITIAL_FROM_DATE,
- timeSpentTo: INITIAL_TO_DATE,
+ timeSpentFrom: INITIAL_FROM_DATE_TIME,
+ timeSpentTo: INITIAL_TO_DATE_TIME,
cursor: {
first: ENTRIES_PER_PAGE,
after: null,
@@ -54,8 +54,8 @@ export default {
before: null,
},
queryVariables: {
- startDate: INITIAL_FROM_DATE,
- endDate: INITIAL_TO_DATE,
+ startTime: INITIAL_FROM_DATE_TIME,
+ endTime: INITIAL_TO_DATE_TIME,
projectId: null,
groupId: null,
username: null,
@@ -108,9 +108,15 @@ export default {
before: null,
};
+ const { timeSpentTo } = this;
+
+ if (timeSpentTo) {
+ timeSpentTo.setDate(timeSpentTo.getDate() + 1);
+ }
+
this.queryVariables = {
- startDate: this.nullIfBlank(this.timeSpentFrom),
- endDate: this.nullIfBlank(this.timeSpentTo),
+ startTime: this.nullIfBlank(this.timeSpentFrom),
+ endTime: this.nullIfBlank(timeSpentTo),
projectId: this.nullIfBlank(this.projectId),
groupId: this.nullIfBlank(this.groupId),
username: this.nullIfBlank(this.username),
@@ -141,8 +147,8 @@ export default {
},
i18n: {
username: s__('TimeTrackingReport|Username'),
- from: s__('TimeTrackingReport|From'),
- to: s__('TimeTrackingReport|To'),
+ from: s__('TimeTrackingReport|From the start of'),
+ to: s__('TimeTrackingReport|To the end of'),
runReport: s__('TimeTrackingReport|Run report'),
totalTimeSpentText: s__('TimeTrackingReport|Total time spent: '),
},
diff --git a/app/assets/javascripts/token_access/components/outbound_token_access.vue b/app/assets/javascripts/token_access/components/outbound_token_access.vue
index 7e1e6cc445c..43aa9b94b3a 100644
--- a/app/assets/javascripts/token_access/components/outbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/outbound_token_access.vue
@@ -27,7 +27,7 @@ export default {
'CICD|Limit access %{italicStart}from%{italicEnd} this project (Deprecated)',
),
toggleHelpText: s__(
- `CICD|Prevent CI/CD job tokens from this project from being used to access other projects unless the other project is added to the allowlist. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API. %{linkStart}Learn more.%{linkEnd}`,
+ `CICD|Prevent CI/CD job tokens from this project from being used to access other projects unless the other project is added to the allowlist. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API. %{linkStart}Learn more%{linkEnd}.`,
),
cardHeaderTitle: s__('CICD|Add an existing project to the scope'),
settingDisabledMessage: s__(
@@ -258,12 +258,12 @@ export default {
<template #help>
<gl-sprintf :message="$options.i18n.toggleHelpText">
<template #link="{ content }">
- <gl-link :href="ciJobTokenHelpPage" class="inline-link" target="_blank">
- {{ content }}
- </gl-link>
- <strong>{{ $options.i18n.disableToggleWarning }} </strong>
+ <gl-link :href="ciJobTokenHelpPage" class="inline-link" target="_blank">{{
+ content
+ }}</gl-link>
</template>
</gl-sprintf>
+ <strong>{{ $options.i18n.disableToggleWarning }} </strong>
</template>
</gl-toggle>
diff --git a/app/assets/javascripts/tracing/components/tracing_details.vue b/app/assets/javascripts/tracing/components/tracing_details.vue
deleted file mode 100644
index d8b2cbc9469..00000000000
--- a/app/assets/javascripts/tracing/components/tracing_details.vue
+++ /dev/null
@@ -1,90 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { createAlert } from '~/alert';
-import { visitUrl, isSafeURL } from '~/lib/utils/url_utility';
-
-export default {
- components: {
- GlLoadingIcon,
- },
- i18n: {
- error: s__('Tracing|Failed to load trace details.'),
- },
- props: {
- observabilityClient: {
- required: true,
- type: Object,
- },
- traceId: {
- required: true,
- type: String,
- },
- tracingIndexUrl: {
- required: true,
- type: String,
- validator: (val) => isSafeURL(val),
- },
- },
- data() {
- return {
- trace: null,
- loading: false,
- };
- },
- created() {
- this.validateAndFetch();
- },
- methods: {
- async validateAndFetch() {
- if (!this.traceId) {
- createAlert({
- message: this.$options.i18n.error,
- });
- }
- this.loading = true;
- try {
- const enabled = await this.observabilityClient.isTracingEnabled();
- if (enabled) {
- await this.fetchTrace();
- } else {
- this.goToTracingIndex();
- }
- } catch (e) {
- createAlert({
- message: this.$options.i18n.error,
- });
- } finally {
- this.loading = false;
- }
- },
- async fetchTrace() {
- this.loading = true;
- try {
- this.trace = await this.observabilityClient.fetchTrace(this.traceId);
- } catch (e) {
- createAlert({
- message: this.$options.i18n.error,
- });
- } finally {
- this.loading = false;
- }
- },
- goToTracingIndex() {
- visitUrl(this.tracingIndexUrl);
- },
- },
-};
-</script>
-
-<template>
- <div v-if="loading" class="gl-py-5">
- <gl-loading-icon size="lg" />
- </div>
-
- <!-- TODO Replace with actual trace-details component-->
- <div v-else-if="trace" data-testid="trace-details">
- <p>{{ tracingIndexUrl }}</p>
- <p>{{ trace }}</p>
- </div>
-</template>
diff --git a/app/assets/javascripts/tracing/components/tracing_empty_state.vue b/app/assets/javascripts/tracing/components/tracing_empty_state.vue
deleted file mode 100644
index f17060db6bc..00000000000
--- a/app/assets/javascripts/tracing/components/tracing_empty_state.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-<script>
-import EMPTY_TRACING_SVG from '@gitlab/svgs/dist/illustrations/monitoring/tracing.svg?url';
-import { GlEmptyState, GlButton } from '@gitlab/ui';
-import { s__ } from '~/locale';
-
-export default {
- EMPTY_TRACING_SVG,
- name: 'TracingEmptyState',
- i18n: {
- title: s__('Tracing|Get started with Tracing'),
- description: s__('Tracing|Monitor your applications with GitLab Distributed Tracing.'),
- enableButtonText: s__('Tracing|Enable'),
- },
- components: {
- GlEmptyState,
- GlButton,
- },
-};
-</script>
-
-<template>
- <gl-empty-state :title="$options.i18n.title" :svg-path="$options.EMPTY_TRACING_SVG">
- <template #description>
- <div>
- <span>{{ $options.i18n.description }}</span>
- </div>
- </template>
-
- <template #actions>
- <gl-button variant="confirm" class="gl-mx-2 gl-mb-3" @click="$emit('enable-tracing')">
- {{ $options.i18n.enableButtonText }}
- </gl-button>
- </template>
- </gl-empty-state>
-</template>
diff --git a/app/assets/javascripts/tracing/components/tracing_list.vue b/app/assets/javascripts/tracing/components/tracing_list.vue
deleted file mode 100644
index 21d1353a86d..00000000000
--- a/app/assets/javascripts/tracing/components/tracing_list.vue
+++ /dev/null
@@ -1,125 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { createAlert } from '~/alert';
-import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
-import UrlSync from '~/vue_shared/components/url_sync.vue';
-import {
- queryToFilterObj,
- filterObjToQuery,
- filterObjToFilterToken,
- filterTokensToFilterObj,
-} from '../filters';
-import TracingEmptyState from './tracing_empty_state.vue';
-import TracingTableList from './tracing_table_list.vue';
-import FilteredSearch from './tracing_list_filtered_search.vue';
-
-export default {
- components: {
- GlLoadingIcon,
- TracingTableList,
- TracingEmptyState,
- FilteredSearch,
- UrlSync,
- },
- props: {
- observabilityClient: {
- required: true,
- type: Object,
- },
- },
- data() {
- return {
- loading: true,
- /**
- * tracingEnabled: boolean | null.
- * null identifies a state where we don't know if tracing is enabled or not (e.g. when fetching the status from the API fails)
- */
- tracingEnabled: null,
- traces: [],
- filters: queryToFilterObj(window.location.search),
- };
- },
- computed: {
- query() {
- return filterObjToQuery(this.filters);
- },
- initialFilterValue() {
- return filterObjToFilterToken(this.filters);
- },
- },
- async created() {
- this.checkEnabled();
- },
- methods: {
- async checkEnabled() {
- this.loading = true;
- try {
- this.tracingEnabled = await this.observabilityClient.isTracingEnabled();
- if (this.tracingEnabled) {
- await this.fetchTraces();
- }
- } catch (e) {
- createAlert({
- message: s__('Tracing|Failed to load page.'),
- });
- } finally {
- this.loading = false;
- }
- },
- async enableTracing() {
- this.loading = true;
- try {
- await this.observabilityClient.enableTraces();
- this.tracingEnabled = true;
- await this.fetchTraces();
- } catch (e) {
- createAlert({
- message: s__('Tracing|Failed to enable tracing.'),
- });
- } finally {
- this.loading = false;
- }
- },
- async fetchTraces() {
- this.loading = true;
- try {
- const traces = await this.observabilityClient.fetchTraces(this.filters);
- this.traces = traces;
- } catch (e) {
- createAlert({
- message: s__('Tracing|Failed to load traces.'),
- });
- } finally {
- this.loading = false;
- }
- },
- selectTrace(trace) {
- visitUrl(joinPaths(window.location.pathname, trace.trace_id));
- },
- handleFilters(filterTokens) {
- this.filters = filterTokensToFilterObj(filterTokens);
- this.fetchTraces();
- },
- },
-};
-</script>
-
-<template>
- <div>
- <div v-if="loading" class="gl-py-5">
- <gl-loading-icon size="lg" />
- </div>
-
- <template v-else-if="tracingEnabled !== null">
- <tracing-empty-state v-if="tracingEnabled === false" @enable-tracing="enableTracing" />
-
- <template v-else>
- <filtered-search :initial-filters="initialFilterValue" @submit="handleFilters" />
- <url-sync :query="query" />
-
- <tracing-table-list :traces="traces" @reload="fetchTraces" @trace-selected="selectTrace" />
- </template>
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/tracing/components/tracing_list_filtered_search.vue b/app/assets/javascripts/tracing/components/tracing_list_filtered_search.vue
deleted file mode 100644
index d086f2d03ff..00000000000
--- a/app/assets/javascripts/tracing/components/tracing_list_filtered_search.vue
+++ /dev/null
@@ -1,87 +0,0 @@
-<script>
-import { GlFilteredSearch, GlFilteredSearchToken } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import {
- OPERATORS_IS,
- OPERATORS_IS_NOT,
-} from '~/vue_shared/components/filtered_search_bar/constants';
-import {
- PERIOD_FILTER_TOKEN_TYPE,
- SERVICE_NAME_FILTER_TOKEN_TYPE,
- OPERATION_FILTER_TOKEN_TYPE,
- TRACE_ID_FILTER_TOKEN_TYPE,
- DURATION_MS_FILTER_TOKEN_TYPE,
-} from '../filters';
-
-export default {
- availableTokens: [
- {
- title: s__('Tracing|Period'),
- icon: 'clock',
- type: PERIOD_FILTER_TOKEN_TYPE,
- token: GlFilteredSearchToken,
- operators: OPERATORS_IS,
- unique: true,
- options: [
- { value: '1m', title: s__('Tracing|Last 1 minute') },
- { value: '15m', title: s__('Tracing|Last 15 minutes') },
- { value: '30m', title: s__('Tracing|Last 30 minutes') },
- { value: '1h', title: s__('Tracing|Last 1 hour') },
- { value: '24h', title: s__('Tracing|Last 24 hours') },
- { value: '7d', title: s__('Tracing|Last 7 days') },
- { value: '14d', title: s__('Tracing|Last 14 days') },
- { value: '30d', title: s__('Tracing|Last 30 days') },
- ],
- },
- {
- title: s__('Tracing|Service'),
- type: SERVICE_NAME_FILTER_TOKEN_TYPE,
- token: GlFilteredSearchToken,
- operators: OPERATORS_IS_NOT,
- },
- {
- title: s__('Tracing|Operation'),
- type: OPERATION_FILTER_TOKEN_TYPE,
- token: GlFilteredSearchToken,
- operators: OPERATORS_IS_NOT,
- },
- {
- title: s__('Tracing|Trace ID'),
- type: TRACE_ID_FILTER_TOKEN_TYPE,
- token: GlFilteredSearchToken,
- operators: OPERATORS_IS_NOT,
- },
- {
- title: s__('Tracing|Duration (ms)'),
- type: DURATION_MS_FILTER_TOKEN_TYPE,
- token: GlFilteredSearchToken,
- operators: [
- { value: '>', description: s__('Tracing|longer than') },
- { value: '<', description: s__('Tracing|shorter than') },
- ],
- },
- ],
- components: {
- GlFilteredSearch,
- },
- props: {
- initialFilters: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
-};
-</script>
-
-<template>
- <div class="vue-filtered-search-bar-container row-content-block gl-border-t-none">
- <gl-filtered-search
- :value="initialFilters"
- terms-as-tokens
- :placeholder="s__('Tracing|Filter Traces')"
- :available-tokens="$options.availableTokens"
- @submit="$emit('submit', $event)"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/tracing/components/tracing_table_list.vue b/app/assets/javascripts/tracing/components/tracing_table_list.vue
deleted file mode 100644
index abb1f3ae88c..00000000000
--- a/app/assets/javascripts/tracing/components/tracing_table_list.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<script>
-import { GlTable, GlLink } from '@gitlab/ui';
-import { s__ } from '~/locale';
-
-export const tableDataClass = 'gl-display-flex gl-md-display-table-cell gl-align-items-center';
-export default {
- name: 'TracingTableList',
- i18n: {
- title: s__('Tracing|Traces'),
- emptyText: s__('Tracing|No traces to display.'),
- emptyLinkText: s__('Tracing|Check again'),
- },
- fields: [
- {
- key: 'timestamp',
- label: s__('Tracing|Date'),
- tdClass: tableDataClass,
- sortable: true,
- },
- {
- key: 'service_name',
- label: s__('Tracing|Service'),
- tdClass: tableDataClass,
- sortable: true,
- },
- {
- key: 'operation',
- label: s__('Tracing|Operation'),
- tdClass: tableDataClass,
- sortable: true,
- },
- {
- key: 'duration',
- label: s__('Tracing|Duration'),
- thClass: 'gl-w-15p',
- tdClass: tableDataClass,
- sortable: true,
- },
- ],
- components: {
- GlTable,
- GlLink,
- },
- props: {
- traces: {
- required: true,
- type: Array,
- },
- },
- methods: {
- onSelect(items) {
- if (items[0]) {
- this.$emit('trace-selected', items[0]);
- }
- },
- },
-};
-</script>
-
-<template>
- <div>
- <h4 class="gl-display-block gl-md-display-none! gl-my-5">{{ $options.i18n.title }}</h4>
-
- <gl-table
- :items="traces"
- :fields="$options.fields"
- show-empty
- sort-by="timestamp"
- :sort-desc="true"
- fixed
- stacked="md"
- tbody-tr-class="table-row"
- selectable
- select-mode="single"
- selected-variant=""
- @row-selected="onSelect"
- >
- <template #cell(timestamp)="data">
- {{ data.item.timestamp }}
- </template>
-
- <template #cell(service_name)="data">
- {{ data.item.service_name }}
- </template>
-
- <template #cell(operation)="data">
- {{ data.item.operation }}
- </template>
-
- <template #cell(duration)="data">
- <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
- {{ `${data.item.duration} ms` }}
- </template>
-
- <template #empty>
- {{ $options.i18n.emptyText }}
- <gl-link @click="$emit('reload')">{{ $options.i18n.emptyLinkText }}</gl-link>
- </template>
- </gl-table>
- </div>
-</template>
diff --git a/app/assets/javascripts/tracing/details_index.vue b/app/assets/javascripts/tracing/details_index.vue
deleted file mode 100644
index 5702a88766c..00000000000
--- a/app/assets/javascripts/tracing/details_index.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<script>
-import ObservabilityContainer from '~/observability/components/observability_container.vue';
-import TracingDetails from './components/tracing_details.vue';
-
-export default {
- components: {
- ObservabilityContainer,
- TracingDetails,
- },
- props: {
- traceId: {
- type: String,
- required: true,
- },
- oauthUrl: {
- type: String,
- required: true,
- },
- tracingUrl: {
- type: String,
- required: true,
- },
- provisioningUrl: {
- type: String,
- required: true,
- },
- tracingIndexUrl: {
- required: true,
- type: String,
- },
- },
-};
-</script>
-
-<template>
- <observability-container
- :oauth-url="oauthUrl"
- :tracing-url="tracingUrl"
- :provisioning-url="provisioningUrl"
- >
- <template #default="{ observabilityClient }">
- <tracing-details
- :trace-id="traceId"
- :tracing-index-url="tracingIndexUrl"
- :observability-client="observabilityClient"
- />
- </template>
- </observability-container>
-</template>
diff --git a/app/assets/javascripts/tracing/filters.js b/app/assets/javascripts/tracing/filters.js
deleted file mode 100644
index 88a54b2e69f..00000000000
--- a/app/assets/javascripts/tracing/filters.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import {
- filterToQueryObject,
- urlQueryToFilter,
- prepareTokens,
- processFilters,
-} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
-import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
-
-export const PERIOD_FILTER_TOKEN_TYPE = 'period';
-export const SERVICE_NAME_FILTER_TOKEN_TYPE = 'service-name';
-export const OPERATION_FILTER_TOKEN_TYPE = 'operation';
-export const TRACE_ID_FILTER_TOKEN_TYPE = 'trace-id';
-export const DURATION_MS_FILTER_TOKEN_TYPE = 'duration-ms';
-
-export function queryToFilterObj(url) {
- const filter = urlQueryToFilter(url, {
- filteredSearchTermKey: 'search',
- customOperators: [
- {
- operator: '>',
- prefix: 'gt',
- },
- {
- operator: '<',
- prefix: 'lt',
- },
- ],
- });
- const {
- period = null,
- service = null,
- operation = null,
- trace_id: traceId = null,
- durationMs = null,
- } = filter;
- const search = filter[FILTERED_SEARCH_TERM];
- return {
- period,
- service,
- operation,
- traceId,
- durationMs,
- search,
- };
-}
-
-export function filterObjToQuery(filters) {
- return filterToQueryObject(
- {
- period: filters.period,
- service: filters.serviceName,
- operation: filters.operation,
- trace_id: filters.traceId,
- durationMs: filters.durationMs,
- [FILTERED_SEARCH_TERM]: filters.search,
- },
- {
- filteredSearchTermKey: 'search',
- customOperators: [
- {
- operator: '>',
- prefix: 'gt',
- applyOnlyToKey: 'durationMs',
- },
- {
- operator: '<',
- prefix: 'lt',
- applyOnlyToKey: 'durationMs',
- },
- ],
- },
- );
-}
-
-export function filterObjToFilterToken(filters) {
- return prepareTokens({
- [PERIOD_FILTER_TOKEN_TYPE]: filters.period,
- [SERVICE_NAME_FILTER_TOKEN_TYPE]: filters.serviceName,
- [OPERATION_FILTER_TOKEN_TYPE]: filters.operation,
- [TRACE_ID_FILTER_TOKEN_TYPE]: filters.traceId,
- [DURATION_MS_FILTER_TOKEN_TYPE]: filters.durationMs,
- [FILTERED_SEARCH_TERM]: filters.search,
- });
-}
-
-export function filterTokensToFilterObj(tokens) {
- const {
- [SERVICE_NAME_FILTER_TOKEN_TYPE]: serviceName,
- [PERIOD_FILTER_TOKEN_TYPE]: period,
- [OPERATION_FILTER_TOKEN_TYPE]: operation,
- [TRACE_ID_FILTER_TOKEN_TYPE]: traceId,
- [DURATION_MS_FILTER_TOKEN_TYPE]: durationMs,
- [FILTERED_SEARCH_TERM]: search,
- } = processFilters(tokens);
-
- return {
- serviceName,
- period,
- operation,
- traceId,
- durationMs,
- search,
- };
-}
diff --git a/app/assets/javascripts/tracing/list_index.vue b/app/assets/javascripts/tracing/list_index.vue
deleted file mode 100644
index 432fbb81506..00000000000
--- a/app/assets/javascripts/tracing/list_index.vue
+++ /dev/null
@@ -1,37 +0,0 @@
-<script>
-import ObservabilityContainer from '~/observability/components/observability_container.vue';
-import TracingList from './components/tracing_list.vue';
-
-export default {
- components: {
- ObservabilityContainer,
- TracingList,
- },
- props: {
- oauthUrl: {
- type: String,
- required: true,
- },
- tracingUrl: {
- type: String,
- required: true,
- },
- provisioningUrl: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <observability-container
- :oauth-url="oauthUrl"
- :tracing-url="tracingUrl"
- :provisioning-url="provisioningUrl"
- >
- <template #default="{ observabilityClient }">
- <tracing-list :observability-client="observabilityClient" />
- </template>
- </observability-container>
-</template>
diff --git a/app/assets/javascripts/tracking/constants.js b/app/assets/javascripts/tracking/constants.js
index 114587bb363..88b7f6d3532 100644
--- a/app/assets/javascripts/tracking/constants.js
+++ b/app/assets/javascripts/tracking/constants.js
@@ -30,4 +30,10 @@ export const GOOGLE_ANALYTICS_ID_COOKIE_NAME = '_ga';
export const GITLAB_INTERNAL_EVENT_CATEGORY = 'InternalEventTracking';
-export const SERVICE_PING_SCHEMA = 'iglu:com.gitlab/gitlab_service_ping/jsonschema/1-0-0';
+export const SERVICE_PING_SCHEMA = 'iglu:com.gitlab/gitlab_service_ping/jsonschema/1-0-1';
+
+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/dispatch_snowplow_event.js b/app/assets/javascripts/tracking/dispatch_snowplow_event.js
index 89d90cf89be..99e4a6aa3c7 100644
--- a/app/assets/javascripts/tracking/dispatch_snowplow_event.js
+++ b/app/assets/javascripts/tracking/dispatch_snowplow_event.js
@@ -15,10 +15,14 @@ export function dispatchSnowplowEvent(
let { value } = data;
const standardContext = getStandardContext({ extra });
- const contexts = [standardContext];
+ let contexts = [standardContext];
if (data.context) {
- contexts.push(data.context);
+ if (Array.isArray(data.context)) {
+ contexts = [...contexts, ...data.context];
+ } else {
+ contexts.push(data.context);
+ }
}
if (value !== undefined) {
diff --git a/app/assets/javascripts/tracking/index.js b/app/assets/javascripts/tracking/index.js
index ffbd932c02b..2ee4703aa0b 100644
--- a/app/assets/javascripts/tracking/index.js
+++ b/app/assets/javascripts/tracking/index.js
@@ -72,4 +72,5 @@ export function initDefaultTrackers() {
InternalEvents.bindInternalEventDocument();
InternalEvents.trackInternalLoadEvents();
+ InternalEvents.initBrowserSDK();
}
diff --git a/app/assets/javascripts/tracking/internal_events.js b/app/assets/javascripts/tracking/internal_events.js
index a5fbb55ff63..9bd0200cad1 100644
--- a/app/assets/javascripts/tracking/internal_events.js
+++ b/app/assets/javascripts/tracking/internal_events.js
@@ -1,10 +1,12 @@
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';
@@ -13,17 +15,24 @@ const InternalEvents = {
/**
*
* @param {string} event
+ * @param {object} data
*/
- track_event(event) {
+ track_event(event, data = {}) {
+ const { context, ...rest } = data;
+
+ const defaultContext = {
+ schema: SERVICE_PING_SCHEMA,
+ data: {
+ event_name: event,
+ data_source: 'redis_hll',
+ },
+ };
+ const mergedContext = context ? [defaultContext, context] : defaultContext;
+
API.trackInternalEvent(event);
Tracking.event(GITLAB_INTERNAL_EVENT_CATEGORY, event, {
- context: {
- schema: SERVICE_PING_SCHEMA,
- data: {
- event_name: event,
- data_source: 'redis_hll',
- },
- },
+ context: mergedContext,
+ ...rest,
});
},
/**
@@ -33,8 +42,8 @@ const InternalEvents = {
mixin() {
return {
methods: {
- track_event(event) {
- InternalEvents.track_event(event);
+ track_event(event, data = {}) {
+ InternalEvents.track_event(event, data);
},
},
};
@@ -78,6 +87,25 @@ const InternalEvents = {
return loadEvents;
},
+ /**
+ * 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 || {},
+ },
+ ],
+ });
+ }
+ },
};
export default InternalEvents;
diff --git a/app/assets/javascripts/tracking/tracker.js b/app/assets/javascripts/tracking/tracker.js
index b69b1714952..b74078475b0 100644
--- a/app/assets/javascripts/tracking/tracker.js
+++ b/app/assets/javascripts/tracking/tracker.js
@@ -257,12 +257,20 @@ export const Tracker = {
const customUrl = `${pageUrl}${appendHash ? window.location.hash : ''}`;
window.snowplow('setCustomUrl', customUrl);
+ // If Browser SDK is enabled set Custom url and Referrer url
+ if (window.glClient) {
+ window.glClient?.setCustomUrl(customUrl);
+ }
if (document.referrer) {
const node = referrers.find((links) => links.originalUrl === document.referrer);
if (node) {
pageLinks.referrer = node.url;
window.snowplow('setReferrerUrl', pageLinks.referrer);
+
+ if (window.glClient) {
+ window.glClient?.setReferrerUrl(pageLinks.referrer);
+ }
}
}
diff --git a/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.stories.js b/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.stories.js
new file mode 100644
index 00000000000..9bf6d27235c
--- /dev/null
+++ b/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.stories.js
@@ -0,0 +1,64 @@
+import { mockGetProjectStorageStatisticsGraphQLResponse } from 'jest/usage_quotas/storage/mock_data';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import getProjectStorageStatisticsQuery from '../queries/project_storage.query.graphql';
+import ProjectStorageApp from './project_storage_app.vue';
+
+const meta = {
+ title: 'usage_quotas/storage/project_storage_app',
+ component: ProjectStorageApp,
+};
+
+export default meta;
+
+const createTemplate = (config = {}) => {
+ let { provide, apolloProvider } = config;
+
+ if (provide == null) {
+ provide = {};
+ }
+
+ if (apolloProvider == null) {
+ const requestHandlers = [
+ [
+ getProjectStorageStatisticsQuery,
+ () => Promise.resolve(mockGetProjectStorageStatisticsGraphQLResponse),
+ ],
+ ];
+ apolloProvider = createMockApollo(requestHandlers);
+ }
+
+ return (args, { argTypes }) => ({
+ components: { ProjectStorageApp },
+ apolloProvider,
+ provide: {
+ projectPath: '/namespace/project',
+ ...provide,
+ },
+ props: Object.keys(argTypes),
+ template: '<project-storage-app />',
+ });
+};
+
+export const Default = {
+ render: createTemplate(),
+};
+
+export const Loading = {
+ render(...args) {
+ const requestHandlers = [[getProjectStorageStatisticsQuery, () => new Promise(() => {})]];
+ const apolloProvider = createMockApollo(requestHandlers);
+ return createTemplate({
+ apolloProvider,
+ })(...args);
+ },
+};
+
+export const LoadingError = {
+ render(...args) {
+ const requestHandlers = [[getProjectStorageStatisticsQuery, () => Promise.reject()]];
+ const apolloProvider = createMockApollo(requestHandlers);
+ return createTemplate({
+ apolloProvider,
+ })(...args);
+ },
+};
diff --git a/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue b/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue
index f271b284d78..a5e1cc398e3 100644
--- a/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue
+++ b/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue
@@ -3,6 +3,7 @@ import { GlAlert, GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { sprintf } from '~/locale';
import { updateRepositorySize } from '~/api/projects_api';
import { numberToHumanSize } from '~/lib/utils/number_utils';
+import SectionedPercentageBar from '~/usage_quotas/components/sectioned_percentage_bar.vue';
import {
ERROR_MESSAGE,
LEARN_MORE_LABEL,
@@ -19,7 +20,6 @@ import {
} from '../constants';
import getProjectStorageStatistics from '../queries/project_storage.query.graphql';
import { getStorageTypesFromProjectStatistics, descendingStorageUsageSort } from '../utils';
-import UsageGraph from './usage_graph.vue';
import ProjectStorageDetail from './project_storage_detail.vue';
export default {
@@ -29,8 +29,8 @@ export default {
GlButton,
GlLink,
GlLoadingIcon,
- UsageGraph,
ProjectStorageDetail,
+ SectionedPercentageBar,
},
inject: ['projectPath'],
apollo: {
@@ -88,6 +88,67 @@ export default {
storageTypeHelpPaths,
);
},
+
+ sections() {
+ if (!this.project?.statistics) {
+ return null;
+ }
+
+ const {
+ buildArtifactsSize,
+ lfsObjectsSize,
+ packagesSize,
+ repositorySize,
+ storageSize,
+ wikiSize,
+ snippetsSize,
+ } = this.project.statistics;
+
+ if (storageSize === 0) {
+ return null;
+ }
+
+ return [
+ {
+ id: 'repository',
+ value: repositorySize,
+ },
+ {
+ id: 'lfsObjects',
+ value: lfsObjectsSize,
+ },
+ {
+ id: 'packages',
+ value: packagesSize,
+ },
+ {
+ id: 'buildArtifacts',
+ value: buildArtifactsSize,
+ },
+ {
+ id: 'wiki',
+ value: wikiSize,
+ },
+ {
+ id: 'snippets',
+ value: snippetsSize,
+ },
+ ]
+ .filter((data) => data.value !== 0)
+ .sort(descendingStorageUsageSort('value'))
+ .map((storageType) => {
+ const storageTypeExtraData = PROJECT_STORAGE_TYPES.find(
+ (type) => storageType.id === type.id,
+ );
+ const label = storageTypeExtraData?.name;
+
+ return {
+ label,
+ formattedValue: numberToHumanSize(storageType.value),
+ ...storageType,
+ };
+ });
+ },
},
methods: {
clearError() {
@@ -123,11 +184,11 @@ export default {
{{ error }}
</gl-alert>
<div v-else>
- <div class="gl-pt-5 gl-px-3">
- <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
+ <div class="gl-pt-5">
+ <div class="gl-display-flex gl-justify-content-space-between">
<div>
- <h2 class="gl-m-0 gl-font-lg gl-font-weight-bold">{{ $options.TOTAL_USAGE_TITLE }}</h2>
- <p class="gl-m-0 gl-text-gray-400">
+ <h4 class="gl-font-lg gl-mb-3 gl-mt-0">{{ $options.TOTAL_USAGE_TITLE }}</h4>
+ <p>
{{ $options.TOTAL_USAGE_SUBTITLE }}
<gl-link
:href="$options.usageQuotasHelpPaths.usageQuotas"
@@ -137,13 +198,16 @@ export default {
>
</p>
</div>
- <p class="gl-m-0 gl-font-size-h-display gl-font-weight-bold" data-testid="total-usage">
+ <p
+ class="gl-m-0 gl-font-size-h-display gl-font-weight-bold gl-white-space-nowrap"
+ data-testid="total-usage"
+ >
{{ totalUsage }}
</p>
</div>
</div>
<div v-if="!isStatisticsEmpty" class="gl-w-full">
- <usage-graph :root-storage-statistics="project.statistics" :limit="0" />
+ <sectioned-percentage-bar class="gl-mt-5" :sections="sections" />
</div>
<div class="gl-w-full gl-my-5">
<gl-button
diff --git a/app/assets/javascripts/usage_quotas/storage/components/usage_graph.vue b/app/assets/javascripts/usage_quotas/storage/components/usage_graph.vue
deleted file mode 100644
index 33f202e69db..00000000000
--- a/app/assets/javascripts/usage_quotas/storage/components/usage_graph.vue
+++ /dev/null
@@ -1,136 +0,0 @@
-<script>
-import { numberToHumanSize } from '~/lib/utils/number_utils';
-import { PROJECT_STORAGE_TYPES } from '../constants';
-import { descendingStorageUsageSort } from '../utils';
-
-export default {
- name: 'UsageGraph',
- props: {
- rootStorageStatistics: {
- required: true,
- type: Object,
- },
- limit: {
- required: true,
- type: Number,
- },
- },
- computed: {
- storageTypes() {
- const {
- buildArtifactsSize,
- lfsObjectsSize,
- packagesSize,
- repositorySize,
- storageSize,
- wikiSize,
- snippetsSize,
- } = this.rootStorageStatistics;
-
- if (storageSize === 0) {
- return null;
- }
-
- return [
- {
- id: 'repository',
- style: this.usageStyle(this.barRatio(repositorySize)),
- class: 'gl-bg-data-viz-blue-500',
- size: repositorySize,
- },
- {
- id: 'lfsObjects',
- style: this.usageStyle(this.barRatio(lfsObjectsSize)),
- class: 'gl-bg-data-viz-orange-600',
- size: lfsObjectsSize,
- },
- {
- id: 'packages',
- style: this.usageStyle(this.barRatio(packagesSize)),
- class: 'gl-bg-data-viz-aqua-500',
- size: packagesSize,
- },
- {
- id: 'buildArtifacts',
- style: this.usageStyle(this.barRatio(buildArtifactsSize)),
- class: 'gl-bg-data-viz-green-500',
- size: buildArtifactsSize,
- },
- {
- id: 'wiki',
- style: this.usageStyle(this.barRatio(wikiSize)),
- class: 'gl-bg-data-viz-magenta-500',
- size: wikiSize,
- },
- {
- id: 'snippets',
- style: this.usageStyle(this.barRatio(snippetsSize)),
- class: 'gl-bg-data-viz-orange-800',
- size: snippetsSize,
- },
- ]
- .filter((data) => data.size !== 0)
- .sort(descendingStorageUsageSort('size'))
- .map((storageType) => {
- const storageTypeExtraData = PROJECT_STORAGE_TYPES.find(
- (type) => storageType.id === type.id,
- );
- const name = storageTypeExtraData?.name;
-
- return {
- name,
- ...storageType,
- };
- });
- },
- },
- methods: {
- formatSize(size) {
- return numberToHumanSize(size);
- },
- usageStyle(ratio) {
- return { flex: ratio };
- },
- barRatio(size) {
- let max = this.rootStorageStatistics.storageSize;
-
- if (this.limit !== 0 && max <= this.limit) {
- max = this.limit;
- }
-
- return size / max;
- },
- },
-};
-</script>
-<template>
- <div v-if="storageTypes" class="gl-display-flex gl-flex-direction-column w-100">
- <div class="gl-h-6 gl-my-5 gl-bg-gray-50 gl-rounded-base gl-display-flex">
- <div
- v-for="storageType in storageTypes"
- :key="storageType.name"
- class="storage-type-usage gl-h-full gl-display-inline-block"
- :class="storageType.class"
- :style="storageType.style"
- data-testid="storage-type-usage"
- ></div>
- </div>
- <div class="row gl-mb-4">
- <div
- v-for="storageType in storageTypes"
- :key="storageType.name"
- class="col-md-auto gl-display-flex gl-align-items-center"
- data-testid="storage-type-legend"
- data-qa-selector="storage_type_legend"
- >
- <div class="gl-h-2 gl-w-5 gl-mr-2 gl-display-inline-block" :class="storageType.class"></div>
- <span class="gl-mr-2 gl-font-weight-bold gl-font-sm">
- {{ storageType.name }}
- </span>
- <span class="gl-text-gray-500 gl-font-sm">
- {{ formatSize(storageType.size) }}
- </span>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/user_lists/components/user_list.vue b/app/assets/javascripts/user_lists/components/user_list.vue
index 29b9b68883b..d4601d1f736 100644
--- a/app/assets/javascripts/user_lists/components/user_list.vue
+++ b/app/assets/javascripts/user_lists/components/user_list.vue
@@ -39,7 +39,7 @@ export default {
),
userIdLabel: s__('UserLists|User IDs'),
userIdColumnHeader: s__('UserLists|User ID'),
- errorMessage: __('Something went wrong on our end. Please try again!'),
+ errorMessage: __('Unable to load user list. Reload the page and try again.'),
editButtonLabel: s__('UserLists|Edit'),
},
classes: {
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index ab707e7e69c..aa68cf5a161 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -32,45 +32,46 @@ function UsersSelect(currentUser, els, options = {}) {
const { handleClick, states } = options;
- $els.each((i, dropdown) => {
- const userSelect = this;
- const $dropdown = $(dropdown);
- const options = {
- states,
- projectId: $dropdown.data('projectId'),
- groupId: $dropdown.data('groupId'),
- showCurrentUser: $dropdown.data('currentUser'),
- todoFilter: $dropdown.data('todoFilter'),
- todoStateFilter: $dropdown.data('todoStateFilter'),
- iid: $dropdown.data('iid'),
- issuableType: $dropdown.data('issuableType'),
- targetBranch: $dropdown.data('targetBranch'),
- authorId: $dropdown.data('authorId'),
- showSuggested: $dropdown.data('showSuggested'),
- };
- const showNullUser = $dropdown.data('nullUser');
- const defaultNullUser = $dropdown.data('nullUserDefault');
- const showMenuAbove = $dropdown.data('showMenuAbove');
- const showAnyUser = $dropdown.data('anyUser');
- const firstUser = $dropdown.data('firstUser');
- const defaultLabel = $dropdown.data('defaultLabel');
- const issueURL = $dropdown.data('issueUpdate');
- const $selectbox = $dropdown.closest('.selectbox');
- const $assignToMeLink = $selectbox.next('.assign-to-me-link');
- let $block = $selectbox.closest('.block');
- const abilityName = $dropdown.data('abilityName');
- let $value = $block.find('.value');
- const $collapsedSidebar = $block.find('.sidebar-collapsed-user');
- const $loading = $block.find('.block-loading').addClass('gl-display-none');
- const selectedIdDefault = defaultNullUser && showNullUser ? 0 : null;
- let selectedId = $dropdown.data('selected');
- let assignTo;
- let assigneeTemplate;
- let collapsedAssigneeTemplate;
-
- const suggestedReviewersHelpPath = $dropdown.data('suggestedReviewersHelpPath');
- const suggestedReviewersHeaderTemplate = template(
- `<div class="gl-display-flex gl-align-items-center">
+ this.dropdowns = $els
+ .map((i, dropdown) => {
+ const userSelect = this;
+ const $dropdown = $(dropdown);
+ const options = {
+ states,
+ projectId: $dropdown.data('projectId'),
+ groupId: $dropdown.data('groupId'),
+ showCurrentUser: $dropdown.data('currentUser'),
+ todoFilter: $dropdown.data('todoFilter'),
+ todoStateFilter: $dropdown.data('todoStateFilter'),
+ iid: $dropdown.data('iid'),
+ issuableType: $dropdown.data('issuableType'),
+ targetBranch: $dropdown.data('targetBranch'),
+ authorId: $dropdown.data('authorId'),
+ showSuggested: $dropdown.data('showSuggested'),
+ };
+ const showNullUser = $dropdown.data('nullUser');
+ const defaultNullUser = $dropdown.data('nullUserDefault');
+ const showMenuAbove = $dropdown.data('showMenuAbove');
+ const showAnyUser = $dropdown.data('anyUser');
+ const firstUser = $dropdown.data('firstUser');
+ const defaultLabel = $dropdown.data('defaultLabel');
+ const issueURL = $dropdown.data('issueUpdate');
+ const $selectbox = $dropdown.closest('.selectbox');
+ const $assignToMeLink = $selectbox.next('.assign-to-me-link');
+ let $block = $selectbox.closest('.block');
+ const abilityName = $dropdown.data('abilityName');
+ let $value = $block.find('.value');
+ const $collapsedSidebar = $block.find('.sidebar-collapsed-user');
+ const $loading = $block.find('.block-loading').addClass('gl-display-none');
+ const selectedIdDefault = defaultNullUser && showNullUser ? 0 : null;
+ let selectedId = $dropdown.data('selected');
+ let assignTo;
+ let assigneeTemplate;
+ let collapsedAssigneeTemplate;
+
+ const suggestedReviewersHelpPath = $dropdown.data('suggestedReviewersHelpPath');
+ const suggestedReviewersHeaderTemplate = template(
+ `<div class="gl-display-flex gl-align-items-center">
<%- header %>
<a
title="${s__('SuggestedReviewers|Learn about suggested reviewers')}"
@@ -82,562 +83,568 @@ function UsersSelect(currentUser, els, options = {}) {
${spriteIcon('question-o', 'gl-ml-3 gl-icon s16')}
</a>
</div>`,
- );
+ );
- if (selectedId === undefined) {
- selectedId = selectedIdDefault;
- }
+ if (selectedId === undefined) {
+ selectedId = selectedIdDefault;
+ }
- const assignYourself = function () {
- const unassignedSelected = $dropdown
- .closest('.selectbox')
- .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`);
+ const assignYourself = function () {
+ const unassignedSelected = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`);
- if (unassignedSelected) {
- unassignedSelected.remove();
- }
+ if (unassignedSelected) {
+ unassignedSelected.remove();
+ }
- // Save current selected user to the DOM
- const currentUserInfo = $dropdown.data('currentUserInfo') || {};
- const currentUser = userSelect.currentUser || {};
- const fieldName = $dropdown.data('fieldName');
- const userName = currentUserInfo.name;
- const userId = currentUserInfo.id || currentUser.id;
+ // Save current selected user to the DOM
+ const currentUserInfo = $dropdown.data('currentUserInfo') || {};
+ const currentUser = userSelect.currentUser || {};
+ const fieldName = $dropdown.data('fieldName');
+ const userName = currentUserInfo.name;
+ const userId = currentUserInfo.id || currentUser.id;
- const inputHtmlString = template(`
+ const inputHtmlString = template(`
<input type="hidden" name="<%- fieldName %>"
data-meta="<%- userName %>"
value="<%- userId %>" />
`)({ fieldName, userName, userId });
- if ($selectbox) {
- $dropdown.parent().before(inputHtmlString);
- } else {
- $dropdown.after(inputHtmlString);
+ if ($selectbox) {
+ $dropdown.parent().before(inputHtmlString);
+ } else {
+ $dropdown.after(inputHtmlString);
+ }
+ };
+
+ if ($block[0]) {
+ $block[0].addEventListener('assignYourself', assignYourself);
}
- };
- if ($block[0]) {
- $block[0].addEventListener('assignYourself', assignYourself);
- }
+ const getSelectedUserInputs = function () {
+ return $selectbox.find(`input[name="${$dropdown.data('fieldName')}"]`);
+ };
- const getSelectedUserInputs = function () {
- return $selectbox.find(`input[name="${$dropdown.data('fieldName')}"]`);
- };
-
- const getSelected = function () {
- return getSelectedUserInputs()
- .map((index, input) => parseInt(input.value, 10))
- .get();
- };
-
- const checkMaxSelect = function () {
- const maxSelect = $dropdown.data('maxSelect');
- if (maxSelect) {
- const selected = getSelected();
-
- if (selected.length > maxSelect) {
- const firstSelectedId = selected[0];
- const firstSelected = $dropdown
- .closest('.selectbox')
- .find(`input[name='${$dropdown.data('fieldName')}'][value=${firstSelectedId}]`);
-
- firstSelected.remove();
-
- if ($dropdown.hasClass(elsClassName)) {
- emitSidebarEvent('sidebar.removeReviewer', {
- id: firstSelectedId,
- });
- } else {
- emitSidebarEvent('sidebar.removeAssignee', {
- id: firstSelectedId,
- });
+ const getSelected = function () {
+ return getSelectedUserInputs()
+ .map((index, input) => parseInt(input.value, 10))
+ .get();
+ };
+
+ const checkMaxSelect = function () {
+ const maxSelect = $dropdown.data('maxSelect');
+ if (maxSelect) {
+ const selected = getSelected();
+
+ if (selected.length > maxSelect) {
+ const firstSelectedId = selected[0];
+ const firstSelected = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${$dropdown.data('fieldName')}'][value=${firstSelectedId}]`);
+
+ firstSelected.remove();
+
+ if ($dropdown.hasClass(elsClassName)) {
+ emitSidebarEvent('sidebar.removeReviewer', {
+ id: firstSelectedId,
+ });
+ } else {
+ emitSidebarEvent('sidebar.removeAssignee', {
+ id: firstSelectedId,
+ });
+ }
}
}
- }
- };
-
- const getMultiSelectDropdownTitle = function (selectedUser, isSelected) {
- const selectedUsers = getSelected().filter((u) => u !== 0);
-
- const firstUser = getSelectedUserInputs()
- .map((index, input) => ({
- name: input.dataset.meta,
- value: parseInt(input.value, 10),
- }))
- .filter((u) => u.id !== 0)
- .get(0);
-
- if (selectedUsers.length === 0) {
- return s__('UsersSelect|Unassigned');
- } else if (selectedUsers.length === 1) {
- return firstUser.name;
- } else if (isSelected) {
- const otherSelected = selectedUsers.filter((s) => s !== selectedUser.id);
+ };
+
+ const getMultiSelectDropdownTitle = function (selectedUser, isSelected) {
+ const selectedUsers = getSelected().filter((u) => u !== 0);
+
+ const firstUser = getSelectedUserInputs()
+ .map((index, input) => ({
+ name: input.dataset.meta,
+ value: parseInt(input.value, 10),
+ }))
+ .filter((u) => u.id !== 0)
+ .get(0);
+
+ if (selectedUsers.length === 0) {
+ return s__('UsersSelect|Unassigned');
+ }
+ if (selectedUsers.length === 1) {
+ return firstUser.name;
+ }
+ if (isSelected) {
+ const otherSelected = selectedUsers.filter((s) => s !== selectedUser.id);
+ return sprintf(s__('UsersSelect|%{name} + %{length} more'), {
+ name: selectedUser.name,
+ length: otherSelected.length,
+ });
+ }
return sprintf(s__('UsersSelect|%{name} + %{length} more'), {
- name: selectedUser.name,
- length: otherSelected.length,
+ name: firstUser.name,
+ length: selectedUsers.length - 1,
});
- }
- return sprintf(s__('UsersSelect|%{name} + %{length} more'), {
- name: firstUser.name,
- length: selectedUsers.length - 1,
- });
- };
-
- $assignToMeLink.on('click', (e) => {
- e.preventDefault();
- $(e.currentTarget).hide();
-
- if ($dropdown.data('multiSelect')) {
- assignYourself();
- checkMaxSelect();
-
- const currentUserInfo = $dropdown.data('currentUserInfo');
- $dropdown
- .find('.dropdown-toggle-text')
- .text(getMultiSelectDropdownTitle(currentUserInfo))
- .removeClass('is-default');
- } else {
- const $input = $(`input[name="${$dropdown.data('fieldName')}"]`);
- $input.val(gon.current_user_id);
- selectedId = $input.val();
- $dropdown
- .find('.dropdown-toggle-text')
- .text(gon.current_user_fullname)
- .removeClass('is-default');
- }
- });
-
- $block.on('click', '.js-assign-yourself', (e) => {
- e.preventDefault();
- return assignTo(userSelect.currentUser.id);
- });
-
- assignTo = function (selected) {
- const data = {};
- data[abilityName] = {};
- data[abilityName].assignee_id = selected != null ? selected : null;
- $loading.removeClass('gl-display-none');
- $dropdown.trigger('loading.gl.dropdown');
-
- return axios.put(issueURL, data).then(({ data }) => {
- let user = {};
- let tooltipTitle;
- $dropdown.trigger('loaded.gl.dropdown');
- $loading.addClass('gl-display-none');
- if (data.assignee) {
- user = {
- name: data.assignee.name,
- username: data.assignee.username,
- avatar: data.assignee.avatar_url,
- };
- tooltipTitle = escape(user.name);
+ };
+
+ $assignToMeLink.on('click', (e) => {
+ e.preventDefault();
+ $(e.currentTarget).hide();
+
+ if ($dropdown.data('multiSelect')) {
+ assignYourself();
+ checkMaxSelect();
+
+ const currentUserInfo = $dropdown.data('currentUserInfo');
+ $dropdown
+ .find('.dropdown-toggle-text')
+ .text(getMultiSelectDropdownTitle(currentUserInfo))
+ .removeClass('is-default');
} else {
- user = {
- name: s__('UsersSelect|Unassigned'),
- username: '',
- avatar: '',
- };
- tooltipTitle = s__('UsersSelect|Assignee');
+ const $input = $(`input[name="${$dropdown.data('fieldName')}"]`);
+ $input.val(gon.current_user_id);
+ selectedId = $input.val();
+ $dropdown
+ .find('.dropdown-toggle-text')
+ .text(gon.current_user_fullname)
+ .removeClass('is-default');
}
- $value.html(assigneeTemplate(user));
- $collapsedSidebar.attr('title', tooltipTitle);
- fixTitle($collapsedSidebar);
+ });
- return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
+ $block.on('click', '.js-assign-yourself', (e) => {
+ e.preventDefault();
+ return assignTo(userSelect.currentUser.id);
});
- };
- collapsedAssigneeTemplate = template(
- `<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> ${spriteIcon(
- 'user',
- )} <% } %>`,
- );
- assigneeTemplate = template(
- `<% if (username) { %> <a class="author-link gl-font-weight-bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself">
+
+ assignTo = function (selected) {
+ const data = {};
+ data[abilityName] = {};
+ data[abilityName].assignee_id = selected != null ? selected : null;
+ $loading.removeClass('gl-display-none');
+ $dropdown.trigger('loading.gl.dropdown');
+
+ return axios.put(issueURL, data).then(({ data }) => {
+ let user = {};
+ let tooltipTitle;
+ $dropdown.trigger('loaded.gl.dropdown');
+ $loading.addClass('gl-display-none');
+ if (data.assignee) {
+ user = {
+ name: data.assignee.name,
+ username: data.assignee.username,
+ avatar: data.assignee.avatar_url,
+ };
+ tooltipTitle = escape(user.name);
+ } else {
+ user = {
+ name: s__('UsersSelect|Unassigned'),
+ username: '',
+ avatar: '',
+ };
+ tooltipTitle = s__('UsersSelect|Assignee');
+ }
+ $value.html(assigneeTemplate(user));
+ $collapsedSidebar.attr('title', tooltipTitle);
+ fixTitle($collapsedSidebar);
+
+ return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
+ });
+ };
+ collapsedAssigneeTemplate = template(
+ `<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> ${spriteIcon(
+ 'user',
+ )} <% } %>`,
+ );
+ assigneeTemplate = template(
+ `<% if (username) { %> <a class="author-link gl-font-weight-bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself">
${sprintf(s__('UsersSelect|No assignee - %{openingTag} assign yourself %{closingTag}'), {
openingTag: '<a href="#" class="js-assign-yourself">',
closingTag: '</a>',
})}</span> <% } %>`,
- );
- return initDeprecatedJQueryDropdown($dropdown, {
- showMenuAbove,
- data(term, callback) {
- return userSelect.users(term, options, (users) => {
- // GitLabDropdownFilter returns this.instance
- // GitLabDropdownRemote returns this.options.instance
- const deprecatedJQueryDropdown = this.instance || this.options.instance;
- deprecatedJQueryDropdown.options.processData(term, users, callback);
- });
- },
- processData(term, dataArg, callback) {
- // Sometimes the `dataArg` can contain special dropdown items like
- // dividers which we don't want to consider here.
- const data = dataArg.filter((x) => !x.type);
-
- let users = data;
-
- // Only show assigned user list when there is no search term
- if ($dropdown.hasClass('js-multiselect') && term.length === 0) {
- const selectedInputs = getSelectedUserInputs();
-
- // Potential duplicate entries when dealing with issue board
- // because issue board is also managed by vue
- const selectedUsers = uniqBy(selectedInputs, (a) => a.value)
- .filter((input) => {
- const userId = parseInt(input.value, 10);
- const inUsersArray = users.find((u) => u.id === userId);
-
- return !inUsersArray && userId !== 0;
- })
- .map((input) => {
- const userId = parseInt(input.value, 10);
- const { avatarUrl, avatar_url, name, username, canMerge } = input.dataset;
- return {
- avatar_url: avatarUrl || avatar_url || gon.default_avatar_url,
- id: userId,
- name,
- username,
- can_merge: parseBoolean(canMerge),
- };
- });
+ );
+
+ return initDeprecatedJQueryDropdown($dropdown, {
+ showMenuAbove,
+ data(term, callback) {
+ return userSelect.users(term, options, (users) => {
+ // GitLabDropdownFilter returns this.instance
+ // GitLabDropdownRemote returns this.options.instance
+ const deprecatedJQueryDropdown = this.instance || this.options.instance;
+ deprecatedJQueryDropdown.options.processData(term, users, callback);
+ });
+ },
+ processData(term, dataArg, callback) {
+ // Sometimes the `dataArg` can contain special dropdown items like
+ // dividers which we don't want to consider here.
+ const data = dataArg.filter((x) => !x.type);
+
+ let users = data;
+
+ // Only show assigned user list when there is no search term
+ if ($dropdown.hasClass('js-multiselect') && term.length === 0) {
+ const selectedInputs = getSelectedUserInputs();
+
+ // Potential duplicate entries when dealing with issue board
+ // because issue board is also managed by vue
+ const selectedUsers = uniqBy(selectedInputs, (a) => a.value)
+ .filter((input) => {
+ const userId = parseInt(input.value, 10);
+ const inUsersArray = users.find((u) => u.id === userId);
+
+ return !inUsersArray && userId !== 0;
+ })
+ .map((input) => {
+ const userId = parseInt(input.value, 10);
+ const { avatarUrl, avatar_url, name, username, canMerge } = input.dataset;
+ return {
+ avatar_url: avatarUrl || avatar_url || gon.default_avatar_url,
+ id: userId,
+ name,
+ username,
+ can_merge: parseBoolean(canMerge),
+ };
+ });
- users = data.concat(selectedUsers);
- }
+ users = data.concat(selectedUsers);
+ }
- let anyUser;
- let index;
- let len;
- let name;
- let obj;
- let showDivider;
- if (term.length === 0) {
- showDivider = 0;
- if (firstUser) {
- // Move current user to the front of the list
- for (index = 0, len = users.length; index < len; index += 1) {
- obj = users[index];
- if (obj.username === firstUser) {
- users.splice(index, 1);
- users.unshift(obj);
- break;
+ let anyUser;
+ let index;
+ let len;
+ let name;
+ let obj;
+ let showDivider;
+ if (term.length === 0) {
+ showDivider = 0;
+ if (firstUser) {
+ // Move current user to the front of the list
+ for (index = 0, len = users.length; index < len; index += 1) {
+ obj = users[index];
+ if (obj.username === firstUser) {
+ users.splice(index, 1);
+ users.unshift(obj);
+ break;
+ }
}
}
- }
- if (showNullUser) {
- showDivider += 1;
- users.unshift({
- beforeDivider: true,
- name: s__('UsersSelect|Unassigned'),
- id: 0,
- });
- }
- if (showAnyUser) {
- showDivider += 1;
- name = showAnyUser;
- if (name === true) {
- name = s__('UsersSelect|Any User');
+ if (showNullUser) {
+ showDivider += 1;
+ users.unshift({
+ beforeDivider: true,
+ name: s__('UsersSelect|Unassigned'),
+ id: 0,
+ });
}
- anyUser = {
- beforeDivider: true,
- name,
- id: null,
- };
- users.unshift(anyUser);
- }
-
- if (showDivider) {
- users.splice(showDivider, 0, { type: 'divider' });
- }
-
- if ($dropdown.hasClass('js-multiselect')) {
- const selected = getSelected().filter((i) => i !== 0);
-
- if ($dropdown.data('showSuggested')) {
- const suggested = this.suggestedUsers(users);
- if (suggested.length) {
- users = users.filter(
- (u) => !u.suggested || (u.suggested && selected.indexOf(u.id) !== -1),
- );
- users.splice(showDivider + 1, 0, ...suggested);
+ if (showAnyUser) {
+ showDivider += 1;
+ name = showAnyUser;
+ if (name === true) {
+ name = s__('UsersSelect|Any User');
}
+ anyUser = {
+ beforeDivider: true,
+ name,
+ id: null,
+ };
+ users.unshift(anyUser);
}
- if (selected.length > 0) {
- if ($dropdown.data('dropdownHeader')) {
- showDivider += 1;
- users.splice(showDivider, 0, {
- type: 'header',
- content: $dropdown.data('dropdownHeader'),
- });
+ if (showDivider) {
+ users.splice(showDivider, 0, { type: 'divider' });
+ }
+
+ if ($dropdown.hasClass('js-multiselect')) {
+ const selected = getSelected().filter((i) => i !== 0);
+
+ if ($dropdown.data('showSuggested')) {
+ const suggested = this.suggestedUsers(users);
+ if (suggested.length) {
+ users = users.filter(
+ (u) => !u.suggested || (u.suggested && selected.indexOf(u.id) !== -1),
+ );
+ users.splice(showDivider + 1, 0, ...suggested);
+ }
}
- const selectedUsers = users
- .filter((u) => selected.indexOf(u.id) !== -1)
- .sort((a, b) => a.name > b.name);
+ if (selected.length > 0) {
+ if ($dropdown.data('dropdownHeader')) {
+ showDivider += 1;
+ users.splice(showDivider, 0, {
+ type: 'header',
+ content: $dropdown.data('dropdownHeader'),
+ });
+ }
- users = users.filter((u) => selected.indexOf(u.id) === -1);
+ const selectedUsers = users
+ .filter((u) => selected.indexOf(u.id) !== -1)
+ .sort((a, b) => a.name > b.name);
- selectedUsers.forEach((selectedUser) => {
- showDivider += 1;
- users.splice(showDivider, 0, selectedUser);
- });
+ users = users.filter((u) => selected.indexOf(u.id) === -1);
- users.splice(showDivider + 1, 0, { type: 'divider' });
+ selectedUsers.forEach((selectedUser) => {
+ showDivider += 1;
+ users.splice(showDivider, 0, selectedUser);
+ });
+
+ users.splice(showDivider + 1, 0, { type: 'divider' });
+ }
}
}
- }
- callback(users);
- if (showMenuAbove) {
- $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove();
- }
- },
- suggestedUsers(users) {
- const selected = getSelected().filter((i) => i !== 0);
- const suggestedUsers = users.filter((u) => u.suggested && selected.indexOf(u.id) === -1);
-
- if (!suggestedUsers.length) return [];
-
- const items = [
- {
- type: 'header',
- content: suggestedReviewersHeaderTemplate({
- header: $dropdown.data('suggestedReviewersHeader'),
- }),
- },
- ...suggestedUsers,
- { type: 'header', content: $dropdown.data('allMembersHeader') },
- ];
- return items;
- },
- filterable: true,
- filterRemote: true,
- search: {
- fields: ['name', 'username'],
- },
- selectable: true,
- fieldName: $dropdown.data('fieldName'),
- toggleLabel(selected, el, deprecatedJQueryDropdown) {
- const inputValue = deprecatedJQueryDropdown.filterInput.val();
-
- if (this.multiSelect && inputValue === '') {
- // Remove non-users from the fullData array
- const users = deprecatedJQueryDropdown.filteredFullData();
- const callback = deprecatedJQueryDropdown.parseData.bind(deprecatedJQueryDropdown);
-
- // Update the data model
- this.processData(inputValue, users, callback);
- }
+ callback(users);
+ if (showMenuAbove) {
+ $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove();
+ }
+ },
+ suggestedUsers(users) {
+ const selected = getSelected().filter((i) => i !== 0);
+ const suggestedUsers = users.filter((u) => u.suggested && selected.indexOf(u.id) === -1);
+
+ if (!suggestedUsers.length) return [];
+
+ const items = [
+ {
+ type: 'header',
+ content: suggestedReviewersHeaderTemplate({
+ header: $dropdown.data('suggestedReviewersHeader'),
+ }),
+ },
+ ...suggestedUsers,
+ { type: 'header', content: $dropdown.data('allMembersHeader') },
+ ];
+ return items;
+ },
+ filterable: true,
+ filterRemote: true,
+ search: {
+ fields: ['name', 'username'],
+ },
+ selectable: true,
+ fieldName: $dropdown.data('fieldName'),
+ toggleLabel(selected, el, deprecatedJQueryDropdown) {
+ const inputValue = deprecatedJQueryDropdown.filterInput.val();
+
+ if (this.multiSelect && inputValue === '') {
+ // Remove non-users from the fullData array
+ const users = deprecatedJQueryDropdown.filteredFullData();
+ const callback = deprecatedJQueryDropdown.parseData.bind(deprecatedJQueryDropdown);
+
+ // Update the data model
+ this.processData(inputValue, users, callback);
+ }
- if (this.multiSelect) {
- return getMultiSelectDropdownTitle(selected, $(el).hasClass('is-active'));
- }
+ if (this.multiSelect) {
+ return getMultiSelectDropdownTitle(selected, $(el).hasClass('is-active'));
+ }
- if (selected && 'id' in selected && $(el).hasClass('is-active')) {
- $dropdown.find('.dropdown-toggle-text').removeClass('is-default');
- if (selected.text) {
- return selected.text;
+ if (selected && 'id' in selected && $(el).hasClass('is-active')) {
+ $dropdown.find('.dropdown-toggle-text').removeClass('is-default');
+ if (selected.text) {
+ return selected.text;
+ }
+ return selected.name;
}
- return selected.name;
- }
- $dropdown.find('.dropdown-toggle-text').addClass('is-default');
- return defaultLabel;
- },
- defaultLabel,
- hidden() {
- if ($dropdown.hasClass('js-multiselect')) {
- if ($dropdown.hasClass(elsClassName)) {
- if (!$dropdown.closest('.merge-request-form').length) {
- $dropdown.data('deprecatedJQueryDropdown').clearMenu();
- $dropdown.closest('.selectbox').children('input[type="hidden"]').remove();
+ $dropdown.find('.dropdown-toggle-text').addClass('is-default');
+ return defaultLabel;
+ },
+ defaultLabel,
+ hidden() {
+ if ($dropdown.hasClass('js-multiselect')) {
+ if ($dropdown.hasClass(elsClassName)) {
+ if (!$dropdown.closest('.merge-request-form').length) {
+ $dropdown.data('deprecatedJQueryDropdown').clearMenu();
+ $dropdown.closest('.selectbox').children('input[type="hidden"]').remove();
+ }
+ emitSidebarEvent('sidebar.saveReviewers');
+ } else {
+ emitSidebarEvent('sidebar.saveAssignees');
}
- emitSidebarEvent('sidebar.saveReviewers');
- } else {
- emitSidebarEvent('sidebar.saveAssignees');
}
- }
- if (!$dropdown.data('alwaysShowSelectbox')) {
- $selectbox.hide();
+ if (!$dropdown.data('alwaysShowSelectbox')) {
+ $selectbox.hide();
- // Recalculate where .value is because vue might have changed it
- $block = $selectbox.closest('.block');
- $value = $block.find('.value');
- // display:block overrides the hide-collapse rule
- $value.css('display', '');
- }
+ // Recalculate where .value is because vue might have changed it
+ $block = $selectbox.closest('.block');
+ $value = $block.find('.value');
+ // display:block overrides the hide-collapse rule
+ $value.css('display', '');
+ }
- $('.dropdown-input-field', $block).val('');
- },
- multiSelect: $dropdown.hasClass('js-multiselect'),
- inputMeta: $dropdown.data('inputMeta'),
- clicked(options) {
- const { $el, e, isMarking } = options;
- const user = options.selectedObj;
+ $('.dropdown-input-field', $block).val('');
+ },
+ multiSelect: $dropdown.hasClass('js-multiselect'),
+ inputMeta: $dropdown.data('inputMeta'),
+ clicked(options) {
+ const { $el, e, isMarking } = options;
+ const user = options.selectedObj;
- dispose($el);
+ dispose($el);
- if ($dropdown.hasClass('js-multiselect')) {
- const isActive = $el.hasClass('is-active');
- const previouslySelected = $dropdown
- .closest('.selectbox')
- .find(`input[name='${$dropdown.data('fieldName')}'][value!=0]`);
+ if ($dropdown.hasClass('js-multiselect')) {
+ const isActive = $el.hasClass('is-active');
+ const previouslySelected = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${$dropdown.data('fieldName')}'][value!=0]`);
- // Enables support for limiting the number of users selected
- // Automatically removes the first on the list if more users are selected
- checkMaxSelect();
+ // Enables support for limiting the number of users selected
+ // Automatically removes the first on the list if more users are selected
+ checkMaxSelect();
- if (user.beforeDivider && user.name.toLowerCase() === 'unassigned') {
- // Unassigned selected
- previouslySelected.each((index, element) => {
- element.remove();
- });
- if ($dropdown.hasClass(elsClassName)) {
- emitSidebarEvent('sidebar.removeAllReviewers');
+ if (user.beforeDivider && user.name.toLowerCase() === 'unassigned') {
+ // Unassigned selected
+ previouslySelected.each((index, element) => {
+ element.remove();
+ });
+ if ($dropdown.hasClass(elsClassName)) {
+ emitSidebarEvent('sidebar.removeAllReviewers');
+ } else {
+ emitSidebarEvent('sidebar.removeAllAssignees');
+ }
+ } else if (isActive) {
+ // user selected
+ if ($dropdown.hasClass(elsClassName)) {
+ emitSidebarEvent('sidebar.addReviewer', user);
+ } else {
+ emitSidebarEvent('sidebar.addAssignee', user);
+ }
+
+ // Remove unassigned selection (if it was previously selected)
+ const unassignedSelected = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`);
+
+ if (unassignedSelected) {
+ unassignedSelected.remove();
+ }
} else {
- emitSidebarEvent('sidebar.removeAllAssignees');
+ if (previouslySelected.length === 0) {
+ // Select unassigned because there is no more selected users
+ this.addInput($dropdown.data('fieldName'), 0, {});
+ }
+
+ // User unselected
+ if ($dropdown.hasClass(elsClassName)) {
+ emitSidebarEvent('sidebar.removeReviewer', user);
+ } else {
+ emitSidebarEvent('sidebar.removeAssignee', user);
+ }
}
- } else if (isActive) {
- // user selected
- if ($dropdown.hasClass(elsClassName)) {
- emitSidebarEvent('sidebar.addReviewer', user);
+
+ if (getSelected().find((u) => u === gon.current_user_id)) {
+ $assignToMeLink.hide();
} else {
- emitSidebarEvent('sidebar.addAssignee', user);
+ $assignToMeLink.show();
}
+ }
- // Remove unassigned selection (if it was previously selected)
- const unassignedSelected = $dropdown
- .closest('.selectbox')
- .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`);
+ const page = $('body').attr('data-page');
+ const isIssueIndex = page === 'projects:issues:index';
+ const isMRIndex = page === page && page === 'projects:merge_requests:index';
+ if (
+ $dropdown.hasClass('js-filter-bulk-update') ||
+ $dropdown.hasClass('js-issuable-form-dropdown')
+ ) {
+ e.preventDefault();
- if (unassignedSelected) {
- unassignedSelected.remove();
- }
- } else {
- if (previouslySelected.length === 0) {
- // Select unassigned because there is no more selected users
- this.addInput($dropdown.data('fieldName'), 0, {});
- }
+ const isSelecting = user.id !== selectedId;
+ selectedId = isSelecting ? user.id : selectedIdDefault;
- // User unselected
- if ($dropdown.hasClass(elsClassName)) {
- emitSidebarEvent('sidebar.removeReviewer', user);
+ if (selectedId === gon.current_user_id) {
+ $('.assign-to-me-link').hide();
} else {
- emitSidebarEvent('sidebar.removeAssignee', user);
+ $('.assign-to-me-link').show();
}
+ return;
}
-
- if (getSelected().find((u) => u === gon.current_user_id)) {
- $assignToMeLink.hide();
- } else {
- $assignToMeLink.show();
+ if (handleClick) {
+ e.preventDefault();
+ handleClick(user, isMarking);
+ } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ return Issuable.filterResults($dropdown.closest('form'));
+ } else if ($dropdown.hasClass('js-filter-submit')) {
+ return $dropdown.closest('form').submit();
+ } else if (!$dropdown.hasClass('js-multiselect')) {
+ const selected = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${$dropdown.data('fieldName')}']`)
+ .val();
+ return assignTo(selected);
}
- }
-
- const page = $('body').attr('data-page');
- const isIssueIndex = page === 'projects:issues:index';
- const isMRIndex = page === page && page === 'projects:merge_requests:index';
- if (
- $dropdown.hasClass('js-filter-bulk-update') ||
- $dropdown.hasClass('js-issuable-form-dropdown')
- ) {
- e.preventDefault();
- const isSelecting = user.id !== selectedId;
- selectedId = isSelecting ? user.id : selectedIdDefault;
+ // Automatically close dropdown after assignee is selected
+ // since CE has no multiple assignees
+ // EE does not have a max-select
+ if ($dropdown.data('maxSelect') && getSelected().length === $dropdown.data('maxSelect')) {
+ // Close the dropdown
+ $dropdown.dropdown('toggle');
+ }
+ },
+ id(user) {
+ return user.id;
+ },
+ opened(e) {
+ const $el = $(e.currentTarget);
+ const selected = getSelected();
+ $el.find('.is-active').removeClass('is-active');
+
+ function highlightSelected(id) {
+ $el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active');
+ }
- if (selectedId === gon.current_user_id) {
- $('.assign-to-me-link').hide();
+ if (selected.length > 0) {
+ getSelected().forEach((selectedId) => highlightSelected(selectedId));
} else {
- $('.assign-to-me-link').show();
+ highlightSelected(selectedId);
}
- return;
- }
- if (handleClick) {
- e.preventDefault();
- handleClick(user, isMarking);
- } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
- return Issuable.filterResults($dropdown.closest('form'));
- } else if ($dropdown.hasClass('js-filter-submit')) {
- return $dropdown.closest('form').submit();
- } else if (!$dropdown.hasClass('js-multiselect')) {
- const selected = $dropdown
- .closest('.selectbox')
- .find(`input[name='${$dropdown.data('fieldName')}']`)
- .val();
- return assignTo(selected);
- }
+ },
+ updateLabel: $dropdown.data('dropdownTitle'),
+ renderRow(user) {
+ const username = user.username ? `@${user.username}` : '';
+ const avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url;
- // Automatically close dropdown after assignee is selected
- // since CE has no multiple assignees
- // EE does not have a max-select
- if ($dropdown.data('maxSelect') && getSelected().length === $dropdown.data('maxSelect')) {
- // Close the dropdown
- $dropdown.dropdown('toggle');
- }
- },
- id(user) {
- return user.id;
- },
- opened(e) {
- const $el = $(e.currentTarget);
- const selected = getSelected();
- $el.find('.is-active').removeClass('is-active');
-
- function highlightSelected(id) {
- $el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active');
- }
-
- if (selected.length > 0) {
- getSelected().forEach((selectedId) => highlightSelected(selectedId));
- } else {
- highlightSelected(selectedId);
- }
- },
- updateLabel: $dropdown.data('dropdownTitle'),
- renderRow(user) {
- const username = user.username ? `@${user.username}` : '';
- const avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url;
-
- let selected = false;
+ let selected = false;
- if (this.multiSelect) {
- selected = getSelected().find((u) => user.id === u);
+ if (this.multiSelect) {
+ selected = getSelected().find((u) => user.id === u);
- const { fieldName } = this;
- const field = $dropdown
- .closest('.selectbox')
- .find(`input[name='${fieldName}'][value='${user.id}']`);
+ const { fieldName } = this;
+ const field = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${fieldName}'][value='${user.id}']`);
- if (field.length) {
- selected = true;
+ if (field.length) {
+ selected = true;
+ }
+ } else {
+ selected = user.id === selectedId;
}
- } else {
- selected = user.id === selectedId;
- }
- let img = '';
- if (user.beforeDivider != null) {
- `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${escape(
- user.name,
- )}</a></li>`;
- } else {
- // 0 margin, because it's now handled by a wrapper
- img = `<img src='${avatar}' class='avatar avatar-inline gl-m-0!' width='32' />`;
- }
+ let img = '';
+ if (user.beforeDivider != null) {
+ `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${escape(
+ user.name,
+ )}</a></li>`;
+ } else {
+ // 0 margin, because it's now handled by a wrapper
+ img = `<img src='${avatar}' class='avatar avatar-inline gl-m-0!' width='32' />`;
+ }
- return userSelect.renderRow(
- options.issuableType,
- user,
- selected,
- username,
- img,
- elsClassName,
- );
- },
- });
- });
+ return userSelect.renderRow(
+ options.issuableType,
+ user,
+ selected,
+ username,
+ img,
+ elsClassName,
+ );
+ },
+ })
+ .get()
+ .map((dropdown) => dropdown.GitLabDropdownInstance);
+ })
+ .get();
}
// Return users list. Filtered by query
diff --git a/app/assets/javascripts/visibility_level/constants.js b/app/assets/javascripts/visibility_level/constants.js
index e30982985b3..0d9ededc550 100644
--- a/app/assets/javascripts/visibility_level/constants.js
+++ b/app/assets/javascripts/visibility_level/constants.js
@@ -1,4 +1,4 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
export const VISIBILITY_LEVEL_PRIVATE_STRING = 'private';
export const VISIBILITY_LEVEL_INTERNAL_STRING = 'internal';
@@ -45,6 +45,12 @@ export const PROJECT_VISIBILITY_TYPE = {
),
};
+export const ORGANIZATION_VISIBILITY_TYPE = {
+ [VISIBILITY_LEVEL_PUBLIC_STRING]: s__(
+ 'Organization|Public - The organization can be accessed without any authentication.',
+ ),
+};
+
export const VISIBILITY_TYPE_ICON = {
[VISIBILITY_LEVEL_PUBLIC_STRING]: 'earth',
[VISIBILITY_LEVEL_INTERNAL_STRING]: 'shield',
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 952ff9b18e9..c49c1316b1b 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
@@ -4,32 +4,26 @@ import {
GlPopover,
GlSprintf,
GlLink,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
GlTooltipDirective,
} from '@gitlab/ui';
-import { sprintf, __ } from '~/locale';
export default {
+ name: 'ActionButtons',
components: {
GlButton,
GlPopover,
GlSprintf,
GlLink,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
- widget: {
- type: String,
- required: false,
- default: '',
- },
tertiaryButtons: {
type: Array,
+ // fix `spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js` before making this required
required: false,
default: () => [],
},
@@ -41,17 +35,26 @@ export default {
};
},
computed: {
- dropdownLabel() {
- if (!this.widget) return undefined;
-
- return sprintf(__('%{widget} options'), { widget: this.widget });
- },
hasOneOption() {
return this.tertiaryButtons.length === 1;
},
hasMultipleOptions() {
return this.tertiaryButtons.length > 1;
},
+ dropdownItems() {
+ return this.tertiaryButtons.map((item) => {
+ return {
+ ...item,
+ text: item.text,
+ href: item.href,
+ extraAttrs: {
+ dataClipboardText: item.dataClipboardText,
+ dataMethod: item.dataMethod,
+ target: item.target,
+ },
+ };
+ });
+ },
},
methods: {
onClickAction(action) {
@@ -135,32 +138,18 @@ export default {
</span>
</template>
<template v-if="hasMultipleOptions">
- <gl-dropdown
+ <gl-disclosure-dropdown
v-gl-tooltip
+ :items="dropdownItems"
:title="__('Options')"
- :text="dropdownLabel"
icon="ellipsis_v"
no-caret
category="tertiary"
- right
- lazy
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>
+ @action="onClickAction"
+ />
<span v-for="(btn, index) in tertiaryButtons" :key="index">
<gl-button
:id="btn.id"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue b/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue
index 5090081d281..3b62345b969 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue
@@ -70,7 +70,8 @@ export default {
message() {
if (this.state === STATUS_CLOSED) {
return s__('mrWidgetCommitsAdded|The changes were not merged into %{targetBranch}.');
- } else if (this.isMerged) {
+ }
+ if (this.isMerged) {
return s__(
'mrWidgetCommitsAdded|Changes merged into %{targetBranch} with %{mergeCommitSha}%{squashedCommits}.',
);
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 95fa01c23f1..4ed470440cc 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
@@ -131,7 +131,8 @@ export default {
variant: 'confirm',
action: () => this.approve(),
};
- } else if (this.showUnapprove) {
+ }
+ if (this.showUnapprove) {
return {
text: s__('mrWidget|Revoke approval'),
variant: 'default',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
index e79d2db4b5a..7a3dd4ca35e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
@@ -1,9 +1,7 @@
<script>
import { createAlert } from '~/alert';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
-import { visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub';
import MRWidgetService from '../../services/mr_widget_service';
import {
@@ -25,7 +23,6 @@ export default {
DeploymentActionButton,
DeploymentViewButton,
},
- mixins: [glFeatureFlagsMixin()],
props: {
computedDeploymentStatus: {
type: String,
@@ -71,10 +68,7 @@ export default {
return this.deployment.details?.playable_build?.play_path;
},
redeployPath() {
- if (this.redeployMrWidgetFeatureFlagEnabled) {
- return this.deployment.retry_url;
- }
- return this.deployment.details?.playable_build?.retry_path;
+ return this.deployment.retry_url;
},
stopUrl() {
return this.deployment.stop_url;
@@ -82,13 +76,8 @@ export default {
environmentAvailable() {
return Boolean(this.deployment.environment_available);
},
- redeployMrWidgetFeatureFlagEnabled() {
- return this.glFeatures.reviewAppsRedeployMrWidget;
- },
showDeploymentActionButton() {
- return (
- this.redeployPath && !this.environmentAvailable && this.redeployMrWidgetFeatureFlagEnabled
- );
+ return this.redeployPath && !this.environmentAvailable;
},
},
actionsConfiguration: {
@@ -137,16 +126,6 @@ export default {
this.actionInProgress = actionName;
MRWidgetService.executeInlineAction(endpoint)
- .then((resp) => {
- if (this.redeployMrWidgetFeatureFlagEnabled) {
- return;
- }
-
- const redirectUrl = resp?.data?.redirect_url;
- if (redirectUrl) {
- visitUrl(redirectUrl);
- }
- })
.catch(() => {
createAlert({
message: errorMessage,
@@ -184,17 +163,6 @@ export default {
>
<span>{{ $options.actionsConfiguration[constants.DEPLOYING].buttonText }}</span>
</deployment-action-button>
- <deployment-action-button
- v-if="canBeManuallyRedeployed && !redeployMrWidgetFeatureFlagEnabled"
- :action-in-progress="actionInProgress"
- :actions-configuration="$options.actionsConfiguration[constants.REDEPLOYING]"
- :computed-deployment-status="computedDeploymentStatus"
- :icon="$options.btnIcons.repeat"
- container-classes="js-manual-redeploy-action"
- @click="redeploy"
- >
- <span>{{ $options.actionsConfiguration[constants.REDEPLOYING].buttonText }}</span>
- </deployment-action-button>
<deployment-view-button
v-if="hasExternalUrls && environmentAvailable"
:app-button-text="appButtonText"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
index c7d34d45f06..efe71ed569a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
@@ -58,7 +58,8 @@ export default {
return s__(
'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB',
);
- } else if (memoryTo < memoryFrom) {
+ }
+ if (memoryTo < memoryFrom) {
return s__(
'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB',
);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index 31bf62b7e52..3e2f3ab4103 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -305,11 +305,7 @@ export default {
</script>
<template>
- <section
- class="media-section"
- data-testid="widget-extension"
- data-qa-selector="mr_widget_extension"
- >
+ <section class="media-section" data-testid="widget-extension">
<state-container
:status="statusIconName"
:is-loading="isLoadingSummary"
@@ -346,11 +342,7 @@ export default {
</template>
</template>
</div>
- <actions
- :widget="$options.label || $options.name"
- :tertiary-buttons="tertiaryActionsButtons"
- @clickedAction="onClickedAction"
- />
+ <actions :tertiary-buttons="tertiaryActionsButtons" @clickedAction="onClickedAction" />
<div
v-if="isCollapsible"
class="gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6"
@@ -363,7 +355,6 @@ export default {
:icon="isCollapsed ? 'chevron-lg-down' : 'chevron-lg-up'"
category="tertiary"
data-testid="toggle-button"
- data-qa-selector="toggle_button"
size="small"
@click="toggleCollapsed"
/>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
index fa369d23b6c..5f0fd973e84 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
@@ -108,7 +108,6 @@ export default {
</gl-badge>
</div>
<actions
- :widget="widgetLabel"
:tertiary-buttons="data.actions"
class="gl-ml-auto gl-pl-3"
@clickedAction="onClickedAction"
@@ -128,7 +127,6 @@ export default {
:modal-id="modalId"
:level="3"
data-testid="child-content"
- data-qa-selector="child_content"
@clickedAction="onClickedAction"
/>
</li>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
index 4f8f8d6cb58..b6bcc68e5e0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
@@ -8,8 +8,10 @@ import {
function simplifyWidgetName(componentName) {
const noWidget = componentName.replace(/^Widget/, '');
+ const camelName = noWidget.charAt(0).toLowerCase() + noWidget.slice(1);
+ const tierlessName = camelName.replace(/(CE|EE)$/, '');
- return noWidget.charAt(0).toLowerCase() + noWidget.slice(1);
+ return tierlessName;
}
function baseRedisEventName(extensionName) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js
index 757178ee336..83f5c1490e2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js
@@ -65,11 +65,8 @@ const createText = (text) => {
export const generateText = (text) => {
if (typeof text === 'string') {
return createText(escapeText(text));
- } else if (
- typeof text === 'object' &&
- typeof text.text === 'string' &&
- typeof text.href === 'string'
- ) {
+ }
+ if (typeof text === 'object' && typeof text.text === 'string' && typeof text.href === 'string') {
return createText(
`${
text.prependText ? `${escapeText(text.prependText)} ` : ''
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 e94e0fbe6dc..bfcd4610379 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
@@ -11,9 +11,9 @@ import {
import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, n__ } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
-import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
-import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.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';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { MT_MERGE_STRATEGY } from '../constants';
@@ -183,7 +183,7 @@ export default {
v-gl-tooltip
:href="ciTroubleshootingDocsPath"
target="_blank"
- :title="__('About this feature')"
+ :title="__('Get more information about troubleshooting pipelines')"
class="gl-display-flex gl-align-items-center gl-ml-2"
>
<gl-icon
@@ -205,9 +205,7 @@ export default {
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-line-height-32 gl-text-gray-900"
- >
+ <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"
@@ -253,7 +251,7 @@ export default {
v-safe-html="sourceBranchLink"
:title="sourceBranch"
truncate-target="child"
- class="label-branch label-truncate gl-font-weight-normal gl-vertical-align-text-bottom"
+ class="label-branch label-truncate gl-font-weight-normal"
/>
</template>
<template v-if="finishedAt">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
index 400759aa086..4f39bd1d972 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
@@ -38,7 +38,7 @@ export default {
},
modifyLinkMessage() {
if (this.isFastForwardEnabled) return __('Modify commit message');
- else if (this.isSquashEnabled) return __('Modify commit messages');
+ if (this.isSquashEnabled) return __('Modify commit messages');
return __('Modify merge commit');
},
ariaLabel() {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
index 61eec503951..bf2c5e52184 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
@@ -30,9 +30,11 @@ export default {
failedText() {
if (this.mr.approvals && !this.mr.isApproved) {
return this.$options.i18n.approvalNeeded;
- } else if (this.mr.detailedMergeStatus === DETAILED_MERGE_STATUS.BLOCKED_STATUS) {
+ }
+ if (this.mr.detailedMergeStatus === DETAILED_MERGE_STATUS.BLOCKED_STATUS) {
return this.$options.i18n.blockingMergeRequests;
- } else if (this.mr.detailedMergeStatus === DETAILED_MERGE_STATUS.EXTERNAL_STATUS_CHECKS) {
+ }
+ if (this.mr.detailedMergeStatus === DETAILED_MERGE_STATUS.EXTERNAL_STATUS_CHECKS) {
return this.$options.i18n.externalStatusChecksFailed;
}
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 7071759b8bb..0ce8389579d 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
@@ -488,7 +488,7 @@ export default {
mergeAndSquashCommitTemplatesHintText: s__(
'mrWidget|To change these default messages, edit the templates for both the merge and squash commit messages. %{linkStart}Learn more%{linkEnd}.',
),
- sourceDivergedFromTargetText: s__('mrWidget|The source branch is %{link} the target branch'),
+ sourceDivergedFromTargetText: s__('mrWidget|The source branch is %{link} the target branch.'),
divergedCommits: (count) => n__('%d commit behind', '%d commits behind', count),
},
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
index 9bb39ba22e0..8249dffcc27 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
@@ -5,6 +5,7 @@ export default {
import(
'~/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue'
),
+ MrTestReportWidget: () => import('~/vue_merge_request_widget/extensions/test_report/index.vue'),
MrTerraformWidget: () => import('~/vue_merge_request_widget/extensions/terraform/index.vue'),
MrCodeQualityWidget: () =>
import('~/vue_merge_request_widget/extensions/code_quality/index.vue'),
@@ -18,6 +19,10 @@ export default {
},
computed: {
+ testReportWidget() {
+ return this.mr.testResultsPath && 'MrTestReportWidget';
+ },
+
terraformPlansWidget() {
return this.mr.terraformReportsPath && 'MrTerraformWidget';
},
@@ -27,9 +32,12 @@ export default {
},
widgets() {
- return [this.codeQualityWidget, this.terraformPlansWidget, 'MrSecurityWidget'].filter(
- (w) => w,
- );
+ return [
+ this.codeQualityWidget,
+ this.testReportWidget,
+ this.terraformPlansWidget,
+ 'MrSecurityWidget',
+ ].filter((w) => w);
},
},
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
index 618d1e71f81..72c041759d9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
@@ -92,7 +92,6 @@ export default {
</div>
<actions
v-if="hasActionButtons"
- :widget="widgetName"
:tertiary-buttons="data.actions"
class="gl-ml-auto gl-pl-3"
@clickedAction="onClickedAction"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
index 2c8bf90064e..d17be3e4037 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
@@ -368,7 +368,6 @@ export default {
<slot name="action-buttons">
<action-buttons
v-if="actionButtons.length > 0"
- :widget="widgetName"
:tertiary-buttons="actionButtons"
@clickedAction="onActionClick"
/>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue
index e67924d28ab..bb82da7796a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue
@@ -128,11 +128,7 @@ export default {
>
</template>
</help-popover>
- <action-buttons
- v-if="hasActionButtons"
- :widget="widgetName"
- :tertiary-buttons="actionButtons"
- />
+ <action-buttons v-if="hasActionButtons" :tertiary-buttons="actionButtons" />
</div>
</div>
<div class="gl-display-flex gl-align-items-baseline">
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
index 713c9e610b3..3af984dcf6c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
@@ -23,14 +23,17 @@ export default {
const { newErrors, resolvedErrors, parsingInProgress } = data;
if (parsingInProgress) {
return i18n.loading;
- } else if (newErrors.length >= 1 && resolvedErrors.length >= 1) {
+ }
+ if (newErrors.length >= 1 && resolvedErrors.length >= 1) {
return i18n.improvementAndDegradationCopy(
i18n.findings(resolvedErrors, codeQualityPrefixes.fixed),
i18n.findings(newErrors, codeQualityPrefixes.new),
);
- } else if (resolvedErrors.length >= 1) {
+ }
+ if (resolvedErrors.length >= 1) {
return i18n.singularCopy(i18n.findings(resolvedErrors, codeQualityPrefixes.fixed));
- } else if (newErrors.length >= 1) {
+ }
+ if (newErrors.length >= 1) {
return i18n.singularCopy(i18n.findings(newErrors, codeQualityPrefixes.new));
}
return i18n.noChanges;
@@ -38,7 +41,8 @@ export default {
statusIcon() {
if (this.collapsedData.newErrors.length >= 1) {
return EXTENSION_ICONS.warning;
- } else if (this.collapsedData.resolvedErrors.length >= 1) {
+ }
+ if (this.collapsedData.resolvedErrors.length >= 1) {
return EXTENSION_ICONS.success;
}
return EXTENSION_ICONS.neutral;
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue
index d30acf24684..cd3a98effa3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue
@@ -36,9 +36,11 @@ export default {
if (!this.pollingFinished) {
return { title: i18n.loading };
- } else if (this.hasError) {
+ }
+ if (this.hasError) {
return { title: i18n.error };
- } else if (
+ }
+ if (
this.collapsedData?.new_errors?.length >= 1 &&
this.collapsedData?.resolved_errors?.length >= 1
) {
@@ -48,11 +50,13 @@ export default {
i18n.findings(new_errors, codeQualityPrefixes.new),
),
};
- } else if (this.collapsedData?.resolved_errors?.length >= 1) {
+ }
+ if (this.collapsedData?.resolved_errors?.length >= 1) {
return {
title: i18n.singularCopy(i18n.findings(resolved_errors, codeQualityPrefixes.fixed)),
};
- } else if (this.collapsedData?.new_errors?.length >= 1) {
+ }
+ if (this.collapsedData?.new_errors?.length >= 1) {
return { title: i18n.singularCopy(i18n.findings(new_errors, codeQualityPrefixes.new)) };
}
return { title: i18n.noChanges };
@@ -95,7 +99,8 @@ export default {
statusIcon() {
if (this.collapsedData?.new_errors?.length >= 1) {
return EXTENSION_ICONS.warning;
- } else if (this.collapsedData?.resolved_errors?.length >= 1) {
+ }
+ if (this.collapsedData?.resolved_errors?.length >= 1) {
return EXTENSION_ICONS.success;
}
return EXTENSION_ICONS.neutral;
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js
deleted file mode 100644
index 6ac462d4ad5..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js
+++ /dev/null
@@ -1,189 +0,0 @@
-import { uniqueId } from 'lodash';
-import { __ } from '~/locale';
-import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
-import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
-import { EXTENSION_ICONS } from '../../constants';
-import {
- summaryTextBuilder,
- reportTextBuilder,
- reportSubTextBuilder,
- countRecentlyFailedTests,
- recentFailuresTextBuilder,
- formatFilePath,
-} from './utils';
-import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants';
-
-export default {
- name: 'WidgetTestSummary',
- enablePolling: true,
- i18n,
- props: ['testResultsPath', 'headBlobPath', 'pipeline'],
- modalComponent: TestCaseDetails,
- computed: {
- failedTestNames() {
- if (!this.collapsedData?.suites) {
- return '';
- }
-
- const newFailures = this.collapsedData?.suites.flatMap((suite) => [suite.new_failures || []]);
- const fileNames = newFailures.flatMap((newFailure) => {
- return newFailure.map((failure) => {
- return failure.file;
- });
- });
-
- return fileNames.join(' ').trim();
- },
- summary(data) {
- if (data.parsingInProgress) {
- return this.$options.i18n.loading;
- }
- if (data.hasSuiteError) {
- return this.$options.i18n.error;
- }
- return {
- subject: summaryTextBuilder(this.$options.i18n.label, data.summary),
- meta: recentFailuresTextBuilder(data.summary),
- };
- },
- statusIcon(data) {
- if (data.status === TESTS_FAILED_STATUS) {
- return EXTENSION_ICONS.warning;
- }
- if (data.hasSuiteError) {
- return EXTENSION_ICONS.failed;
- }
- return EXTENSION_ICONS.success;
- },
- tertiaryButtons() {
- const actionButtons = [];
-
- if (this.failedTestNames().length > 0) {
- actionButtons.push({
- dataClipboardText: this.failedTestNames(),
- id: uniqueId('copy-to-clipboard'),
- icon: 'copy-to-clipboard',
- testId: 'copy-failed-specs-btn',
- text: this.$options.i18n.copyFailedSpecs,
- tooltipText: this.$options.i18n.copyFailedSpecsTooltip,
- tooltipOnClick: __('Copied'),
- });
- }
-
- actionButtons.push({
- text: this.$options.i18n.fullReport,
- href: `${this.pipeline.path}/test_report`,
- target: '_blank',
- trackFullReportClicked: true,
- testId: 'full-report-link',
- });
-
- return actionButtons;
- },
- },
- methods: {
- fetchCollapsedData() {
- return axios.get(this.testResultsPath).then((response) => {
- const { data = {}, status } = response;
- const { suites = [], summary = {} } = data;
-
- return {
- ...response,
- data: {
- hasSuiteError: suites.some((suite) => suite.status === ERROR_STATUS),
- parsingInProgress: status === HTTP_STATUS_NO_CONTENT,
- ...data,
- summary: {
- recentlyFailed: countRecentlyFailedTests(suites),
- ...summary,
- },
- },
- };
- });
- },
- fetchFullData() {
- return Promise.resolve(this.prepareReports());
- },
- suiteIcon(suite) {
- if (suite.status === ERROR_STATUS) {
- return EXTENSION_ICONS.error;
- }
- if (suite.status === TESTS_FAILED_STATUS) {
- return EXTENSION_ICONS.failed;
- }
- return EXTENSION_ICONS.success;
- },
- testHeader(test, sectionHeader, index) {
- const headers = [];
- if (index === 0) {
- headers.push(sectionHeader);
- }
- if (test.recent_failures?.count && test.recent_failures?.base_branch) {
- headers.push(i18n.recentFailureCount(test.recent_failures));
- }
- return headers;
- },
- mapTestAsChild({ iconName, sectionHeader }) {
- return (test, index) => {
- return {
- id: uniqueId('test-'),
- header: this.testHeader(test, sectionHeader, index),
- modal: {
- text: test.name,
- onClick: () => {
- this.modalData = {
- testCase: {
- filePath: test.file && `${this.headBlobPath}/${formatFilePath(test.file)}`,
- ...test,
- },
- };
- },
- },
- icon: { name: iconName },
- };
- };
- },
- prepareReports() {
- return this.collapsedData.suites
- .map((suite) => {
- return {
- ...suite,
- summary: {
- recentlyFailed: countRecentlyFailedTests(suite),
- ...suite.summary,
- },
- };
- })
- .map((suite) => {
- return {
- id: uniqueId('suite-'),
- text: reportTextBuilder(suite),
- subtext: reportSubTextBuilder(suite),
- icon: {
- name: this.suiteIcon(suite),
- },
- children: [
- ...[...suite.new_failures, ...suite.new_errors].map(
- this.mapTestAsChild({
- sectionHeader: i18n.newHeader,
- iconName: EXTENSION_ICONS.failed,
- }),
- ),
- ...[...suite.existing_failures, ...suite.existing_errors].map(
- this.mapTestAsChild({
- iconName: EXTENSION_ICONS.failed,
- }),
- ),
- ...[...suite.resolved_failures, ...suite.resolved_errors].map(
- this.mapTestAsChild({
- sectionHeader: i18n.fixedHeader,
- iconName: EXTENSION_ICONS.success,
- }),
- ),
- ],
- };
- });
- },
- },
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue
new file mode 100644
index 00000000000..1b03b9c04e1
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue
@@ -0,0 +1,313 @@
+<script>
+import { uniqueId, uniq } from 'lodash';
+import { __ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
+import TestCaseDetails from '~/ci/pipeline_details/test_reports/test_case_details.vue';
+import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue';
+import MrWidgetRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
+import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
+import { EXTENSION_ICONS } from '../../constants';
+import {
+ summaryTextBuilder,
+ reportTextBuilder,
+ reportSubTextBuilder,
+ countRecentlyFailedTests,
+ recentFailuresTextBuilder,
+ formatFilePath,
+} from './utils';
+import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants';
+
+export default {
+ name: 'WidgetTestReport',
+ components: {
+ MrWidget,
+ MrWidgetRow,
+ DynamicScroller,
+ DynamicScrollerItem,
+ TestCaseDetails,
+ },
+ i18n,
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ collapsedData: {},
+ suites: [],
+ modalData: null,
+ };
+ },
+ computed: {
+ failedTestNames() {
+ const { data: { suites = [] } = {} } = this.collapsedData;
+
+ if (!this.hasSuites) {
+ return '';
+ }
+
+ const newFailures = suites.flatMap((suite) => [suite.new_failures || []]);
+ const fileNames = newFailures.flatMap((newFailure) => {
+ return newFailure.map((failure) => {
+ return failure.file;
+ });
+ });
+
+ return uniq(fileNames).join(' ').trim();
+ },
+ summary() {
+ const {
+ data: { parsingInProgress = false, hasSuiteError = false, summary = {} } = {},
+ } = this.collapsedData;
+
+ if (parsingInProgress) {
+ return { title: this.$options.i18n.loading };
+ }
+ if (hasSuiteError) {
+ return { title: this.$options.i18n.error };
+ }
+ return {
+ title: summaryTextBuilder(this.$options.i18n.label, summary),
+ subtitle: recentFailuresTextBuilder(summary),
+ };
+ },
+ statusIcon() {
+ const { data: { status = null, hasSuiteError = false } = {} } = this.collapsedData;
+
+ if (status === TESTS_FAILED_STATUS) {
+ return EXTENSION_ICONS.warning;
+ }
+ if (hasSuiteError) {
+ return EXTENSION_ICONS.failed;
+ }
+ return EXTENSION_ICONS.success;
+ },
+ tertiaryButtons() {
+ const actionButtons = [];
+
+ if (this.failedTestNames.length > 0) {
+ actionButtons.push({
+ dataClipboardText: this.failedTestNames,
+ id: uniqueId('copy-to-clipboard'),
+ icon: 'copy-to-clipboard',
+ testId: 'copy-failed-specs-btn',
+ text: this.$options.i18n.copyFailedSpecs,
+ tooltipText: this.$options.i18n.copyFailedSpecsTooltip,
+ tooltipOnClick: __('Copied'),
+ });
+ }
+
+ actionButtons.push({
+ text: this.$options.i18n.fullReport,
+ href: `${this.mr.pipeline.path}/test_report`,
+ target: '_blank',
+ trackFullReportClicked: true,
+ testId: 'full-report-link',
+ });
+
+ return actionButtons;
+ },
+ testResultsPath() {
+ return this.mr.testResultsPath;
+ },
+ hasSuites() {
+ return this.suites.length > 0;
+ },
+ },
+ methods: {
+ fetchCollapsedData() {
+ return axios.get(this.testResultsPath).then((response) => {
+ const { data = {}, status } = response;
+ const { suites = [], summary = {} } = data;
+
+ this.collapsedData = {
+ ...response,
+ data: {
+ hasSuiteError: suites.some((suite) => suite.status === ERROR_STATUS),
+ parsingInProgress: status === HTTP_STATUS_NO_CONTENT,
+ ...data,
+ summary: {
+ recentlyFailed: countRecentlyFailedTests(suites),
+ ...summary,
+ },
+ },
+ };
+ this.suites = this.prepareSuites(this.collapsedData);
+
+ return response;
+ });
+ },
+ suiteIcon(suite) {
+ if (suite.status === ERROR_STATUS) {
+ return EXTENSION_ICONS.error;
+ }
+ if (suite.status === TESTS_FAILED_STATUS) {
+ return EXTENSION_ICONS.failed;
+ }
+ return EXTENSION_ICONS.success;
+ },
+ testHeader(test, sectionHeader, index) {
+ const headers = [];
+ if (index === 0) {
+ headers.push(sectionHeader);
+ }
+ if (test.recent_failures?.count && test.recent_failures?.base_branch) {
+ headers.push(i18n.recentFailureCount(test.recent_failures));
+ }
+ return headers;
+ },
+ mapTestAsChild({ iconName, sectionHeader }) {
+ return (test, index) => {
+ return {
+ id: uniqueId('test-'),
+ header: this.testHeader(test, sectionHeader, index),
+ text: test.name,
+ actions: [
+ {
+ text: __('View details'),
+ onClick: () => {
+ this.modalData = {
+ testCase: {
+ filePath: test.file && `${this.mr.headBlobPath}/${formatFilePath(test.file)}`,
+ ...test,
+ },
+ };
+ },
+ },
+ ],
+ icon: { name: iconName },
+ };
+ };
+ },
+ onModalHidden() {
+ this.modalData = null;
+ },
+ prepareSuites(collapsedData) {
+ const {
+ data: { suites = [] },
+ } = collapsedData;
+
+ return suites
+ .map((suite) => {
+ return {
+ ...suite,
+ summary: {
+ recentlyFailed: countRecentlyFailedTests(suite),
+ ...suite.summary,
+ },
+ };
+ })
+ .map((suite) => {
+ return {
+ id: uniqueId('suite-'),
+ text: reportTextBuilder(suite),
+ subtext: reportSubTextBuilder(suite),
+ icon: {
+ name: this.suiteIcon(suite),
+ },
+ children: [
+ ...[...suite.new_failures, ...suite.new_errors].map(
+ this.mapTestAsChild({
+ sectionHeader: i18n.newHeader,
+ iconName: EXTENSION_ICONS.failed,
+ }),
+ ),
+ ...[...suite.existing_failures, ...suite.existing_errors].map(
+ this.mapTestAsChild({
+ iconName: EXTENSION_ICONS.failed,
+ }),
+ ),
+ ...[...suite.resolved_failures, ...suite.resolved_errors].map(
+ this.mapTestAsChild({
+ sectionHeader: i18n.fixedHeader,
+ iconName: EXTENSION_ICONS.success,
+ }),
+ ),
+ ],
+ };
+ });
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <mr-widget
+ :error-text="$options.i18n.error"
+ :status-icon-name="statusIcon"
+ :loading-text="$options.i18n.loading"
+ :action-buttons="tertiaryButtons"
+ :help-popover="$options.helpPopover"
+ :widget-name="$options.name"
+ :summary="summary"
+ :fetch-collapsed-data="fetchCollapsedData"
+ :is-collapsible="hasSuites"
+ >
+ <template #content>
+ <mr-widget-row
+ v-for="suite in suites"
+ :key="suite.id"
+ :level="2"
+ :status-icon-name="suite.icon.name"
+ :widget-name="$options.name"
+ data-testid="extension-list-item"
+ >
+ <template #header>
+ <div class="gl-flex-direction-column">
+ <div>{{ suite.text }}</div>
+ <div
+ v-for="(subtext, i) in suite.subtext"
+ :key="`${suite.id}-subtext-${i}`"
+ class="gl-font-sm gl-text-gray-700"
+ >
+ {{ subtext }}
+ </div>
+ </div>
+ </template>
+ <template #body>
+ <div v-if="suite.children.length > 0" class="gl-mt-2 gl-w-full">
+ <dynamic-scroller
+ :items="suite.children"
+ :min-item-size="32"
+ :style="{ maxHeight: '170px' }"
+ key-field="id"
+ class="gl-pr-5"
+ >
+ <template #default="{ item, active }">
+ <dynamic-scroller-item :item="item" :active="active">
+ <strong
+ v-for="(headerText, i) in item.header"
+ :key="`${item.id}-headerText-${i}`"
+ class="gl-display-block gl-mt-2"
+ >
+ {{ headerText }}
+ </strong>
+ <mr-widget-row
+ :key="item.id"
+ :level="3"
+ :widget-name="$options.name"
+ :status-icon-name="item.icon.name"
+ :action-buttons="item.actions"
+ class="gl-mt-2"
+ >
+ <template #header>{{ item.text }}</template>
+ </mr-widget-row>
+ </dynamic-scroller-item>
+ </template>
+ </dynamic-scroller>
+ </div>
+ </template>
+ </mr-widget-row>
+ </template>
+ </mr-widget>
+ <test-case-details
+ :modal-id="`modal${$options.name}`"
+ :visible="modalData !== null"
+ v-bind="modalData"
+ @hidden="onModalHidden"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
index 37f9964d23a..24f6b3e69ff 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
@@ -62,7 +62,7 @@ export const reportSubTextBuilder = ({ suite_errors: suiteErrors, summary }) =>
}
return errors;
}
- return recentFailuresTextBuilder(summary);
+ return [recentFailuresTextBuilder(summary)];
};
export const countRecentlyFailedTests = (subject) => {
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 acdcbf7afd7..175a0b0563f 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
@@ -56,7 +56,6 @@ import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variab
import getStateQuery from './queries/get_state.query.graphql';
import getStateSubscription from './queries/get_state.subscription.graphql';
import accessibilityExtension from './extensions/accessibility';
-import testReportExtension from './extensions/test_report';
import ReportWidgetContainer from './components/report_widget_container.vue';
import MrWidgetReadyToMerge from './components/states/new_ready_to_merge.vue';
@@ -225,9 +224,6 @@ export default {
this.mr.mergePipelinesEnabled && this.mr.sourceProjectId !== this.mr.targetProjectId,
);
},
- shouldRenderTestReport() {
- return Boolean(this.mr?.testResultsPath);
- },
mergeError() {
let { mergeError } = this.mr;
@@ -281,11 +277,6 @@ export default {
this.registerAccessibilityExtension();
}
},
- shouldRenderTestReport(newVal) {
- if (newVal) {
- this.registerTestReportExtension();
- }
- },
},
mounted() {
MRWidgetService.fetchInitialData()
@@ -525,11 +516,6 @@ export default {
registerExtension(accessibilityExtension);
}
},
- registerTestReportExtension() {
- if (this.shouldRenderTestReport) {
- registerExtension(testReportExtension);
- }
- },
},
};
</script>
@@ -569,7 +555,7 @@ export default {
v-if="hasMergeError"
type="danger"
dismissible
- data-testid="merge_error"
+ data-testid="merge-error"
>
<span v-safe-html="mergeError"></span>
</mr-widget-alert-message>
@@ -577,6 +563,7 @@ export default {
v-if="showMergePipelineForkWarning"
type="warning"
:help-path="mr.mergeRequestPipelinesHelpPath"
+ data-testid="merge-pipeline-fork-warning"
>
{{
s__(
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index f90056a8e1a..d6bab074f3f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -4,31 +4,44 @@ import { stateKey } from './state_maps';
export default function deviseState() {
if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.PREPARING) {
return stateKey.preparing;
- } else if (!this.commitsCount) {
+ }
+ if (!this.commitsCount) {
return stateKey.nothingToMerge;
- } else if (this.projectArchived) {
+ }
+ if (this.projectArchived) {
return stateKey.archived;
- } else if (this.branchMissing) {
+ }
+ if (this.branchMissing) {
return stateKey.missingBranch;
- } else if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.CHECKING) {
+ }
+ if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.CHECKING) {
return stateKey.checking;
- } else if (this.hasConflicts) {
+ }
+ if (this.hasConflicts) {
return stateKey.conflicts;
- } else if (this.shouldBeRebased) {
+ }
+ if (this.shouldBeRebased) {
return stateKey.rebase;
- } else if (this.hasMergeChecksFailed && !this.autoMergeEnabled) {
+ }
+ if (this.hasMergeChecksFailed && !this.autoMergeEnabled) {
return stateKey.mergeChecksFailed;
- } else if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.CI_MUST_PASS) {
+ }
+ if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.CI_MUST_PASS) {
return stateKey.pipelineFailed;
- } else if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.DRAFT_STATUS) {
+ }
+ if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.DRAFT_STATUS) {
return stateKey.draft;
- } else if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.DISCUSSIONS_NOT_RESOLVED) {
+ }
+ if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.DISCUSSIONS_NOT_RESOLVED) {
return stateKey.unresolvedDiscussions;
- } else if (this.canMerge && this.isSHAMismatch) {
+ }
+ if (this.canMerge && this.isSHAMismatch) {
return stateKey.shaMismatch;
- } else if (this.autoMergeEnabled && !this.mergeError) {
+ }
+ if (this.autoMergeEnabled && !this.mergeError) {
return stateKey.autoMergeEnabled;
- } else if (
+ }
+ if (
this.detailedMergeStatus === DETAILED_MERGE_STATUS.MERGEABLE ||
this.detailedMergeStatus === DETAILED_MERGE_STATUS.CI_STILL_RUNNING
) {
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 b1c069d9b1e..bb74f82145f 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
@@ -1,8 +1,8 @@
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
-import { badgeState } from '~/issuable/components/status_box.vue';
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 {
MTWPS_MERGE_STRATEGY,
MT_MERGE_STRATEGY,
@@ -351,11 +351,14 @@ export default class MergeRequestStore {
if (availableAutoMergeStrategies.includes(MTWPS_MERGE_STRATEGY)) {
return MTWPS_MERGE_STRATEGY;
- } else if (availableAutoMergeStrategies.includes(MT_MERGE_STRATEGY)) {
+ }
+ if (availableAutoMergeStrategies.includes(MT_MERGE_STRATEGY)) {
return MT_MERGE_STRATEGY;
- } else if (availableAutoMergeStrategies.includes(MWCP_MERGE_STRATEGY)) {
+ }
+ if (availableAutoMergeStrategies.includes(MWCP_MERGE_STRATEGY)) {
return MWCP_MERGE_STRATEGY;
- } else if (availableAutoMergeStrategies.includes(MWPS_MERGE_STRATEGY)) {
+ }
+ if (availableAutoMergeStrategies.includes(MWPS_MERGE_STRATEGY)) {
return MWPS_MERGE_STRATEGY;
}
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js b/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js
index 106dd7a3b97..957e642fcb8 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js
@@ -1 +1,5 @@
export const HIGHLIGHT_CLASS_NAME = 'hll';
+export const MARKUP_FILE_TYPE = 'markup';
+export const MARKUP_CONTENT_SELECTOR = '.js-markup-content';
+export const ELEMENTS_PER_CHUNK = 20;
+export const CONTENT_LOADED_EVENT = 'richContentLoaded';
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 11ce6afbb1d..27bdcc69120 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
@@ -3,7 +3,14 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { handleBlobRichViewer } from '~/blob/viewer';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
import { handleLocationHash } from '~/lib/utils/common_utils';
+import { sanitize } from '~/lib/dompurify';
import ViewerMixin from './mixins';
+import {
+ MARKUP_FILE_TYPE,
+ MARKUP_CONTENT_SELECTOR,
+ ELEMENTS_PER_CHUNK,
+ CONTENT_LOADED_EVENT,
+} from './constants';
export default {
components: {
@@ -16,21 +23,77 @@ export default {
data() {
return {
isLoading: true,
+ initialContent: null,
+ remainingContent: [],
};
},
+ computed: {
+ rawContent() {
+ return this.initialContent || this.richViewer || this.content;
+ },
+ isMarkup() {
+ return this.type === MARKUP_FILE_TYPE;
+ },
+ },
+ created() {
+ this.optimizeMarkupRendering();
+ },
mounted() {
- window.requestIdleCallback(async () => {
+ this.renderRemainingMarkup();
+ handleBlobRichViewer(this.$refs.content, this.type);
+ handleLocationHash();
+ },
+ methods: {
+ optimizeMarkupRendering() {
+ /**
+ * If content is markup we optimize rendering by splitting it into two parts:
+ * - initialContent (top section of the file - is rendered right away)
+ * - remainingContent (remaining content - is rendered over a longer time period)
+ *
+ * This is done so that the browser doesn't render the whole file at once (improves TBT)
+ */
+
+ if (!this.isMarkup) return;
+
+ const tmpWrapper = document.createElement('div');
+ tmpWrapper.innerHTML = sanitize(this.rawContent, this.$options.safeHtmlConfig);
+
+ const fileContent = tmpWrapper.querySelector(MARKUP_CONTENT_SELECTOR);
+ if (!fileContent) return;
+
+ const initialContent = [...fileContent.childNodes].slice(0, ELEMENTS_PER_CHUNK);
+ this.remainingContent = [...fileContent.childNodes].slice(ELEMENTS_PER_CHUNK);
+
+ fileContent.innerHTML = '';
+ fileContent.append(...initialContent);
+ this.initialContent = tmpWrapper.outerHTML;
+ },
+ renderRemainingMarkup() {
/**
- * Rendering Markdown usually takes long due to the amount of HTML being parsed.
- * This ensures that content is loaded only when the browser goes into idle.
+ * Rendering large Markdown files can block the main thread due to the amount of HTML being parsed.
+ * The optimization below ensures that content is rendered over a longer time period instead of all at once.
* More details here: https://gitlab.com/gitlab-org/gitlab/-/issues/331448
* */
- this.isLoading = false;
- await this.$nextTick();
- handleBlobRichViewer(this.$refs.content, this.type);
- handleLocationHash();
- this.$emit('richContentLoaded');
- });
+
+ if (!this.isMarkup || !this.remainingContent.length) {
+ this.$emit(CONTENT_LOADED_EVENT);
+ this.isLoading = false;
+ return;
+ }
+
+ const fileContent = this.$refs.content.$el.querySelector(MARKUP_CONTENT_SELECTOR);
+
+ for (let i = 0; i < this.remainingContent.length; i += ELEMENTS_PER_CHUNK) {
+ const nextChunkEnd = i + ELEMENTS_PER_CHUNK;
+ const content = this.remainingContent.slice(i, nextChunkEnd);
+ setTimeout(() => {
+ fileContent.append(...content);
+ if (nextChunkEnd < this.remainingContent.length) return;
+ this.$emit(CONTENT_LOADED_EVENT);
+ this.isLoading = false;
+ }, i);
+ }
+ },
},
safeHtmlConfig: {
ADD_TAGS: ['gl-emoji', 'copy-code'],
@@ -39,8 +102,8 @@ export default {
</script>
<template>
<markdown-field-view
- v-if="!isLoading"
ref="content"
- v-safe-html:[$options.safeHtmlConfig]="richViewer || content"
+ v-safe-html:[$options.safeHtmlConfig]="rawContent"
+ :is-loading="isLoading"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
index 14e99977a85..2a47e96b2e2 100644
--- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
@@ -50,11 +50,14 @@ export default {
tooltipTitle() {
if (!this.showTooltip) {
return undefined;
- } else if (this.file.deleted) {
+ }
+ if (this.file.deleted) {
return __('Deleted');
- } else if (this.file.tempFile) {
+ }
+ if (this.file.tempFile) {
return __('Added');
- } else if (this.file.changed) {
+ }
+ if (this.file.changed) {
return __('Modified');
}
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 9aa7a7d6c49..1f45b4c5c9d 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -1,6 +1,7 @@
<script>
import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
import CiIcon from './ci_icon.vue';
+
/**
* Renders CI Badge link with CI icon and status text based on
* API response shared between all places where it is used.
@@ -48,7 +49,7 @@ export default {
required: false,
default: true,
},
- badgeSize: {
+ size: {
type: String,
required: false,
default: badgeSizeOptions.md,
@@ -59,7 +60,7 @@ export default {
},
computed: {
isSmallBadgeSize() {
- return this.badgeSize === badgeSizeOptions.sm;
+ return this.size === badgeSizeOptions.sm;
},
title() {
return !this.showText ? this.status?.text : '';
@@ -120,13 +121,12 @@ export default {
<template>
<gl-badge
v-gl-tooltip
- :class="{ 'gl-pl-0!': isSmallBadgeSize }"
+ :class="{ 'gl-pl-2': isSmallBadgeSize }"
:title="title"
:href="detailsPath"
- :size="badgeSize"
+ :size="size"
:variant="badgeStyles.variant"
- :data-testid="`ci-badge-${status.text}`"
- data-qa-selector="status_badge_link"
+ data-testid="ci-badge-link"
@click="$emit('ciStatusBadgeClick')"
>
<ci-icon :status="status" />
diff --git a/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue b/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue
index 31c98d1e3a7..025e38a55ad 100644
--- a/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue
+++ b/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue
@@ -1,10 +1,11 @@
<script>
-import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
+import { GlBadge, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { confidentialityInfoText } from '../constants';
export default {
components: {
GlBadge,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -18,22 +19,31 @@ export default {
type: String,
required: true,
},
+ hideTextInSmallScreens: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
confidentialTooltip() {
return confidentialityInfoText(this.workspaceType, this.issuableType);
},
+ confidentialTextClass() {
+ return {
+ 'gl-display-none gl-sm-display-block': this.hideTextInSmallScreens,
+ 'gl-ml-2': true,
+ };
+ },
},
};
</script>
<template>
- <gl-badge
- v-gl-tooltip.bottom
- :title="confidentialTooltip"
- icon="eye-slash"
- variant="warning"
- class="gl-display-inline gl-mr-3"
- >{{ __('Confidential') }}</gl-badge
- >
+ <gl-badge v-gl-tooltip :title="confidentialTooltip" variant="warning">
+ <gl-icon name="eye-slash" :size="16" />
+ <span data-testid="confidential-badge-text" :class="confidentialTextClass">{{
+ __('Confidential')
+ }}</span>
+ </gl-badge>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
index 65a601ed927..a1ef1f30ebb 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
@@ -38,7 +38,16 @@ export default {
default: CONFIRM_DANGER_MODAL_CANCEL,
},
},
+ model: {
+ prop: 'visible',
+ event: 'change',
+ },
props: {
+ visible: {
+ type: Boolean,
+ required: false,
+ default: null,
+ },
modalId: {
type: String,
required: true,
@@ -89,12 +98,15 @@ export default {
<template>
<gl-modal
ref="modal"
+ :visible="visible"
:modal-id="modalId"
:data-testid="modalId"
:title="$options.i18n.CONFIRM_DANGER_MODAL_TITLE"
:action-primary="actionPrimary"
:action-cancel="actionCancel"
+ size="sm"
@primary="$emit('confirm')"
+ @change="$emit('change', $event)"
>
<gl-alert
v-if="confirmDangerMessage"
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
deleted file mode 100644
index d8a2789a419..00000000000
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
+++ /dev/null
@@ -1,283 +0,0 @@
-<script>
-import { GlIcon, GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui';
-import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range';
-import { __, sprintf } from '~/locale';
-
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-import DateTimePickerInput from './date_time_picker_input.vue';
-import {
- defaultTimeRanges,
- defaultTimeRange,
- isValidInputString,
- inputStringToIsoDate,
- isoDateToInputString,
-} from './date_time_picker_lib';
-
-const events = {
- input: 'input',
- invalid: 'invalid',
-};
-
-export default {
- components: {
- GlIcon,
- GlButton,
- GlDropdown,
- GlDropdownItem,
- GlFormGroup,
- TooltipOnTruncate,
- DateTimePickerInput,
- },
- props: {
- value: {
- type: Object,
- required: false,
- default: () => defaultTimeRange,
- },
- options: {
- type: Array,
- required: false,
- default: () => defaultTimeRanges,
- },
- customEnabled: {
- type: Boolean,
- required: false,
- default: true,
- },
- utc: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- timeRange: this.value,
-
- /**
- * Valid start iso date string, null if not valid value
- */
- startDate: null,
- /**
- * Invalid start date string as input by the user
- */
- startFallbackVal: '',
-
- /**
- * Valid end iso date string, null if not valid value
- */
- endDate: null,
- /**
- * Invalid end date string as input by the user
- */
- endFallbackVal: '',
- };
- },
- computed: {
- startInputValid() {
- return isValidInputString(this.startDate);
- },
- endInputValid() {
- return isValidInputString(this.endDate);
- },
- isValid() {
- return this.startInputValid && this.endInputValid;
- },
-
- startInput: {
- get() {
- return this.dateToInput(this.startDate) || this.startFallbackVal;
- },
- set(val) {
- try {
- this.startDate = this.inputToDate(val);
- this.startFallbackVal = null;
- } catch (e) {
- this.startDate = null;
- this.startFallbackVal = val;
- }
- this.timeRange = null;
- },
- },
- endInput: {
- get() {
- return this.dateToInput(this.endDate) || this.endFallbackVal;
- },
- set(val) {
- try {
- this.endDate = this.inputToDate(val);
- this.endFallbackVal = null;
- } catch (e) {
- this.endDate = null;
- this.endFallbackVal = val;
- }
- this.timeRange = null;
- },
- },
-
- timeWindowText() {
- try {
- const timeRange = findTimeRange(this.value, this.options);
- if (timeRange) {
- return timeRange.label;
- }
-
- const { start, end } = convertToFixedRange(this.value);
- if (isValidInputString(start) && isValidInputString(end)) {
- return sprintf(__('%{start} to %{end}'), {
- start: this.stripZerosInDateTime(this.dateToInput(start)),
- end: this.stripZerosInDateTime(this.dateToInput(end)),
- });
- }
- } catch {
- return __('Invalid date range');
- }
- return '';
- },
-
- customLabel() {
- if (this.utc) {
- return __('Custom range (UTC)');
- }
- return __('Custom range');
- },
- },
- watch: {
- value(newValue) {
- const { start, end } = convertToFixedRange(newValue);
- this.timeRange = this.value;
- this.startDate = start;
- this.endDate = end;
- },
- },
- mounted() {
- try {
- const { start, end } = convertToFixedRange(this.timeRange);
- this.startDate = start;
- this.endDate = end;
- } catch {
- // when dates cannot be parsed, emit error.
- this.$emit(events.invalid);
- }
-
- // Validate on mounted, and trigger an update if needed
- if (!this.isValid) {
- this.$emit(events.invalid);
- }
- },
- methods: {
- dateToInput(date) {
- if (date === null) {
- return null;
- }
- return isoDateToInputString(date, this.utc);
- },
- inputToDate(value) {
- return inputStringToIsoDate(value, this.utc);
- },
- stripZerosInDateTime(str = '') {
- return str.replace(' 00:00:00', '');
- },
- closeDropdown() {
- this.$refs.dropdown.hide();
- },
- isOptionActive(option) {
- return isEqualTimeRanges(option, this.timeRange);
- },
- setQuickRange(option) {
- this.timeRange = option;
- this.$emit(events.input, this.timeRange);
- },
- setFixedRange() {
- this.timeRange = convertToFixedRange({
- start: this.startDate,
- end: this.endDate,
- });
- this.$emit(events.input, this.timeRange);
- },
- },
-};
-</script>
-<template>
- <tooltip-on-truncate
- :title="timeWindowText"
- :truncate-target="(elem) => elem.querySelector('.gl-dropdown-toggle-text')"
- placement="top"
- class="d-inline-block"
- >
- <gl-dropdown
- ref="dropdown"
- :text="timeWindowText"
- v-bind="$attrs"
- class="date-time-picker w-100"
- menu-class="date-time-picker-menu"
- toggle-class="date-time-picker-toggle text-truncate"
- >
- <template #button-content>
- <span class="gl-flex-grow-1 text-truncate">{{ timeWindowText }}</span>
- <span v-if="utc" class="gl-text-gray-500 gl-font-weight-bold gl-font-sm">{{
- __('UTC')
- }}</span>
- <gl-icon class="gl-dropdown-caret" name="chevron-down" />
- </template>
-
- <div class="d-flex justify-content-between gl-p-2">
- <gl-form-group
- v-if="customEnabled"
- :label="customLabel"
- label-for="custom-from-time"
- label-class="gl-pb-2"
- class="custom-time-range-form-group col-md-7 gl-pl-2 gl-pr-0 m-0"
- >
- <div class="gl-pt-3">
- <date-time-picker-input
- id="custom-time-from"
- v-model="startInput"
- :label="__('From')"
- :state="startInputValid"
- />
- <date-time-picker-input
- id="custom-time-to"
- v-model="endInput"
- :label="__('To')"
- :state="endInputValid"
- />
- </div>
- <gl-form-group>
- <gl-button data-testid="cancelButton" @click="closeDropdown">{{
- __('Cancel')
- }}</gl-button>
- <gl-button
- variant="confirm"
- category="primary"
- :disabled="!isValid"
- @click="setFixedRange()"
- >
- {{ __('Apply') }}
- </gl-button>
- </gl-form-group>
- </gl-form-group>
- <gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-px-2 m-0">
- <template #label>
- <span class="gl-pl-7">{{ __('Quick range') }}</span>
- </template>
-
- <gl-dropdown-item
- v-for="(option, index) in options"
- :key="index"
- :active="isOptionActive(option)"
- active-class="active"
- @click="setQuickRange(option)"
- >
- <gl-icon
- name="mobile-issue-close"
- class="align-bottom"
- :class="{ invisible: !isOptionActive(option) }"
- />
- {{ option.label }}
- </gl-dropdown-item>
- </gl-form-group>
- </div>
- </gl-dropdown>
- </tooltip-on-truncate>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue
deleted file mode 100644
index 190d4e1f104..00000000000
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue
+++ /dev/null
@@ -1,77 +0,0 @@
-<script>
-import { GlFormGroup, GlFormInput } from '@gitlab/ui';
-import { uniqueId } from 'lodash';
-import { __, sprintf } from '~/locale';
-import { dateFormats } from './date_time_picker_lib';
-
-const inputGroupText = {
- invalidFeedback: sprintf(__('Format: %{dateFormat}'), {
- dateFormat: dateFormats.inputFormat,
- }),
- placeholder: dateFormats.inputFormat,
-};
-
-export default {
- components: {
- GlFormGroup,
- GlFormInput,
- },
- props: {
- state: {
- default: null,
- required: true,
- validator: (prop) => typeof prop === 'boolean' || prop === null,
- },
- value: {
- default: null,
- required: false,
- validator: (prop) => typeof prop === 'string' || prop === null,
- },
- label: {
- type: String,
- default: '',
- required: true,
- },
- id: {
- type: String,
- required: false,
- default: () => uniqueId('dateTimePicker_'),
- },
- },
- data() {
- return {
- inputGroupText,
- };
- },
- computed: {
- invalidFeedback() {
- return this.state ? '' : this.inputGroupText.invalidFeedback;
- },
- inputState() {
- // When the state is valid we want to show no
- // green outline. Hence passing null and not true.
- if (this.state === true) {
- return null;
- }
- return this.state;
- },
- },
- methods: {
- onInputBlur(e) {
- this.$emit('input', e.target.value.trim() || null);
- },
- },
-};
-</script>
-
-<template>
- <gl-form-group :label="label" label-size="sm" :label-for="id" :invalid-feedback="invalidFeedback">
- <gl-form-input
- :id="id"
- :value="value"
- :state="inputState"
- :placeholder="inputGroupText.placeholder"
- @blur="onInputBlur"
- />
- </gl-form-group>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js
deleted file mode 100644
index 38b1a587b34..00000000000
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import dateformat from '~/lib/dateformat';
-import { __ } from '~/locale';
-
-/**
- * Default time ranges for the date picker.
- * @see app/assets/javascripts/lib/utils/datetime_range.js
- */
-export const defaultTimeRanges = [
- {
- duration: { seconds: 60 * 30 },
- label: __('30 minutes'),
- },
- {
- duration: { seconds: 60 * 60 * 3 },
- label: __('3 hours'),
- },
- {
- duration: { seconds: 60 * 60 * 8 },
- label: __('8 hours'),
- default: true,
- },
- {
- duration: { seconds: 60 * 60 * 24 * 1 },
- label: __('1 day'),
- },
-];
-
-export const defaultTimeRange = defaultTimeRanges.find((tr) => tr.default);
-
-export const dateFormats = {
- /**
- * Format used by users to input dates
- *
- * Note: Should be a format that can be parsed by Date.parse.
- */
- inputFormat: 'yyyy-mm-dd HH:MM:ss',
- /**
- * Format used to strip timezone from inputs
- */
- stripTimezoneFormat: "yyyy-mm-dd'T'HH:MM:ss'Z'",
-};
-
-/**
- * Returns true if the date can be parsed succesfully after
- * being typed by a user.
- *
- * It allows some ambiguity so validation is not strict.
- *
- * @param {string} value - Value as typed by the user
- * @returns true if the value can be parsed as a valid date, false otherwise
- */
-export const isValidInputString = (value) => {
- try {
- // dateformat throws error that can be caught.
- // This is better than using `new Date()`
- if (value && value.trim()) {
- dateformat(value, 'isoDateTime');
- return true;
- }
- return false;
- } catch (e) {
- return false;
- }
-};
-
-/**
- * Convert the input in time picker component to an ISO date.
- *
- * @param {string} value
- * @param {Boolean} utc - If true, it forces the date to by
- * formatted using UTC format, ignoring the local time.
- * @returns {Date}
- */
-export const inputStringToIsoDate = (value, utc = false) => {
- let date = new Date(value);
- if (utc) {
- // Forces date to be interpreted as UTC by stripping the timezone
- // by formatting to a string with 'Z' and skipping timezone
- date = dateformat(date, dateFormats.stripTimezoneFormat);
- }
- return dateformat(date, 'isoUtcDateTime');
-};
-
-/**
- * Converts a iso date string to a formatted string for the Time picker component.
- *
- * @param {String} ISO Formatted date
- * @returns {string}
- */
-export const isoDateToInputString = (date, utc = false) =>
- dateformat(date, dateFormats.inputFormat, utc);
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
index 7080e046b30..535f1c5f645 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
@@ -65,7 +65,8 @@ export default {
viewer() {
if (this.diffViewerMode === diffViewerModes.renamed) {
return RenamedFile;
- } else if (this.diffMode === diffModes.mode_changed) {
+ }
+ if (this.diffMode === diffModes.mode_changed) {
return ModeChanged;
}
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/empty_file.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/empty_file.vue
deleted file mode 100644
index 53210cbcc93..00000000000
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/empty_file.vue
+++ /dev/null
@@ -1,3 +0,0 @@
-<template>
- <div class="nothing-here-block">{{ __('Empty file') }}</div>
-</template>
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 3bb168e9051..b34a6b11092 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
@@ -64,6 +64,11 @@ export default {
required: false,
default: undefined,
},
+ noOptionsText: {
+ type: String,
+ required: false,
+ default: __('No options found'),
+ },
},
computed: {
isSearchEmpty() {
@@ -72,6 +77,9 @@ export default {
noOptionsFound() {
return !this.isSearchEmpty && this.options.length === 0;
},
+ noOptions() {
+ return this.isSearchEmpty && this.options.length === 0;
+ },
},
methods: {
selectOption(option) {
@@ -177,6 +185,9 @@ export default {
<gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
{{ $options.i18n.noMatchingResults }}
</gl-dropdown-item>
+ <gl-dropdown-item v-if="noOptions">
+ {{ noOptionsText }}
+ </gl-dropdown-item>
</template>
</gl-dropdown-form>
</slot>
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
index 71e3bf4ff63..eb7b20fa4c1 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
@@ -19,6 +19,11 @@ export default {
EntitySelect,
},
props: {
+ apiParams: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
label: {
type: String,
required: false,
@@ -48,7 +53,7 @@ export default {
default: null,
},
groupsFilter: {
- type: String,
+ type: String, // Two supported values: `descendant_groups` and `subgroups` See app/assets/javascripts/vue_shared/components/entity_select/utils.js.
required: false,
default: null,
},
@@ -62,17 +67,15 @@ export default {
async fetchGroups(searchString = '', page = 1) {
let groups = [];
let totalPages = 0;
+ const params = {
+ search: searchString,
+ per_page: DEFAULT_PER_PAGE,
+ page,
+ ...this.apiParams,
+ };
try {
- const { data = [], headers } = await axios.get(
- Api.buildUrl(groupsPath(this.groupsFilter, this.parentGroupID)),
- {
- params: {
- search: searchString,
- per_page: DEFAULT_PER_PAGE,
- page,
- },
- },
- );
+ const url = groupsPath(this.groupsFilter, this.parentGroupID);
+ const { data = [], headers } = await axios.get(url, { params });
groups = data.map((group) => ({
...group,
text: group.full_name,
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/utils.js b/app/assets/javascripts/vue_shared/components/entity_select/utils.js
index 0a4622269f4..857a3ab4c74 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/utils.js
+++ b/app/assets/javascripts/vue_shared/components/entity_select/utils.js
@@ -1,15 +1,26 @@
import Api from '~/api';
+/**
+ * @param {'descendant_groups'|'subgroups'|null} [groupsFilter] - type of group filtering
+ * @param {string|null} [parentGroupID] - parent group is needed for 'descendant_groups' and 'subgroups' filtering.
+ */
export const groupsPath = (groupsFilter, parentGroupID) => {
- if (groupsFilter !== undefined && parentGroupID === undefined) {
+ if (groupsFilter && !parentGroupID) {
throw new Error('Cannot use groupsFilter without a parentGroupID');
}
+
+ let url = '';
switch (groupsFilter) {
case 'descendant_groups':
- return Api.descendantGroupsPath.replace(':id', parentGroupID);
+ url = Api.descendantGroupsPath.replace(':id', parentGroupID);
+ break;
case 'subgroups':
- return Api.subgroupsPath.replace(':id', parentGroupID);
+ url = Api.subgroupsPath.replace(':id', parentGroupID);
+ break;
default:
- return Api.groupsPath;
+ url = Api.groupsPath;
+ break;
}
+
+ return Api.buildUrl(url);
};
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
index db0b0ea185b..226f44a1541 100644
--- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
@@ -145,7 +145,8 @@ export default {
el.classList.contains('inputarea')
) {
return true;
- } else if (combo === 'mod+p') {
+ }
+ if (combo === 'mod+p') {
return false;
}
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 721f87ff4d6..cecd1be82e9 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -141,7 +141,6 @@ export default {
ref="textOutput"
class="file-row-name"
:title="file.name"
- data-qa-selector="file_name_content"
:data-qa-file-name="file.name"
data-testid="file-row-name-container"
:class="[fileClasses, { 'str-truncated': !truncateMiddle, 'gl-min-w-0': truncateMiddle }]"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index 2b3d1b2c1f5..c698b94749d 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -73,6 +73,7 @@ export const TOKEN_TITLE_RELEASE = __('Release');
export const TOKEN_TITLE_REVIEWER = s__('SearchToken|Reviewer');
export const TOKEN_TITLE_SOURCE_BRANCH = __('Source Branch');
export const TOKEN_TITLE_STATUS = __('Status');
+export const TOKEN_TITLE_JOBS_RUNNER_TYPE = s__('Job|Runner type');
export const TOKEN_TITLE_TARGET_BRANCH = __('Target Branch');
export const TOKEN_TITLE_TYPE = __('Type');
export const TOKEN_TITLE_SEARCH_WITHIN = __('Search Within');
@@ -100,6 +101,7 @@ export const TOKEN_TYPE_RELEASE = 'release';
export const TOKEN_TYPE_REVIEWER = 'reviewer';
export const TOKEN_TYPE_SOURCE_BRANCH = 'source-branch';
export const TOKEN_TYPE_STATUS = 'status';
+export const TOKEN_TYPE_JOBS_RUNNER_TYPE = 'jobs-runner-type';
export const TOKEN_TYPE_TARGET_BRANCH = 'target-branch';
export const TOKEN_TYPE_TYPE = 'type';
export const TOKEN_TYPE_WEIGHT = 'weight';
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index f31d4d53a23..346384e3023 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -73,7 +73,8 @@ export default {
},
searchInputPlaceholder: {
type: String,
- required: true,
+ required: false,
+ default: __('Search or filter results…'),
},
suggestionsListClass: {
type: String,
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
index 8322fe92de4..77108ad3628 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
@@ -2,6 +2,8 @@
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
+import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
+import searchMilestonesQuery from '~/issues/list/queries/search_milestones.query.graphql';
import { sortMilestonesByDueDate } from '~/milestones/utils';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { stripQuotes } from '~/lib/utils/text_utility';
@@ -36,6 +38,14 @@ export default {
defaultMilestones() {
return this.config.defaultMilestones || DEFAULT_MILESTONES;
},
+ namespace() {
+ return this.config.isProject ? WORKSPACE_PROJECT : WORKSPACE_GROUP;
+ },
+ fetchMilestonesQuery() {
+ return this.config.fetchMilestones
+ ? this.config.fetchMilestones
+ : this.fetchMilestonesBySearchTerm;
+ },
},
methods: {
getActiveMilestone(milestones, data) {
@@ -51,10 +61,17 @@ export default {
) || this.defaultMilestones.find(({ value }) => value === data)
);
},
+ fetchMilestonesBySearchTerm(search) {
+ return this.$apollo
+ .query({
+ query: searchMilestonesQuery,
+ variables: { fullPath: this.config.fullPath, search, isProject: this.config.isProject },
+ })
+ .then(({ data }) => data[this.namespace]?.milestones.nodes);
+ },
fetchMilestones(searchTerm) {
this.loading = true;
- this.config
- .fetchMilestones(searchTerm)
+ this.fetchMilestonesQuery(searchTerm)
.then((response) => {
const data = Array.isArray(response) ? response : response.data;
diff --git a/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue b/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue
index 7da45169fee..a375a167c68 100644
--- a/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue
+++ b/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue
@@ -24,6 +24,7 @@ export default {
:key="group.id"
:group="group"
:show-group-icon="showGroupIcon"
+ @delete="$emit('delete', $event)"
/>
</ul>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue b/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
index 8a301cd0dd0..ca1e7400f2d 100644
--- a/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
@@ -1,5 +1,6 @@
<script>
import { GlAvatarLabeled, GlIcon, GlTooltipDirective, GlTruncateText } from '@gitlab/ui';
+import uniqueId from 'lodash/uniqueId';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/visibility_level/constants';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
@@ -7,6 +8,9 @@ import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.
import { __ } from '~/locale';
import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import SafeHtml from '~/vue_shared/directives/safe_html';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
+import ListActions from '~/vue_shared/components/list_actions/list_actions.vue';
+import DangerConfirmModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue';
export default {
i18n: {
@@ -25,6 +29,8 @@ export default {
GlIcon,
UserAccessRoleBadge,
GlTruncateText,
+ ListActions,
+ DangerConfirmModal,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -41,6 +47,12 @@ export default {
default: false,
},
},
+ data() {
+ return {
+ isDeleteModalVisible: false,
+ modalId: uniqueId('groups-list-item-modal-id-'),
+ };
+ },
computed: {
visibility() {
return this.group.visibility;
@@ -75,94 +87,131 @@ export default {
groupMembersCount() {
return numberToMetricPrefix(this.group.groupMembersCount);
},
+ actions() {
+ return {
+ [ACTION_EDIT]: {
+ href: this.group.editPath,
+ },
+ [ACTION_DELETE]: {
+ action: this.onActionDelete,
+ },
+ };
+ },
+ hasActions() {
+ return this.group.availableActions?.length;
+ },
+ hasActionDelete() {
+ return this.group.availableActions?.includes(ACTION_DELETE);
+ },
+ },
+ methods: {
+ onActionDelete() {
+ this.isDeleteModalVisible = true;
+ },
},
};
</script>
<template>
- <li class="groups-list-item gl-py-5 gl-md-display-flex gl-align-items-center gl-border-b">
- <div class="gl-display-flex gl-flex-grow-1">
- <gl-icon
- v-if="showGroupIcon"
- class="gl-mr-3 gl-mt-3 gl-md-mt-5 gl-flex-shrink-0 gl-text-secondary"
- :name="groupIconName"
- />
- <gl-avatar-labeled
- :entity-id="group.id"
- :entity-name="group.fullName"
- :label="group.fullName"
- :label-link="group.webUrl"
- shape="rect"
- :size="$options.avatarSize"
- >
- <template #meta>
- <div class="gl-px-2">
- <div class="gl-mx-n2 gl-display-flex gl-align-items-center gl-flex-wrap">
- <div class="gl-px-2">
- <gl-icon
- v-if="visibility"
- v-gl-tooltip="visibilityTooltip"
- :name="visibilityIcon"
- class="gl-text-secondary"
- />
- </div>
- <div class="gl-px-2">
- <user-access-role-badge v-if="shouldShowAccessLevel">{{
- accessLevelLabel
- }}</user-access-role-badge>
+ <li class="groups-list-item gl-py-5 gl-border-b gl-display-flex gl-align-items-flex-start">
+ <div class="gl-md-display-flex gl-align-items-center gl-flex-grow-1">
+ <div class="gl-display-flex gl-flex-grow-1">
+ <gl-icon
+ v-if="showGroupIcon"
+ class="gl-mr-3 gl-mt-3 gl-md-mt-5 gl-flex-shrink-0 gl-text-secondary"
+ :name="groupIconName"
+ />
+ <gl-avatar-labeled
+ :entity-id="group.id"
+ :entity-name="group.fullName"
+ :label="group.fullName"
+ :label-link="group.webUrl"
+ shape="rect"
+ :size="$options.avatarSize"
+ >
+ <template #meta>
+ <div class="gl-px-2">
+ <div class="gl-mx-n2 gl-display-flex gl-align-items-center gl-flex-wrap">
+ <div class="gl-px-2">
+ <gl-icon
+ v-if="visibility"
+ v-gl-tooltip="visibilityTooltip"
+ :name="visibilityIcon"
+ class="gl-text-secondary"
+ />
+ </div>
+ <div class="gl-px-2">
+ <user-access-role-badge v-if="shouldShowAccessLevel">{{
+ accessLevelLabel
+ }}</user-access-role-badge>
+ </div>
</div>
</div>
+ </template>
+ <gl-truncate-text
+ v-if="group.descriptionHtml"
+ :lines="2"
+ :mobile-lines="2"
+ :show-more-text="$options.i18n.showMore"
+ :show-less-text="$options.i18n.showLess"
+ class="gl-mt-2"
+ >
+ <div
+ v-safe-html:[$options.safeHtmlConfig]="group.descriptionHtml"
+ class="gl-font-sm md"
+ data-testid="group-description"
+ ></div>
+ </gl-truncate-text>
+ </gl-avatar-labeled>
+ </div>
+ <div
+ class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-md-pl-0 gl-md-mt-0 gl-md-ml-3"
+ :class="statsPadding"
+ >
+ <div class="gl-display-flex gl-align-items-center gl-gap-x-3">
+ <div
+ v-gl-tooltip="$options.i18n.subgroups"
+ :aria-label="$options.i18n.subgroups"
+ class="gl-text-secondary"
+ data-testid="subgroups-count"
+ >
+ <gl-icon name="subgroup" />
+ <span>{{ descendantGroupsCount }}</span>
</div>
- </template>
- <gl-truncate-text
- v-if="group.descriptionHtml"
- :lines="2"
- :mobile-lines="2"
- :show-more-text="$options.i18n.showMore"
- :show-less-text="$options.i18n.showLess"
- class="gl-mt-2"
- >
<div
- v-safe-html:[$options.safeHtmlConfig]="group.descriptionHtml"
- class="gl-font-sm md"
- data-testid="group-description"
- ></div>
- </gl-truncate-text>
- </gl-avatar-labeled>
- </div>
- <div
- class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-md-pl-0 gl-md-mt-0 gl-md-ml-3"
- :class="statsPadding"
- >
- <div class="gl-display-flex gl-align-items-center gl-gap-x-3">
- <div
- v-gl-tooltip="$options.i18n.subgroups"
- :aria-label="$options.i18n.subgroups"
- class="gl-text-secondary"
- data-testid="subgroups-count"
- >
- <gl-icon name="subgroup" />
- <span>{{ descendantGroupsCount }}</span>
- </div>
- <div
- v-gl-tooltip="$options.i18n.projects"
- :aria-label="$options.i18n.projects"
- class="gl-text-secondary"
- data-testid="projects-count"
- >
- <gl-icon name="project" />
- <span>{{ projectsCount }}</span>
- </div>
- <div
- v-gl-tooltip="$options.i18n.directMembers"
- :aria-label="$options.i18n.directMembers"
- class="gl-text-secondary"
- data-testid="members-count"
- >
- <gl-icon name="users" />
- <span>{{ groupMembersCount }}</span>
+ v-gl-tooltip="$options.i18n.projects"
+ :aria-label="$options.i18n.projects"
+ class="gl-text-secondary"
+ data-testid="projects-count"
+ >
+ <gl-icon name="project" />
+ <span>{{ projectsCount }}</span>
+ </div>
+ <div
+ v-gl-tooltip="$options.i18n.directMembers"
+ :aria-label="$options.i18n.directMembers"
+ class="gl-text-secondary"
+ data-testid="members-count"
+ >
+ <gl-icon name="users" />
+ <span>{{ groupMembersCount }}</span>
+ </div>
</div>
</div>
</div>
+ <list-actions
+ v-if="hasActions"
+ class="gl-ml-3 gl-md-align-self-center"
+ :actions="actions"
+ :available-actions="group.availableActions"
+ />
+
+ <danger-confirm-modal
+ v-if="hasActionDelete"
+ v-model="isDeleteModalVisible"
+ :modal-id="modalId"
+ :phrase="group.fullName"
+ @confirm="$emit('delete', group)"
+ />
</li>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/incidents/utils.js b/app/assets/javascripts/vue_shared/components/incidents/utils.js
deleted file mode 100644
index bcb578a6ba6..00000000000
--- a/app/assets/javascripts/vue_shared/components/incidents/utils.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import { noop } from 'lodash';
-
-export const isValidSlaDueAt = noop;
diff --git a/app/assets/javascripts/vue_shared/components/list_actions/constants.js b/app/assets/javascripts/vue_shared/components/list_actions/constants.js
new file mode 100644
index 00000000000..b1506ae1e93
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_actions/constants.js
@@ -0,0 +1,16 @@
+import { __ } from '~/locale';
+
+export const ACTION_EDIT = 'edit';
+export const ACTION_DELETE = 'delete';
+
+export const BASE_ACTIONS = {
+ [ACTION_EDIT]: {
+ text: __('Edit'),
+ },
+ [ACTION_DELETE]: {
+ text: __('Delete'),
+ extraAttrs: {
+ class: 'gl-text-red-500!',
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/components/list_actions/list_actions.stories.js b/app/assets/javascripts/vue_shared/components/list_actions/list_actions.stories.js
new file mode 100644
index 00000000000..d34729c2373
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_actions/list_actions.stories.js
@@ -0,0 +1,44 @@
+import { makeContainer } from 'storybook_addons/make_container';
+import ListActions from './list_actions.vue';
+import { ACTION_DELETE, ACTION_EDIT } from './constants';
+
+export default {
+ component: ListActions,
+ title: 'vue_shared/list_actions',
+ decorators: [makeContainer({ height: '115px' })],
+ parameters: {
+ docs: {
+ description: {
+ component: `
+This component renders actions used by lists of resources such as groups and projects.
+Currently it is used by \`ProjectsListItem\`. There are base actions defined in \`~/vue_shared/components/list_actions\`
+that help reduce the amount of boilerplate needed for common actions such as edit and delete. This component accepts an
+\`actions\` prop that can extend the base actions and/or add custom actions. These actions should follow the format of
+a [disclosure dropdown item](https://gitlab-org.gitlab.io/gitlab-ui/?path=/docs/base-new-dropdowns-disclosure--docs#setting-disclosure-dropdown-items).
+The \`availableActions\` prop defines what actions to render and in what order. This prop will generally be set by checking
+permissions of the current user.
+`,
+ },
+ },
+ },
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { ListActions },
+ props: Object.keys(argTypes),
+ template: '<list-actions v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ actions: {
+ [ACTION_EDIT]: {
+ href: '/?path=/story/vue-shared-list-actions--default',
+ },
+ [ACTION_DELETE]: {
+ // eslint-disable-next-line no-console
+ action: () => console.log('Deleted'),
+ },
+ },
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
+};
diff --git a/app/assets/javascripts/vue_shared/components/list_actions/list_actions.vue b/app/assets/javascripts/vue_shared/components/list_actions/list_actions.vue
new file mode 100644
index 00000000000..7b78cc1da8f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_actions/list_actions.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { BASE_ACTIONS } from './constants';
+
+export default {
+ name: 'ListActions',
+ i18n: {
+ actions: __('Actions'),
+ },
+ components: {
+ GlDisclosureDropdown,
+ },
+ props: {
+ // Can extend `BASE_ACTIONS` and/or add new actions.
+ // Expected format: https://gitlab-org.gitlab.io/gitlab-ui/?path=/docs/base-new-dropdowns-disclosure--docs#setting-disclosure-dropdown-items
+ actions: {
+ type: Object,
+ required: true,
+ },
+ availableActions: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ items() {
+ return this.availableActions.reduce((accumulator, action) => {
+ return [
+ ...accumulator,
+ {
+ ...BASE_ACTIONS[action],
+ ...this.actions[action],
+ },
+ ];
+ }, []);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown
+ :items="items"
+ icon="ellipsis_v"
+ no-caret
+ :toggle-text="$options.i18n.actions"
+ text-sr-only
+ placement="right"
+ category="tertiary"
+ />
+</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 a570abae9d3..05ce007e615 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
@@ -1,9 +1,9 @@
<script>
-import { GlDropdown, GlDropdownForm, GlFormTextarea, GlButton, GlAlert } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlForm, GlFormTextarea, GlButton, GlAlert } from '@gitlab/ui';
import { __, n__ } from '~/locale';
export default {
- components: { GlDropdown, GlDropdownForm, GlFormTextarea, GlButton, GlAlert },
+ components: { GlDisclosureDropdown, GlForm, GlFormTextarea, GlButton, GlAlert },
props: {
disabled: {
type: Boolean,
@@ -39,43 +39,58 @@ export default {
return n__('Apply %d suggestion', 'Apply %d suggestions', this.batchSuggestionsCount);
},
+ helperText() {
+ if (this.batchSuggestionsCount <= 1) {
+ return __('This also resolves this thread');
+ }
+
+ return __('This also resolves all related threads');
+ },
},
methods: {
onApply() {
this.$emit('apply', this.message);
},
+ focusCommitMessageInput() {
+ this.$refs.commitMessage.$el.focus();
+ },
},
};
</script>
<template>
- <gl-dropdown
- :text="dropdownText"
- :disabled="disabled"
- size="small"
- boundary="window"
- right
- lazy
- menu-class="gl-w-full!"
+ <gl-disclosure-dropdown
data-qa-selector="apply_suggestion_dropdown"
- @shown="$refs.commitMessage.$el.focus()"
+ fluid-width
+ placement="right"
+ size="small"
+ :disabled="disabled"
+ :toggle-text="dropdownText"
+ @shown="focusCommitMessageInput"
>
- <gl-dropdown-form class="gl-px-4! gl-m-0!">
+ <gl-form class="gl-display-flex gl-flex-direction-column gl-px-4! gl-mx-0! gl-my-2!">
<label for="commit-message">{{ __('Commit message') }}</label>
<gl-alert v-if="errorMessage" variant="danger" :dismissible="false" class="gl-mb-4">
{{ errorMessage }}
</gl-alert>
+
<gl-form-textarea
id="commit-message"
ref="commitMessage"
v-model="message"
+ class="apply-suggestions-input-min-width"
:placeholder="defaultCommitMessage"
submit-on-enter
data-qa-selector="commit_message_field"
@submit="onApply"
/>
+
+ <span class="gl-mt-2 gl-text-secondary">
+ {{ helperText }}
+ </span>
+
<gl-button
- class="gl-w-auto! gl-mt-3 gl-text-center! gl-transition-medium! float-right"
+ class="gl-w-auto! gl-mt-3 gl-align-self-end"
category="primary"
variant="confirm"
data-qa-selector="commit_with_custom_message_button"
@@ -83,6 +98,6 @@ export default {
>
{{ __('Apply') }}
</gl-button>
- </gl-dropdown-form>
- </gl-dropdown>
+ </gl-form>
+ </gl-disclosure-dropdown>
</template>
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 b1c6f5e6056..f7f5ccdbf31 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
@@ -4,7 +4,11 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { getDerivedMergeRequestInformation } from '~/diffs/utils/merge_request';
import { InternalEvents } from '~/tracking';
import savedRepliesQuery from './saved_replies.query.graphql';
-import { TRACKING_SAVED_REPLIES_USE, TRACKING_SAVED_REPLIES_USE_IN_MR } from './constants';
+import {
+ TRACKING_SAVED_REPLIES_USE,
+ TRACKING_SAVED_REPLIES_USE_IN_MR,
+ TRACKING_SAVED_REPLIES_USE_IN_OTHER,
+} from './constants';
export default {
apollo: {
@@ -61,9 +65,9 @@ export default {
if (savedReply) {
this.$emit('select', savedReply.content);
this.track_event(TRACKING_SAVED_REPLIES_USE);
- if (isInMr) {
- this.track_event(TRACKING_SAVED_REPLIES_USE_IN_MR);
- }
+ this.track_event(
+ isInMr ? TRACKING_SAVED_REPLIES_USE_IN_MR : TRACKING_SAVED_REPLIES_USE_IN_OTHER,
+ );
}
},
},
diff --git a/app/assets/javascripts/vue_shared/components/markdown/constants.js b/app/assets/javascripts/vue_shared/components/markdown/constants.js
index 47ef7cccbc2..7b31c4a59e3 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/constants.js
+++ b/app/assets/javascripts/vue_shared/components/markdown/constants.js
@@ -1,2 +1,3 @@
export const TRACKING_SAVED_REPLIES_USE = 'i_code_review_saved_replies_use';
export const TRACKING_SAVED_REPLIES_USE_IN_MR = 'i_code_review_saved_replies_use_in_mr';
+export const TRACKING_SAVED_REPLIES_USE_IN_OTHER = 'i_code_review_saved_replies_use_in_other';
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field_view.vue b/app/assets/javascripts/vue_shared/components/markdown/field_view.vue
index 84d40db07bb..c70197c6715 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field_view.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field_view.vue
@@ -2,8 +2,26 @@
import { renderGFM } from '~/behaviors/markdown/render_gfm';
export default {
+ props: {
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ watch: {
+ isLoading() {
+ this.handleGFM();
+ },
+ },
mounted() {
- renderGFM(this.$el);
+ this.handleGFM();
+ },
+ methods: {
+ handleGFM() {
+ if (this.isLoading) return;
+ renderGFM(this.$el);
+ },
},
};
</script>
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 493b329f1b1..fc7e0a7c732 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -119,9 +119,11 @@ export default {
},
},
data() {
+ const editingMode =
+ localStorage.getItem(this.$options.EDITING_MODE_KEY) || EDITING_MODE_MARKDOWN_FIELD;
return {
markdown: this.value || (this.autosaveKey ? getDraft(this.autosaveKey) : '') || '',
- editingMode: EDITING_MODE_MARKDOWN_FIELD,
+ editingMode,
autofocused: false,
};
},
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 0b0867ae84c..6c2f084591e 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
@@ -72,7 +72,7 @@ export function mountMarkdownEditor(options = {}) {
quickActionsDocsPath,
formFieldPlaceholder,
formFieldClasses,
- qaSelector,
+ testid,
newIssuePath,
} = el.dataset;
@@ -115,7 +115,7 @@ export function mountMarkdownEditor(options = {}) {
id: formFieldId,
name: formFieldName,
class: formFieldClasses,
- 'data-qa-selector': qaSelector,
+ 'data-testid': testid,
},
autosaveKey,
enableAutocomplete,
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 855c7a449c4..8a0ca8ebac1 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
@@ -77,9 +77,7 @@ export default {
return this.inapplicableReason;
}
- return this.batchSuggestionsCount > 1
- ? __('This also resolves all related threads')
- : __('This also resolves this thread');
+ return false;
},
isDisableButton() {
return this.isApplying || !this.canApply;
diff --git a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
index 9179331cdec..0ec8b6e2a0a 100644
--- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
@@ -6,6 +6,9 @@ const noteableTypeText = {
Issue: __('issue'),
Epic: __('epic'),
MergeRequest: __('merge request'),
+ Task: __('task'),
+ KeyResult: __('key result'),
+ Objective: __('objective'),
};
export default {
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
index df1188d365b..77fd197978f 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
@@ -1,5 +1,3 @@
-import { __ } from '~/locale';
-
export const tdClass =
'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
export const thClass = 'gl-hover-bg-blue-50';
@@ -15,7 +13,3 @@ export const initialPaginationState = {
firstPageSize: defaultPageSize,
lastPageSize: null,
};
-
-export const defaultI18n = {
- searchPlaceholder: __('Search or filter results…'),
-};
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
index ab9e6e092d9..0c3d175684c 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
@@ -14,11 +14,10 @@ import {
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
-import { initialPaginationState, defaultI18n, defaultPageSize } from './constants';
+import { initialPaginationState, defaultPageSize } from './constants';
import { isAny } from './utils';
export default {
- defaultI18n,
components: {
GlAlert,
GlBadge,
@@ -300,7 +299,6 @@ export default {
<div class="filtered-search-wrapper">
<filtered-search-bar
:namespace="projectPath"
- :search-input-placeholder="$options.defaultI18n.searchPlaceholder"
:tokens="filteredSearchTokens"
:initial-filter-value="filteredSearchValue"
initial-sortby="created_desc"
diff --git a/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue b/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
index e1f042f78ab..76bedc0feeb 100644
--- a/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
@@ -64,7 +64,7 @@ export default {
<template>
<gl-pagination
v-if="showPagination"
- class="gl-mt-3"
+ class="gl-mt-5"
v-bind="$attrs"
align="center"
:value="pageInfo.page"
diff --git a/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
index c1246b2bf44..4f580d4a848 100644
--- a/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
@@ -1,5 +1,11 @@
<script>
-import { GlDropdown, GlDropdownItem, GlIcon, GlSprintf } from '@gitlab/ui';
+import {
+ GlButton,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlIcon,
+ GlSprintf,
+} from '@gitlab/ui';
import { __ } from '~/locale';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
@@ -9,8 +15,9 @@ const DEFAULT_PAGE_SIZES = [20, 50, 100];
export default {
components: {
PaginationLinks,
- GlDropdown,
- GlDropdownItem,
+ GlButton,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlIcon,
GlSprintf,
LocalStorageSync,
@@ -80,25 +87,31 @@ export default {
@input="setPageSize"
/>
<pagination-links :change="setPage" :page-info="pageInfo" class="gl-m-0" />
- <gl-dropdown category="tertiary" class="gl-ml-auto" data-testid="page-size">
- <template #button-content>
- <span class="gl-font-weight-bold">
+ <gl-disclosure-dropdown category="tertiary" class="gl-ml-auto" data-testid="page-size">
+ <template #toggle>
+ <gl-button class="gl-font-weight-bold" category="tertiary">
<gl-sprintf :message="__('%{count} items per page')">
<template #count>
{{ pageInfo.perPage }}
</template>
</gl-sprintf>
- </span>
- <gl-icon class="gl-button-icon dropdown-chevron" name="chevron-down" />
+ <gl-icon class="gl-button-icon dropdown-chevron" name="chevron-down" />
+ </gl-button>
</template>
- <gl-dropdown-item v-for="size in pageSizes" :key="size" @click="setPageSize(size)">
- <gl-sprintf :message="__('%{count} items per page')">
- <template #count>
- {{ size }}
- </template>
- </gl-sprintf>
- </gl-dropdown-item>
- </gl-dropdown>
+ <gl-disclosure-dropdown-item
+ v-for="size in pageSizes"
+ :key="size"
+ @action="setPageSize(size)"
+ >
+ <template #list-item>
+ <gl-sprintf :message="__('%{count} items per page')">
+ <template #count>
+ {{ size }}
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown>
<div class="gl-ml-2" data-testid="information">
<gl-sprintf :message="s__('BulkImport|Showing %{start}-%{end} of %{total}')">
<template #start>
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/constants.js b/app/assets/javascripts/vue_shared/components/projects_list/constants.js
deleted file mode 100644
index aa0b1418a06..00000000000
--- a/app/assets/javascripts/vue_shared/components/projects_list/constants.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export const ACTION_EDIT = 'edit';
-export const ACTION_DELETE = 'delete';
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
index 9fc4571b0dc..ce75e305473 100644
--- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
@@ -7,7 +7,6 @@ import {
GlTooltipDirective,
GlPopover,
GlSprintf,
- GlDisclosureDropdown,
} from '@gitlab/ui';
import uniqueId from 'lodash/uniqueId';
@@ -20,8 +19,9 @@ import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import { truncate } from '~/lib/utils/text_utility';
import SafeHtml from '~/vue_shared/directives/safe_html';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import ListActions from '~/vue_shared/components/list_actions/list_actions.vue';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import DeleteModal from '~/projects/components/shared/delete_modal.vue';
-import { ACTION_EDIT, ACTION_DELETE } from './constants';
const MAX_TOPICS_TO_SHOW = 3;
const MAX_TOPIC_TITLE_LENGTH = 15;
@@ -51,8 +51,8 @@ export default {
GlPopover,
GlSprintf,
TimeAgoTooltip,
- GlDisclosureDropdown,
DeleteModal,
+ ListActions,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -163,30 +163,21 @@ export default {
return numberToMetricPrefix(this.project.openIssuesCount);
},
- actionsDropdownItems() {
- return [
- {
- id: ACTION_EDIT,
- text: __('Edit'),
+ actions() {
+ return {
+ [ACTION_EDIT]: {
href: this.project.editPath,
},
- {
- id: ACTION_DELETE,
- text: __('Delete'),
- extraAttrs: {
- class: 'gl-text-red-500!',
- },
- action: () => {
- this.isDeleteModalVisible = true;
- },
+ [ACTION_DELETE]: {
+ action: this.onActionDelete,
},
- ].filter(({ id }) => this.project.actions?.includes(id));
+ };
},
hasActions() {
- return this.actionsDropdownItems.length;
+ return this.project.availableActions?.length;
},
- hasDeleteAction() {
- return this.actionsDropdownItems.find((action) => action.id === ACTION_DELETE);
+ hasActionDelete() {
+ return this.project.availableActions?.includes(ACTION_DELETE);
},
},
methods: {
@@ -204,6 +195,9 @@ export default {
return null;
},
+ onActionDelete() {
+ this.isDeleteModalVisible = true;
+ },
},
};
</script>
@@ -336,20 +330,15 @@ export default {
</div>
</div>
</div>
- <gl-disclosure-dropdown
+ <list-actions
v-if="hasActions"
class="gl-ml-3 gl-md-align-self-center"
- :items="actionsDropdownItems"
- icon="ellipsis_v"
- no-caret
- :toggle-text="$options.i18n.actions"
- text-sr-only
- placement="right"
- category="tertiary"
+ :actions="actions"
+ :available-actions="project.availableActions"
/>
<delete-modal
- v-if="hasDeleteAction"
+ v-if="hasActionDelete"
v-model="isDeleteModalVisible"
:confirm-phrase="project.name"
:is-fork="project.isForked"
diff --git a/app/assets/javascripts/vue_shared/components/source_editor.vue b/app/assets/javascripts/vue_shared/components/source_editor.vue
index 7b7d3d48d9e..53c16fccba1 100644
--- a/app/assets/javascripts/vue_shared/components/source_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/source_editor.vue
@@ -104,7 +104,7 @@ export default {
:id="`source-editor-${fileGlobalId}`"
ref="editor"
data-editor-loading
- data-qa-selector="source_editor_container"
+ data-testid="source-editor-container"
@[$options.readyEvent]="$emit($options.readyEvent, $event)"
>
<pre class="editor-loading-content">{{ value }}</pre>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
index b89fa3f8292..8dac6327a99 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
@@ -82,6 +82,7 @@ export default {
methods: {
handleChunkAppear() {
this.hasAppeared = true;
+ this.$emit('appear');
},
calculateLineNumber(index) {
return this.startingFrom + index + 1;
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 a4d50466f8f..797a38d8171 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
@@ -33,6 +33,7 @@ export default {
components: {
GlLoadingIcon,
Chunk,
+ CodeownersValidation: () => import('ee_component/blob/components/codeowners_validation.vue'),
},
mixins: [Tracking.mixin()],
props: {
@@ -40,6 +41,14 @@ export default {
type: Object,
required: true,
},
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ currentRef: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -49,7 +58,6 @@ export default {
firstChunk: null,
chunks: {},
isLoading: true,
- isLineSelected: false,
lineHighlighter: null,
};
},
@@ -66,7 +74,8 @@ export default {
if (this.blob.name && this.blob.name.endsWith(`.${SVELTE_LANGUAGE}`)) {
// override for svelte files until https://github.com/rouge-ruby/rouge/issues/1717 is resolved
return SVELTE_LANGUAGE;
- } else if (this.blob.name === this.$options.codeownersFileName) {
+ }
+ if (this.isCodeownersFile) {
// override for codeowners files
return this.$options.codeownersLanguage;
}
@@ -87,6 +96,9 @@ export default {
totalChunks() {
return Object.keys(this.chunks).length;
},
+ isCodeownersFile() {
+ return this.blob.name === CODEOWNERS_FILE_NAME;
+ },
},
async created() {
if (this.isLfsBlob) {
@@ -121,7 +133,7 @@ export default {
this.generateRemainingChunks();
this.isLoading = false;
await this.$nextTick();
- this.lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' });
+ this.selectLine();
});
},
methods: {
@@ -227,18 +239,16 @@ export default {
return languageDefinition;
},
async selectLine() {
- if (this.isLineSelected || !this.lineHighlighter) {
- return;
+ if (!this.lineHighlighter) {
+ this.lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' });
}
-
- this.isLineSelected = true;
await this.$nextTick();
- this.lineHighlighter.highlightHash(this.$route.hash);
+ const scrollEnabled = false;
+ this.lineHighlighter.highlightHash(this.$route.hash, scrollEnabled);
},
},
userColorScheme: window.gon.user_color_scheme,
currentlySelectedLine: null,
- codeownersFileName: CODEOWNERS_FILE_NAME,
codeownersLanguage: CODEOWNERS_LANGUAGE,
};
</script>
@@ -250,6 +260,13 @@ export default {
:data-path="blob.path"
data-qa-selector="blob_viewer_file_content"
>
+ <codeowners-validation
+ v-if="isCodeownersFile"
+ class="gl-text-black-normal"
+ :current-ref="currentRef"
+ :project-path="projectPath"
+ :file-path="blob.path"
+ />
<chunk
v-if="firstChunk"
:lines="firstChunk.lines"
@@ -263,20 +280,21 @@ export default {
/>
<gl-loading-icon v-if="isLoading" size="sm" class="gl-my-5" />
- <chunk
- v-for="(chunk, key, index) in chunks"
- v-else
- :key="key"
- :lines="chunk.lines"
- :content="chunk.content"
- :total-lines="chunk.totalLines"
- :starting-from="chunk.startingFrom"
- :is-highlighted="chunk.isHighlighted"
- :chunk-index="index"
- :language="chunk.language"
- :blame-path="blob.blamePath"
- :total-chunks="totalChunks"
- @appear="highlightChunk"
- />
+ <template v-else>
+ <chunk
+ v-for="(chunk, key, index) in chunks"
+ :key="key"
+ :lines="chunk.lines"
+ :content="chunk.content"
+ :total-lines="chunk.totalLines"
+ :starting-from="chunk.startingFrom"
+ :is-highlighted="chunk.isHighlighted"
+ :chunk-index="index"
+ :language="chunk.language"
+ :blame-path="blob.blamePath"
+ :total-chunks="totalChunks"
+ @appear="highlightChunk"
+ />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
index 0fb6e577f32..c7353ed6785 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
@@ -41,8 +41,14 @@ export default {
addBlobLinksTracking();
},
mounted() {
- const { hash } = this.$route;
- this.lineHighlighter.highlightHash(hash);
+ this.selectLine();
+ },
+ methods: {
+ async selectLine() {
+ await this.$nextTick();
+ const scrollEnabled = false;
+ this.lineHighlighter.highlightHash(this.$route.hash, scrollEnabled);
+ },
},
userColorScheme: window.gon.user_color_scheme,
};
@@ -66,6 +72,7 @@ export default {
:total-lines="chunk.totalLines"
:starting-from="chunk.startingFrom"
:blame-path="blob.blamePath"
+ @appear="selectLine"
/>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/split_button.vue b/app/assets/javascripts/vue_shared/components/split_button.vue
deleted file mode 100644
index c0aef42b0f2..00000000000
--- a/app/assets/javascripts/vue_shared/components/split_button.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<script>
-import { GlDropdown, GlDropdownDivider, GlDropdownItem } from '@gitlab/ui';
-import { isString } from 'lodash';
-
-const isValidItem = (item) =>
- isString(item.eventName) && isString(item.title) && isString(item.description);
-
-export default {
- components: {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- },
-
- props: {
- actionItems: {
- type: Array,
- required: true,
- validator(value) {
- return value.length > 1 && value.every(isValidItem);
- },
- },
- menuClass: {
- type: String,
- required: false,
- default: '',
- },
- variant: {
- type: String,
- required: false,
- default: 'default',
- },
- },
-
- data() {
- return {
- selectedItem: this.actionItems[0],
- };
- },
-
- computed: {
- dropdownToggleText() {
- return this.selectedItem.title;
- },
- },
-
- methods: {
- triggerEvent() {
- this.$emit(this.selectedItem.eventName);
- },
- changeSelectedItem(item) {
- this.selectedItem = item;
- this.$emit('change', item);
- },
- },
-};
-</script>
-
-<template>
- <gl-dropdown
- :menu-class="menuClass"
- split
- :text="dropdownToggleText"
- :variant="variant"
- v-bind="$attrs"
- @click="triggerEvent"
- >
- <template v-for="(item, itemIndex) in actionItems">
- <gl-dropdown-item
- :key="item.eventName"
- is-check-item
- :is-checked="selectedItem === item"
- @click="changeSelectedItem(item)"
- >
- <strong>{{ item.title }}</strong>
- <div>{{ item.description }}</div>
- </gl-dropdown-item>
-
- <gl-dropdown-divider
- v-if="itemIndex < actionItems.length - 1"
- :key="`${item.eventName}-divider`"
- />
- </template>
- </gl-dropdown>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue
index bda88a48e48..9ba5e8724f9 100644
--- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue
+++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue
@@ -70,7 +70,8 @@ export default {
selectTarget() {
if (isFunction(this.truncateTarget)) {
return this.truncateTarget(this.$el);
- } else if (this.truncateTarget === 'child') {
+ }
+ if (this.truncateTarget === 'child') {
return this.$el.childNodes[0];
}
return this.$el;
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index 446c8c97df0..30f616dd8e1 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -83,9 +83,11 @@ export default {
if (this.user.status.emoji && this.user.status.message_html) {
return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message_html}`;
- } else if (this.user.status.message_html) {
+ }
+ if (this.user.status.message_html) {
return this.user.status.message_html;
- } else if (this.user.status.emoji) {
+ }
+ if (this.user.status.emoji) {
return glEmojiTag(this.user.status.emoji);
}
diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
index 4879baced0d..863c43b0e55 100644
--- a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
+++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
@@ -13,7 +13,7 @@ import { __ } from '~/locale';
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { participantsQueries, userSearchQueries } from '~/sidebar/constants';
+import { participantsQueries, userSearchQueries } from '~/sidebar/queries/constants';
import { TYPENAME_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
@@ -130,11 +130,11 @@ export default {
},
update(data) {
return (
- data.workspace?.users?.nodes
- .filter((x) => x?.user)
- .map((node) => ({
- ...node.user,
- canMerge: node.mergeRequestInteraction?.canMerge || false,
+ data.workspace?.users
+ .filter((user) => user)
+ .map((user) => ({
+ ...user,
+ canMerge: user.mergeRequestInteraction?.canMerge || false,
})) || []
);
},
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 79d14b5f2d0..beb8321a271 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -195,9 +195,11 @@ export default {
webIdeActionText() {
if (this.webIdeText) {
return this.webIdeText;
- } else if (this.isBlob) {
+ }
+ if (this.isBlob) {
return __('Open in Web IDE');
- } else if (this.isFork) {
+ }
+ if (this.isFork) {
return __('Edit fork in Web IDE');
}
diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js
index d9bc2c82688..9c001fa2e9a 100644
--- a/app/assets/javascripts/vue_shared/constants.js
+++ b/app/assets/javascripts/vue_shared/constants.js
@@ -86,7 +86,7 @@ export const confidentialityInfoText = (workspaceType, issuableType) =>
),
{
workspaceType: workspaceType === WORKSPACE_PROJECT ? __('project') : __('group'),
- issuableType: issuableType.toLowerCase(),
+ issuableType: issuableType.toLowerCase().replaceAll('_', ' '),
permissions:
issuableType === TYPE_ISSUE
? __('at least the Reporter role, the author, and assignees')
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 699b41f3bf3..1cfa3f6d3d7 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
@@ -36,7 +36,7 @@ export default {
ariaLabel: __('Description'),
class: 'rspec-issuable-form-description',
placeholder: __('Write a comment or drag your files here…'),
- dataQaSelector: 'issuable_form_description_field',
+ dataTestid: 'issuable-form-description-field',
id: 'issuable-description',
name: 'issuable-description',
},
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 31dd49ca415..690d9523a63 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,6 +8,7 @@ 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';
@@ -24,6 +25,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml,
},
mixins: [timeagoMixin],
props: {
@@ -80,14 +82,20 @@ export default {
author() {
return this.issuable.author || {};
},
+ externalAuthor() {
+ return this.issuable.externalAuthor;
+ },
webUrl() {
return this.issuable.gitlabWebUrl || this.issuable.webUrl;
},
authorId() {
return getIdFromGraphQLId(this.author.id);
},
+ isIssueTrackerExternal() {
+ return Boolean(this.issuable.externalTracker);
+ },
isIssuableUrlExternal() {
- return isExternal(this.webUrl);
+ return isExternal(this.webUrl ?? '');
},
reference() {
return this.issuable.reference || `${this.issuableSymbol}${this.issuable.iid}`;
@@ -130,7 +138,8 @@ export default {
return sprintf(__('closed %{timeago}'), {
timeago: this.timeFormatted(this.issuable.closedAt),
});
- } else if (this.issuable.updatedAt !== this.issuable.createdAt) {
+ }
+ if (this.issuable.updatedAt !== this.issuable.createdAt) {
return sprintf(__('updated %{timeAgo}'), {
timeAgo: this.timeFormatted(this.issuable.updatedAt),
});
@@ -242,6 +251,7 @@ export default {
<div data-testid="issuable-title" class="issue-title title">
<work-item-type-icon
v-if="showWorkItemTypeIcon"
+ class="gl-mr-2"
:work-item-type="type"
show-tooltip-on-hover
/>
@@ -259,18 +269,33 @@ export default {
:title="__('This issue is hidden because its author has been banned')"
:aria-label="__('Hidden')"
/>
- <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 }}
+ <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-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2" />
- </gl-link>
+ </template>
<span
v-if="taskStatus"
class="task-status gl-display-none gl-sm-display-inline-block! gl-ml-2 gl-font-sm"
@@ -298,6 +323,9 @@ export default {
</span>
</template>
<template #author>
+ <span v-if="externalAuthor" data-testid="external-author"
+ >{{ externalAuthor }} {{ __('via') }}</span
+ >
<slot v-if="hasSlotContents('author')" name="author"></slot>
<gl-link
v-else
@@ -344,7 +372,7 @@ export default {
</div>
<div class="issuable-meta">
<ul v-if="showIssuableMeta" class="controls">
- <li v-if="hasSlotContents('status')" class="issuable-status">
+ <li v-if="hasSlotContents('status')">
<slot name="status"></slot>
</li>
<li v-if="assignees.length">
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
index 7a9404e06c7..0db7417cebc 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
@@ -5,6 +5,7 @@ import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import PageSizeSelector from '~/vue_shared/components/page_size_selector.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -51,7 +52,8 @@ export default {
},
searchInputPlaceholder: {
type: String,
- required: true,
+ required: false,
+ default: __('Search or filter results…'),
},
searchTokens: {
type: Array,
@@ -344,7 +346,7 @@ export default {
:show-friendly-text="showFilteredSearchFriendlyText"
terms-as-tokens
class="gl-flex-grow-1 gl-border-t-none row-content-block"
- data-qa-selector="issuable_search_container"
+ data-testid="issuable-search-container"
@checked-input="handleAllIssuablesCheckedInput"
@onFilter="$emit('filter', $event)"
@onSort="$emit('sort', $event)"
@@ -377,7 +379,7 @@ export default {
v-for="issuable in issuables"
:key="issuableId(issuable)"
:class="{ 'gl-cursor-grab': isManualOrdering }"
- data-qa-selector="issuable_container"
+ data-testid="issuable-container"
:data-qa-issuable-title="issuable.title"
:has-scoped-labels-feature="hasScopedLabelsFeature"
:issuable-symbol="issuableSymbol"
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue
index 0691bc02b5c..ab71842ae13 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue
@@ -56,7 +56,7 @@ export default {
@click="$emit('click', tab.name)"
>
<template #title>
- <span :title="tab.titleTooltip" :data-qa-selector="`${tab.name}_issuables_tab`">
+ <span :title="tab.titleTooltip" :data-testid="`${tab.name}-issuables-tab`">
{{ tab.title }}
</span>
<gl-badge
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue
index ce1851ab873..01389cd90a9 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue
@@ -34,7 +34,7 @@ export default {
<div
class="description"
:class="{ 'js-task-list-container': canEdit && enableTaskList }"
- data-qa-selector="description_content"
+ data-testid="description-content"
>
<div ref="gfmContainer" v-safe-html="issuable.descriptionHtml" class="md"></div>
<textarea
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 29aef89a991..c4b92454ac0 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
@@ -162,8 +162,8 @@ export default {
<template>
<div class="detail-page-header gl-flex-direction-column gl-sm-flex-direction-row">
- <div class="detail-page-header-body gl-flex-wrap">
- <gl-badge class="gl-mr-2" :variant="badgeVariant">
+ <div class="detail-page-header-body gl-flex-wrap gl-gap-2">
+ <gl-badge :variant="badgeVariant" data-testid="issue-state-badge">
<gl-icon v-if="statusIcon" :name="statusIcon" :class="statusIconClass" />
<span class="gl-display-none gl-sm-display-block" :class="{ 'gl-ml-2': statusIcon }">
<slot name="status-badge">{{ badgeText }}</slot>
@@ -193,18 +193,18 @@ export default {
<work-item-type-icon
v-if="shouldShowWorkItemTypeIcon"
show-text
- :work-item-type="issuableType.toUpperCase()"
+ :work-item-type="issuableType"
/>
<gl-sprintf :message="createdMessage">
<template #timeAgo>
- <time-ago-tooltip class="gl-mx-2" :time="createdAt" />
+ <time-ago-tooltip :time="createdAt" />
</template>
<template #email>
{{ serviceDeskReplyTo }}
</template>
<template #author>
<gl-link
- class="gl-font-weight-bold gl-mx-2 js-user-link"
+ class="gl-font-weight-bold js-user-link"
:href="author.webUrl"
:data-user-id="authorId"
>
@@ -225,7 +225,6 @@ export default {
<gl-icon
v-if="isFirstContribution"
v-gl-tooltip
- class="gl-mr-2"
name="first-contribution"
:title="__('1st contribution!')"
:aria-label="__('1st contribution!')"
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
index 2bc57ecba55..3878c16c8d0 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
@@ -92,6 +92,11 @@ export default {
required: false,
default: false,
},
+ workspaceType: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
methods: {
handleKeydownTitle(e, issuableMeta) {
@@ -105,7 +110,7 @@ export default {
</script>
<template>
- <div class="issuable-show-container" data-qa-selector="issuable_show_container">
+ <div class="issuable-show-container" data-testid="issuable-show-container">
<issuable-header
:issuable-state="issuable.state"
:status-icon="statusIcon"
@@ -116,6 +121,7 @@ export default {
:author="issuable.author"
:task-completion-status="taskCompletionStatus"
:issuable-type="issuable.type"
+ :workspace-type="workspaceType"
:show-work-item-type-icon="showWorkItemTypeIcon"
>
<template #status-badge>
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
index 841d92fd63d..da71adc8abd 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
@@ -60,8 +60,7 @@ export default {
v-safe-html="issuable.titleHtml || issuable.title"
class="title gl-font-size-h-display"
dir="auto"
- data-qa-selector="title_content"
- data-testid="title"
+ data-testid="issuable-title"
></h1>
<gl-button
v-if="enableEdit"
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 4503ba6e561..f54c4c52743 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
@@ -88,6 +88,10 @@ export default {
showSuperSidebarToggle() {
return gon.use_new_navigation && sidebarState.isCollapsed;
},
+
+ topBarClasses() {
+ return gon.use_new_navigation ? 'top-bar-fixed container-fluid' : '';
+ },
},
created() {
@@ -120,15 +124,17 @@ export default {
<template>
<div>
- <div
- class="top-bar-container gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid"
- >
- <super-sidebar-toggle
- v-if="showSuperSidebarToggle"
- class="gl-mr-2"
- :class="$options.JS_TOGGLE_EXPAND_CLASS"
- />
- <gl-breadcrumb :items="breadcrumbs" data-testid="breadcrumb-links" />
+ <div :class="topBarClasses" data-testid="top-bar">
+ <div
+ class="top-bar-container gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid"
+ >
+ <super-sidebar-toggle
+ v-if="showSuperSidebarToggle"
+ class="gl-mr-2"
+ :class="$options.JS_TOGGLE_EXPAND_CLASS"
+ />
+ <gl-breadcrumb :items="breadcrumbs" data-testid="breadcrumb-links" />
+ </div>
</div>
<template v-if="activePanel">
diff --git a/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue b/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue
index 28618cb96a3..61bca18b050 100644
--- a/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue
@@ -1,15 +1,11 @@
<script>
-import { GlDropdown, GlDropdownItem, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
export default {
name: 'SecurityReportDownloadDropdown',
components: {
- GlDropdown,
- GlDropdownItem,
- },
- directives: {
- GlTooltip,
+ GlDisclosureDropdown,
},
props: {
artifacts: {
@@ -26,19 +22,23 @@ export default {
required: false,
default: '',
},
- title: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
showDropdown() {
return this.loading || this.artifacts.length > 0;
},
+ items() {
+ return this.artifacts.map(({ name, path }) => ({
+ text: this.artifactText(name),
+ href: path,
+ extraAttrs: {
+ download: '',
+ },
+ }));
+ },
},
methods: {
- artifactText({ name }) {
+ artifactText(name) {
return sprintf(s__('SecurityReports|Download %{artifactName}'), {
artifactName: name,
});
@@ -48,23 +48,13 @@ export default {
</script>
<template>
- <gl-dropdown
+ <gl-disclosure-dropdown
v-if="showDropdown"
- v-gl-tooltip
- :text="text"
- :title="title"
+ :items="items"
+ :toggle-text="text"
:loading="loading"
icon="download"
size="small"
- right
- >
- <gl-dropdown-item
- v-for="artifact in artifacts"
- :key="artifact.path"
- :href="artifact.path"
- download
- >
- {{ artifactText(artifact) }}
- </gl-dropdown-item>
- </gl-dropdown>
+ placement="right"
+ />
</template>
diff --git a/app/assets/javascripts/vuex_shared/bindings.js b/app/assets/javascripts/vuex_shared/bindings.js
index bc3741a3880..07b16a13e68 100644
--- a/app/assets/javascripts/vuex_shared/bindings.js
+++ b/app/assets/javascripts/vuex_shared/bindings.js
@@ -20,7 +20,8 @@ export const mapComputed = (list, defaultUpdateFn, root) => {
get() {
if (getter) {
return this.$store.getters[getter];
- } else if (root) {
+ }
+ if (root) {
if (typeof root === 'function') {
return root(this.$store.state)[key];
}
diff --git a/app/assets/javascripts/webpack.js b/app/assets/javascripts/webpack.js
index 1c6e632135d..ef82142289d 100644
--- a/app/assets/javascripts/webpack.js
+++ b/app/assets/javascripts/webpack.js
@@ -7,6 +7,7 @@
* e.g. the `window` scope, because it needs to be executed in the scope of webpack.
*/
-if (gon && gon.webpack_public_path) {
+// eslint-disable-next-line camelcase
+if (gon && gon.webpack_public_path && typeof __webpack_public_path__ !== 'undefined') {
__webpack_public_path__ = gon.webpack_public_path; // eslint-disable-line camelcase
}
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue b/app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue
index 1ead16c944b..425679c1400 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue
@@ -1,13 +1,12 @@
<script>
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import Tracking from '~/tracking';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
export default {
components: {
- GlDropdown,
- GlDropdownItem,
+ GlCollapsibleListbox,
LocalStorageSync,
},
mixins: [Tracking.mixin()],
@@ -25,7 +24,7 @@ export default {
type: String,
required: true,
},
- filterOptions: {
+ items: {
type: Array,
required: true,
},
@@ -63,8 +62,7 @@ export default {
},
selectedSortOption() {
return (
- this.filterOptions.find(({ key }) => this.sortFilterProp === key) ||
- this.defaultSortFilterProp
+ this.items.find(({ key }) => this.sortFilterProp === key) || this.defaultSortFilterProp
);
},
},
@@ -94,23 +92,14 @@ export default {
as-string
@input="setDiscussionFilterOption"
/>
- <gl-dropdown
- class="gl-xs-w-full"
- size="small"
- :text="getDropdownSelectedText"
+ <gl-collapsible-listbox
+ :toggle-text="getDropdownSelectedText"
:disabled="loading"
- right
- >
- <gl-dropdown-item
- v-for="{ text, key, testid } in filterOptions"
- :key="text"
- :data-testid="testid"
- is-check-item
- :is-checked="isSortDropdownItemActive(key)"
- @click="fetchFilteredDiscussions(key)"
- >
- {{ text }}
- </gl-dropdown-item>
- </gl-dropdown>
+ :items="items"
+ :selected="sortFilterProp"
+ placement="right"
+ size="small"
+ @select="fetchFilteredDiscussions"
+ />
</div>
</template>
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 66ad3d50287..57faed61280 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
@@ -74,6 +74,11 @@ export default {
required: false,
default: false,
},
+ isWorkItemConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -263,6 +268,7 @@ export default {
:work-item-id="workItemId"
:autofocus="autofocus"
:comment-button-text="commentButtonText"
+ :is-work-item-confidential="isWorkItemConfidential"
@submitForm="updateWorkItem"
@cancelEditing="cancelEditing"
@error="$emit('error', $event)"
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 b143c529014..a79169bde1e 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
@@ -3,11 +3,13 @@ import { GlButton, GlFormCheckbox, GlIcon, GlTooltipDirective } from '@gitlab/ui
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
-import { STATE_OPEN, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { STATE_OPEN, TRACKING_CATEGORY_SHOW, TASK_TYPE_NAME } from '~/work_items/constants';
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
+import CommentFieldLayout from '~/notes/components/comment_field_layout.vue';
export default {
i18n: {
@@ -22,6 +24,7 @@ export default {
markdownDocsPath: helpPagePath('user/markdown'),
},
components: {
+ CommentFieldLayout,
GlButton,
MarkdownEditor,
GlFormCheckbox,
@@ -89,6 +92,11 @@ export default {
required: false,
default: false,
},
+ isWorkItemConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -119,6 +127,23 @@ export default {
commentButtonTextComputed() {
return this.isNoteInternal ? this.$options.i18n.addInternalNote : this.commentButtonText;
},
+ workItemDocPath() {
+ return this.workItemType === TASK_TYPE_NAME ? 'user/tasks.html' : 'user/okrs.html';
+ },
+ workItemDocAnchor() {
+ return this.workItemType === TASK_TYPE_NAME ? 'confidential-tasks' : 'confidential-okrs';
+ },
+ getWorkItemData() {
+ return {
+ confidential: this.isWorkItemConfidential,
+ confidential_issues_docs_path: helpPagePath(this.workItemDocPath, {
+ anchor: this.workItemDocAnchor,
+ }),
+ };
+ },
+ workItemTypeKey() {
+ return capitalizeFirstCharacter(this.workItemType).replace(' ', '');
+ },
},
methods: {
setCommentText(newText) {
@@ -158,66 +183,73 @@ export default {
<template>
<div class="timeline-discussion-body gl-overflow-visible!">
<div class="note-body gl-p-0! gl-overflow-visible!">
- <form class="common-note-form gfm-form js-main-target-form gl-flex-grow-1">
- <markdown-editor
- :value="commentText"
- :render-markdown-path="markdownPreviewPath"
- :markdown-docs-path="$options.constantOptions.markdownDocsPath"
- :autocomplete-data-sources="autocompleteDataSources"
- :form-field-props="formFieldProps"
- :add-spacing-classes="false"
- data-testid="work-item-add-comment"
- class="gl-mb-5"
- use-bottom-toolbar
- supports-quick-actions
- :autofocus="autofocus"
- @input="setCommentText"
- @keydown.meta.enter="$emit('submitForm', { commentText, isNoteInternal })"
- @keydown.ctrl.enter="$emit('submitForm', { commentText, isNoteInternal })"
- @keydown.esc.stop="cancelEditing"
- />
- <gl-form-checkbox
- v-if="isNewDiscussion"
- v-model="isNoteInternal"
- class="gl-mb-2"
- data-testid="internal-note-checkbox"
+ <form class="common-note-form gfm-form js-main-target-form gl-flex-grow-1 new-note">
+ <comment-field-layout
+ :with-alert-container="isWorkItemConfidential"
+ :noteable-data="getWorkItemData"
+ :noteable-type="workItemTypeKey"
>
- {{ $options.i18n.internal }}
- <gl-icon
- v-gl-tooltip:tooltipcontainer.bottom
- name="question-o"
- :size="16"
- :title="$options.i18n.internalVisibility"
- class="gl-text-blue-500"
+ <markdown-editor
+ :value="commentText"
+ :render-markdown-path="markdownPreviewPath"
+ :markdown-docs-path="$options.constantOptions.markdownDocsPath"
+ :autocomplete-data-sources="autocompleteDataSources"
+ :form-field-props="formFieldProps"
+ :add-spacing-classes="false"
+ data-testid="work-item-add-comment"
+ use-bottom-toolbar
+ supports-quick-actions
+ :autofocus="autofocus"
+ @input="setCommentText"
+ @keydown.meta.enter="$emit('submitForm', { commentText, isNoteInternal })"
+ @keydown.ctrl.enter="$emit('submitForm', { commentText, isNoteInternal })"
+ @keydown.esc.stop="cancelEditing"
+ />
+ </comment-field-layout>
+ <div class="note-form-actions">
+ <gl-form-checkbox
+ v-if="isNewDiscussion"
+ v-model="isNoteInternal"
+ class="gl-mb-2"
+ data-testid="internal-note-checkbox"
+ >
+ {{ $options.i18n.internal }}
+ <gl-icon
+ v-gl-tooltip:tooltipcontainer.bottom
+ name="question-o"
+ :size="16"
+ :title="$options.i18n.internalVisibility"
+ class="gl-text-blue-500"
+ />
+ </gl-form-checkbox>
+ <gl-button
+ category="primary"
+ variant="confirm"
+ data-testid="confirm-button"
+ :disabled="!commentText.length"
+ :loading="isSubmitting"
+ @click="$emit('submitForm', { commentText, isNoteInternal })"
+ >{{ commentButtonTextComputed }}
+ </gl-button>
+ <work-item-state-toggle-button
+ v-if="isNewDiscussion"
+ class="gl-ml-3"
+ :work-item-id="workItemId"
+ :work-item-state="workItemState"
+ :work-item-type="workItemType"
+ can-update
+ @error="$emit('error', $event)"
/>
- </gl-form-checkbox>
- <gl-button
- category="primary"
- variant="confirm"
- data-testid="confirm-button"
- :disabled="!commentText.length"
- :loading="isSubmitting"
- @click="$emit('submitForm', { commentText, isNoteInternal })"
- >{{ commentButtonTextComputed }}
- </gl-button>
- <work-item-state-toggle-button
- v-if="isNewDiscussion"
- class="gl-ml-3"
- :work-item-id="workItemId"
- :work-item-state="workItemState"
- :work-item-type="workItemType"
- can-update
- @error="$emit('error', $event)"
- />
- <gl-button
- v-else
- data-testid="cancel-button"
- category="primary"
- class="gl-ml-3"
- :loading="updateInProgress"
- @click="cancelEditing"
- >{{ $options.i18n.cancelButtonText }}
- </gl-button>
+ <gl-button
+ v-else
+ data-testid="cancel-button"
+ category="primary"
+ class="gl-ml-3"
+ :loading="updateInProgress"
+ @click="cancelEditing"
+ >{{ $options.i18n.cancelButtonText }}
+ </gl-button>
+ </div>
</form>
</div>
</div>
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 f030363664f..fd8842aa01a 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
@@ -65,6 +65,11 @@ export default {
required: false,
default: false,
},
+ isWorkItemConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -235,6 +240,7 @@ export default {
:autocomplete-data-sources="autocompleteDataSources"
:markdown-preview-path="markdownPreviewPath"
:is-internal-thread="note.internal"
+ :is-work-item-confidential="isWorkItemConfidential"
@startReplying="showReplyForm"
@cancelEditing="hideReplyForm"
@replied="onReplied"
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 92560f2da9e..b5e3ea68725 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
@@ -163,6 +163,9 @@ export default {
projectName() {
return this.workItem?.project?.name;
},
+ isWorkItemConfidential() {
+ return this.workItem?.confidential;
+ },
},
apollo: {
workItem: {
@@ -314,6 +317,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:work-item-id="workItemId"
:autofocus="isEditing"
+ :is-work-item-confidential="isWorkItemConfidential"
class="gl-pl-3 gl-mt-3"
@cancelEditing="isEditing = false"
@submitForm="updateNote"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue b/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue
index 0c1419e983f..1578c78ac4f 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue
@@ -64,7 +64,7 @@ export default {
:work-item-type="workItemType"
:loading="disableActivityFilterSort"
:sort-filter-prop="discussionFilter"
- :filter-options="$options.WORK_ITEM_ACTIVITY_FILTER_OPTIONS"
+ :items="$options.WORK_ITEM_ACTIVITY_FILTER_OPTIONS"
:storage-key="$options.WORK_ITEM_NOTES_FILTER_KEY"
:default-sort-filter-prop="$options.WORK_ITEM_NOTES_FILTER_ALL_NOTES"
tracking-action="work_item_notes_filter_changed"
@@ -77,7 +77,7 @@ export default {
:work-item-type="workItemType"
:loading="disableActivityFilterSort"
:sort-filter-prop="sortOrder"
- :filter-options="$options.WORK_ITEM_ACTIVITY_SORT_OPTIONS"
+ :items="$options.WORK_ITEM_ACTIVITY_SORT_OPTIONS"
:storage-key="$options.WORK_ITEM_NOTES_SORT_ORDER_KEY"
:default-sort-filter-prop="$options.ASC"
tracking-action="work_item_notes_sort_order_changed"
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 0a38dcb77f6..f50cfac90f7 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,15 +43,6 @@ export default {
type: Boolean,
required: true,
},
- parentWorkItemId: {
- type: String,
- required: true,
- },
- workItemType: {
- type: String,
- required: false,
- default: '',
- },
childPath: {
type: String,
required: true,
@@ -158,7 +149,7 @@ export default {
</span>
<gl-link
:href="childPath"
- class="gl-text-truncate gl-text-black-normal! gl-font-weight-semibold"
+ class="gl-text-truncate gl-font-weight-semibold"
data-testid="item-title"
@click="$emit('click', $event)"
@mouseover="$emit('mouseover')"
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 ddeac2b92ae..38d8d239a7e 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
@@ -42,7 +42,8 @@ export default {
assigneesContainerClass() {
if (this.assignees.length === 2) {
return 'fixed-width-avatars-2';
- } else if (this.assignees.length > 2) {
+ }
+ if (this.assignees.length > 2) {
return 'fixed-width-avatars-3';
}
return '';
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue b/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue
index 53e8eedf060..12b7bade31d 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue
@@ -1,29 +1,28 @@
<script>
-import { GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
export default {
components: {
- GlDropdownItem,
- GlDropdown,
- GlIcon,
+ GlDisclosureDropdownItem,
+ GlDisclosureDropdown,
},
};
</script>
<template>
<div class="gl-ml-5">
- <gl-dropdown
+ <gl-disclosure-dropdown
category="tertiary"
toggle-class="btn-icon btn-sm"
- :right="true"
+ icon="ellipsis_v"
data-testid="work_items_links_menu"
+ :aria-label="__(`More actions`)"
+ text-sr-only
+ no-caret
>
- <template #button-content>
- <gl-icon name="ellipsis_v" :size="14" />
- </template>
- <gl-dropdown-item @click="$emit('removeChild')">
- {{ s__('WorkItem|Remove') }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <gl-disclosure-dropdown-item @action="$emit('removeChild')">
+ <template #list-item>{{ s__('WorkItem|Remove') }}</template>
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown>
</div>
</template>
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
new file mode 100644
index 00000000000..7b38e838033
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
@@ -0,0 +1,145 @@
+<script>
+import { GlTokenSelector } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+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';
+
+export default {
+ components: {
+ GlTokenSelector,
+ },
+ props: {
+ value: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ childrenType: {
+ type: String,
+ required: false,
+ default: WORK_ITEM_TYPE_ENUM_TASK,
+ },
+ childrenIds: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ parentWorkItemId: {
+ type: String,
+ required: true,
+ },
+ areWorkItemsToAddValid: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ apollo: {
+ availableWorkItems: {
+ query: projectWorkItemsQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ searchTerm: this.search?.title || this.search,
+ types: [this.childrenType],
+ in: this.search ? 'TITLE' : undefined,
+ };
+ },
+ skip() {
+ return !this.searchStarted;
+ },
+ update(data) {
+ return data.workspace.workItems.nodes.filter(
+ (wi) => !this.childrenIds.includes(wi.id) && this.parentWorkItemId !== wi.id,
+ );
+ },
+ },
+ },
+ data() {
+ return {
+ availableWorkItems: [],
+ search: '',
+ searchStarted: false,
+ };
+ },
+ computed: {
+ workItemsToAdd: {
+ get() {
+ return this.value;
+ },
+ set(workItemsToAdd) {
+ this.$emit('input', workItemsToAdd);
+ },
+ },
+ isLoading() {
+ return this.$apollo.queries.availableWorkItems.loading;
+ },
+ addInputPlaceholder() {
+ return sprintfWorkItem(I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER, this.childrenTypeName);
+ },
+ childrenTypeName() {
+ return WORK_ITEMS_TYPE_MAP[this.childrenType]?.name;
+ },
+ tokenSelectorContainerClass() {
+ return !this.areWorkItemsToAddValid ? 'gl-inset-border-1-red-500!' : '';
+ },
+ },
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ methods: {
+ getIdFromGraphQLId,
+ setSearchKey(value) {
+ this.search = value;
+ },
+ handleFocus() {
+ this.searchStarted = true;
+ },
+ handleMouseOver() {
+ this.timeout = setTimeout(() => {
+ this.searchStarted = true;
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ handleMouseOut() {
+ clearTimeout(this.timeout);
+ },
+ },
+};
+</script>
+<template>
+ <gl-token-selector
+ v-model="workItemsToAdd"
+ :dropdown-items="availableWorkItems"
+ :loading="isLoading"
+ :placeholder="addInputPlaceholder"
+ menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
+ :container-class="tokenSelectorContainerClass"
+ data-testid="work-item-token-select-input"
+ @text-input="debouncedSearchKeyUpdate"
+ @focus="handleFocus"
+ @mouseover.native="handleMouseOver"
+ @mouseout.native="handleMouseOut"
+ >
+ <template #token-content="{ token }">
+ {{ 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-truncate">{{ dropdownItem.title }}</div>
+ </div>
+ </template>
+ </gl-token-selector>
+</template>
diff --git a/app/assets/javascripts/work_items/components/widget_wrapper.vue b/app/assets/javascripts/work_items/components/widget_wrapper.vue
index f343f787358..27de858fe4e 100644
--- a/app/assets/javascripts/work_items/components/widget_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/widget_wrapper.vue
@@ -14,6 +14,11 @@ export default {
required: false,
default: '',
},
+ widgetName: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -30,6 +35,12 @@ export default {
isOpenString() {
return this.isOpen ? 'true' : 'false';
},
+ anchorLink() {
+ return `#${this.widgetName}`;
+ },
+ anchorLinkId() {
+ return `user-content-${this.widgetName}-links`;
+ },
},
methods: {
hide() {
@@ -46,14 +57,14 @@ export default {
</script>
<template>
- <div id="tasks" class="gl-new-card" :aria-expanded="isOpenString">
+ <div :id="widgetName" class="gl-new-card" :aria-expanded="isOpenString">
<div class="gl-new-card-header">
<div class="gl-new-card-title-wrapper">
<h3 class="gl-new-card-title">
<gl-link
- id="user-content-tasks-links"
- class="anchor position-absolute gl-text-decoration-none"
- href="#tasks"
+ :id="anchorLinkId"
+ class="gl-text-decoration-none"
+ :href="anchorLink"
aria-hidden="true"
/>
<slot name="header"></slot>
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 e8fe64c932b..18aa4d55086 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -1,8 +1,7 @@
<script>
import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownForm,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlDropdownDivider,
GlModal,
GlModalDirective,
@@ -53,9 +52,8 @@ export default {
emailAddressCopied: __('Email address copied'),
},
components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownForm,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlDropdownDivider,
GlModal,
GlToggle,
@@ -180,13 +178,16 @@ export default {
navigator.clipboard.writeText(text);
}
toast(message);
+ this.closeDropdown();
},
handleToggleWorkItemConfidentiality() {
this.track('click_toggle_work_item_confidentiality');
this.$emit('toggleWorkItemConfidentiality', !this.isConfidential);
+ this.closeDropdown();
},
handleDelete() {
this.$refs.modal.show();
+ this.closeDropdown();
},
handleDeleteWorkItem() {
this.track('click_delete_work_item');
@@ -275,6 +276,9 @@ export default {
throwConvertError() {
this.$emit('error', this.i18n.convertError);
},
+ closeDropdown() {
+ this.$refs.workItemsMoreActions.close();
+ },
async promoteToObjective() {
try {
const {
@@ -300,6 +304,8 @@ export default {
} catch (error) {
this.throwConvertError();
Sentry.captureException(error);
+ } finally {
+ this.closeDropdown();
}
},
},
@@ -308,77 +314,87 @@ export default {
<template>
<div>
- <gl-dropdown
+ <gl-disclosure-dropdown
+ ref="workItemsMoreActions"
icon="ellipsis_v"
data-testid="work-item-actions-dropdown"
text-sr-only
:text="__('More actions')"
category="tertiary"
+ :auto-close="false"
no-caret
right
>
<template v-if="$options.isLoggedIn">
- <gl-dropdown-form
- class="work-item-notifications-form"
+ <gl-disclosure-dropdown-item
+ class="gl-display-flex gl-justify-content-end gl-w-full"
:data-testid="$options.notificationsToggleFormTestId"
>
- <div class="gl-px-5 gl-pb-2 gl-pt-1">
+ <template #list-item>
<gl-toggle
:value="subscribedToNotifications"
:label="$options.i18n.notifications"
:data-testid="$options.notificationsToggleTestId"
+ class="work-item-notification-toggle"
label-position="left"
label-id="notifications-toggle"
@change="toggleNotifications($event)"
/>
- </div>
- </gl-dropdown-form>
+ </template>
+ </gl-disclosure-dropdown-item>
<gl-dropdown-divider />
</template>
- <gl-dropdown-item
+
+ <gl-disclosure-dropdown-item
v-if="canPromoteToObjective"
:data-testid="$options.promoteActionTestId"
- @click="promoteToObjective"
+ @action="promoteToObjective"
>
- {{ __('Promote to objective') }}
- </gl-dropdown-item>
+ <template #list-item>{{ __('Promote to objective') }}</template>
+ </gl-disclosure-dropdown-item>
<template v-if="canUpdate && !isParentConfidential">
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
:data-testid="$options.confidentialityTestId"
- @click="handleToggleWorkItemConfidentiality"
- >{{
+ @action="handleToggleWorkItemConfidentiality"
+ ><template #list-item>{{
isConfidential
? $options.i18n.disableTaskConfidentiality
: $options.i18n.enableTaskConfidentiality
- }}</gl-dropdown-item
+ }}</template></gl-disclosure-dropdown-item
>
</template>
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
ref="workItemReference"
:data-testid="$options.copyReferenceTestId"
:data-clipboard-text="workItemReference"
- @click="copyToClipboard(workItemReference, $options.i18n.referenceCopied)"
- >{{ $options.i18n.copyReference }}</gl-dropdown-item
+ @action="copyToClipboard(workItemReference, $options.i18n.referenceCopied)"
+ ><template #list-item>{{
+ $options.i18n.copyReference
+ }}</template></gl-disclosure-dropdown-item
>
<template v-if="$options.isLoggedIn && workItemCreateNoteEmail">
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
ref="workItemCreateNoteEmail"
:data-testid="$options.copyCreateNoteEmailTestId"
:data-clipboard-text="workItemCreateNoteEmail"
- @click="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)"
- >{{ i18n.copyCreateNoteEmail }}</gl-dropdown-item
+ @action="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)"
+ ><template #list-item>{{
+ i18n.copyCreateNoteEmail
+ }}</template></gl-disclosure-dropdown-item
>
- <gl-dropdown-divider v-if="canDelete" />
</template>
- <gl-dropdown-item
+ <gl-dropdown-divider v-if="canDelete" />
+ <gl-disclosure-dropdown-item
v-if="canDelete"
:data-testid="$options.deleteActionTestId"
variant="danger"
- @click="handleDelete"
+ @action="handleDelete"
>
- {{ i18n.deleteWorkItem }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <template #list-item
+ ><span class="text-danger">{{ i18n.deleteWorkItem }}</span></template
+ >
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown>
<gl-modal
ref="modal"
modal-id="work-item-confirm-delete"
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 4b4aa7f96ca..f9527884adc 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -388,7 +388,7 @@ export default {
:display-text="__('Invite members')"
trigger-element="side-nav"
icon="plus"
- trigger-source="work-item-assignees-dropdown"
+ trigger-source="work_item_assignees_dropdown"
classes="gl-display-block gl-text-body! gl-hover-text-decoration-none gl-pb-2"
/>
</gl-dropdown-item>
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 f93ea4a0753..14e55134048 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
@@ -84,13 +84,13 @@ export default {
<gl-loading-icon v-if="updateInProgress" :inline="true" class="gl-mr-3" />
<confidentiality-badge
v-if="isWorkItemConfidential"
- class="gl-vertical-align-middle gl-display-inline-flex!"
- data-testid="confidential"
- :workspace-type="$options.WORKSPACE_PROJECT"
+ class="gl-vertical-align-middle gl-display-inline-flex! gl-mr-2"
:issuable-type="workItemType"
+ :workspace-type="$options.WORKSPACE_PROJECT"
+ hide-text-in-small-screens
/>
<work-item-type-icon
- class="gl-vertical-align-middle gl-mr-0!"
+ class="gl-vertical-align-middle"
:work-item-icon-name="workItemIconName"
:work-item-type="workItemType"
show-text
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 d826ef9cbe7..edecd7addcc 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -31,6 +31,7 @@ import {
WORK_ITEM_TYPE_VALUE_ISSUE,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
WIDGET_TYPE_NOTES,
+ WIDGET_TYPE_LINKED_ITEMS,
} from '../constants';
import workItemUpdatedSubscription from '../graphql/work_item_updated.subscription.graphql';
@@ -50,6 +51,7 @@ import WorkItemNotes from './work_item_notes.vue';
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';
export default {
i18n,
@@ -79,6 +81,7 @@ export default {
AbuseCategorySelector,
GlIntersectionObserver,
ConfidentialityBadge,
+ WorkItemRelationships,
},
mixins: [glFeatureFlagMixin()],
inject: ['fullPath', 'reportAbusePath'],
@@ -259,6 +262,15 @@ export default {
showIntersectionObserver() {
return !this.isModal && this.workItemsMvc2Enabled;
},
+ hasLinkedWorkItems() {
+ return this.glFeatures.linkedWorkItems;
+ },
+ workItemLinkedItems() {
+ return this.isWidgetPresent(WIDGET_TYPE_LINKED_ITEMS);
+ },
+ showWorkItemLinkedItems() {
+ return this.hasLinkedWorkItems && this.workItemLinkedItems;
+ },
},
mounted() {
if (this.modalWorkItemIid) {
@@ -515,9 +527,9 @@ export default {
<gl-loading-icon v-if="updateInProgress" class="gl-mr-3" />
<confidentiality-badge
v-if="workItem.confidential"
- data-testid="confidential"
- :workspace-type="$options.WORKSPACE_PROJECT"
+ class="gl-mr-3"
:issuable-type="workItemType"
+ :workspace-type="$options.WORKSPACE_PROJECT"
/>
<work-item-todos
v-if="showWorkItemCurrentUserTodos"
@@ -591,6 +603,12 @@ export default {
@show-modal="openInModal"
@addChild="$emit('addChild')"
/>
+ <work-item-relationships
+ v-if="showWorkItemLinkedItems"
+ :work-item-iid="workItemIid"
+ :work-item-full-path="workItem.project.fullPath"
+ @showModal="openInModal"
+ />
<work-item-notes
v-if="workItemNotes"
:work-item-id="workItem.id"
@@ -600,6 +618,7 @@ export default {
:assignees="workItemAssignees && workItemAssignees.assignees.nodes"
:can-set-work-item-metadata="canAssignUnassignUser"
:report-abuse-path="reportAbusePath"
+ :is-work-item-confidential="workItem.confidential"
class="gl-pt-5"
@error="updateError = $event"
@has-notes="updateHasNotes"
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 bf427feaa35..9d9414b5399 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
@@ -56,14 +56,14 @@ export default {
return isLoggedIn() && this.canUpdate;
},
treeRootWrapper() {
- return this.canReorder ? Draggable : 'div';
+ return this.canReorder ? Draggable : 'ul';
},
treeRootOptions() {
const options = {
...defaultSortableOptions,
fallbackOnBody: false,
group: 'sortable-container',
- tag: 'div',
+ tag: 'ul',
'ghost-class': 'tree-item-drag-active',
'data-parent-id': this.workItemId,
value: this.children,
@@ -248,6 +248,7 @@ export default {
<component
:is="treeRootWrapper"
v-bind="treeRootOptions"
+ class="content-list"
:class="{ 'gl-cursor-grab sortable-container': canReorder }"
@end="handleDragOnEnd"
>
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 a9b0c2b98bf..679287338c8 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,6 +13,7 @@ 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';
@@ -90,7 +91,7 @@ export default {
return this.isItemOpen ? __('Created') : __('Closed');
},
childPath() {
- return `${gon?.relative_url_root || ''}/${this.fullPath}/-/work_items/${this.childItem.iid}`;
+ return workItemPath(this.fullPath, this.childItem.iid);
},
chevronType() {
return this.isExpanded ? 'chevron-down' : 'chevron-right';
@@ -212,7 +213,7 @@ export default {
</script>
<template>
- <div class="tree-item">
+ <li class="tree-item">
<div
class="gl-display-flex gl-align-items-flex-start"
:class="{ 'gl-ml-6': canHaveChildren && !hasChildren && hasIndirectChildren }"
@@ -249,5 +250,5 @@ export default {
@removeChild="removeChild"
@click="$emit('click', $event)"
/>
- </div>
+ </li>
</template>
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 a0ff693e156..eb836007e75 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
@@ -103,6 +103,7 @@ export default {
isReportDrawerOpen: false,
reportedUserId: 0,
reportedUrl: '',
+ widgetName: 'tasks',
};
},
computed: {
@@ -166,7 +167,6 @@ export default {
this.updateWorkItemIdUrlQuery(child);
},
async closeModal() {
- this.activeChild = {};
this.updateWorkItemIdUrlQuery();
},
handleWorkItemDeleted(child) {
@@ -206,6 +206,7 @@ export default {
<widget-wrapper
ref="wrapper"
:error="error"
+ :widget-name="widgetName"
data-testid="work-item-links"
@dismissAlert="error = undefined"
>
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 4960189fb48..55440e1603c 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
@@ -3,19 +3,15 @@ import {
GlAlert,
GlFormGroup,
GlForm,
- GlTokenSelector,
GlButton,
GlFormInput,
GlFormCheckbox,
GlTooltip,
} from '@gitlab/ui';
-import { debounce } from 'lodash';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
+import WorkItemTokenInput from '../shared/work_item_token_input.vue';
import { addHierarchyChild } from '../../graphql/cache_utils';
import projectWorkItemTypesQuery from '../../graphql/project_work_item_types.query.graphql';
-import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import createWorkItemMutation from '../../graphql/create_work_item.mutation.graphql';
import {
@@ -23,7 +19,6 @@ import {
WORK_ITEMS_TYPE_MAP,
WORK_ITEM_TYPE_ENUM_TASK,
I18N_WORK_ITEM_CREATE_BUTTON_LABEL,
- I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER,
I18N_WORK_ITEM_ADD_BUTTON_LABEL,
I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL,
I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL,
@@ -35,12 +30,12 @@ export default {
components: {
GlAlert,
GlForm,
- GlTokenSelector,
GlButton,
GlFormGroup,
GlFormInput,
GlFormCheckbox,
GlTooltip,
+ WorkItemTokenInput,
},
inject: ['fullPath', 'hasIterationsFeature'],
props: {
@@ -101,35 +96,14 @@ export default {
return data.workspace?.workItemTypes?.nodes;
},
},
- availableWorkItems: {
- query: projectWorkItemsQuery,
- variables() {
- return {
- fullPath: this.fullPath,
- searchTerm: this.search?.title || this.search,
- types: [this.childrenType],
- in: this.search ? 'TITLE' : undefined,
- };
- },
- skip() {
- return !this.searchStarted;
- },
- update(data) {
- return data.workspace.workItems.nodes.filter(
- (wi) => !this.childrenIds.includes(wi.id) && this.issuableGid !== wi.id,
- );
- },
- },
},
data() {
return {
workItemTypes: [],
- availableWorkItems: [],
- search: '',
- searchStarted: false,
+ workItemsToAdd: [],
error: null,
+ search: '',
childToCreateTitle: null,
- workItemsToAdd: [],
confidential: this.parentConfidential,
};
},
@@ -177,7 +151,8 @@ export default {
addOrCreateButtonLabel() {
if (this.isCreateForm) {
return sprintfWorkItem(I18N_WORK_ITEM_CREATE_BUTTON_LABEL, this.childrenTypeName);
- } else if (this.workItemsToAdd.length > 1) {
+ }
+ if (this.workItemsToAdd.length > 1) {
return sprintfWorkItem(I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL, this.childrenTypeName);
}
return sprintfWorkItem(I18N_WORK_ITEM_ADD_BUTTON_LABEL, this.childrenTypeName);
@@ -216,15 +191,6 @@ export default {
}
return this.workItemsToAdd.length === 0 || !this.areWorkItemsToAddValid;
},
- isLoading() {
- return this.$apollo.queries.availableWorkItems.loading;
- },
- addInputPlaceholder() {
- return sprintfWorkItem(I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER, this.childrenTypeName);
- },
- tokenSelectorContainerClass() {
- return !this.areWorkItemsToAddValid ? 'gl-inset-border-1-red-500!' : '';
- },
invalidWorkItemsToAdd() {
return this.parentConfidential
? this.workItemsToAdd.filter((workItem) => !workItem.confidential)
@@ -249,11 +215,7 @@ export default {
);
},
},
- created() {
- this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- },
methods: {
- getIdFromGraphQLId,
getConfidentialityTooltipTarget() {
// We want tooltip to be anchored to `input` within checkbox component
// but `$el.querySelector('input')` doesn't work. 🤷‍♂️
@@ -317,20 +279,6 @@ export default {
this.childToCreateTitle = null;
});
},
- setSearchKey(value) {
- this.search = value;
- },
- handleFocus() {
- this.searchStarted = true;
- },
- handleMouseOver() {
- this.timeout = setTimeout(() => {
- this.searchStarted = true;
- }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- },
- handleMouseOut() {
- clearTimeout(this.timeout);
- },
},
i18n: {
inputLabel: __('Title'),
@@ -385,30 +333,16 @@ export default {
>{{ confidentialityCheckboxTooltip }}</gl-tooltip
>
<div class="gl-mb-4">
- <gl-token-selector
+ <work-item-token-input
v-if="!isCreateForm"
v-model="workItemsToAdd"
- :dropdown-items="availableWorkItems"
- :loading="isLoading"
- :placeholder="addInputPlaceholder"
- menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
- :container-class="tokenSelectorContainerClass"
- data-testid="work-item-token-select-input"
- @text-input="debouncedSearchKeyUpdate"
- @focus="handleFocus"
- @mouseover.native="handleMouseOver"
- @mouseout.native="handleMouseOut"
- >
- <template #token-content="{ token }">
- {{ 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-truncate">{{ dropdownItem.title }}</div>
- </div>
- </template>
- </gl-token-selector>
+ :is-create-form="isCreateForm"
+ :parent-work-item-id="issuableGid"
+ :children-type="childrenType"
+ :children-ids="childrenIds"
+ :are-work-items-to-add-valid="areWorkItemsToAddValid"
+ :full-path="fullPath"
+ />
<div
v-if="showWorkItemsToAddInvalidMessage"
class="gl-text-red-500"
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 246eac82c78..bc3f5201fb8 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
@@ -64,6 +64,7 @@ export default {
isShownAddForm: false,
formType: null,
childType: null,
+ widgetName: 'tasks',
};
},
computed: {
@@ -101,6 +102,7 @@ export default {
<template>
<widget-wrapper
ref="wrapper"
+ :widget-name="widgetName"
:error="error"
data-testid="work-item-tree"
@dismissAlert="error = undefined"
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 8fc460294e6..256f8ed53d1 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -79,6 +79,11 @@ export default {
type: String,
required: true,
},
+ isWorkItemConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -124,6 +129,7 @@ export default {
isNewDiscussion: true,
markdownPreviewPath: this.markdownPreviewPath,
autocompleteDataSources: this.autocompleteDataSources,
+ isWorkItemConfidential: this.isWorkItemConfidential,
};
},
notesArray() {
@@ -366,6 +372,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:assignees="assignees"
:can-set-work-item-metadata="canSetWorkItemMetadata"
+ :is-work-item-confidential="isWorkItemConfidential"
@deleteNote="showDeleteNoteModal($event, discussion)"
@reportAbuse="reportAbuse(true, $event)"
@error="$emit('error', $event)"
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
new file mode 100644
index 00000000000..cbe830f9565
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
@@ -0,0 +1,61 @@
+<script>
+import WorkItemLinkChildContents from '../shared/work_item_link_child_contents.vue';
+import { workItemPath } from '../../utils';
+
+export default {
+ components: {
+ WorkItemLinkChildContents,
+ },
+ props: {
+ linkedItems: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ heading: {
+ type: String,
+ required: true,
+ },
+ canUpdate: {
+ type: Boolean,
+ required: true,
+ },
+ workItemFullPath: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ linkedItemPath(fullPath, id) {
+ return workItemPath(fullPath, id);
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <h4
+ v-if="heading"
+ data-testid="work-items-list-heading"
+ class="gl-font-sm gl-font-weight-semibold gl-text-gray-700 gl-mx-2 gl-mt-3 gl-mb-2"
+ >
+ {{ heading }}
+ </h4>
+ <div class="work-items-list-body">
+ <ul ref="list" class="work-items-list content-list">
+ <li
+ v-for="linkedItem in linkedItems"
+ :key="linkedItem.workItem.id"
+ class="gl-pt-0! gl-pb-0! gl-border-b-0!"
+ >
+ <work-item-link-child-contents
+ :child-item="linkedItem.workItem"
+ :can-update="canUpdate"
+ :child-path="linkedItemPath(workItemFullPath, linkedItem.workItem.iid)"
+ @click="$emit('showModal', { event: $event, child: linkedItem.workItem })"
+ />
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
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
new file mode 100644
index 00000000000..4f6879e9605
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
@@ -0,0 +1,185 @@
+<script>
+import { GlLoadingIcon, GlIcon, GlButton } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+
+import workItemByIidQuery from '../../graphql/work_item_by_iid.query.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';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlIcon,
+ GlButton,
+ WidgetWrapper,
+ WorkItemRelationshipList,
+ },
+ props: {
+ workItemIid: {
+ type: String,
+ required: true,
+ },
+ workItemFullPath: {
+ type: String,
+ required: true,
+ },
+ },
+ apollo: {
+ workItem: {
+ query: workItemByIidQuery,
+ variables() {
+ return {
+ fullPath: this.workItemFullPath,
+ iid: this.workItemIid,
+ };
+ },
+ update(data) {
+ return data.workspace.workItems.nodes[0] ?? {};
+ },
+ context: {
+ isSingleRequest: true,
+ },
+ skip() {
+ return !this.workItemIid;
+ },
+ error(e) {
+ this.error = e.message || this.$options.i18n.fetchError;
+ },
+ async result() {
+ // When work items are switched in a modal, the data props are not getting reset.
+ // Thus, duplicating the work items in the list.
+ // Here, the existing list are cleared before the new items are pushed.
+ this.linksRelatesTo = [];
+ this.linksIsBlockedBy = [];
+ this.linksBlocks = [];
+
+ this.linkedWorkItems.forEach((item) => {
+ if (item.linkType === LINKED_CATEGORIES_MAP.RELATES_TO) {
+ this.linksRelatesTo.push(item);
+ } else if (item.linkType === LINKED_CATEGORIES_MAP.IS_BLOCKED_BY) {
+ this.linksIsBlockedBy.push(item);
+ } else if (item.linkType === LINKED_CATEGORIES_MAP.BLOCKS) {
+ this.linksBlocks.push(item);
+ }
+ });
+ },
+ },
+ },
+ data() {
+ return {
+ error: '',
+ linksRelatesTo: [],
+ linksIsBlockedBy: [],
+ linksBlocks: [],
+ widgetName: 'linkeditems',
+ };
+ },
+ computed: {
+ canUpdate() {
+ // This will be false untill we implement remove item mutation
+ return false;
+ },
+ isLoading() {
+ return this.$apollo.queries.workItem.loading;
+ },
+ linkedWorkItemsWidget() {
+ return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LINKED_ITEMS);
+ },
+ linkedWorkItems() {
+ return this.linkedWorkItemsWidget?.linkedItems?.nodes || [];
+ },
+ linkedWorkItemsCount() {
+ return this.linkedWorkItems.length;
+ },
+ isEmptyRelatedWorkItems() {
+ return !this.error && this.linkedWorkItems.length === 0;
+ },
+ },
+ i18n: {
+ title: s__('WorkItem|Linked Items'),
+ fetchError: s__('WorkItem|Something went wrong when fetching tasks. Please refresh this page.'),
+ emptyStateMessage: s__(
+ "WorkItem|Link work items together to show that they're related or that one is blocking others.",
+ ),
+ addChildButtonLabel: s__('WorkItem|Add'),
+ relatedToTitle: s__('WorkItem|Related to'),
+ blockingTitle: s__('WorkItem|Blocking'),
+ blockedByTitle: s__('WorkItem|Blocked by'),
+ addLinkedWorkItemButtonLabel: s__('WorkItem|Add'),
+ },
+};
+</script>
+<template>
+ <widget-wrapper
+ :error="error"
+ class="work-item-relationships"
+ :widget-name="widgetName"
+ @dismissAlert="error = undefined"
+ >
+ <template #header>
+ <div class="gl-new-card-title-wrapper">
+ <h3 class="gl-new-card-title">
+ {{ $options.i18n.title }}
+ </h3>
+ <div v-if="linkedWorkItemsCount" class="gl-new-card-count">
+ <gl-icon name="link" class="gl-mr-2" />
+ <span data-testid="linked-items-count">{{ linkedWorkItemsCount }}</span>
+ </div>
+ </div>
+ </template>
+ <template #header-right>
+ <gl-button size="small" class="gl-ml-3">
+ <slot name="add-button-text">{{ $options.i18n.addLinkedWorkItemButtonLabel }}</slot>
+ </gl-button>
+ </template>
+ <template #body>
+ <div class="gl-new-card-content">
+ <gl-loading-icon v-if="isLoading" color="dark" class="gl-my-2" />
+ <template v-else>
+ <div v-if="isEmptyRelatedWorkItems" data-testid="links-empty">
+ <p class="gl-new-card-empty">
+ {{ $options.i18n.emptyStateMessage }}
+ </p>
+ </div>
+ <template v-else>
+ <work-item-relationship-list
+ v-if="linksBlocks.length"
+ :class="{
+ 'gl-pb-3 gl-mb-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100':
+ linksIsBlockedBy.length,
+ }"
+ :linked-items="linksBlocks"
+ :heading="$options.i18n.blockingTitle"
+ :work-item-full-path="workItemFullPath"
+ :can-update="canUpdate"
+ @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ />
+ <work-item-relationship-list
+ v-if="linksIsBlockedBy.length"
+ :class="{
+ 'gl-pb-3 gl-mb-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100':
+ linksRelatesTo.length,
+ }"
+ :linked-items="linksIsBlockedBy"
+ :heading="$options.i18n.blockedByTitle"
+ :work-item-full-path="workItemFullPath"
+ :can-update="canUpdate"
+ @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ />
+ <work-item-relationship-list
+ v-if="linksRelatesTo.length"
+ :linked-items="linksRelatesTo"
+ :heading="$options.i18n.relatedToTitle"
+ :work-item-full-path="workItemFullPath"
+ :can-update="canUpdate"
+ @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ />
+ </template>
+ </template>
+ </div>
+ </template>
+ </widget-wrapper>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_state_badge.vue b/app/assets/javascripts/work_items/components/work_item_state_badge.vue
index 1d1bc7352b1..5c5b41b38e6 100644
--- a/app/assets/javascripts/work_items/components/work_item_state_badge.vue
+++ b/app/assets/javascripts/work_items/components/work_item_state_badge.vue
@@ -1,11 +1,12 @@
<script>
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { STATE_OPEN } from '../constants';
export default {
components: {
GlBadge,
+ GlIcon,
},
props: {
workItemState: {
@@ -31,11 +32,8 @@ export default {
</script>
<template>
- <gl-badge
- :icon="workItemStateIcon"
- :variant="workItemStateVariant"
- class="gl-mr-2 gl-vertical-align-middle"
- >
- {{ stateText }}
+ <gl-badge :variant="workItemStateVariant" class="gl-mr-2 gl-vertical-align-middle">
+ <gl-icon :name="workItemStateIcon" :size="16" />
+ <span class="gl-display-none gl-sm-display-block gl-ml-2">{{ stateText }}</span>
</gl-badge>
</template>
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 f27ae5f4e6d..5426f3965b3 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
@@ -53,7 +53,7 @@ export default {
</script>
<template>
- <span class="gl-mr-2">
+ <span>
<gl-icon
v-gl-tooltip.hover="showTooltipOnHover"
:name="iconName"
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 57206550328..2b118247426 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -26,6 +26,7 @@ export const WIDGET_TYPE_MILESTONE = 'MILESTONE';
export const WIDGET_TYPE_ITERATION = 'ITERATION';
export const WIDGET_TYPE_NOTES = 'NOTES';
export const WIDGET_TYPE_HEALTH_STATUS = 'HEALTH_STATUS';
+export const WIDGET_TYPE_LINKED_ITEMS = 'LINKED_ITEMS';
export const WORK_ITEM_TYPE_ENUM_INCIDENT = 'INCIDENT';
export const WORK_ITEM_TYPE_ENUM_ISSUE = 'ISSUE';
@@ -35,6 +36,7 @@ export const WORK_ITEM_TYPE_ENUM_REQUIREMENTS = 'REQUIREMENTS';
export const WORK_ITEM_TYPE_ENUM_OBJECTIVE = 'OBJECTIVE';
export const WORK_ITEM_TYPE_ENUM_KEY_RESULT = 'KEY_RESULT';
+export const WORK_ITEM_TYPE_VALUE_EPIC = 'Epic';
export const WORK_ITEM_TYPE_VALUE_INCIDENT = 'Incident';
export const WORK_ITEM_TYPE_VALUE_ISSUE = 'Issue';
export const WORK_ITEM_TYPE_VALUE_TASK = 'Task';
@@ -57,6 +59,9 @@ export const i18n = {
export const I18N_WORK_ITEM_ERROR_FETCHING_LABELS = s__(
'WorkItem|Something went wrong when fetching labels. Please try again.',
);
+export const I18N_WORK_ITEM_ERROR_FETCHING_TYPES = s__(
+ 'WorkItem|Something went wrong when fetching work item types. Please try again',
+);
export const I18N_WORK_ITEM_ERROR_CREATING = s__(
'WorkItem|Something went wrong when creating %{workItemType}. Please try again.',
);
@@ -208,24 +213,22 @@ export const WORK_ITEM_NOTES_FILTER_KEY = 'filter_key_work_item';
export const WORK_ITEM_ACTIVITY_FILTER_OPTIONS = [
{
- key: WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ value: WORK_ITEM_NOTES_FILTER_ALL_NOTES,
text: s__('WorkItem|All activity'),
},
{
- key: WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
+ value: WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
text: s__('WorkItem|Comments only'),
- testid: 'comments-activity',
},
{
- key: WORK_ITEM_NOTES_FILTER_ONLY_HISTORY,
+ value: WORK_ITEM_NOTES_FILTER_ONLY_HISTORY,
text: s__('WorkItem|History only'),
- testid: 'history-activity',
},
];
export const WORK_ITEM_ACTIVITY_SORT_OPTIONS = [
- { key: DESC, text: __('Newest first'), testid: 'newest-first' },
- { key: ASC, text: __('Oldest first') },
+ { value: DESC, text: __('Newest first') },
+ { value: ASC, text: __('Oldest first') },
];
export const TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION = 'confidentiality-toggle-action';
@@ -241,10 +244,6 @@ export const TODO_DONE_ICON = 'todo-done';
export const TODO_DONE_STATE = 'done';
export const TODO_PENDING_STATE = 'pending';
-export const CURRENT_USER_TODOS_TYPENAME = 'WorkItemWidgetCurrentUserTodos';
-
-export const EMOJI_ACTION_ADD = 'ADD';
-export const EMOJI_ACTION_REMOVE = 'REMOVE';
export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown';
@@ -257,3 +256,9 @@ export const WORK_ITEM_TO_ISSUE_MAP = {
[WIDGET_TYPE_HEALTH_STATUS]: 'healthStatus',
[WIDGET_TYPE_AWARD_EMOJI]: 'awardEmoji',
};
+
+export const LINKED_CATEGORIES_MAP = {
+ RELATES_TO: 'relates_to',
+ IS_BLOCKED_BY: 'is_blocked_by',
+ BLOCKS: 'blocks',
+};
diff --git a/app/assets/javascripts/work_items/graphql/group_work_item_types.query.graphql b/app/assets/javascripts/work_items/graphql/group_work_item_types.query.graphql
new file mode 100644
index 00000000000..30757f57234
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/group_work_item_types.query.graphql
@@ -0,0 +1,11 @@
+query groupWorkItemTypes($fullPath: ID!) {
+ workspace: group(fullPath: $fullPath) {
+ id
+ workItemTypes {
+ nodes {
+ id
+ name
+ }
+ }
+ }
+}
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 383d003e78c..ffc9fe2f7f7 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
@@ -100,4 +100,31 @@ fragment WorkItemWidgets on WorkItemWidget {
... on WorkItemWidgetAwardEmoji {
type
}
+
+ ... on WorkItemWidgetLinkedItems {
+ type
+ linkedItems {
+ nodes {
+ linkId
+ linkType
+ workItem {
+ id
+ iid
+ confidential
+ workItemType {
+ id
+ name
+ iconName
+ }
+ title
+ state
+ createdAt
+ closedAt
+ widgets {
+ ...WorkItemMetadataWidgets
+ }
+ }
+ }
+ }
+ }
}
diff --git a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
index fe7cb719bbb..a853018a931 100644
--- a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
+++ b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
@@ -1,5 +1,7 @@
<script>
import * as Sentry from '@sentry/browser';
+import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
+import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import { STATUS_OPEN } from '~/issues/constants';
import { __, s__ } from '~/locale';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
@@ -8,12 +10,11 @@ import { STATE_CLOSED } from '../../constants';
import getWorkItemsQuery from '../queries/get_work_items.query.graphql';
export default {
- i18n: {
- searchPlaceholder: __('Search or filter results...'),
- },
issuableListTabs,
components: {
IssuableList,
+ IssueCardStatistics,
+ IssueCardTimeInfo,
},
inject: ['fullPath'],
data() {
@@ -57,17 +58,33 @@ export default {
:current-tab="state"
:error="error"
:issuables="workItems"
+ :issuables-loading="$apollo.queries.workItems.loading"
namespace="work-items"
recent-searches-storage-key="issues"
- :search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
show-work-item-type-icon
:sort-options="sortOptions"
:tabs="$options.issuableListTabs"
@dismiss-alert="error = undefined"
>
+ <template #nav-actions>
+ <slot name="nav-actions"></slot>
+ </template>
+
+ <template #timeframe="{ issuable = {} }">
+ <issue-card-time-info :issue="issuable" />
+ </template>
+
<template #status="{ issuable }">
{{ getStatus(issuable) }}
</template>
+
+ <template #statistics="{ issuable = {} }">
+ <issue-card-statistics :issue="issuable" />
+ </template>
+
+ <template #list-body>
+ <slot name="list-body"></slot>
+ </template>
</issuable-list>
</template>
diff --git a/app/assets/javascripts/work_items/list/index.js b/app/assets/javascripts/work_items/list/index.js
index 5cd38600779..885cea2c1d6 100644
--- a/app/assets/javascripts/work_items/list/index.js
+++ b/app/assets/javascripts/work_items/list/index.js
@@ -1,7 +1,8 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import WorkItemsListApp from './components/work_items_list_app.vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import WorkItemsListApp from 'ee_else_ce/work_items/list/components/work_items_list_app.vue';
export const mountWorkItemsListApp = () => {
const el = document.querySelector('.js-work-items-list-root');
@@ -12,6 +13,13 @@ export const mountWorkItemsListApp = () => {
Vue.use(VueApollo);
+ const {
+ fullPath,
+ hasEpicsFeature,
+ hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature,
+ } = el.dataset;
+
return new Vue({
el,
name: 'WorkItemsListRoot',
@@ -19,7 +27,10 @@ export const mountWorkItemsListApp = () => {
defaultClient: createDefaultClient(),
}),
provide: {
- fullPath: el.dataset.fullPath,
+ fullPath,
+ hasEpicsFeature: parseBoolean(hasEpicsFeature),
+ hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
+ hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
},
render: (createComponent) => createComponent(WorkItemsListApp),
});
diff --git a/app/assets/javascripts/work_items/list/queries/base_work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/list/queries/base_work_item_widgets.fragment.graphql
new file mode 100644
index 00000000000..1198973d184
--- /dev/null
+++ b/app/assets/javascripts/work_items/list/queries/base_work_item_widgets.fragment.graphql
@@ -0,0 +1,38 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+fragment BaseWorkItemWidgets on WorkItemWidget {
+ ... on WorkItemWidgetAssignees {
+ type
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ }
+ ... on WorkItemWidgetLabels {
+ type
+ allowsScopedLabels
+ labels {
+ nodes {
+ id
+ color
+ description
+ title
+ }
+ }
+ }
+ ... on WorkItemWidgetMilestone {
+ type
+ milestone {
+ id
+ dueDate
+ startDate
+ title
+ webPath
+ }
+ }
+ ... on WorkItemWidgetStartAndDueDate {
+ type
+ dueDate
+ }
+}
diff --git a/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql b/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
index 7ada2cf12dd..623527302f1 100644
--- a/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
+++ b/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
@@ -1,3 +1,5 @@
+#import "ee_else_ce/work_items/list/queries/work_item_widgets.fragment.graphql"
+
query getWorkItems($fullPath: ID!) {
group(fullPath: $fullPath) {
id
@@ -21,30 +23,7 @@ query getWorkItems($fullPath: ID!) {
updatedAt
webUrl
widgets {
- ... on WorkItemWidgetAssignees {
- assignees {
- nodes {
- id
- avatarUrl
- name
- username
- webUrl
- }
- }
- type
- }
- ... on WorkItemWidgetLabels {
- allowsScopedLabels
- labels {
- nodes {
- id
- color
- description
- title
- }
- }
- type
- }
+ ...WorkItemWidgets
}
workItemType {
id
diff --git a/app/assets/javascripts/work_items/list/queries/work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/list/queries/work_item_widgets.fragment.graphql
new file mode 100644
index 00000000000..6862df5d330
--- /dev/null
+++ b/app/assets/javascripts/work_items/list/queries/work_item_widgets.fragment.graphql
@@ -0,0 +1,5 @@
+#import "./base_work_item_widgets.fragment.graphql"
+
+fragment WorkItemWidgets on WorkItemWidget {
+ ...BaseWorkItemWidgets
+}
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 49ec12db4e1..b5705b21b5a 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -3,7 +3,11 @@ import { GlButton, GlAlert, GlLoadingIcon, GlFormSelect } from '@gitlab/ui';
import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import { getPreferredLocales, s__ } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-import { sprintfWorkItem, I18N_WORK_ITEM_ERROR_CREATING } from '../constants';
+import {
+ I18N_WORK_ITEM_ERROR_CREATING,
+ I18N_WORK_ITEM_ERROR_FETCHING_TYPES,
+ sprintfWorkItem,
+} from '../constants';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
@@ -11,9 +15,6 @@ import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import ItemTitle from '../components/item_title.vue';
export default {
- fetchTypesErrorText: s__(
- 'WorkItem|Something went wrong when fetching work item types. Please try again',
- ),
components: {
GlButton,
GlAlert,
@@ -53,7 +54,7 @@ export default {
}));
},
error() {
- this.error = this.$options.fetchTypesErrorText;
+ this.error = I18N_WORK_ITEM_ERROR_FETCHING_TYPES;
},
},
},
diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js
index 5a882977bc2..1443e4b509d 100644
--- a/app/assets/javascripts/work_items/utils.js
+++ b/app/assets/javascripts/work_items/utils.js
@@ -1,9 +1,26 @@
-import { WIDGET_TYPE_ASSIGNEES, WIDGET_TYPE_HIERARCHY, WIDGET_TYPE_LABELS } from './constants';
+import { joinPaths } from '~/lib/utils/url_utility';
+import {
+ WIDGET_TYPE_ASSIGNEES,
+ WIDGET_TYPE_HEALTH_STATUS,
+ WIDGET_TYPE_HIERARCHY,
+ WIDGET_TYPE_LABELS,
+ WIDGET_TYPE_MILESTONE,
+ WIDGET_TYPE_START_AND_DUE_DATE,
+ WIDGET_TYPE_WEIGHT,
+} from './constants';
export const isAssigneesWidget = (widget) => widget.type === WIDGET_TYPE_ASSIGNEES;
+export const isHealthStatusWidget = (widget) => widget.type === WIDGET_TYPE_HEALTH_STATUS;
+
export const isLabelsWidget = (widget) => widget.type === WIDGET_TYPE_LABELS;
+export const isMilestoneWidget = (widget) => widget.type === WIDGET_TYPE_MILESTONE;
+
+export const isStartAndDueDateWidget = (widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE;
+
+export const isWeightWidget = (widget) => widget.type === WIDGET_TYPE_WEIGHT;
+
export const findHierarchyWidgets = (widgets) =>
widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY);
@@ -26,3 +43,7 @@ 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/javascripts/work_items_hierarchy/hierarchy_util.js b/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js
index 61d93acdb91..57f4783955c 100644
--- a/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js
+++ b/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js
@@ -3,7 +3,8 @@ import { LICENSE_PLAN } from './constants';
export function inferLicensePlan({ hasSubEpics, hasEpics }) {
if (hasSubEpics) {
return LICENSE_PLAN.ULTIMATE;
- } else if (hasEpics) {
+ }
+ if (hasEpics) {
return LICENSE_PLAN.PREMIUM;
}
return LICENSE_PLAN.FREE;
diff --git a/app/assets/stylesheets/disable_animations.scss b/app/assets/stylesheets/disable_animations.scss
index 1e63cdcfa39..7472340896f 100644
--- a/app/assets/stylesheets/disable_animations.scss
+++ b/app/assets/stylesheets/disable_animations.scss
@@ -1,4 +1,7 @@
-* {
+*:not(
+ /* Keep transition enabled where it would otherwise break specs */
+ .always-animate
+) {
/* stylelint-disable property-no-vendor-prefix */
-o-transition: none !important;
-moz-transition: none !important;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 7b8d9281148..514247d2913 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -2,6 +2,7 @@
:root {
--performance-bar-height: 0px;
--system-header-height: 0px;
+ --header-height: 0px;
--top-bar-height: 0px;
--system-footer-height: 0px;
--mr-review-bar-height: 0px;
@@ -22,6 +23,10 @@
--system-header-height: #{$system-header-height};
}
+.with-header {
+ --header-height: #{$header-height};
+}
+
.with-top-bar {
--top-bar-height: #{$top-bar-height};
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 884cb70cb9f..a467d9e8c8a 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -287,6 +287,23 @@
}
}
+ .non-blocking-loader & {
+ &.is-loading{
+ .dropdown-content {
+ display: block;
+ height: 2rem;
+
+ ul{
+ display: none;
+ }
+ }
+ }
+
+ .dropdown-loading{
+ position: relative;
+ }
+ }
+
ul {
margin: 0;
padding: 0;
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index b78b07f953b..67e96f08cb0 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -369,22 +369,6 @@
.board-labels-toggle-wrapper {
margin-bottom: $gl-input-padding;
}
-
- .board-swimlanes-toggle-wrapper {
- @include gl-h-auto;
- margin-bottom: $gl-input-padding;
-
- > span,
- > .dropdown,
- .gl-dropdown-toggle {
- @include gl-w-full;
- @include gl-m-0;
- }
-
- > .dropdown {
- @include gl-mt-2;
- }
- }
}
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index b9fbcfb642c..32735679ded 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -5,7 +5,7 @@ $search-input-field-x-min-width: 200px;
padding: 0 16px;
z-index: $header-zindex;
margin-bottom: 0;
- min-height: $header-height;
+ min-height: var(--header-height);
border: 0;
position: fixed;
top: $calc-system-headers-height;
@@ -22,7 +22,7 @@ $search-input-field-x-min-width: 200px;
display: flex;
justify-content: space-between;
position: relative;
- min-height: $header-height;
+ min-height: var(--header-height);
padding-left: 0;
.title {
@@ -505,7 +505,7 @@ $search-input-field-x-min-width: 200px;
.navbar-empty {
justify-content: center;
- height: $header-height;
+ height: var(--header-height);
background: $white;
border-bottom: 1px solid $gray-100;
@@ -642,7 +642,7 @@ header.navbar-gitlab.super-sidebar-logged-out {
&:focus,
&:active {
- box-shadow: inset 0 0 0 $gl-border-size-1 $white;
+ @include gl-focus;
}
&:active {
diff --git a/app/assets/stylesheets/framework/job_log.scss b/app/assets/stylesheets/framework/job_log.scss
index f77f64f1d76..e409facd081 100644
--- a/app/assets/stylesheets/framework/job_log.scss
+++ b/app/assets/stylesheets/framework/job_log.scss
@@ -6,7 +6,7 @@
word-break: break-all;
word-wrap: break-word;
color: color-yiq($builds-log-bg);
- border-radius: $border-radius-small;
+ border-radius: 0 0 $border-radius-default $border-radius-default;
min-height: 42px;
background-color: $builds-log-bg;
}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 086a16edda2..171f070d776 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -4,6 +4,12 @@ html {
&.touch .tooltip {
display: none !important;
}
+
+ @include media-breakpoint-up(sm) {
+ &.logged-out-marketing-header {
+ --header-height: 72px;
+ }
+ }
}
body {
@@ -197,9 +203,3 @@ body {
padding-right: 0;
}
}
-
-@include media-breakpoint-up(sm) {
- .logged-out-marketing-header {
- --header-height: 72px;
- }
-}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index b953ff3024b..b87fd3e67d4 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -76,7 +76,7 @@
.referenced-users {
color: $gl-text-color;
- padding-top: 10px;
+ padding: 0 $gl-padding $gl-padding-8;
}
.referenced-commands {
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index edebe9c95ad..a32663b17d3 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -263,7 +263,7 @@
@mixin build-log-top-bar($height) {
@include build-log-bar($height);
position: sticky;
- top: $calc-application-header-height;
+ top: calc(#{$calc-application-header-height} - 1px);
}
/*
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 5f90dd62426..f2afa94e000 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -231,18 +231,6 @@
}
}
-.health-status {
- .dropdown-body {
- .health-divider {
- border-top-color: $gray-100;
- }
-
- .dropdown-item:not(.health-dropdown-item) {
- padding: 0;
- }
- }
-}
-
.toggle-right-sidebar-button {
@include side-panel-toggle;
border-bottom: 1px solid $border-color;
@@ -799,29 +787,6 @@
}
}
-.participants-more,
-.user-list-more {
- margin-left: 5px;
-
- a,
- .btn-link {
- color: $gl-text-color-secondary;
- }
-
- .btn-link {
- padding: 0;
- }
-
- .btn-link:hover {
- color: $blue-800;
- text-decoration: none;
- }
-
- .btn-link:focus {
- text-decoration: none;
- }
-}
-
.sidebar-help-wrap {
.sidebar-help-state {
margin: 16px -20px -20px;
diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss
index 8610c41b43f..fbf9d8c8ca6 100644
--- a/app/assets/stylesheets/framework/super_sidebar.scss
+++ b/app/assets/stylesheets/framework/super_sidebar.scss
@@ -104,27 +104,6 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
box-shadow: none;
}
- .context-switcher .gl-new-dropdown-custom-toggle {
- width: 100%;
- }
-
- .context-switcher .gl-new-dropdown-panel {
- overflow-y: auto;
- }
-
- .context-switcher-search-box input {
- @include gl-font-sm;
- }
-
- .gl-new-dropdown-custom-toggle .context-switcher-toggle {
- &[aria-expanded='true'] {
- background-color: $t-gray-a-08;
- }
-
- &:focus {
- @include gl-focus($inset: true); }
- }
-
.btn-with-notification {
position: relative;
@@ -158,15 +137,6 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
}
}
- .nav-item-link {
- &:hover,
- &:focus-within {
- .nav-item-badge {
- opacity: 0;
- }
- }
- }
-
#trial-status-sidebar-widget:hover {
text-decoration: none;
@include gl-text-contrast-light;
@@ -177,6 +147,15 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
display: none;
}
+.super-sidebar-has-peeked {
+ margin-top: calc(#{$header-height} - #{$gl-spacing-scale-2});
+ margin-bottom: #{$gl-spacing-scale-2};
+}
+
+.super-sidebar-peek {
+ margin-left: #{$gl-spacing-scale-2};
+}
+
.super-sidebar-peek,
.super-sidebar-peek-hint {
@include gl-shadow;
@@ -189,6 +168,14 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
}
}
+.super-sidebar-peek {
+ border-radius: $border-radius-default;
+
+ .user-bar {
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ }
+}
+
.page-with-super-sidebar {
padding-left: 0;
@@ -295,19 +282,71 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
}
}
+.transition-opacity-on-hover--context {
+ .transition-opacity-on-hover--target {
+ transition: opacity $gl-transition-duration-fast linear;
+
+ &:hover {
+ transition-delay: $gl-transition-duration-slow;
+ }
+ }
+
+ &:hover {
+ .transition-opacity-on-hover--target {
+ transition-delay: $gl-transition-duration-slow;
+ }
+ }
+}
+
.show-on-focus-or-hover--context {
.show-on-focus-or-hover--target {
opacity: 0;
+
+ &:hover,
+ &:focus {
+ opacity: 1;
+ }
}
&:hover,
- &:focus {
+ &:focus-within {
+ .show-on-focus-or-hover--control {
+ @include gl-bg-t-gray-a-08;
+ }
+
.show-on-focus-or-hover--target {
opacity: 1;
}
}
- .show-on-focus-or-hover--target:focus {
+ .show-on-focus-or-hover--control {
+ &:hover,
+ &:focus {
+ + .show-on-focus-or-hover--target {
+ opacity: 1;
+ }
+ }
+ }
+}
+
+.hide-on-focus-or-hover--context {
+ .hide-on-focus-or-hover--target {
opacity: 1;
}
+
+ &:hover,
+ &:focus-within {
+ .hide-on-focus-or-hover--target {
+ opacity: 0;
+ }
+ }
+
+ .hide-on-focus-or-hover--control {
+ &:hover,
+ &:focus {
+ .hide-on-focus-or-hover--target {
+ opacity: 0;
+ }
+ }
+ }
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index d632689a4f6..e83f6af603a 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -283,75 +283,79 @@ $color-ranges: (
// GitLab themes
-$indigo-50: #f7f7ff;
-$indigo-100: #ebebfa;
-$indigo-200: #d1d1f0;
-$indigo-300: #a6a6de;
-$indigo-400: #7c7ccc;
+$indigo-50: #f1f1ff;
+$indigo-100: #dbdbf8;
+$indigo-200: #c7c7f2;
+$indigo-300: #a2a2e6;
+$indigo-400: #8181d7;
$indigo-500: #6666c4;
-$indigo-600: #5b5bbd;
-$indigo-700: #4b4ba3;
-$indigo-800: #393982;
-$indigo-900: #292961;
-$indigo-950: #1a1a40;
+$indigo-600: #5252b5;
+$indigo-700: #41419f;
+$indigo-800: #303083;
+$indigo-900: #222261;
+$indigo-950: #14143d;
// To do this variant right for darkmode, we need to create a variable for it.
$indigo-900-alpha-008: rgba($indigo-900, 0.08);
-$theme-blue-50: #f4f8fc;
-$theme-blue-100: #e6edf5;
-$theme-blue-200: #c8d7e6;
-$theme-blue-300: #97b3cf;
-$theme-blue-400: #648cb4;
-$theme-blue-500: #4a79a8;
-$theme-blue-600: #3e6fa0;
-$theme-blue-700: #305c88;
-$theme-blue-800: #25496e;
-$theme-blue-900: #1a3652;
-$theme-blue-950: #0f2235;
-
-$theme-light-blue-50: #f2f7fc;
-$theme-light-blue-100: #ebf1f7;
-$theme-light-blue-200: #c9dcf2;
-$theme-light-blue-300: #83abd4;
-$theme-light-blue-400: #4d86bf;
-$theme-light-blue-500: #367cc2;
-$theme-light-blue-600: #3771ab;
-$theme-light-blue-700: #2261a1;
-
-$theme-green-50: #f2faf6;
-$theme-green-100: #e4f3ea;
-$theme-green-200: #c0dfcd;
-$theme-green-300: #8ac2a1;
-$theme-green-400: #52a274;
-$theme-green-500: #35935c;
-$theme-green-600: #288a50;
-$theme-green-700: #1c7441;
-$theme-green-800: #145d33;
-$theme-green-900: #0d4524;
-$theme-green-950: #072d16;
-
-$theme-light-green-700: #156b39;
-
-$theme-red-50: #fcf4f2;
-$theme-red-100: #fae9e6;
-$theme-red-200: #ebcac5;
-$theme-red-300: #d99b91;
-$theme-red-400: #b0655a;
+$theme-blue-50: #cdd8e3;
+$theme-blue-100: #b9cadc;
+$theme-blue-200: #a6bdd5;
+$theme-blue-300: #81a5c9;
+$theme-blue-400: #628eb9;
+$theme-blue-500: #4977a5;
+$theme-blue-600: #346596;
+$theme-blue-700: #235180;
+$theme-blue-800: #153c63;
+$theme-blue-900: #0b2640;
+$theme-blue-950: #04101c;
+
+$theme-light-blue-50: #dde6ee;
+$theme-light-blue-100: #c1d4e6;
+$theme-light-blue-200: #a0bedc;
+$theme-light-blue-300: #74a3d3;
+$theme-light-blue-400: #4f8bc7;
+$theme-light-blue-500: #3476b9;
+$theme-light-blue-600: #2268ae;
+$theme-light-blue-700: #145aa1;
+$theme-light-blue-800: #0e4d8d;
+$theme-light-blue-900: #0c4277;
+$theme-light-blue-950: #0a3764;
+
+$theme-green-50: #dde9de;
+$theme-green-100: #b1d6b5;
+$theme-green-200: #8cc497;
+$theme-green-300: #69af7d;
+$theme-green-400: #499767;
+$theme-green-500: #308258;
+$theme-green-600: #25744c;
+$theme-green-700: #1b653f;
+$theme-green-800: #155635;
+$theme-green-900: #0e4328;
+$theme-green-950: #052e19;
+
+$theme-red-50: #f4e9e7;
+$theme-red-100: #ecd3d0;
+$theme-red-200: #e3bab5;
+$theme-red-300: #d59086;
+$theme-red-400: #c66e60;
$theme-red-500: #ad4a3b;
-$theme-red-600: #9e4133;
-$theme-red-700: #912f20;
-$theme-red-800: #78291d;
-$theme-red-900: #691a16;
-$theme-red-950: #36140f;
-
-$theme-light-red-50: #fff6f5;
-$theme-light-red-100: #fae2de;
-$theme-light-red-200: #f7d5d0;
-$theme-light-red-300: #d9796a;
-$theme-light-red-400: #cf604e;
+$theme-red-600: #a13322;
+$theme-red-700: #8f2110;
+$theme-red-800: #761405;
+$theme-red-900: #580d02;
+$theme-red-950: #380700;
+
+$theme-light-red-50: #faf2f1;
+$theme-light-red-100: #f6d9d5;
+$theme-light-red-200: #ebada2;
+$theme-light-red-300: #e07f6f;
+$theme-light-red-400: #d36250;
$theme-light-red-500: #c24b38;
-$theme-light-red-600: #b03927;
-$theme-light-red-700: #a62e21;
+$theme-light-red-600: #b53a26;
+$theme-light-red-700: #a02e1c;
+$theme-light-red-800: #8b2212;
+$theme-light-red-900: #751709;
+$theme-light-red-950: #5c1105;
// Data visualization color palette
@@ -459,7 +463,7 @@ $browser-scrollbar-size: 10px;
/*
* Misc
*/
-$header-height: var(--header-height, 48px);
+$header-height: 48px;
$content-wrapper-padding: 100px;
$header-zindex: 1000;
$zindex-dropdown-menu: 300;
@@ -501,7 +505,7 @@ $pages-group-name-color: #4c4e54;
* Calculated heights
*/
$calc-system-headers-height: calc(var(--system-header-height) + var(--performance-bar-height));
-$calc-application-bars-height: calc(#{$header-height} + #{$calc-system-headers-height});
+$calc-application-bars-height: calc(var(--header-height) + #{$calc-system-headers-height});
$calc-application-header-height: calc(#{$calc-application-bars-height} + var(--top-bar-height));
$calc-application-footer-height: var(--system-footer-height);
$calc-application-viewport-height: calc(100vh - #{$calc-application-header-height} - #{$calc-application-footer-height});
@@ -692,14 +696,6 @@ $ci-skipped-color: #888;
*/
$issue-boards-font-size: 14px;
$issue-boards-card-shadow: rgba(0, 0, 0, 0.1);
-/*
- The following heights are used in environment_logs.scss and are used for calculation of the log viewer height.
-*/
-$environment-logs-breadcrumbs-height: 63px;
-$environment-logs-breadcrumbs-height-md: $top-bar-height;
-
-$environment-logs-difference-xs-up: calc(#{$header-height} + #{$environment-logs-breadcrumbs-height});
-$environment-logs-difference-md-up: calc(#{$header-height} + #{$environment-logs-breadcrumbs-height-md});
/*
* Avatar
@@ -845,12 +841,6 @@ $perf-bar-bucket-box-shadow-to: rgba($black, 0.25);
$perf-bar-canary-text: $orange-400;
/*
-Issuable warning
-*/
-$issuable-warning-size: 24px;
-$issuable-warning-icon-margin: 4px;
-
-/*
Image Commenting cursor
*/
$image-comment-cursor-left-offset: 12;
diff --git a/app/assets/stylesheets/page_bundles/admin/jobs_index.scss b/app/assets/stylesheets/page_bundles/admin/jobs_index.scss
deleted file mode 100644
index 7844cae5f87..00000000000
--- a/app/assets/stylesheets/page_bundles/admin/jobs_index.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.admin-builds-table {
- td:last-child {
- min-width: 120px;
- }
-}
diff --git a/app/assets/stylesheets/page_bundles/build.scss b/app/assets/stylesheets/page_bundles/build.scss
index 5114f484e53..09c4d184f3f 100644
--- a/app/assets/stylesheets/page_bundles/build.scss
+++ b/app/assets/stylesheets/page_bundles/build.scss
@@ -15,6 +15,9 @@
.top-bar {
@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);
&.has-archived-block {
top: calc(#{$calc-application-header-height} + 28px);
@@ -86,8 +89,6 @@
}
.right-sidebar.build-sidebar {
- padding: 0;
-
&.right-sidebar-collapsed {
display: none;
}
@@ -100,29 +101,6 @@
-webkit-overflow-scrolling: touch;
}
- .blocks-container {
- padding: 0 $gl-padding;
- width: 289px;
- }
-
- .trigger-variables-btn-container {
- justify-content: space-between;
- align-items: center;
-
- .trigger-variables-btn {
- margin-top: -5px;
- margin-bottom: -5px;
- }
- }
-
- .trigger-build-variables {
- margin: 0;
- overflow-x: auto;
- width: 100%;
- -ms-overflow-style: scrollbar;
- -webkit-overflow-scrolling: touch;
- }
-
.trigger-build-variable {
font-weight: $gl-font-weight-normal;
color: var(--gray-950, $gray-950);
@@ -142,38 +120,20 @@
vertical-align: top;
}
- .badge.badge-pill {
- margin-left: 2px;
+ .blocks-container {
+ width: 289px;
}
- .stage-item {
- cursor: pointer;
-
- &:hover {
- color: var(--gl-text-color, $gl-text-color);
- }
+ .block {
+ width: 262px;
}
.builds-container {
- background-color: var(--white, $white);
- border-top: 1px solid var(--border-color, $border-color);
- border-bottom: 1px solid var(--border-color, $border-color);
- max-height: 300px;
- width: 289px;
overflow: auto;
- a {
- padding: $gl-padding 10px $gl-padding 40px;
- width: 270px;
-
- &:hover {
- color: var(--gl-text-color, $gl-text-color);
- }
- }
-
.icon-arrow-right {
- left: 15px;
- top: 20px;
+ left: 8px;
+ top: 12px;
}
.build-job {
@@ -192,9 +152,15 @@
.container-fluid.container-limited {
max-width: 100%;
}
+}
- .content-wrapper {
- padding-bottom: 6px;
+.build-sidebar-item {
+ display: grid;
+ grid-template-columns: 1fr 2fr;
+ grid-gap: $gl-padding-8;
+
+ &:last-of-type {
+ @include gl-mb-0;
}
}
@@ -205,12 +171,3 @@
margin-bottom: 0;
}
}
-
-@include media-breakpoint-down(md) {
- .content-list {
- &.builds-content-list {
- width: 100%;
- overflow: auto;
- }
- }
-}
diff --git a/app/assets/stylesheets/page_bundles/incidents.scss b/app/assets/stylesheets/page_bundles/incidents.scss
index fde35ab3d39..c7873473b86 100644
--- a/app/assets/stylesheets/page_bundles/incidents.scss
+++ b/app/assets/stylesheets/page_bundles/incidents.scss
@@ -43,8 +43,7 @@
}
}
- &:last-child,
- &.create-timeline-event {
+ &:last-child {
&::before {
top: - #{$gl-spacing-scale-5} !important; // Override default positioning
@include gl-h-8;
diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss
index 1b5da0368c6..5397f3d8895 100644
--- a/app/assets/stylesheets/page_bundles/issuable.scss
+++ b/app/assets/stylesheets/page_bundles/issuable.scss
@@ -1,15 +1,6 @@
@import 'mixins_and_variables_and_functions';
-.status-box {
- padding: 0 $gl-btn-padding;
- border-radius: $border-radius-default;
- display: block;
- float: left;
- margin-right: $gl-padding-8;
- color: var(--white, $white);
- font-size: $gl-font-size;
- line-height: $gl-line-height-24;
-}
+$issuable-warning-size: 24px;
.issuable-warning-icon {
background-color: var(--orange-50, $orange-50);
@@ -18,7 +9,6 @@
width: $issuable-warning-size;
height: $issuable-warning-size;
text-align: center;
- margin-right: $issuable-warning-icon-margin;
line-height: $gl-line-height-24;
flex: 0 0 auto;
}
diff --git a/app/assets/stylesheets/page_bundles/merge_request.scss b/app/assets/stylesheets/page_bundles/merge_request.scss
index 113a50c4efa..f03efb82860 100644
--- a/app/assets/stylesheets/page_bundles/merge_request.scss
+++ b/app/assets/stylesheets/page_bundles/merge_request.scss
@@ -3,6 +3,12 @@
$tabs-holder-z-index: 250;
$comparison-empty-state-height: 62px;
+.apply-suggestions-input-min-width {
+ @include media-breakpoint-up(lg) {
+ width: $gl-dropdown-width-wide;
+ }
+}
+
.space-children {
@include clearfix;
@@ -199,6 +205,7 @@ $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);
@include media-breakpoint-up(md) {
position: sticky;
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index f39247f06c2..b00e1813696 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -372,7 +372,6 @@ $tabs-holder-z-index: 250;
white-space: nowrap;
}
- /* stylelint-disable scss/at-rule-no-unknown */
@container mr-widget-extension (max-width: 600px) {
flex-direction: column;
align-items: flex-start;
@@ -417,7 +416,7 @@ $tabs-holder-z-index: 250;
.media-body {
min-width: 0;
- font-size: 12px;
+ font-size: $gl-font-size-sm;
margin-left: 32px;
}
@@ -548,6 +547,7 @@ $tabs-holder-z-index: 250;
}
.mr-widget-section:not(:first-child) > div,
+ .mr-widget-section:not(:first-child) > section,
.mr-widget-section .mr-widget-section > div {
border-top: solid 1px var(--border-color, $border-color);
}
@@ -649,7 +649,6 @@ $tabs-holder-z-index: 250;
.label-branch {
@include gl-font-monospace;
- font-size: 95%;
overflow: hidden;
word-break: break-all;
}
@@ -663,7 +662,7 @@ $tabs-holder-z-index: 250;
> span {
display: inline-block;
max-width: 12.5em;
- margin-bottom: -5px;
+ margin-bottom: px-to-rem(-5px);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
@@ -987,6 +986,14 @@ $tabs-holder-z-index: 250;
}
}
+.container-fluid.diffs-container-limited {
+ .flash-container {
+ @include gl-mx-auto;
+ @include gl-max-w-container-xl;
+ @include gl-px-5;
+ }
+}
+
.submit-review-dropdown {
&.show .dropdown-menu {
width: calc(100vw - 20px);
diff --git a/app/assets/stylesheets/page_bundles/pipeline_schedules.scss b/app/assets/stylesheets/page_bundles/pipeline_schedules.scss
deleted file mode 100644
index af2dac7739e..00000000000
--- a/app/assets/stylesheets/page_bundles/pipeline_schedules.scss
+++ /dev/null
@@ -1,82 +0,0 @@
-@import 'mixins_and_variables_and_functions';
-
-.ci-variable-list {
- margin-left: 0;
- margin-bottom: 0;
- padding-left: 0;
- list-style: none;
- clear: both;
-}
-
-.ci-variable-row {
- display: flex;
- align-items: flex-start;
-
- @include media-breakpoint-down(xs) {
- align-items: flex-end;
- }
-
- &:not(:last-child) {
- margin-bottom: $gl-btn-padding;
-
- @include media-breakpoint-down(xs) {
- margin-bottom: 3 * $gl-btn-padding;
- }
- }
-
- &:last-child {
- .ci-variable-body-item:last-child {
- margin-right: $ci-variable-remove-button-width;
-
- @include media-breakpoint-down(xs) {
- margin-right: 0;
- }
- }
-
- .ci-variable-row-remove-button {
- display: none;
- }
-
- @include media-breakpoint-down(xs) {
- .ci-variable-row-body {
- margin-right: $ci-variable-remove-button-width;
- }
- }
- }
-}
-
-.ci-variable-row-body {
- display: flex;
- align-items: flex-start;
- width: 100%;
- padding-bottom: $gl-padding;
-
- @include media-breakpoint-down(xs) {
- display: block;
- }
-}
-
-.ci-variable-body-item {
- flex: 1;
-
- &:not(:last-child) {
- margin-right: $gl-btn-padding;
-
- @include media-breakpoint-down(xs) {
- margin-right: 0;
- margin-bottom: $gl-btn-padding;
- }
- }
-}
-
-.pipeline-schedule-form {
- .gl-field-error {
- margin: 10px 0 0;
- }
-}
-
-.pipeline-schedule-table-row {
- a {
- color: var(--gl-text-color, $gl-text-color);
- }
-}
diff --git a/app/assets/stylesheets/page_bundles/profiles/preferences.scss b/app/assets/stylesheets/page_bundles/profiles/preferences.scss
index 1a59f96c6ee..d09ad42a722 100644
--- a/app/assets/stylesheets/page_bundles/profiles/preferences.scss
+++ b/app/assets/stylesheets/page_bundles/profiles/preferences.scss
@@ -1,9 +1,9 @@
@import 'page_bundles/mixins_and_variables_and_functions';
.application-theme {
- $ui-gray-bg: #303030;
- $ui-light-gray-bg: #f0f0f0;
- $ui-dark-mode-bg: #1f1f1f;
+ $ui-gray-bg: $gray-900;
+ $ui-light-gray-bg: $gray-50;
+ $ui-dark-mode-bg: $gray-950;
.preview {
font-size: 0;
@@ -33,7 +33,7 @@
}
&.ui-light-green {
- background-color: $theme-light-green-700;
+ background-color: $theme-green-700;
}
&.ui-red {
diff --git a/app/assets/stylesheets/page_bundles/projects_usage_quotas.scss b/app/assets/stylesheets/page_bundles/projects_usage_quotas.scss
deleted file mode 100644
index 8f2cbc402c9..00000000000
--- a/app/assets/stylesheets/page_bundles/projects_usage_quotas.scss
+++ /dev/null
@@ -1,19 +0,0 @@
-@import 'mixins_and_variables_and_functions';
-
-.storage-type-usage {
- &:first-child {
- @include gl-rounded-top-left-base;
- @include gl-rounded-bottom-left-base;
- }
-
- &:last-child {
- @include gl-rounded-top-right-base;
- @include gl-rounded-bottom-right-base;
- }
-
- &:not(:last-child) {
- @include gl-border-r-2;
- @include gl-border-r-solid;
- border-right-color: var(--white, $white);
- }
-}
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index e8fa93e1504..f36cbc129a7 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -173,3 +173,13 @@ $work-item-sticky-header-height: 52px;
min-width: 100%;
}
}
+
+.work-item-notification-toggle {
+ .gl-toggle {
+ margin-left: auto;
+ }
+
+ .gl-toggle-label {
+ font-weight: normal;
+ }
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 0c9d151e3cd..38686d5e713 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -136,7 +136,7 @@
}
.icon {
- margin-right: $issuable-warning-icon-margin;
+ margin-right: $gl-padding-4;
vertical-align: text-bottom;
fill: $orange-600;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index b9cae28537d..9ce470dbcf2 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -306,8 +306,6 @@
}
.project-details {
- max-width: 625px;
-
p,
.commit-row-message {
@include gl-mb-0;
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index 030e41046d3..7616f573412 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -235,7 +235,7 @@ body.gl-dark {
}
- .navbar-gitlab {
+ .navbar.navbar-gitlab {
background-color: var(--gray-50);
box-shadow: 0 1px 0 0 var(--gray-100);
diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss
index 8f0e0781918..db20034419a 100644
--- a/app/assets/stylesheets/themes/theme_helper.scss
+++ b/app/assets/stylesheets/themes/theme_helper.scss
@@ -20,7 +20,7 @@
// Header
- .navbar-gitlab {
+ .navbar-gitlab:not(.super-sidebar-logged-out) {
background-color: $navbar-theme-color;
.navbar-collapse {
@@ -283,11 +283,12 @@
$theme-color,
$theme-color-darkest,
) {
+ --sidebar-background: #{mix(white, $theme-color-lightest, 50%)};
--transparent-white-16: rgba(255, 255, 255, 0.16);
--transparent-white-24: rgba(255, 255, 255, 0.24);
.super-sidebar {
- background-color: $theme-color-lightest;
+ background-color: var(--sidebar-background);
}
.super-sidebar .user-bar {
@@ -335,4 +336,8 @@
.active-indicator {
background-color: $theme-color;
}
+
+ .super-sidebar-context-header {
+ color: $theme-color;
+ }
}
diff --git a/app/assets/stylesheets/themes/theme_light_gray.scss b/app/assets/stylesheets/themes/theme_light_gray.scss
index 9b7fc10e769..e8357647f48 100644
--- a/app/assets/stylesheets/themes/theme_light_gray.scss
+++ b/app/assets/stylesheets/themes/theme_light_gray.scss
@@ -10,7 +10,7 @@ body {
$gray-500
);
- .navbar-gitlab {
+ .navbar-gitlab:not(.super-sidebar-logged-out) {
background-color: $gray-50;
box-shadow: 0 1px 0 0 $border-color;
diff --git a/app/assets/stylesheets/themes/theme_light_green.scss b/app/assets/stylesheets/themes/theme_light_green.scss
index 720a0ec58b8..6b058b2dd7b 100644
--- a/app/assets/stylesheets/themes/theme_light_green.scss
+++ b/app/assets/stylesheets/themes/theme_light_green.scss
@@ -6,7 +6,7 @@ body {
$theme-green-200,
$theme-green-500,
$theme-green-500,
- $theme-light-green-700,
+ $theme-green-700,
$white
);
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index d5e9d35983a..b756e0ed704 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -132,19 +132,6 @@
fill: $red-500;
}
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3569
-.gl-mb-n5 {
- margin-bottom: -$gl-spacing-scale-5;
-}
-
-.gl-mb-n7 {
- margin-bottom: -$gl-spacing-scale-7;
-}
-
-.gl-mb-n8 {
- margin-bottom: -$gl-spacing-scale-8;
-}
-
.gl-hover-border-gray-100:hover {
border-color: $gray-100;
}
diff --git a/app/channels/noteable/notes_channel.rb b/app/channels/noteable/notes_channel.rb
index 021bc3ccd1b..8ce6c15e123 100644
--- a/app/channels/noteable/notes_channel.rb
+++ b/app/channels/noteable/notes_channel.rb
@@ -13,7 +13,6 @@ module Noteable
}).target
return reject if noteable.nil?
- return reject if Feature.disabled?(:action_cable_notes, project || noteable.try(:group))
stream_for noteable
rescue ActiveRecord::RecordNotFound
diff --git a/app/components/pajamas/banner_component.html.haml b/app/components/pajamas/banner_component.html.haml
index c2eeae2d8c9..8a177edddb5 100644
--- a/app/components/pajamas/banner_component.html.haml
+++ b/app/components/pajamas/banner_component.html.haml
@@ -19,5 +19,4 @@
- actions.each do |action|
= action
- %button.gl-button.gl-banner-close.btn-sm.btn-icon.js-close{ @close_options, class: close_class, type: 'button' }
- = sprite_icon('close', size: 16, css_class: 'dismiss-icon')
+ = render Pajamas::ButtonComponent.new(category: :tertiary, variant: close_button_variant, size: :small, icon: 'close', button_options: @close_options)
diff --git a/app/components/pajamas/banner_component.rb b/app/components/pajamas/banner_component.rb
index 1a03f3fdd58..5291db91fb2 100644
--- a/app/components/pajamas/banner_component.rb
+++ b/app/components/pajamas/banner_component.rb
@@ -27,7 +27,7 @@ module Pajamas
@svg_path = svg_path.to_s
@banner_options = banner_options
@button_options = button_options
- @close_options = close_options
+ @close_options = format_options(options: close_options, css_classes: %w[js-close gl-banner-close])
end
VARIANT_OPTIONS = [:introduction, :promotion].freeze
@@ -41,11 +41,11 @@ module Pajamas
classes.join(' ')
end
- def close_class
+ def close_button_variant
if introduction?
- 'btn-confirm btn-confirm-tertiary'
+ :confirm
else
- 'btn-default btn-default-tertiary'
+ :default
end
end
diff --git a/app/controllers/activity_pub/application_controller.rb b/app/controllers/activity_pub/application_controller.rb
new file mode 100644
index 00000000000..f9c2b14fe77
--- /dev/null
+++ b/app/controllers/activity_pub/application_controller.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ class ApplicationController < ::ApplicationController
+ include RoutableActions
+
+ before_action :ensure_feature_flag
+ skip_before_action :authenticate_user!
+ after_action :set_content_type
+
+ def can?(object, action, subject = :global)
+ Ability.allowed?(object, action, subject)
+ end
+
+ def route_not_found
+ head :not_found
+ end
+
+ def set_content_type
+ self.content_type = "application/activity+json"
+ end
+
+ def ensure_feature_flag
+ not_found unless ::Feature.enabled?(:activity_pub)
+ end
+ end
+end
diff --git a/app/controllers/activity_pub/projects/application_controller.rb b/app/controllers/activity_pub/projects/application_controller.rb
new file mode 100644
index 00000000000..e54a457743d
--- /dev/null
+++ b/app/controllers/activity_pub/projects/application_controller.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ module Projects
+ class ApplicationController < ::ActivityPub::ApplicationController
+ before_action :project
+ before_action :ensure_project_feature_flag
+
+ private
+
+ def project
+ return unless params[:project_id] || params[:id]
+
+ path = File.join(params[:namespace_id], params[:project_id] || params[:id])
+
+ @project = find_routable!(Project, path, request.fullpath, extra_authorization_proc: auth_proc)
+ end
+
+ def auth_proc
+ ->(project) { project.public? && !project.pending_delete? }
+ end
+
+ def ensure_project_feature_flag
+ not_found unless ::Feature.enabled?(:activity_pub_project, project)
+ end
+ end
+ end
+end
diff --git a/app/controllers/activity_pub/projects/releases_controller.rb b/app/controllers/activity_pub/projects/releases_controller.rb
new file mode 100644
index 00000000000..7c4c2a0322b
--- /dev/null
+++ b/app/controllers/activity_pub/projects/releases_controller.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ module Projects
+ class ReleasesController < ApplicationController
+ feature_category :release_orchestration
+
+ def index
+ opts = {
+ inbox: nil,
+ outbox: outbox_project_releases_url(@project)
+ }
+
+ render json: ActivityPub::ReleasesActorSerializer.new.represent(@project, opts)
+ end
+
+ def outbox
+ serializer = ActivityPub::ReleasesOutboxSerializer.new.with_pagination(request, response)
+ render json: serializer.represent(releases)
+ end
+
+ private
+
+ def releases(params = {})
+ ReleasesFinder.new(@project, current_user, params).execute
+ end
+ end
+ end
+end
diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb
index 329c4e4921a..b48d6f4f7c2 100644
--- a/app/controllers/admin/abuse_reports_controller.rb
+++ b/app/controllers/admin/abuse_reports_controller.rb
@@ -3,8 +3,11 @@
class Admin::AbuseReportsController < Admin::ApplicationController
feature_category :insider_threat
- before_action :set_status_param, only: :index, if: -> { Feature.enabled?(:abuse_reports_list) }
+ before_action :set_status_param, only: :index
before_action :find_abuse_report, only: [:show, :moderate_user, :update, :destroy]
+ before_action only: :show do
+ push_frontend_feature_flag(:abuse_report_labels)
+ end
def index
@abuse_reports = AbuseReportsFinder.new(params).execute
@@ -12,14 +15,11 @@ class Admin::AbuseReportsController < Admin::ApplicationController
def show; end
- # Kept for backwards compatibility.
- # TODO: See https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/167?work_item_iid=443
- # In 16.4 remove or re-use this endpoint after frontend has migrated to using moderate_user endpoint
def update
- response = Admin::AbuseReports::ModerateUserService.new(@abuse_report, current_user, permitted_params).execute
+ response = Admin::AbuseReports::UpdateService.new(@abuse_report, current_user, permitted_params).execute
if response.success?
- render json: { message: response.message }
+ head :ok
else
render json: { message: response.message }, status: :unprocessable_entity
end
@@ -53,6 +53,6 @@ class Admin::AbuseReportsController < Admin::ApplicationController
end
def permitted_params
- params.permit(:user_action, :close, :reason, :comment)
+ params.permit(:user_action, :close, :reason, :comment, { label_ids: [] })
end
end
diff --git a/app/controllers/admin/jobs_controller.rb b/app/controllers/admin/jobs_controller.rb
index 5ea8c672993..d0ade3e6024 100644
--- a/app/controllers/admin/jobs_controller.rb
+++ b/app/controllers/admin/jobs_controller.rb
@@ -7,18 +7,10 @@ class Admin::JobsController < Admin::ApplicationController
urgency :low
before_action do
- push_frontend_feature_flag(:admin_jobs_vue)
+ push_frontend_feature_flag(:admin_jobs_filter_runner_type, type: :ops)
end
- def index
- # We need all builds for tabs counters
- @all_builds = Ci::JobsFinder.new(current_user: current_user).execute
-
- @scope = params[:scope]
- @builds = Ci::JobsFinder.new(current_user: current_user, params: params).execute
- @builds = @builds.eager_load_everything
- @builds = @builds.page(params[:page]).per(BUILDS_PER_PAGE).without_count
- end
+ def index; end
def cancel_all
Ci::Build.running_or_pending.each(&:cancel)
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index f05b03c2787..1f05e4e7b21 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -221,8 +221,7 @@ class Admin::UsersController < Admin::ApplicationController
respond_to do |format|
result = Users::UpdateService.new(current_user, user_params_with_pass.merge(user: user)).execute do |user|
- user.skip_reconfirmation!
- user.send_only_admin_changed_your_password_notification! if admin_making_changes_for_another_user?
+ prepare_user_for_update(user)
end
if result[:status] == :success
@@ -393,6 +392,12 @@ class Admin::UsersController < Admin::ApplicationController
@can_impersonate = helpers.can_impersonate_user(user, impersonation_in_progress?)
@impersonation_error_text = @can_impersonate ? nil : helpers.impersonation_error_text(user, impersonation_in_progress?)
end
+
+ # method overriden in EE
+ def prepare_user_for_update(user)
+ user.skip_reconfirmation!
+ user.send_only_admin_changed_your_password_notification! if admin_making_changes_for_another_user?
+ end
end
Admin::UsersController.prepend_mod_with('Admin::UsersController')
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 08e4f4956df..7c69f43fa3d 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -38,7 +38,6 @@ class ApplicationController < ActionController::Base
before_action :active_user_check, unless: :devise_controller?
before_action :set_usage_stats_consent_flag
before_action :check_impersonation_availability
- before_action :required_signup_info
# Make sure the `auth_user` is memoized so it can be logged, we do this after
# all other before filters that could have set the user.
@@ -115,6 +114,24 @@ class ApplicationController < ActionController::Base
content_security_policy do |p|
next if p.directives.blank?
+
+ if Rails.env.development? && Feature.enabled?(:vite)
+ vite_host = ViteRuby.instance.config.host
+ vite_port = ViteRuby.instance.config.port
+ vite_origin = "#{vite_host}:#{vite_port}"
+ http_origin = "http://#{vite_origin}"
+ ws_origin = "ws://#{vite_origin}"
+ wss_origin = "wss://#{vite_origin}"
+ gitlab_ws_origin = Gitlab::Utils.append_path(Gitlab.config.gitlab.url, 'vite-dev/')
+ http_path = Gitlab::Utils.append_path(http_origin, 'vite-dev/')
+
+ connect_sources = p.directives['connect-src']
+ p.connect_src(*(Array.wrap(connect_sources) | [ws_origin, wss_origin, http_path]))
+
+ worker_sources = p.directives['worker-src']
+ p.worker_src(*(Array.wrap(worker_sources) | [gitlab_ws_origin, http_path]))
+ end
+
next unless Gitlab::CurrentSettings.snowplow_enabled? && !Gitlab::CurrentSettings.snowplow_collector_hostname.blank?
default_connect_src = p.directives['connect-src'] || p.directives['default-src']
@@ -326,9 +343,12 @@ class ApplicationController < ActionController::Base
end
def check_password_expiration
- return if session[:impersonator_id] || !current_user&.allow_password_authentication?
+ return if session[:impersonator_id]
+ return if current_user.nil?
- redirect_to new_profile_password_path if current_user&.password_expired?
+ if current_user.password_expired? && current_user.allow_password_authentication?
+ redirect_to new_profile_password_path
+ end
end
def active_user_check
@@ -555,15 +575,6 @@ class ApplicationController < ActionController::Base
def context_user
auth_user if strong_memoized?(:auth_user)
end
-
- def required_signup_info
- return unless current_user
- return unless current_user.role_required?
-
- store_location_for :user, request.fullpath
-
- redirect_to users_sign_up_welcome_path
- end
end
ApplicationController.prepend_mod
diff --git a/app/controllers/clusters/agents/dashboard_controller.rb b/app/controllers/clusters/agents/dashboard_controller.rb
new file mode 100644
index 00000000000..1f72aaa4775
--- /dev/null
+++ b/app/controllers/clusters/agents/dashboard_controller.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Agents
+ class DashboardController < ApplicationController
+ include KasCookie
+
+ before_action :check_feature_flag!
+ before_action :find_agent
+ before_action :authorize_read_cluster_agent!
+ before_action :set_kas_cookie, only: [:show], if: -> { current_user }
+
+ feature_category :deployment_management
+
+ def show
+ head :ok
+ end
+
+ private
+
+ def find_agent
+ @agent = ::Clusters::Agent.find(params[:agent_id])
+ end
+
+ def check_feature_flag!
+ not_found unless ::Feature.enabled?(:k8s_dashboard, current_user)
+ end
+
+ def authorize_read_cluster_agent!
+ not_found unless can?(current_user, :read_cluster_agent, @agent)
+ end
+ end
+ end
+end
diff --git a/app/controllers/concerns/access_tokens_actions.rb b/app/controllers/concerns/access_tokens_actions.rb
index 84cbdda1581..de53fd4d835 100644
--- a/app/controllers/concerns/access_tokens_actions.rb
+++ b/app/controllers/concerns/access_tokens_actions.rb
@@ -69,6 +69,7 @@ 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/harbor/access.rb b/app/controllers/concerns/harbor/access.rb
index 211566aeda7..9466952e98e 100644
--- a/app/controllers/concerns/harbor/access.rb
+++ b/app/controllers/concerns/harbor/access.rb
@@ -5,21 +5,13 @@ module Harbor
extend ActiveSupport::Concern
included do
- before_action :harbor_registry_enabled!
before_action :authorize_read_harbor_registry!
- before_action do
- push_frontend_feature_flag(:harbor_registry_integration)
- end
feature_category :integrations
end
private
- def harbor_registry_enabled!
- render_404 unless Feature.enabled?(:harbor_registry_integration, defined?(group) ? group : project)
- end
-
def authorize_read_harbor_registry!
raise NotImplementedError
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 1b49cffd408..28e1056092d 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -174,22 +174,11 @@ module IssuableActions
if Gitlab::Database.read_only? || params[:persist_filter] == 'false'
notes_filter_param || current_user&.notes_filter_for(issuable)
else
- notes_filter = current_user&.set_notes_filter(notes_filter_param, issuable) || notes_filter_param
-
- # We need to invalidate the cache for polling notes otherwise it will
- # ignore the filter.
- # The ideal would be to invalidate the cache for each user.
- issuable.expire_note_etag_cache if notes_filter_updated?
-
- notes_filter
+ current_user&.set_notes_filter(notes_filter_param, issuable) || notes_filter_param
end
end
end
- def notes_filter_updated?
- current_user&.user_preference&.previous_changes&.any?
- end
-
def discussion_cache_context
[current_user&.cache_key, project.team.human_max_access(current_user&.id), 'v2'].join(':')
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index b02a636ff74..5479154f667 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -20,26 +20,10 @@ module IssuableCollections
set_pagination
return if redirect_out_of_range(@issuables, @total_pages)
-
- if params[:label_name].present? && @project
- labels_params = { project_id: @project.id, title: params[:label_name] }
- @labels = LabelsFinder.new(current_user, labels_params).execute
- end
-
- @users = []
- if params[:assignee_id].present?
- assignee = User.find_by_id(params[:assignee_id])
- @users.push(assignee) if assignee
- end
-
- if params[:author_id].present?
- author = User.find_by_id(params[:author_id])
- @users.push(author) if author
- end
end
def set_pagination
- row_count = finder.row_count
+ row_count = request.format.atom? ? -1 : finder.row_count
@issuables = @issuables.page(params[:page])
@issuables = per_page_for_relative_position if params[:sort] == 'relative_position'
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 93cf1d15086..31b3d311865 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -33,9 +33,6 @@ module NotesActions
notes.map { |note| note_json(note) }
end
- # Only present an ETag for the empty response
- ::Gitlab::EtagCaching::Middleware.skip!(response) if notes.present?
-
render json: meta.merge(notes: notes)
end
diff --git a/app/controllers/concerns/onboarding/status.rb b/app/controllers/concerns/onboarding/status.rb
index 5112ebb3b5d..8a99f5a6c12 100644
--- a/app/controllers/concerns/onboarding/status.rb
+++ b/app/controllers/concerns/onboarding/status.rb
@@ -31,12 +31,6 @@ module Onboarding
last_invited_member&.source
end
- def invite_with_tasks_to_be_done?
- return false if members.empty?
-
- MemberTask.for_members(members).exists?
- end
-
private
attr_reader :user
diff --git a/app/controllers/concerns/preferred_language_switcher.rb b/app/controllers/concerns/preferred_language_switcher.rb
index 872652100c9..529d1fb78bd 100644
--- a/app/controllers/concerns/preferred_language_switcher.rb
+++ b/app/controllers/concerns/preferred_language_switcher.rb
@@ -2,6 +2,8 @@
module PreferredLanguageSwitcher
extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
+ include PreferredLanguageSwitcherHelper
private
@@ -11,8 +13,37 @@ module PreferredLanguageSwitcher
def preferred_language
cookies[:preferred_language].presence_in(Gitlab::I18n.available_locales) ||
+ selectable_language(marketing_site_language) ||
+ selectable_language(browser_languages) ||
Gitlab::CurrentSettings.default_preferred_language
end
+
+ def selectable_language(language_options)
+ language_options.find { |lan| ordered_selectable_locales_codes.include?(lan) }
+ end
+
+ def ordered_selectable_locales_codes
+ ordered_selectable_locales.pluck(:value) # rubocop:disable CodeReuse/ActiveRecord
+ end
+
+ def browser_languages
+ formatted_http_language_header = request.env['HTTP_ACCEPT_LANGUAGE']&.tr('-', '_')
+
+ return [] unless formatted_http_language_header
+
+ formatted_http_language_header.split(%r{[;,]}).reject { |str| str.start_with?('q') }
+ end
+ strong_memoize_attr :browser_languages
+
+ def marketing_site_language
+ return [] unless params[:glm_source]
+
+ locale = params[:glm_source].scan(%r{(\w{2})-(\w{2})}).flatten
+
+ return [] if locale.empty?
+
+ [locale[0], "#{locale[0]}_#{locale[1]}"]
+ end
end
PreferredLanguageSwitcher.prepend_mod
diff --git a/app/controllers/concerns/search_rate_limitable.rb b/app/controllers/concerns/search_rate_limitable.rb
index 1105e9bbbfd..e32fc2f4dd6 100644
--- a/app/controllers/concerns/search_rate_limitable.rb
+++ b/app/controllers/concerns/search_rate_limitable.rb
@@ -11,7 +11,8 @@ module SearchRateLimitable
# scopes to get counts, we apply rate limits on the search scope if it is present.
#
# If abusive search is detected, we have stricter limits and ignore the search scope.
- check_rate_limit!(:search_rate_limit, scope: [current_user, safe_search_scope].compact)
+ check_rate_limit!(:search_rate_limit, scope: [current_user, safe_search_scope].compact,
+ users_allowlist: Gitlab::CurrentSettings.current_application_settings.search_rate_limit_allowlist)
else
check_rate_limit!(:search_rate_limit_unauthenticated, scope: [request.ip])
end
diff --git a/app/controllers/concerns/verifies_with_email.rb b/app/controllers/concerns/verifies_with_email.rb
index 6affd7bb4cc..cb8aef11e8d 100644
--- a/app/controllers/concerns/verifies_with_email.rb
+++ b/app/controllers/concerns/verifies_with_email.rb
@@ -9,7 +9,6 @@ module VerifiesWithEmail
included do
prepend_before_action :verify_with_email, only: :create, unless: -> { skip_verify_with_email? }
- skip_before_action :required_signup_info, only: :successful_verification
end
# rubocop:disable Metrics/PerceivedComplexity
diff --git a/app/controllers/concerns/web_hooks/hook_log_actions.rb b/app/controllers/concerns/web_hooks/hook_log_actions.rb
index 321cee5a452..dcea7596790 100644
--- a/app/controllers/concerns/web_hooks/hook_log_actions.rb
+++ b/app/controllers/concerns/web_hooks/hook_log_actions.rb
@@ -20,8 +20,13 @@ module WebHooks
end
def retry
- execute_hook
- redirect_to after_retry_redirect_path
+ if hook_log.url_current?
+ execute_hook
+ redirect_to after_retry_redirect_path
+ else
+ flash[:warning] = _('The hook URL has changed, and this log entry cannot be retried')
+ redirect_back(fallback_location: after_retry_redirect_path)
+ end
end
private
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index f7c7ee62c1a..5ceabaa734a 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -7,7 +7,6 @@ class ConfirmationsController < Devise::ConfirmationsController
include GoogleAnalyticsCSP
include GoogleSyndicationCSP
- skip_before_action :required_signup_info
prepend_before_action :check_recaptcha, only: :create
before_action :load_recaptcha, only: :new
diff --git a/app/controllers/groups/email_campaigns_controller.rb b/app/controllers/groups/email_campaigns_controller.rb
deleted file mode 100644
index 8ae429de490..00000000000
--- a/app/controllers/groups/email_campaigns_controller.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-# frozen_string_literal: true
-
-class Groups::EmailCampaignsController < Groups::ApplicationController
- EMAIL_CAMPAIGNS_SCHEMA_URL = 'iglu:com.gitlab/email_campaigns/jsonschema/1-0-0'
-
- feature_category :experimentation_activation
- urgency :low
-
- before_action :check_params
-
- def index
- track_click
- redirect_to redirect_link
- end
-
- private
-
- def track_click
- if Gitlab.com?
- message = Gitlab::Email::Message::InProductMarketing.for(@track).new(group: group, user: current_user, series: @series)
-
- data = {
- namespace_id: group.id,
- track: @track.to_s,
- series: @series,
- subject_line: message.subject_line
- }
- context = SnowplowTracker::SelfDescribingJson.new(EMAIL_CAMPAIGNS_SCHEMA_URL, data)
-
- ::Gitlab::Tracking.event(self.class.name, 'click', context: [context], user: current_user, namespace: group)
- else
- ::Users::InProductMarketingEmail.save_cta_click(current_user, @track, @series)
- end
- end
-
- def redirect_link
- case @track
- when :create
- create_track_url
- when :verify
- project_pipelines_url(group.projects.first)
- when :trial, :trial_short
- 'https://about.gitlab.com/free-trial/'
- when :team, :team_short
- group_group_members_url(group)
- when :admin_verify
- project_settings_ci_cd_path(group.projects.first, anchor: 'js-runners-settings')
- end
- end
-
- def create_track_url
- [
- new_project_url,
- new_project_url(anchor: 'import_project'),
- help_page_url('user/project/repository/repository_mirroring')
- ][@series]
- end
-
- def check_params
- @track = params[:track]&.to_sym
- @series = params[:series]&.to_i
-
- track_valid = @track.in?(Namespaces::InProductMarketingEmailsService::TRACKS.keys)
- return render_404 unless track_valid
-
- series_valid = @series.in?(0..Namespaces::InProductMarketingEmailsService::TRACKS[@track][:interval_days].size - 1)
- render_404 unless series_valid
- end
-end
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index f927cae90b1..9535b83e769 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -98,7 +98,10 @@ class Groups::LabelsController < Groups::ApplicationController
end
def label_params
- params.require(:label).permit(:title, :description, :color)
+ allowed = [:title, :description, :color]
+ allowed << :lock_on_merge if @group.supports_lock_on_merge?
+
+ params.require(:label).permit(allowed)
end
def redirect_back_or_group_labels_path(options = {})
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
index b3539da8429..3600a0fbed5 100644
--- a/app/controllers/groups/runners_controller.rb
+++ b/app/controllers/groups/runners_controller.rb
@@ -42,6 +42,8 @@ class Groups::RunnersController < Groups::ApplicationController
@runner ||= Ci::RunnersFinder.new(current_user: current_user, params: group_params).execute
.except(:limit, :offset)
.find(params[:id])
+ rescue Gitlab::Access::AccessDeniedError
+ nil
end
def runner_params
diff --git a/app/controllers/groups/work_items_controller.rb b/app/controllers/groups/work_items_controller.rb
index d1e15c81471..bd85f12119b 100644
--- a/app/controllers/groups/work_items_controller.rb
+++ b/app/controllers/groups/work_items_controller.rb
@@ -7,5 +7,9 @@ module Groups
def index
not_found unless Feature.enabled?(:namespace_level_work_items, group)
end
+
+ def show
+ not_found unless Feature.enabled?(:namespace_level_work_items, group)
+ end
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 344de886a93..edc590e1370 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -37,7 +37,6 @@ class GroupsController < Groups::ApplicationController
push_frontend_feature_flag(:frontend_caching, group)
push_force_frontend_feature_flag(:work_items, group.work_items_feature_flag_enabled?)
push_frontend_feature_flag(:issues_grid_view)
- push_frontend_feature_flag(:new_graphql_users_autocomplete, group)
end
before_action only: :merge_requests do
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 9635e476510..df8128f24fe 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -9,7 +9,7 @@ class HelpController < ApplicationController
# Taken from Jekyll
# https://github.com/jekyll/jekyll/blob/3.5-stable/lib/jekyll/document.rb#L13
- YAML_FRONT_MATTER_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m.freeze
+ YAML_FRONT_MATTER_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m
def index
@help_index = get_markdown_without_frontmatter(path_to_doc('index.md'))
diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb
index e17cd00d053..ba2743e1002 100644
--- a/app/controllers/import/bitbucket_server_controller.rb
+++ b/app/controllers/import/bitbucket_server_controller.rb
@@ -22,8 +22,8 @@ class Import::BitbucketServerController < Import::BaseController
# (https://community.atlassian.com/t5/Answers-Developer-Questions/stash-repository-names/qaq-p/499054)
#
# Bitbucket Server starts personal project names with a tilde.
- VALID_BITBUCKET_PROJECT_CHARS = /\A~?[\w\-\.\s]+\z/.freeze
- VALID_BITBUCKET_CHARS = /\A[\w\-\.\s]+\z/.freeze
+ VALID_BITBUCKET_PROJECT_CHARS = /\A~?[\w\-\.\s]+\z/
+ VALID_BITBUCKET_CHARS = /\A[\w\-\.\s]+\z/
def new
end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 8a8ae38c6f3..c058329680a 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -83,8 +83,6 @@ class InvitesController < ApplicationController
def authenticate_user!
return if current_user
- store_location_for(:user, invite_details[:path]) if member
-
if user_sign_up?
set_session_invite_params
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index a1d4df6ff48..a541e7e703f 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -14,7 +14,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
# include the call to session.delete
def new
if pre_auth.authorizable?
- if skip_authorization? || matching_token?
+ if skip_authorization? || (matching_token? && pre_auth.client.application.confidential?)
auth = authorization.authorize
parsed_redirect_uri = URI.parse(auth.redirect_uri)
session.delete(:user_return_to)
diff --git a/app/controllers/organizations/application_controller.rb b/app/controllers/organizations/application_controller.rb
index 568cfe6399d..d3c3e878bdf 100644
--- a/app/controllers/organizations/application_controller.rb
+++ b/app/controllers/organizations/application_controller.rb
@@ -2,7 +2,7 @@
module Organizations
class ApplicationController < ::ApplicationController
- skip_before_action :authenticate_user!
+ before_action :check_feature_flag!
before_action :organization
layout 'organization'
@@ -16,11 +16,16 @@ module Organizations
end
strong_memoize_attr :organization
- def authorize_action!(action)
- return if Feature.enabled?(:ui_for_organizations, current_user) &&
- can?(current_user, action, organization)
+ def check_feature_flag!
+ access_denied! unless Feature.enabled?(:ui_for_organizations, current_user)
+ end
+
+ def authorize_create_organization!
+ access_denied! unless can?(current_user, :create_organization)
+ end
- access_denied!
+ def authorize_read_organization!
+ access_denied! unless can?(current_user, :read_organization, organization)
end
end
end
diff --git a/app/controllers/organizations/organizations_controller.rb b/app/controllers/organizations/organizations_controller.rb
index 650ec97c264..88c6c9b3cef 100644
--- a/app/controllers/organizations/organizations_controller.rb
+++ b/app/controllers/organizations/organizations_controller.rb
@@ -4,10 +4,20 @@ module Organizations
class OrganizationsController < ApplicationController
feature_category :cell
- before_action { authorize_action!(:read_organization) }
+ skip_before_action :authenticate_user!, except: [:index, :new]
- def show; end
+ def index; end
- def groups_and_projects; end
+ def new
+ authorize_create_organization!
+ end
+
+ def show
+ authorize_read_organization!
+ end
+
+ def groups_and_projects
+ authorize_read_organization!
+ end
end
end
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index 38839497fb6..d1ca16bd8fb 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -43,6 +43,7 @@ class PasswordsController < Devise::PasswordsController
resource.password_expires_at = nil
resource.save(validate: false) if resource.changed?
else
+ log_audit_reset_failure(@user)
track_weak_password_error(@user, self.class.name, 'create')
end
end
@@ -50,6 +51,9 @@ class PasswordsController < Devise::PasswordsController
protected
+ # overriden in EE
+ def log_audit_reset_failure(_user); end
+
def resource_from_email
email = resource_params[:email]
self.resource = resource_class.find_by_email(email)
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index 02f7dbf8e6f..57e5ca4d55a 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -25,7 +25,7 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end
def user_params
- params.require(:user).permit(:notification_email, :email_opted_in, :notified_of_own_activity)
+ params.require(:user).permit(:notification_email, :notified_of_own_activity)
end
private
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 4b6e2f768fa..0e4d9f3c154 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -61,6 +61,7 @@ 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/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 3e8555a4ed1..931070ecdd4 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -55,6 +55,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:gitpod_enabled,
:render_whitespace_in_code,
:project_shortcut_buttons,
+ :keyboard_shortcuts_enabled,
:markdown_surround_selection,
:markdown_automatic_lists,
:use_new_navigation,
diff --git a/app/controllers/projects/alerting/notifications_controller.rb b/app/controllers/projects/alerting/notifications_controller.rb
index 281ac14d3ce..b596cd74b03 100644
--- a/app/controllers/projects/alerting/notifications_controller.rb
+++ b/app/controllers/projects/alerting/notifications_controller.rb
@@ -66,15 +66,11 @@ module Projects
def integration
AlertManagement::HttpIntegrationsFinder.new(
project,
- endpoint_identifier: endpoint_identifier,
+ endpoint_identifier: params[:endpoint_identifier],
active: true
).execute.first
end
- def endpoint_identifier
- params[:endpoint_identifier] || AlertManagement::HttpIntegration::LEGACY_IDENTIFIERS
- end
-
def notification_payload
@notification_payload ||= params.permit![:notification]
end
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 94cd324f312..2d2712ebe4d 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -45,6 +45,8 @@ class Projects::CommitsController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord
def signatures
+ Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/424527')
+
respond_to do |format|
format.json do
render json: {
diff --git a/app/controllers/projects/environments/sample_metrics_controller.rb b/app/controllers/projects/environments/sample_metrics_controller.rb
deleted file mode 100644
index 80344c83ab7..00000000000
--- a/app/controllers/projects/environments/sample_metrics_controller.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-class Projects::Environments::SampleMetricsController < Projects::ApplicationController
- feature_category :metrics
- urgency :low
-
- def query
- result = Metrics::SampleMetricsService.new(params[:identifier], range_start: params[:start], range_end: params[:end]).query
-
- if result
- render json: { "status": "success", "data": { "resultType": "matrix", "result": result } }
- else
- render_404
- end
- end
-end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 127fe40b0e3..aabea122fb6 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -8,14 +8,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project'
- before_action only: [:show] do
- push_frontend_feature_flag(:environment_details_vue, @project)
- end
-
- before_action only: [:index, :edit, :new] do
- push_frontend_feature_flag(:flux_resource_for_environment)
- end
-
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_stop_environment!, only: [:stop]
@@ -113,10 +105,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController
job = stop_actions.first if stop_actions&.count == 1
action_or_env_url =
- if job.instance_of?(::Ci::Build)
- polymorphic_url([project, job])
- elsif job.instance_of?(::Ci::Bridge)
- project_pipeline_url(project, job.pipeline_id)
+ if job
+ project_job_url(project, job)
else
project_environment_url(project, @environment)
end
diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb
index e73e2a38149..fce7de4c0de 100644
--- a/app/controllers/projects/graphs_controller.rb
+++ b/app/controllers/projects/graphs_controller.rb
@@ -34,7 +34,7 @@ class Projects::GraphsController < Projects::ApplicationController
{
author_name: commit.author_name,
author_email: commit.author_email,
- date: commit.committed_date.strftime("%Y-%m-%d")
+ date: commit.committed_date.to_date.iso8601
}
end
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
index 6109e29b169..69d349b1f1d 100644
--- a/app/controllers/projects/incidents_controller.rb
+++ b/app/controllers/projects/incidents_controller.rb
@@ -12,6 +12,7 @@ class Projects::IncidentsController < Projects::ApplicationController
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?)
end
feature_category :incident_management
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 83947c443f4..9abcc108ace 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -62,7 +62,6 @@ class Projects::IssuesController < Projects::ApplicationController
before_action only: [:index, :service_desk] do
push_frontend_feature_flag(:or_issuable_queries, project)
push_frontend_feature_flag(:frontend_caching, project&.group)
- push_frontend_feature_flag(:new_graphql_users_autocomplete, project)
end
before_action only: :show do
@@ -73,7 +72,7 @@ class Projects::IssuesController < Projects::ApplicationController
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_frontend_feature_flag(:action_cable_notes, project)
+ push_force_frontend_feature_flag(:linked_work_items, project.linked_work_items_feature_flag_enabled?)
end
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
@@ -114,12 +113,6 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to do |format|
format.html
format.atom { render layout: 'xml' }
- format.json do
- render json: {
- html: view_to_html_string("projects/issues/_issues"),
- labels: @labels.as_json(methods: :text_color)
- }
- end
end
end
@@ -282,7 +275,6 @@ class Projects::IssuesController < Projects::ApplicationController
def service_desk
@issues = @issuables
- @users.push(User.support_bot)
end
protected
@@ -433,7 +425,7 @@ class Projects::IssuesController < Projects::ApplicationController
if service_desk?
options.reject! { |key| key == 'author_username' || key == 'author_id' }
- options[:author_id] = User.support_bot
+ options[:author_id] = Users::Internal.support_bot
end
options
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 4e0b304a2ee..802ffd99e41 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -8,8 +8,8 @@ class Projects::JobsController < Projects::ApplicationController
urgency :low, [:index, :show, :trace, :retry, :play, :cancel, :unschedule, :erase, :raw]
- before_action :find_job_as_build, except: [:index, :play, :retry]
- before_action :find_job_as_processable, only: [:play, :retry]
+ before_action :find_job_as_build, except: [:index, :play, :retry, :show]
+ before_action :find_job_as_processable, only: [:play, :retry, :show]
before_action :authorize_read_build_trace!, only: [:trace, :raw]
before_action :authorize_read_build!
before_action :authorize_update_build!,
@@ -27,17 +27,13 @@ class Projects::JobsController < Projects::ApplicationController
feature_category :continuous_integration
urgency :low
- def index
- # We need all builds for tabs counters
- @all_builds = Ci::JobsFinder.new(current_user: current_user, project: @project).execute
-
- @scope = params[:scope]
- @builds = Ci::JobsFinder.new(current_user: current_user, project: @project, params: params).execute
- @builds = @builds.eager_load_everything
- @builds = @builds.page(params[:page]).per(30).without_count
- end
+ def index; end
def show
+ if @build.instance_of?(::Ci::Bridge)
+ redirect_to project_pipeline_path(@build.downstream_pipeline.project, @build.downstream_pipeline.id)
+ end
+
respond_to do |format|
format.html
format.json do
@@ -74,6 +70,8 @@ class Projects::JobsController < Projects::ApplicationController
end
def retry
+ Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/424184')
+
response = Ci::RetryJobService.new(project, current_user).execute(@build)
if response.success?
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 67cff16a76b..e62f912e0f7 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -155,7 +155,10 @@ class Projects::LabelsController < Projects::ApplicationController
protected
def label_params
- params.require(:label).permit(:title, :description, :color)
+ allowed = [:title, :description, :color]
+ allowed << :lock_on_merge if @project.supports_lock_on_merge?
+
+ params.require(:label).permit(allowed)
end
def label
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 6d1b1ced4eb..81ff6c215f9 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -14,6 +14,18 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
private
+ # Normally the methods with `check_(\w+)_available!` pattern are
+ # handled by the `method_missing` defined in `ProjectsController::ApplicationController`
+ # but that logic does not take the member roles into account, therefore, we handle this
+ # case here manually.
+ def check_merge_requests_available!
+ render_404 if project_policy.merge_requests_disabled?
+ end
+
+ def project_policy
+ ProjectPolicy.new(current_user, project)
+ end
+
def merge_request
@issuable =
@merge_request ||=
diff --git a/app/controllers/projects/merge_requests/conflicts_controller.rb b/app/controllers/projects/merge_requests/conflicts_controller.rb
index 66a358963e2..26f4286233a 100644
--- a/app/controllers/projects/merge_requests/conflicts_controller.rb
+++ b/app/controllers/projects/merge_requests/conflicts_controller.rb
@@ -67,7 +67,7 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap
flash[:notice] = _('All merge conflicts were resolved. The merge request can now be merged.')
- render json: { redirect_to: project_merge_request_url(@project, @merge_request, resolved_conflicts: true) }
+ render json: { redirect_to: project_merge_request_path(@project, @merge_request, resolved_conflicts: true) }
rescue Gitlab::Git::Conflict::Resolver::ResolutionError => e
render status: :bad_request, json: { message: e.message }
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 30168558eff..53fd7256b19 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -45,12 +45,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:sast_reports_in_inline_diff, project)
push_frontend_feature_flag(:mr_experience_survey, project)
push_frontend_feature_flag(:saved_replies, current_user)
- push_frontend_feature_flag(:code_quality_inline_drawer, project)
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(:review_apps_redeploy_mr_widget, project)
push_frontend_feature_flag(:ci_job_failures_in_mr, project)
- push_frontend_feature_flag(:action_cable_notes, project)
+ push_frontend_feature_flag(:mr_pipelines_graphql, project)
end
before_action only: [:edit] do
@@ -106,11 +104,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
respond_to do |format|
format.html
format.atom { render layout: 'xml' }
- format.json do
- render json: {
- html: view_to_html_string("projects/merge_requests/_merge_requests")
- }
- end
end
end
@@ -389,20 +382,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
private
- # NOTE: Remove this disable with add_prepared_state_to_mr FF removal
- # rubocop: disable Metrics/AbcSize
def show_merge_request
close_merge_request_if_no_source_project
@merge_request.check_mergeability(async: true)
- # NOTE: Remove the created_at check when removing the FF check
- if ::Feature.enabled?(:add_prepared_state_to_mr, @merge_request.project) &&
- @merge_request.created_at < 5.minutes.ago &&
- !@merge_request.prepared?
-
- @merge_request.prepare
- end
-
respond_to do |format|
format.html do
# use next to appease Rubocop
@@ -446,7 +429,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
end
end
- # rubocop: enable Metrics/AbcSize
def render_html_page
preload_assignees_for_render(@merge_request)
diff --git a/app/controllers/projects/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb
index acbd26cbdf6..a24273488fb 100644
--- a/app/controllers/projects/mirrors_controller.rb
+++ b/app/controllers/projects/mirrors_controller.rb
@@ -81,6 +81,7 @@ class Projects::MirrorsController < Projects::ApplicationController
only_protected_branches
keep_divergent_refs
auth_method
+ user
password
ssh_known_hosts
regenerate_ssh_private_key
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 7fcdf220bd2..3d8a787afcb 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -14,8 +14,7 @@ class Projects::NotesController < Projects::ApplicationController
feature_category :team_planning, [:index, :create, :update, :destroy, :delete_attachment, :toggle_award_emoji]
feature_category :code_review_workflow, [:resolve, :unresolve, :outdated_line_change]
- urgency :medium, [:index]
- urgency :low, [:create, :update, :destroy, :resolve, :unresolve, :toggle_award_emoji, :outdated_line_change]
+ urgency :low
override :feature_category
def feature_category
diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb
index 02579cd4283..5b32eb8e58e 100644
--- a/app/controllers/projects/pages_controller.rb
+++ b/app/controllers/projects/pages_controller.rb
@@ -65,7 +65,15 @@ class Projects::PagesController < Projects::ApplicationController
end
def project_params_attributes
- [:pages_https_only, { project_setting_attributes: [:pages_unique_domain_enabled] }]
+ [
+ :pages_https_only,
+ { project_setting_attributes: project_setting_attributes }
+ ]
+ end
+
+ # overridden in EE
+ def project_setting_attributes
+ [:pages_unique_domain_enabled]
end
end
diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb
index 42b6d83ee85..83a64579446 100644
--- a/app/controllers/projects/pipeline_schedules_controller.rb
+++ b/app/controllers/projects/pipeline_schedules_controller.rb
@@ -9,7 +9,6 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
before_action :authorize_create_pipeline_schedule!, only: [:new, :create]
before_action :authorize_update_pipeline_schedule!, only: [:edit, :update]
before_action :authorize_admin_pipeline_schedule!, only: [:take_ownership, :destroy]
- before_action :push_schedule_feature_flag, only: [:index, :new, :edit]
feature_category :continuous_integration
urgency :low
@@ -120,8 +119,4 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
def authorize_admin_pipeline_schedule!
return access_denied! unless can?(current_user, :admin_pipeline_schedule, schedule)
end
-
- def push_schedule_feature_flag
- push_frontend_feature_flag(:pipeline_schedules_vue, @project)
- end
end
diff --git a/app/controllers/projects/pipelines/tests_controller.rb b/app/controllers/projects/pipelines/tests_controller.rb
index d77cf095a4f..4b522c88023 100644
--- a/app/controllers/projects/pipelines/tests_controller.rb
+++ b/app/controllers/projects/pipelines/tests_controller.rb
@@ -50,7 +50,7 @@ module Projects
end
def test_suite
- suite = builds.sum do |build|
+ suite = builds.sum(Gitlab::Ci::Reports::TestSuite.new) do |build|
test_report = build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new)
test_report.get_suite(build.test_suite_name)
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index a96ee2215c2..036ea45cc78 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -3,7 +3,6 @@
class Projects::PipelinesController < Projects::ApplicationController
include ::Gitlab::Utils::StrongMemoize
include ProductAnalyticsTracking
- include ProductAnalyticsTracking
include ProjectStatsRefreshConflictsGuard
urgency :low, [
@@ -34,9 +33,9 @@ class Projects::PipelinesController < Projects::ApplicationController
label: 'redis_hll_counters.analytics.analytics_total_unique_counts_monthly',
destinations: %i[redis_hll snowplow]
- track_event :charts, name: 'p_analytics_ci_cd_pipelines', conditions: -> { should_track_ci_cd_pipelines? }
- track_event :charts, name: 'p_analytics_ci_cd_deployment_frequency', conditions: -> { should_track_ci_cd_deployment_frequency? }
- track_event :charts, name: 'p_analytics_ci_cd_lead_time', conditions: -> { should_track_ci_cd_lead_time? }
+ track_internal_event :charts, name: 'p_analytics_ci_cd_pipelines', conditions: -> { should_track_ci_cd_pipelines? }
+ track_internal_event :charts, name: 'p_analytics_ci_cd_deployment_frequency', conditions: -> { should_track_ci_cd_deployment_frequency? }
+ track_internal_event :charts, name: 'p_analytics_ci_cd_lead_time', conditions: -> { should_track_ci_cd_lead_time? }
track_event :charts, name: 'p_analytics_ci_cd_time_to_restore_service', conditions: -> { should_track_ci_cd_time_to_restore_service? }
track_event :charts, name: 'p_analytics_ci_cd_change_failure_rate', conditions: -> { should_track_ci_cd_change_failure_rate? }
diff --git a/app/controllers/projects/prometheus/alerts_controller.rb b/app/controllers/projects/prometheus/alerts_controller.rb
deleted file mode 100644
index 80a8dbf4729..00000000000
--- a/app/controllers/projects/prometheus/alerts_controller.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module Prometheus
- class AlertsController < Projects::ApplicationController
- respond_to :json
-
- protect_from_forgery except: [:notify]
-
- skip_before_action :project, only: [:notify]
-
- prepend_before_action :repository, :project_without_auth, only: [:notify]
-
- before_action :authorize_read_prometheus_alerts!, except: [:notify]
-
- feature_category :incident_management
- urgency :low
-
- def notify
- token = extract_alert_manager_token(request)
- result = notify_service.execute(token)
-
- head result.http_status
- end
-
- private
-
- def notify_service
- Projects::Prometheus::Alerts::NotifyService
- .new(project, params.permit!)
- end
-
- def extract_alert_manager_token(request)
- Doorkeeper::OAuth::Token.from_bearer_authorization(request)
- end
-
- def project_without_auth
- @project ||= Project
- .find_by_full_path("#{params[:namespace_id]}/#{params[:project_id]}")
- end
- end
- end
-end
diff --git a/app/controllers/projects/service_desk_controller.rb b/app/controllers/projects/service_desk_controller.rb
index b1e30e7a45b..ca3cecf5949 100644
--- a/app/controllers/projects/service_desk_controller.rb
+++ b/app/controllers/projects/service_desk_controller.rb
@@ -36,7 +36,7 @@ class Projects::ServiceDeskController < Projects::ApplicationController
service_desk_settings = project.service_desk_setting
{
- service_desk_address: project.service_desk_address,
+ service_desk_address: project.service_desk_system_address,
service_desk_enabled: project.service_desk_enabled,
issue_template_key: service_desk_settings&.issue_template_key,
template_file_missing: service_desk_settings&.issue_template_missing?,
diff --git a/app/controllers/projects/tracing_controller.rb b/app/controllers/projects/tracing_controller.rb
deleted file mode 100644
index 45e773bf62b..00000000000
--- a/app/controllers/projects/tracing_controller.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- class TracingController < Projects::ApplicationController
- include ::Observability::ContentSecurityPolicy
-
- feature_category :tracing
-
- before_action :check_tracing_enabled
-
- def index; end
-
- def show
- @trace_id = params[:id]
- end
-
- private
-
- def check_tracing_enabled
- render_404 unless Gitlab::Observability.tracing_enabled?(project)
- end
- end
-end
diff --git a/app/controllers/projects/work_items_controller.rb b/app/controllers/projects/work_items_controller.rb
index 7da31c199a1..c3986be31b0 100644
--- a/app/controllers/projects/work_items_controller.rb
+++ b/app/controllers/projects/work_items_controller.rb
@@ -12,6 +12,7 @@ class Projects::WorkItemsController < 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_force_frontend_feature_flag(:saved_replies, current_user)
+ push_force_frontend_feature_flag(:linked_work_items, project&.linked_work_items_feature_flag_enabled?)
end
feature_category :team_planning
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 2ad0f11dc91..6a246219f7d 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -46,6 +46,7 @@ class ProjectsController < Projects::ApplicationController
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
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_force_frontend_feature_flag(:linked_work_items, @project&.linked_work_items_feature_flag_enabled?)
end
layout :determine_layout
diff --git a/app/controllers/pwa_controller.rb b/app/controllers/pwa_controller.rb
index bb47bdc8050..8de1b10e1f1 100644
--- a/app/controllers/pwa_controller.rb
+++ b/app/controllers/pwa_controller.rb
@@ -6,7 +6,7 @@ class PwaController < ApplicationController # rubocop:disable Gitlab/NamespacedC
feature_category :navigation
urgency :low
- skip_before_action :authenticate_user!, :required_signup_info
+ skip_before_action :authenticate_user!
def manifest
end
diff --git a/app/controllers/registrations/welcome_controller.rb b/app/controllers/registrations/welcome_controller.rb
index 68f8248d114..f7a601ec0bd 100644
--- a/app/controllers/registrations/welcome_controller.rb
+++ b/app/controllers/registrations/welcome_controller.rb
@@ -8,7 +8,9 @@ module Registrations
include ::Gitlab::Utils::StrongMemoize
layout 'minimal'
- skip_before_action :required_signup_info, :check_two_factor_requirement
+ # 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
@@ -43,7 +45,7 @@ module Registrations
end
def completed_welcome_step?
- current_user.role.present? && !current_user.setup_for_company.nil?
+ !current_user.setup_for_company.nil?
end
def update_params
@@ -61,9 +63,7 @@ module Registrations
end
def update_success_path
- if onboarding_status.invite_with_tasks_to_be_done?
- issues_dashboard_path(assignee_username: current_user.username)
- elsif onboarding_status.continue_full_onboarding? # trials/regular registration on .com
+ 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)
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index d8064bbbe82..a8b5ca81f49 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -12,6 +12,8 @@ class RegistrationsController < Devise::RegistrationsController
include PreferredLanguageSwitcher
include Gitlab::Tracking::Helpers::WeakPasswordErrorEvent
include SkipsAlreadySignedInMessage
+ include Gitlab::RackLoadBalancingHelpers
+ include ::Gitlab::Utils::StrongMemoize
layout 'devise'
@@ -46,7 +48,6 @@ class RegistrationsController < Devise::RegistrationsController
accept_pending_invitations if new_user.persisted?
persist_accepted_terms_if_required(new_user)
- set_role_required(new_user)
send_custom_confirmation_instructions
track_weak_password_error(new_user, self.class.name, 'create')
@@ -89,10 +90,6 @@ class RegistrationsController < Devise::RegistrationsController
Users::RespondToTermsService.new(new_user, terms).execute(accepted: true)
end
- def set_role_required(new_user)
- new_user.set_role_required! if new_user.persisted?
- end
-
def destroy_confirmation_valid?
if current_user.confirm_deletion_with_password?
current_user.valid_password?(params[:password])
@@ -138,7 +135,7 @@ class RegistrationsController < Devise::RegistrationsController
if identity_verification_enabled?
session[:verification_user_id] = resource.id # This is needed to find the user on the identity verification page
- User.sticking.stick_or_unstick_request(request.env, :user, resource.id)
+ load_balancer_stick_request(::User, :user, resource.id)
return identity_verification_redirect_path
end
@@ -251,6 +248,7 @@ class RegistrationsController < Devise::RegistrationsController
sign_up_params[:email] == invite_email
end
+ strong_memoize_attr :registered_with_invite_email?
def load_recaptcha
Gitlab::Recaptcha.load_configurations!
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 6c1d9a20570..d247490402f 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -35,10 +35,6 @@ class SearchController < ApplicationController
update_scope_for_code_search
end
- before_action only: :show do
- push_frontend_feature_flag(:search_projects_hide_archived, current_user)
- end
-
rescue_from ActiveRecord::QueryCanceled, with: :render_timeout
layout 'search'
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
index 6c5e709a98a..4f61088ab17 100644
--- a/app/controllers/sent_notifications_controller.rb
+++ b/app/controllers/sent_notifications_controller.rb
@@ -29,7 +29,7 @@ class SentNotificationsController < ApplicationController
def unsubscribe_and_redirect
noteable.unsubscribe(@sent_notification.recipient, @sent_notification.project)
- if noteable.is_a?(Issue) && @sent_notification.recipient_id == User.support_bot.id
+ if noteable.is_a?(Issue) && @sent_notification.recipient_id == Users::Internal.support_bot.id
noteable.unsubscribe_email_participant(noteable.external_author)
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 66ace16400a..afbadc7f4ac 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -16,6 +16,8 @@ class SessionsController < Devise::SessionsController
include GoogleSyndicationCSP
include PreferredLanguageSwitcher
include SkipsAlreadySignedInMessage
+ include AcceptsPendingInvitations
+ extend ::Gitlab::Utils::Override
skip_before_action :check_two_factor_requirement, only: [:destroy]
skip_before_action :check_password_expiration, only: [:destroy]
@@ -78,6 +80,8 @@ class SessionsController < Devise::SessionsController
flash[:notice] = nil
end
+ accept_pending_invitations
+
log_audit_event(current_user, resource, with: authentication_method)
log_user_activity(current_user)
end
@@ -94,6 +98,13 @@ class SessionsController < Devise::SessionsController
private
+ override :after_pending_invitations_hook
+ def after_pending_invitations_hook
+ member = resource.members.last
+
+ store_location_for(:user, member.source.activity_path) if member
+ end
+
def captcha_enabled?
request.headers[CAPTCHA_HEADER] && helpers.recaptcha_enabled?
end
diff --git a/app/controllers/users/namespace_visits_controller.rb b/app/controllers/users/namespace_visits_controller.rb
new file mode 100644
index 00000000000..7c96d78e26e
--- /dev/null
+++ b/app/controllers/users/namespace_visits_controller.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Users
+ class NamespaceVisitsController < ApplicationController
+ 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
+ head :ok
+ end
+ end
+end
diff --git a/app/experiments/ios_specific_templates_experiment.rb b/app/experiments/ios_specific_templates_experiment.rb
index 1731fa87be8..5bd4a3d0287 100644
--- a/app/experiments/ios_specific_templates_experiment.rb
+++ b/app/experiments/ios_specific_templates_experiment.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class IosSpecificTemplatesExperiment < ApplicationExperiment
+ control
+
before_run(if: :skip_experiment) { throw(:abort) } # rubocop:disable Cop/BanCatchThrow
private
diff --git a/app/finders/abuse_reports_finder.rb b/app/finders/abuse_reports_finder.rb
index 43cebd16d92..ee14372fcd9 100644
--- a/app/finders/abuse_reports_finder.rb
+++ b/app/finders/abuse_reports_finder.rb
@@ -17,6 +17,8 @@ class AbuseReportsFinder
end
def execute
+ @reports = reports.with_labels if Feature.enabled?(:abuse_report_labels)
+
filter_reports
aggregate_reports
sort_reports
@@ -27,30 +29,16 @@ class AbuseReportsFinder
private
def filter_reports
- if Feature.disabled?(:abuse_reports_list)
- filter_by_user_id
- return
- end
-
filter_by_status
filter_by_user
filter_by_reporter
filter_by_category
end
- def filter_by_user_id
- return unless params[:user_id].present?
-
- @reports = @reports.by_user_id(params[:user_id])
- end
-
def filter_by_status
return unless params[:status].present?
- status = params[:status]
- status = STATUS_OPEN unless status.in?(AbuseReport.statuses.keys)
-
- case status
+ case status_filter
when 'open'
@reports = @reports.open
when 'closed'
@@ -92,11 +80,6 @@ class AbuseReportsFinder
end
def sort_reports
- if Feature.disabled?(:abuse_reports_list)
- @reports = @reports.with_order_id_desc
- return
- end
-
# let sub_query in aggregate_reports do the sorting if sorting by number of reports
return if sort_key.in?(SORT_BY_COUNT)
@@ -107,15 +90,6 @@ class AbuseReportsFinder
User.by_username(username).pick(:id)
end
- def status_open?
- return unless Feature.enabled?(:abuse_reports_list) && params[:status].present?
-
- status = params[:status]
- status = STATUS_OPEN unless status.in?(AbuseReport.statuses.keys)
-
- status == STATUS_OPEN
- end
-
def aggregate_reports
if status_open?
sort_by_count = sort_key.in?(SORT_BY_COUNT)
@@ -124,4 +98,19 @@ class AbuseReportsFinder
@reports
end
+
+ def status_filter
+ @status_filter ||=
+ if params[:status].in?(AbuseReport.statuses.keys)
+ params[:status]
+ else
+ STATUS_OPEN
+ end
+ end
+
+ def status_open?
+ return false if params[:status].blank?
+
+ status_filter == STATUS_OPEN
+ end
end
diff --git a/app/finders/ci/jobs_finder.rb b/app/finders/ci/jobs_finder.rb
index 8620dff6973..efacd8143bc 100644
--- a/app/finders/ci/jobs_finder.rb
+++ b/app/finders/ci/jobs_finder.rb
@@ -5,8 +5,8 @@ module Ci
include Gitlab::Allowable
def initialize(current_user:, pipeline: nil, project: nil, runner: nil, params: {}, type: ::Ci::Build)
- @pipeline = pipeline
@current_user = current_user
+ @pipeline = pipeline
@project = project
@runner = runner
@params = params
@@ -16,8 +16,7 @@ module Ci
def execute
builds = init_collection.order_id_desc
- builds = filter_by_with_artifacts(builds)
- filter_by_scope(builds)
+ filter_builds(builds)
rescue Gitlab::Access::AccessDeniedError
type.none
end
@@ -58,6 +57,13 @@ module Ci
params[:include_retried] ? jobs_scope : jobs_scope.latest
end
+ # Overriden in EE
+ def filter_builds(builds)
+ builds = filter_by_with_artifacts(builds)
+ builds = filter_by_runner_types(builds)
+ filter_by_scope(builds)
+ end
+
def filter_by_scope(builds)
return filter_by_statuses!(builds) if params[:scope].is_a?(Array)
@@ -73,12 +79,21 @@ module Ci
end
end
+ def filter_by_runner_types(builds)
+ return builds unless use_runner_type_filter?
+
+ builds.with_runner_type(params[:runner_type])
+ end
+
+ # Overriden in EE
+ def use_runner_type_filter?
+ params[:runner_type].present? && Feature.enabled?(:admin_jobs_filter_runner_type, project, type: :ops)
+ end
+
def filter_by_with_artifacts(builds)
- if params[:with_artifacts]
- builds.with_any_artifacts
- else
- builds
- end
+ return builds.with_any_artifacts if params[:with_artifacts]
+
+ builds
end
def filter_by_statuses!(builds)
@@ -100,3 +115,5 @@ module Ci
end
end
end
+
+Ci::JobsFinder.prepend_mod
diff --git a/app/finders/ci/runners_finder.rb b/app/finders/ci/runners_finder.rb
index 630be17e64b..331f732bff7 100644
--- a/app/finders/ci/runners_finder.rb
+++ b/app/finders/ci/runners_finder.rb
@@ -24,9 +24,6 @@ module Ci
request_tag_list!
@runners
-
- rescue Gitlab::Access::AccessDeniedError
- Ci::Runner.none
end
def sort_key
diff --git a/app/finders/ci/triggers_finder.rb b/app/finders/ci/triggers_finder.rb
new file mode 100644
index 00000000000..a4b429539d6
--- /dev/null
+++ b/app/finders/ci/triggers_finder.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Ci
+ class TriggersFinder
+ def initialize(current_user, project)
+ @current_user = current_user
+ @project = project
+ end
+
+ def execute
+ return Ci::Trigger.none unless Ability.allowed?(@current_user, :admin_build, @project)
+
+ @project.triggers
+ end
+ end
+end
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index 639db58b00d..0336135835a 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -20,6 +20,8 @@ class GroupMembersFinder < UnionFinder
# search: string
# created_after: datetime
# created_before: datetime
+ # non_invite: boolean
+ # with_custom_role: boolean
attr_reader :params
def initialize(group, user = nil, params: {})
@@ -34,7 +36,10 @@ class GroupMembersFinder < UnionFinder
Group.shared_into_ancestors(group).public_or_visible_to_user(user)
end
- members = all_group_members(groups, shared_from_groups).distinct_on_user_with_max_access_level
+ members = all_group_members(groups, shared_from_groups)
+ if static_roles_only?
+ members = members.distinct_on_user_with_max_access_level
+ end
filter_members(members)
end
@@ -70,7 +75,10 @@ class GroupMembersFinder < UnionFinder
members = filter_by_user_type(members)
members = apply_additional_filters(members)
- by_created_at(members)
+ members = by_created_at(members)
+ members = members.non_invite if params[:non_invite]
+
+ members
end
def can_manage_members
@@ -137,6 +145,10 @@ class GroupMembersFinder < UnionFinder
# overridden in EE to include additional filtering conditions.
members
end
+
+ def static_roles_only?
+ true
+ end
end
GroupMembersFinder.prepend_mod_with('GroupMembersFinder')
diff --git a/app/finders/groups/accepting_group_transfers_finder.rb b/app/finders/groups/accepting_group_transfers_finder.rb
index c95318d0098..e757ecff015 100644
--- a/app/finders/groups/accepting_group_transfers_finder.rb
+++ b/app/finders/groups/accepting_group_transfers_finder.rb
@@ -14,7 +14,9 @@ module Groups
return Group.none unless can_transfer_group?
items = find_all_groups
- items = by_search(items)
+
+ # Search will perform an ORDER BY to ensure exact matches are returned first.
+ return by_search(items, exact_matches_first: true) if params[:search].present?
sort(items)
end
diff --git a/app/finders/groups/base.rb b/app/finders/groups/base.rb
index 9d2f9f60a63..26d2ad85fd4 100644
--- a/app/finders/groups/base.rb
+++ b/app/finders/groups/base.rb
@@ -8,10 +8,10 @@ module Groups
items.reorder(Group.arel_table[:path].asc, Group.arel_table[:id].asc) # rubocop: disable CodeReuse/ActiveRecord
end
- def by_search(items)
+ def by_search(items, exact_matches_first: false)
return items if params[:search].blank?
- items.search(params[:search], include_parents: true)
+ items.search(params[:search], include_parents: true, exact_matches_first: exact_matches_first)
end
end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index bbbf14bb0d0..93b7292bb69 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -48,7 +48,7 @@ class IssuableFinder
requires_cross_project_access unless: -> { params.project? }
FULL_TEXT_SEARCH_TERM_PATTERN = '[\u0000-\u02FF\u1E00-\u1EFF\u2070-\u218F]*'
- FULL_TEXT_SEARCH_TERM_REGEX = /\A#{FULL_TEXT_SEARCH_TERM_PATTERN}\z/.freeze
+ FULL_TEXT_SEARCH_TERM_REGEX = /\A#{FULL_TEXT_SEARCH_TERM_PATTERN}\z/
NEGATABLE_PARAMS_HELPER_KEYS = %i[project_id scope status include_subgroups].freeze
attr_accessor :current_user, :params
diff --git a/app/finders/organizations/groups_finder.rb b/app/finders/organizations/groups_finder.rb
new file mode 100644
index 00000000000..2b59a3106a3
--- /dev/null
+++ b/app/finders/organizations/groups_finder.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+# Organizations::GroupsFinder
+#
+# Used to find Groups within an Organization
+module Organizations
+ class GroupsFinder
+ # @param organization [Organizations::Organization]
+ # @param current_user [User]
+ # @param params [{ sort: { field: [String], direction: [String] }, search: [String] }]
+ def initialize(organization:, current_user:, params: {})
+ @organization = organization
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ return Group.none if organization.nil? || !authorized?
+
+ filter_groups(all_accessible_groups)
+ .then { |groups| sort(groups) }
+ .then(&:with_route)
+ end
+
+ private
+
+ attr_reader :organization, :params, :current_user
+
+ def all_accessible_groups
+ current_user.authorized_groups.in_organization(organization)
+ end
+
+ def filter_groups(groups)
+ by_search(groups)
+ end
+
+ def by_search(groups)
+ return groups unless params[:search].present?
+
+ groups.search(params[:search])
+ end
+
+ def sort(groups)
+ return default_sort_order(groups) if params[:sort].blank?
+
+ field = params[:sort][:field]
+ direction = params[:sort][:direction]
+ groups.reorder(field => direction) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ def default_sort_order(groups)
+ groups.sort_by_attribute('name_asc')
+ end
+
+ def authorized?
+ Ability.allowed?(current_user, :read_organization, organization)
+ end
+ end
+end
diff --git a/app/finders/organizations/organization_users_finder.rb b/app/finders/organizations/organization_users_finder.rb
new file mode 100644
index 00000000000..899cda7487c
--- /dev/null
+++ b/app/finders/organizations/organization_users_finder.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+# Organizations::OrganizationUsersFinder
+#
+# Used to find Users of an Organization
+module Organizations
+ class OrganizationUsersFinder
+ # @param organization [Organizations::Organization]
+ # @param current_user [User]
+ def initialize(organization:, current_user:)
+ @organization = organization
+ @current_user = current_user
+ end
+
+ def execute
+ return User.none if organization.nil? || !authorized?
+
+ all_organization_users
+ end
+
+ private
+
+ attr_reader :organization, :current_user
+
+ def all_organization_users
+ organization.organization_users
+ end
+
+ def authorized?
+ Ability.allowed?(current_user, :read_organization_user, organization)
+ end
+ end
+end
diff --git a/app/finders/packages/npm/packages_for_user_finder.rb b/app/finders/packages/npm/packages_for_user_finder.rb
new file mode 100644
index 00000000000..f42e49f9184
--- /dev/null
+++ b/app/finders/packages/npm/packages_for_user_finder.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Packages
+ module Npm
+ class PackagesForUserFinder < ::Packages::GroupOrProjectPackageFinder
+ def execute
+ packages
+ end
+
+ private
+
+ def packages
+ base.npm
+ .with_name(@params[:package_name])
+ end
+ end
+ end
+end
diff --git a/app/finders/packages/nuget/package_finder.rb b/app/finders/packages/nuget/package_finder.rb
index 064698d3c37..684b99f8647 100644
--- a/app/finders/packages/nuget/package_finder.rb
+++ b/app/finders/packages/nuget/package_finder.rb
@@ -32,8 +32,7 @@ module Packages
result
.with_nuget_version_or_normalized_version(
@params[:package_version],
- with_normalized: Feature.enabled?(:nuget_normalized_version, @project_or_group) &&
- client_forces_normalized_version?
+ with_normalized: client_forces_normalized_version?
)
end
diff --git a/app/finders/repositories/changelog_commits_finder.rb b/app/finders/repositories/changelog_commits_finder.rb
index b80b8e94e59..863a1186205 100644
--- a/app/finders/repositories/changelog_commits_finder.rb
+++ b/app/finders/repositories/changelog_commits_finder.rb
@@ -21,7 +21,7 @@ module Repositories
COMMITS_PER_PAGE = 1024
# The regex to use for extracting the SHA of a reverted commit.
- REVERT_REGEX = /^This reverts commit (?<sha>[0-9a-f]{40})/i.freeze
+ REVERT_REGEX = /^This reverts commit (?<sha>[0-9a-f]{40})/i
# The `project` argument specifies the project for which to obtain the
# commits.
diff --git a/app/finders/work_items/namespace_work_items_finder.rb b/app/finders/work_items/namespace_work_items_finder.rb
index aad99d710b6..da6437e0907 100644
--- a/app/finders/work_items/namespace_work_items_finder.rb
+++ b/app/finders/work_items/namespace_work_items_finder.rb
@@ -2,19 +2,14 @@
module WorkItems
class NamespaceWorkItemsFinder < WorkItemsFinder
+ FilterNotAvailableError = Class.new(ArgumentError)
+
def initialize(...)
super
self.parent_param = namespace
end
- def execute
- items = init_collection
- items = by_namespace(items)
-
- sort(items)
- end
-
override :with_confidentiality_access_check
def with_confidentiality_access_check
return model_class.all if params.user_can_see_all_issuables?
@@ -31,6 +26,12 @@ module WorkItems
private
+ def filter_items(items)
+ items = super(items)
+
+ by_namespace(items)
+ end
+
def by_namespace(items)
if namespace.blank? || !Ability.allowed?(current_user, "read_#{namespace.to_ability_name}".to_sym, namespace)
return klass.none
@@ -39,11 +40,23 @@ module WorkItems
items.in_namespaces(namespace)
end
+ override :by_search
+ def by_search(items)
+ return items unless search
+
+ raise FilterNotAvailableError, 'Searching is not available for work items at the namespace level yet'
+ end
+
def namespace
return if params[:namespace_id].blank?
params[:namespace_id].is_a?(Namespace) ? params[:namespace_id] : Namespace.find_by_id(params[:namespace_id])
end
strong_memoize_attr :namespace
+
+ override :by_project
+ def by_project(items)
+ items
+ end
end
end
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 0c7195c5be3..0b56d6f4a90 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -147,6 +147,12 @@ class GitlabSchema < GraphQL::Schema
global_ids.map { |gid| parse_gid(gid, ctx) }
end
+ def unauthorized_field(error)
+ return error.field.if_unauthorized if error.field.respond_to?(:if_unauthorized) && error.field.if_unauthorized
+
+ super
+ end
+
private
def max_query_complexity(ctx)
diff --git a/app/graphql/mutations/admin/abuse_report_labels/create.rb b/app/graphql/mutations/admin/abuse_report_labels/create.rb
new file mode 100644
index 00000000000..6ec96297da4
--- /dev/null
+++ b/app/graphql/mutations/admin/abuse_report_labels/create.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Admin
+ module AbuseReportLabels
+ class Create < BaseMutation
+ graphql_name 'AbuseReportLabelCreate'
+
+ field :label, Types::LabelType, null: true, description: 'Label after mutation.'
+
+ argument :title, GraphQL::Types::String, required: true, description: 'Title of the label.'
+
+ argument :color, GraphQL::Types::String, required: false, default_value: Label::DEFAULT_COLOR,
+ see: {
+ 'List of color keywords at mozilla.org' =>
+ 'https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords'
+ },
+ description: <<~DESC
+ The color of the label given in 6-digit hex notation with leading '#' sign
+ (for example, `#FFAABB`) or one of the CSS color names.
+ DESC
+
+ def resolve(args)
+ raise_resource_not_available_error! unless current_user.can?(:admin_all_resources)
+
+ label = ::Admin::AbuseReportLabels::CreateService.new(args).execute
+
+ {
+ label: label.persisted? ? label : nil,
+ errors: errors_on_object(label)
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/project_ci_cd_settings_update.rb b/app/graphql/mutations/ci/project_ci_cd_settings_update.rb
index 082c345adf6..7df277641bf 100644
--- a/app/graphql/mutations/ci/project_ci_cd_settings_update.rb
+++ b/app/graphql/mutations/ci/project_ci_cd_settings_update.rb
@@ -6,6 +6,7 @@ module Mutations
graphql_name 'ProjectCiCdSettingsUpdate'
include FindsProject
+ include Gitlab::Utils::StrongMemoize
authorize :admin_project
@@ -37,13 +38,11 @@ module Mutations
description: 'CI/CD settings after mutation.'
def resolve(full_path:, **args)
- project = authorized_find!(full_path)
-
if args[:job_token_scope_enabled]
raise Gitlab::Graphql::Errors::ArgumentError, 'job_token_scope_enabled can only be set to false'
end
- settings = project.ci_cd_settings
+ settings = project(full_path).ci_cd_settings
settings.update(args)
{
@@ -51,6 +50,14 @@ module Mutations
errors: errors_on_object(settings)
}
end
+
+ private
+
+ def project(full_path)
+ strong_memoize_with(:project, full_path) do
+ authorized_find!(full_path)
+ end
+ end
end
end
end
diff --git a/app/graphql/mutations/issues/bulk_update.rb b/app/graphql/mutations/issues/bulk_update.rb
index 9c9dd3cf2fc..05e83fc82bb 100644
--- a/app/graphql/mutations/issues/bulk_update.rb
+++ b/app/graphql/mutations/issues/bulk_update.rb
@@ -15,7 +15,7 @@ module Mutations
argument :parent_id, ::Types::GlobalIDType[::IssueParent],
required: true,
description: 'Global ID of the parent to which the bulk update will be scoped. ' \
- 'The parent can be a project **(FREE)** or a group **(PREMIUM)**. ' \
+ 'The parent can be a project **(FREE ALL)** or a group **(PREMIUM ALL)**. ' \
'Example `IssueParentID` are `"gid://gitlab/Project/1"` and `"gid://gitlab/Group/1"`.'
argument :ids, [::Types::GlobalIDType[::Issue]],
diff --git a/app/graphql/mutations/issues/update.rb b/app/graphql/mutations/issues/update.rb
index 35deb9e0af8..cd02c96e000 100644
--- a/app/graphql/mutations/issues/update.rb
+++ b/app/graphql/mutations/issues/update.rb
@@ -34,7 +34,8 @@ module Mutations
argument :time_estimate, GraphQL::Types::String,
required: false,
- description: 'Estimated time to complete the issue, or `0` to remove the current estimate.'
+ description: 'Estimated time to complete the issue. ' \
+ 'Use `null` or `0` to remove the current estimate.'
def resolve(project_path:, iid:, **args)
issue = authorized_find!(project_path: project_path, iid: iid)
@@ -67,8 +68,9 @@ module Mutations
args[:remove_label_ids] = parse_label_ids(args[:remove_label_ids])
args[:label_ids] = parse_label_ids(args[:label_ids])
- unless args[:time_estimate].nil?
- args[:time_estimate] = Gitlab::TimeTrackingFormatter.parse(args[:time_estimate], keep_zero: true)
+ if args.key?(:time_estimate)
+ args[:time_estimate] =
+ args[:time_estimate].nil? ? 0 : Gitlab::TimeTrackingFormatter.parse(args[:time_estimate], keep_zero: true)
end
args
diff --git a/app/graphql/mutations/merge_requests/update.rb b/app/graphql/mutations/merge_requests/update.rb
index 470292df86c..427e07e4a70 100644
--- a/app/graphql/mutations/merge_requests/update.rb
+++ b/app/graphql/mutations/merge_requests/update.rb
@@ -28,7 +28,8 @@ module Mutations
argument :time_estimate, GraphQL::Types::String,
required: false,
- description: 'Estimated time to complete the merge request, or `0` to remove the current estimate.'
+ description: 'Estimated time to complete the merge request. ' \
+ 'Use `null` or `0` to remove the current estimate.'
def resolve(project_path:, iid:, **args)
merge_request = authorized_find!(project_path: project_path, iid: iid)
@@ -55,8 +56,9 @@ module Mutations
private
def parse_arguments(args)
- unless args[:time_estimate].nil?
- args[:time_estimate] = Gitlab::TimeTrackingFormatter.parse(args[:time_estimate], keep_zero: true)
+ if args.key?(:time_estimate)
+ args[:time_estimate] =
+ args[:time_estimate].nil? ? 0 : Gitlab::TimeTrackingFormatter.parse(args[:time_estimate], keep_zero: true)
end
args.compact
diff --git a/app/graphql/mutations/metrics/dashboard/annotations/base.rb b/app/graphql/mutations/metrics/dashboard/annotations/base.rb
deleted file mode 100644
index ad52f84378d..00000000000
--- a/app/graphql/mutations/metrics/dashboard/annotations/base.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module Mutations
- module Metrics
- module Dashboard
- module Annotations
- class Base < BaseMutation
- private
-
- # This method is defined here in order to be used by `authorized_find!` in the subclasses.
- def find_object(id:)
- GitlabSchema.object_from_id(id, expected_type: ::Metrics::Dashboard::Annotation)
- end
- end
- end
- end
- end
-end
diff --git a/app/graphql/mutations/metrics/dashboard/annotations/create.rb b/app/graphql/mutations/metrics/dashboard/annotations/create.rb
index 59ddffe3aad..e544b96f679 100644
--- a/app/graphql/mutations/metrics/dashboard/annotations/create.rb
+++ b/app/graphql/mutations/metrics/dashboard/annotations/create.rb
@@ -70,28 +70,8 @@ module Mutations
private
- def ready?(**args)
- raise_resource_not_available_error! if Feature.enabled?(:remove_monitor_metrics)
-
- # Raise error if both cluster_id and environment_id are present or neither is present
- unless args[:cluster_id].present? ^ args[:environment_id].present?
- raise Gitlab::Graphql::Errors::ArgumentError, ANNOTATION_SOURCE_ARGUMENT_ERROR
- end
-
- super(**args)
- end
-
- def annotation_create_params(args)
- annotation_source = AnnotationSource.new(object: annotation_source(args))
-
- args[annotation_source.type] = annotation_source.object
-
- args
- end
-
- def annotation_source(args)
- annotation_source_id = args[:cluster_id] || args[:environment_id]
- authorized_find!(id: annotation_source_id)
+ def ready?(**_args)
+ raise_resource_not_available_error!
end
end
end
diff --git a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb
index 61fcf8e0b13..d2f2d9a0e32 100644
--- a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb
+++ b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb
@@ -4,12 +4,12 @@ module Mutations
module Metrics
module Dashboard
module Annotations
- class Delete < Base
+ class Delete < BaseMutation
graphql_name 'DeleteAnnotation'
authorize :admin_metrics_dashboard_annotation
- argument :id, ::Types::GlobalIDType[::Metrics::Dashboard::Annotation],
+ argument :id, GraphQL::Types::String,
required: true,
description: 'Global ID of the annotation to delete.'
diff --git a/app/graphql/mutations/work_items/linked_items/add.rb b/app/graphql/mutations/work_items/linked_items/add.rb
index b346b074e85..e0c17a61205 100644
--- a/app/graphql/mutations/work_items/linked_items/add.rb
+++ b/app/graphql/mutations/work_items/linked_items/add.rb
@@ -9,6 +9,9 @@ module Mutations
argument :link_type, ::Types::WorkItems::RelatedLinkTypeEnum,
required: false, description: 'Type of link. Defaults to `RELATED`.'
+ argument :work_items_ids, [::Types::GlobalIDType[::WorkItem]],
+ required: true,
+ description: "Global IDs of the items to link. Maximum number of IDs you can provide: #{MAX_WORK_ITEMS}."
private
diff --git a/app/graphql/mutations/work_items/linked_items/base.rb b/app/graphql/mutations/work_items/linked_items/base.rb
index 1d8d74b02ac..a1d9bced930 100644
--- a/app/graphql/mutations/work_items/linked_items/base.rb
+++ b/app/graphql/mutations/work_items/linked_items/base.rb
@@ -10,9 +10,6 @@ module Mutations
argument :id, ::Types::GlobalIDType[::WorkItem],
required: true, description: 'Global ID of the work item.'
- argument :work_items_ids, [::Types::GlobalIDType[::WorkItem]],
- required: true,
- description: "Global IDs of the items to link. Maximum number of IDs you can provide: #{MAX_WORK_ITEMS}."
field :work_item, Types::WorkItemType,
null: true, description: 'Updated work item.'
@@ -26,7 +23,7 @@ module Mutations
if args[:work_items_ids].size > MAX_WORK_ITEMS
raise Gitlab::Graphql::Errors::ArgumentError,
format(
- _('No more than %{max_work_items} work items can be linked at the same time.'),
+ _('No more than %{max_work_items} work items can be modified at the same time.'),
max_work_items: MAX_WORK_ITEMS
)
end
@@ -50,7 +47,7 @@ module Mutations
private
def update_links(work_item, params)
- raise NotImplementedError
+ raise NotImplementedError, "#{self.class} does not implement #{__method__}"
end
end
end
diff --git a/app/graphql/mutations/work_items/linked_items/remove.rb b/app/graphql/mutations/work_items/linked_items/remove.rb
new file mode 100644
index 00000000000..078f05d2025
--- /dev/null
+++ b/app/graphql/mutations/work_items/linked_items/remove.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Mutations
+ module WorkItems
+ module LinkedItems
+ class Remove < Base
+ graphql_name 'WorkItemRemoveLinkedItems'
+ description 'Remove items linked to the work item.'
+
+ argument :work_items_ids, [::Types::GlobalIDType[::WorkItem]],
+ required: true,
+ description: "Global IDs of the items to unlink. Maximum number of IDs you can provide: #{MAX_WORK_ITEMS}."
+
+ private
+
+ def update_links(work_item, params)
+ gids = params.delete(:work_items_ids)
+ raise Gitlab::Graphql::Errors::ArgumentError, "workItemsIds cannot be empty" if gids.empty?
+
+ work_item_ids = gids.filter_map { |gid| gid.model_id.to_i }
+ ::WorkItems::RelatedWorkItemLinks::DestroyService
+ .new(work_item, current_user, { item_ids: work_item_ids })
+ .execute
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/blame_resolver.rb b/app/graphql/resolvers/blame_resolver.rb
new file mode 100644
index 00000000000..f8b985e6582
--- /dev/null
+++ b/app/graphql/resolvers/blame_resolver.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class BlameResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type Types::Blame::BlameType, null: true
+ calls_gitaly!
+
+ argument :from_line, GraphQL::Types::Int,
+ required: false,
+ default_value: 1,
+ description: 'Range starting from the line. Cannot be less than 1 or greater than `to_line`.'
+ argument :to_line, GraphQL::Types::Int,
+ required: false,
+ default_value: 1,
+ description: 'Range ending on the line. Cannot be less than 1 or less than `to_line`.'
+
+ alias_method :blob, :object
+
+ def ready?(**args)
+ validate_line_params!(args) if feature_enabled?
+
+ super
+ end
+
+ def resolve(from_line:, to_line:)
+ return unless feature_enabled?
+
+ authorize!
+
+ Gitlab::Blame.new(blob, blob.repository.commit(blob.commit_id),
+ range: (from_line..to_line))
+ end
+
+ private
+
+ def authorize!
+ read_code? || raise_resource_not_available_error!
+ end
+
+ def read_code?
+ Ability.allowed?(current_user, :read_code, blob.repository.project)
+ end
+
+ def feature_enabled?
+ Feature.enabled?(:graphql_git_blame, blob.repository.project)
+ end
+
+ def validate_line_params!(args)
+ if args[:from_line] <= 0 || args[:to_line] <= 0
+ raise Gitlab::Graphql::Errors::ArgumentError,
+ '`from_line` and `to_line` must be greater than or equal to 1'
+ end
+
+ return unless args[:from_line] > args[:to_line]
+
+ raise Gitlab::Graphql::Errors::ArgumentError,
+ '`to_line` must be greater than or equal to `from_line`'
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/all_jobs_resolver.rb b/app/graphql/resolvers/ci/all_jobs_resolver.rb
index 5d0193e0e1c..3012a7defa6 100644
--- a/app/graphql/resolvers/ci/all_jobs_resolver.rb
+++ b/app/graphql/resolvers/ci/all_jobs_resolver.rb
@@ -11,14 +11,24 @@ module Resolvers
required: false,
description: 'Filter jobs by status.'
- def resolve_with_lookahead(statuses: nil)
- jobs = ::Ci::JobsFinder.new(current_user: current_user, params: { scope: statuses }).execute
+ argument :runner_types, [::Types::Ci::RunnerTypeEnum],
+ required: false,
+ alpha: { milestone: '16.4' },
+ description: 'Filter jobs by runner type if ' \
+ 'feature flag `:admin_jobs_filter_runner_type` is enabled.'
+
+ def resolve_with_lookahead(**args)
+ jobs = ::Ci::JobsFinder.new(current_user: current_user, params: params_data(args)).execute
apply_lookahead(jobs)
end
private
+ def params_data(args)
+ { scope: args[:statuses], runner_type: args[:runner_types] }
+ end
+
def preloads
{
previous_stage_jobs_or_needs: [:needs, :pipeline],
@@ -32,9 +42,21 @@ module Resolvers
browse_artifacts_path: [{ project: { namespace: [:route] } }],
play_path: [{ project: { namespace: [:route] } }],
web_path: [{ project: { namespace: [:route] } }],
- tags: [:tags]
+ tags: [:tags],
+ ai_failure_analysis: [{ project: [:project_feature, :namespace] }],
+ trace: [{ project: [:namespace] }, :job_artifacts_trace]
}
end
+
+ def nested_preloads
+ super.merge({
+ trace: {
+ html_summary: [:trace_chunks]
+ }
+ })
+ end
end
end
end
+
+Resolvers::Ci::AllJobsResolver.prepend_mod
diff --git a/app/graphql/resolvers/ci/pipeline_triggers_resolver.rb b/app/graphql/resolvers/ci/pipeline_triggers_resolver.rb
index 3d6e3b3e75d..ecbae1f7b55 100644
--- a/app/graphql/resolvers/ci/pipeline_triggers_resolver.rb
+++ b/app/graphql/resolvers/ci/pipeline_triggers_resolver.rb
@@ -10,7 +10,8 @@ module Resolvers
type Types::Ci::PipelineTriggerType.connection_type, null: false
def resolve_with_lookahead
- apply_lookahead(object.triggers)
+ triggers = ::Ci::TriggersFinder.new(current_user, object).execute
+ apply_lookahead(triggers)
end
private
diff --git a/app/graphql/resolvers/ci/runner_jobs_resolver.rb b/app/graphql/resolvers/ci/runner_jobs_resolver.rb
index 9fe25a4d13d..b005702a71d 100644
--- a/app/graphql/resolvers/ci/runner_jobs_resolver.rb
+++ b/app/graphql/resolvers/ci/runner_jobs_resolver.rb
@@ -18,6 +18,8 @@ module Resolvers
alias_method :runner, :object
def resolve_with_lookahead(statuses: nil)
+ context[:job_field_authorization] = :read_build # Instruct JobType to perform field-level authorization
+
jobs = ::Ci::JobsFinder.new(current_user: current_user, runner: runner, params: { scope: statuses }).execute
apply_lookahead(jobs)
@@ -30,7 +32,7 @@ module Resolvers
previous_stage_jobs_or_needs: [:needs, :pipeline],
artifacts: [:job_artifacts],
pipeline: [:user],
- project: [{ project: [:route, { namespace: [:route] }] }],
+ project: [{ project: [:route, { namespace: [:route] }, :project_feature] }],
detailed_status: [
:metadata,
{ pipeline: [:merge_request] },
@@ -42,9 +44,19 @@ module Resolvers
play_path: [{ project: { namespace: [:route] } }],
web_path: [{ project: { namespace: [:route] } }],
short_sha: [:pipeline],
- tags: [:tags]
+ tags: [:tags],
+ ai_failure_analysis: [{ project: [:project_feature, :namespace] }],
+ trace: [{ project: [:namespace] }, :job_artifacts_trace]
}
end
+
+ def nested_preloads
+ super.merge({
+ trace: {
+ html_summary: [:trace_chunks]
+ }
+ })
+ end
end
end
end
diff --git a/app/graphql/resolvers/ci/runners_resolver.rb b/app/graphql/resolvers/ci/runners_resolver.rb
index 632655d3681..3289f1d0056 100644
--- a/app/graphql/resolvers/ci/runners_resolver.rb
+++ b/app/graphql/resolvers/ci/runners_resolver.rb
@@ -46,10 +46,16 @@ module Resolvers
::Ci::RunnersFinder
.new(current_user: current_user, params: runners_finder_params(args))
.execute)
+ rescue Gitlab::Access::AccessDeniedError
+ handle_access_denied_error!
end
protected
+ def handle_access_denied_error!
+ raise_resource_not_available_error!
+ end
+
def runners_finder_params(params)
# Give preference to paused argument over the deprecated 'active' argument
paused = params.fetch(:paused, params[:active] ? !params[:active] : nil)
@@ -85,24 +91,6 @@ module Resolvers
tag_list: [:tags]
})
end
-
- def nested_preloads
- {
- created_by: {
- creator: {
- full_path: [:route],
- web_path: [:route],
- web_url: [:route]
- }
- },
- owner_project: {
- owner_project: {
- full_path: [:route, { namespace: [:route] }],
- web_url: [:route, { namespace: [:route] }]
- }
- }
- }
- end
end
end
end
diff --git a/app/graphql/resolvers/ci/test_suite_resolver.rb b/app/graphql/resolvers/ci/test_suite_resolver.rb
index a2d3af9c664..cccf77452e3 100644
--- a/app/graphql/resolvers/ci/test_suite_resolver.rb
+++ b/app/graphql/resolvers/ci/test_suite_resolver.rb
@@ -27,7 +27,7 @@ module Resolvers
private
def load_test_suite_data(builds)
- suite = builds.sum do |build|
+ suite = builds.sum(Gitlab::Ci::Reports::TestSuite.new) do |build|
test_report = build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new)
test_report.get_suite(build.test_suite_name)
end
diff --git a/app/graphql/resolvers/codequality_reports_comparer_resolver.rb b/app/graphql/resolvers/codequality_reports_comparer_resolver.rb
new file mode 100644
index 00000000000..1c034887c0d
--- /dev/null
+++ b/app/graphql/resolvers/codequality_reports_comparer_resolver.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class CodequalityReportsComparerResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type ::Types::Security::CodequalityReportsComparerType, null: true
+
+ authorize :read_build
+
+ def resolve
+ return unless Feature.enabled?(:sast_reports_in_inline_diff, object.project)
+
+ authorize!(object.actual_head_pipeline)
+
+ object.compare_codequality_reports
+ end
+ end
+end
diff --git a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb
index b967460c7ff..946f10a10fa 100644
--- a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb
+++ b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb
@@ -17,8 +17,6 @@ module Resolvers
alias_method :dashboard, :object
def resolve(**_args)
- return if Feature.enabled?(:remove_monitor_metrics)
-
[]
end
end
diff --git a/app/graphql/resolvers/namespaces/work_item_resolver.rb b/app/graphql/resolvers/namespaces/work_item_resolver.rb
new file mode 100644
index 00000000000..d49ef3f53af
--- /dev/null
+++ b/app/graphql/resolvers/namespaces/work_item_resolver.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Namespaces
+ class WorkItemResolver < Resolvers::BaseResolver
+ type ::Types::WorkItemType, null: true
+
+ argument :iid, GraphQL::Types::String, required: true, description: 'IID of the work item.'
+
+ def ready?(**args)
+ return false if Feature.disabled?(:namespace_level_work_items, resource_parent)
+
+ super
+ end
+
+ def resolve(iid:)
+ ::WorkItem.find_by_namespace_id_and_iid(resource_parent.id, iid)
+ end
+
+ private
+
+ def resource_parent
+ # The namespace could have been loaded in batch by `BatchLoader`.
+ # At this point we need the `id` of the namespace to query for work items, so
+ # make sure it's loaded and not `nil` before continuing.
+ object.respond_to?(:sync) ? object.sync : object
+ end
+ strong_memoize_attr :resource_parent
+ end
+ end
+end
diff --git a/app/graphql/resolvers/namespaces/work_items_resolver.rb b/app/graphql/resolvers/namespaces/work_items_resolver.rb
index 54bb8392071..6985a7a898a 100644
--- a/app/graphql/resolvers/namespaces/work_items_resolver.rb
+++ b/app/graphql/resolvers/namespaces/work_items_resolver.rb
@@ -2,33 +2,31 @@
module Resolvers
module Namespaces
- class WorkItemsResolver < BaseResolver
- prepend ::WorkItems::LookAheadPreloads
+ # rubocop:disable Graphql/ResolverType (inherited from Resolvers::WorkItemsResolver)
+ class WorkItemsResolver < ::Resolvers::WorkItemsResolver
+ def ready?(**args)
+ return false if Feature.disabled?(:namespace_level_work_items, resource_parent)
- type Types::WorkItemType.connection_type, null: true
-
- def resolve_with_lookahead(**args)
- return unless Feature.enabled?(:namespace_level_work_items, resource_parent)
- return WorkItem.none if resource_parent.nil?
-
- finder = ::WorkItems::NamespaceWorkItemsFinder.new(current_user, args.merge(
- namespace_id: resource_parent
- ))
+ super
+ end
- Gitlab::Graphql::Loaders::IssuableLoader.new(resource_parent, finder).batching_find_all do |q|
- apply_lookahead(q)
- end
+ override :resolve_with_lookahead
+ def resolve_with_lookahead(...)
+ super
+ rescue ::WorkItems::NamespaceWorkItemsFinder::FilterNotAvailableError => e
+ raise Gitlab::Graphql::Errors::ArgumentError, e.message
end
private
- def resource_parent
- # The project could have been loaded in batch by `BatchLoader`.
- # At this point we need the `id` of the project to query for work items, so
- # make sure it's loaded and not `nil` before continuing.
- object.respond_to?(:sync) ? object.sync : object
+ override :finder
+ def finder(args)
+ ::WorkItems::NamespaceWorkItemsFinder.new(
+ current_user,
+ args.merge(namespace_id: resource_parent)
+ )
end
- strong_memoize_attr :resource_parent
end
+ # rubocop:enable Graphql/ResolverType
end
end
diff --git a/app/graphql/resolvers/organizations/groups_resolver.rb b/app/graphql/resolvers/organizations/groups_resolver.rb
new file mode 100644
index 00000000000..0f50713b9b4
--- /dev/null
+++ b/app/graphql/resolvers/organizations/groups_resolver.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Organizations
+ class GroupsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+ include ResolvesGroups
+
+ type Types::GroupType.connection_type, null: true
+
+ authorize :read_group
+
+ argument :search,
+ GraphQL::Types::String,
+ required: false,
+ description: 'Search query for group name or full path.',
+ alpha: { milestone: '16.4' }
+
+ argument :sort,
+ Types::Organizations::GroupSortEnum,
+ description: 'Criteria to sort organization groups by.',
+ required: false,
+ default_value: { field: 'name', direction: :asc },
+ alpha: { milestone: '16.4' }
+
+ private
+
+ def resolve_groups(**args)
+ return Group.none if Feature.disabled?(:resolve_organization_groups, context[:current_user])
+
+ ::Organizations::GroupsFinder
+ .new(organization: object, current_user: context[:current_user], params: args)
+ .execute
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/organizations/organization_resolver.rb b/app/graphql/resolvers/organizations/organization_resolver.rb
new file mode 100644
index 00000000000..9194d9a32c5
--- /dev/null
+++ b/app/graphql/resolvers/organizations/organization_resolver.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Organizations
+ class OrganizationResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ authorize :read_organization
+
+ type Types::Organizations::OrganizationType, null: true
+
+ argument :id,
+ Types::GlobalIDType[::Organizations::Organization],
+ required: true,
+ description: 'ID of the organization.'
+
+ def resolve(id:)
+ authorized_find!(id: id)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/organizations/organization_users_resolver.rb b/app/graphql/resolvers/organizations/organization_users_resolver.rb
new file mode 100644
index 00000000000..b4790da6c0a
--- /dev/null
+++ b/app/graphql/resolvers/organizations/organization_users_resolver.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Organizations
+ class OrganizationUsersResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+ include LooksAhead
+
+ type Types::Organizations::OrganizationUserType.connection_type, null: true
+
+ authorize :read_organization_user
+
+ alias_method :organization, :object
+
+ def resolve_with_lookahead
+ authorize!(object)
+
+ apply_lookahead(organization_users)
+ end
+
+ private
+
+ def organization_users
+ ::Organizations::OrganizationUsersFinder
+ .new(organization: organization, current_user: context[:current_user])
+ .execute
+ end
+
+ def preloads
+ {
+ user: [:user]
+ }
+ 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 9c71cd7c0c9..35a6974163a 100644
--- a/app/graphql/resolvers/work_items/linked_items_resolver.rb
+++ b/app/graphql/resolvers/work_items/linked_items_resolver.rb
@@ -5,10 +5,16 @@ module Resolvers
class LinkedItemsResolver < BaseResolver
alias_method :linked_items_widget, :object
+ argument :filter, Types::WorkItems::RelatedLinkTypeEnum,
+ required: false,
+ description: "Filter by link type. " \
+ "Supported values: #{Types::WorkItems::RelatedLinkTypeEnum.values.keys.to_sentence}. " \
+ 'Returns all types if omitted.'
+
type Types::WorkItems::LinkedItemType.connection_type, null: true
- def resolve
- related_work_items.map do |related_work_item|
+ def resolve(filter: nil)
+ related_work_items(filter).map do |related_work_item|
{
link_id: related_work_item.issue_link_id,
link_type: related_work_item.issue_link_type,
@@ -21,10 +27,10 @@ module Resolvers
private
- def related_work_items
+ def related_work_items(type)
return [] unless work_item.project.linked_work_items_feature_flag_enabled?
- work_item.related_issues(current_user, preload: { project: [:project_feature, :group] })
+ work_item.linked_work_items(current_user, preload: { project: [:project_feature, :group] }, link_type: type)
end
def work_item
diff --git a/app/graphql/resolvers/work_items_resolver.rb b/app/graphql/resolvers/work_items_resolver.rb
index d4f73361e05..995f54f35d8 100644
--- a/app/graphql/resolvers/work_items_resolver.rb
+++ b/app/graphql/resolvers/work_items_resolver.rb
@@ -21,13 +21,18 @@ module Resolvers
def resolve_with_lookahead(**args)
return WorkItem.none if resource_parent.nil?
- finder = ::WorkItems::WorkItemsFinder.new(current_user, prepare_finder_params(args))
-
- Gitlab::Graphql::Loaders::IssuableLoader.new(resource_parent, finder).batching_find_all { |q| apply_lookahead(q) }
+ Gitlab::Graphql::Loaders::IssuableLoader.new(
+ resource_parent,
+ finder(prepare_finder_params(args))
+ ).batching_find_all { |q| apply_lookahead(q) }
end
private
+ def finder(args)
+ ::WorkItems::WorkItemsFinder.new(current_user, args)
+ end
+
def prepare_finder_params(args)
params = super(args)
params[:iids] ||= [params.delete(:iid)].compact if params[:iid]
diff --git a/app/graphql/types/blame/blame_type.rb b/app/graphql/types/blame/blame_type.rb
new file mode 100644
index 00000000000..7e7ba14988d
--- /dev/null
+++ b/app/graphql/types/blame/blame_type.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Types
+ module Blame
+ # rubocop: disable Graphql/AuthorizeTypes
+ class BlameType < BaseObject
+ # This is presented through `Repository` that has its own authorization
+ graphql_name 'Blame'
+
+ present_using Gitlab::BlamePresenter
+
+ field :first_line, GraphQL::Types::String, null: true,
+ description: 'First line of Git Blame for given range.', calls_gitaly: true
+ field :groups, [Types::Blame::GroupsType], null: true,
+ description: 'Git Blame grouped by contiguous lines for commit.', calls_gitaly: true,
+ method: :groups_commit_data
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/blame/commit_data_type.rb b/app/graphql/types/blame/commit_data_type.rb
new file mode 100644
index 00000000000..faac34b06f4
--- /dev/null
+++ b/app/graphql/types/blame/commit_data_type.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Types
+ module Blame
+ # rubocop: disable Graphql/AuthorizeTypes
+ class CommitDataType < BaseObject
+ # This is presented through `Repository` that has its own authorization
+ graphql_name 'CommitData'
+
+ field :age_map_class, GraphQL::Types::String, null: false, description: 'CSS class for age of commit.'
+ field :author_avatar, GraphQL::Types::String, null: false, description: 'Link to author avatar.'
+ field :commit_author_link, GraphQL::Types::String, null: false, description: 'Link to the commit author.'
+ field :commit_link, GraphQL::Types::String, null: false, description: 'Link to the commit.'
+ field :project_blame_link, GraphQL::Types::String,
+ null: true, description: 'Link to blame prior to the change.'
+ field :time_ago_tooltip, GraphQL::Types::String, null: false, description: 'Time of commit.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/blame/groups_type.rb b/app/graphql/types/blame/groups_type.rb
new file mode 100644
index 00000000000..754f3f95364
--- /dev/null
+++ b/app/graphql/types/blame/groups_type.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Types
+ module Blame
+ # rubocop: disable Graphql/AuthorizeTypes
+ class GroupsType < BaseObject
+ # This is presented through `Repository` that has its own authorization
+ graphql_name 'Groups'
+
+ field :commit, Types::CommitType, null: false, description: 'Commit responsible for specified group.'
+ field :commit_data, Types::Blame::CommitDataType, null: true,
+ description: 'HTML data derived from commit needed to present blame.', calls_gitaly: true
+ field :lineno, GraphQL::Types::Int, null: false, description: 'Starting line number for the commit group.'
+ field :lines, [GraphQL::Types::String], null: false, description: 'Array of lines added for the commit group.'
+ field :span, GraphQL::Types::Int, null: false,
+ description: 'Number of contiguous lines which the blame spans for the commit group.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/ci/ci_cd_setting_type.rb b/app/graphql/types/ci/ci_cd_setting_type.rb
index 45ecbf5c084..8a49c5a6a95 100644
--- a/app/graphql/types/ci/ci_cd_setting_type.rb
+++ b/app/graphql/types/ci/ci_cd_setting_type.rb
@@ -29,6 +29,7 @@ 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,
@@ -41,3 +42,5 @@ module Types
end
end
end
+
+Types::Ci::CiCdSettingType.prepend_mod_with('Types::Ci::CiCdSettingType')
diff --git a/app/graphql/types/ci/job_base_field.rb b/app/graphql/types/ci/job_base_field.rb
new file mode 100644
index 00000000000..979f1748494
--- /dev/null
+++ b/app/graphql/types/ci/job_base_field.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # JobBaseField ensures that only allow-listed fields can be returned without a permission check.
+ # All other fields go through a permissions check based on the :job_field_authorization value passed in the context.
+ # rubocop: disable Graphql/AuthorizeTypes
+ class JobBaseField < ::Types::BaseField
+ PUBLIC_FIELDS = %i[allow_failure duration id kind status created_at finished_at queued_at queued_duration
+ updated_at runner].freeze
+
+ attr_accessor :if_unauthorized
+
+ def initialize(**kwargs, &block)
+ @if_unauthorized = kwargs.delete(:if_unauthorized)
+
+ super
+ end
+
+ def authorized?(object, args, ctx)
+ current_user = ctx[:current_user]
+ permission = ctx[:job_field_authorization]
+
+ if permission.nil? ||
+ PUBLIC_FIELDS.include?(ctx[:current_field].original_name) ||
+ current_user.can?(permission, object)
+ return super
+ end
+
+ false
+ end
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/ci/job_failure_reason_enum.rb b/app/graphql/types/ci/job_failure_reason_enum.rb
new file mode 100644
index 00000000000..3b9c13536d6
--- /dev/null
+++ b/app/graphql/types/ci/job_failure_reason_enum.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class JobFailureReasonEnum < BaseEnum
+ graphql_name 'CiJobFailureReason'
+
+ ::Enums::Ci::CommitStatus.failure_reasons.each_key do |reason|
+ value reason.to_s.upcase,
+ description: "A job that failed due to #{reason.to_s.tr('_', ' ')}.",
+ value: reason
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/job_trace_type.rb b/app/graphql/types/ci/job_trace_type.rb
index a68e26106b8..405c640115d 100644
--- a/app/graphql/types/ci/job_trace_type.rb
+++ b/app/graphql/types/ci/job_trace_type.rb
@@ -5,13 +5,24 @@ module Types
module Ci
class JobTraceType < BaseObject
graphql_name 'CiJobTrace'
+ MAX_SIZE_KB = 16
+ MAX_SIZE_B = MAX_SIZE_KB * 1024
field :html_summary, GraphQL::Types::String, null: false,
- alpha: { milestone: '15.11' }, # As we want the option to change from 10 if needed
- description: "HTML summary containing the last 10 lines of the trace."
+ alpha: { milestone: '15.11' },
+ description: 'HTML summary that contains the tail lines of the trace. ' \
+ "Returns at most #{MAX_SIZE_KB}KB of raw bytes from the trace. " \
+ 'The returned string might start with an unexpected invalid UTF-8 code point due to truncation.' do
+ argument :last_lines, Integer,
+ required: false, default_value: 10,
+ description: 'Number of tail lines to return, up to a maximum of 100 lines.'
+ end
- def html_summary
- object.html(last_lines: 10).html_safe
+ 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
+ ).html_safe
end
end
end
diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb
index 22eb32993c5..5956d372fe4 100644
--- a/app/graphql/types/ci/job_type.rb
+++ b/app/graphql/types/ci/job_type.rb
@@ -8,6 +8,7 @@ module Types
graphql_name 'CiJob'
present_using ::Ci::BuildPresenter
+ field_class Types::Ci::JobBaseField
connection_type_class Types::LimitedCountableConnectionType
@@ -87,10 +88,14 @@ module Types
description: 'Play path of the job.'
field :playable, GraphQL::Types::Boolean, null: false, method: :playable?,
description: 'Indicates the job can be played.'
+ field :previous_stage_jobs, Types::Ci::JobType.connection_type,
+ null: true,
+ description: 'Jobs from the previous stage.'
field :previous_stage_jobs_or_needs, Types::Ci::JobNeedUnion.connection_type,
null: true,
description: 'Jobs that must complete before the job runs. Returns `BuildNeed`, ' \
- 'which is the needed jobs if the job uses the `needs` keyword, or the previous stage jobs otherwise.'
+ 'which is the needed jobs if the job uses the `needs` keyword, or the previous stage jobs otherwise.',
+ deprecated: { reason: 'Replaced by previousStageJobs and needs fields', milestone: '16.4' }
field :ref_name, GraphQL::Types::String, null: true,
description: 'Ref name of the job.'
field :ref_path, GraphQL::Types::String, null: true,
@@ -104,7 +109,8 @@ module Types
field :scheduling_type, GraphQL::Types::String, null: true,
description: 'Type of job scheduling. Value is `dag` if the job uses the `needs` keyword, and `stage` otherwise.'
field :short_sha, type: GraphQL::Types::String, null: false,
- description: 'Short SHA1 ID of the commit.'
+ description: 'Short SHA1 ID of the commit.',
+ if_unauthorized: 'Unauthorized'
field :stuck, GraphQL::Types::Boolean, null: false, method: :stuck?,
description: 'Indicates the job is stuck.'
field :trace, Types::Ci::JobTraceType, null: true,
@@ -174,17 +180,17 @@ module Types
end
def previous_stage_jobs
- BatchLoader::GraphQL.for([object.pipeline, object.stage_idx - 1]).batch(default_value: []) do |tuples, loader|
- tuples.group_by(&:first).each do |pipeline, keys|
- positions = keys.map(&:second)
+ BatchLoader::GraphQL.for([object.pipeline_id, object.stage_idx - 1]).batch(default_value: []) do |tuples, loader|
+ pipeline_ids = tuples.map(&:first).uniq
+ stage_idxs = tuples.map(&:second).uniq
- stages = pipeline.stages.by_position(positions)
+ # This query can fetch unneeded jobs when querying for more than one pipeline.
+ # It was decided that fetching and discarding the jobs is preferable to making a more complex query.
+ jobs = CommitStatus.in_pipelines(pipeline_ids).for_stage(stage_idxs).latest
+ grouped_jobs = jobs.group_by { |job| [job.pipeline_id, job.stage_idx] }
- stages.each do |stage|
- # Without `.to_a`, the memoization will only preserve the activerecord relation object. And when there is
- # a call, the SQL query will be executed again.
- loader.call([pipeline, stage.position], stage.latest_statuses.to_a)
- end
+ tuples.each do |tuple|
+ loader.call(tuple, grouped_jobs.fetch(tuple, []))
end
end
end
diff --git a/app/graphql/types/ci/pipeline_schedule_type.rb b/app/graphql/types/ci/pipeline_schedule_type.rb
index 71a1f28ea38..e2e3bd8cfbd 100644
--- a/app/graphql/types/ci/pipeline_schedule_type.rb
+++ b/app/graphql/types/ci/pipeline_schedule_type.rb
@@ -68,7 +68,10 @@ module Types
null: false, description: 'Timestamp of when the pipeline schedule was last updated.'
def ref_path
- ::Gitlab::Routing.url_helpers.project_commits_path(object.project, object.ref_for_display)
+ ref_for_display = object.ref_for_display
+ return unless ref_for_display
+
+ ::Gitlab::Routing.url_helpers.project_commits_path(object.project, ref_for_display)
end
def edit_path
diff --git a/app/graphql/types/ci/runner_job_execution_status_enum.rb b/app/graphql/types/ci/runner_job_execution_status_enum.rb
index 686ea085199..0db79c56a93 100644
--- a/app/graphql/types/ci/runner_job_execution_status_enum.rb
+++ b/app/graphql/types/ci/runner_job_execution_status_enum.rb
@@ -8,12 +8,12 @@ module Types
value 'IDLE',
description: "Runner is idle.",
value: :idle,
- deprecated: { milestone: '15.7', reason: :alpha }
+ alpha: { milestone: '15.7' }
value 'RUNNING',
description: 'Runner is executing jobs.',
value: :running,
- deprecated: { milestone: '15.7', reason: :alpha }
+ alpha: { milestone: '15.7' }
end
end
end
diff --git a/app/graphql/types/ci/runner_membership_filter_enum.rb b/app/graphql/types/ci/runner_membership_filter_enum.rb
index d59a68b427b..eb691166944 100644
--- a/app/graphql/types/ci/runner_membership_filter_enum.rb
+++ b/app/graphql/types/ci/runner_membership_filter_enum.rb
@@ -21,7 +21,7 @@ module Types
"Include all runners. This list includes runners for all projects in the group " \
"and subgroups, as well as for the parent groups and instance.",
value: :all_available,
- deprecated: { milestone: '15.5', reason: :alpha }
+ alpha: { milestone: '15.5' }
end
end
end
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
index 258cf1539fb..74e7f256b44 100644
--- a/app/graphql/types/group_type.rb
+++ b/app/graphql/types/group_type.rb
@@ -268,6 +268,12 @@ module Types
alpha: { milestone: '16.3' },
resolver: ::Resolvers::Namespaces::WorkItemsResolver
+ field :work_item, Types::WorkItemType,
+ resolver: Resolvers::Namespaces::WorkItemResolver,
+ alpha: { milestone: '16.4' },
+ description: 'Find a work item by IID directly associated with the group. Returns `null` if the ' \
+ '`namespace_level_work_items` feature flag is disabled.'
+
field :autocomplete_users,
null: true,
resolver: Resolvers::AutocompleteUsersResolver,
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index 4b7118d75a5..1c8a654a841 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -171,6 +171,8 @@ module Types
field :escalation_status, Types::IncidentManagement::EscalationStatusEnum, null: true,
description: 'Escalation status of the issue.'
+ field :external_author, GraphQL::Types::String, null: true, description: 'Email address of non-GitLab user reporting the issue. For guests, the email address is obfuscated.'
+
markdown_field :title_html, null: true
markdown_field :description_html, null: true
diff --git a/app/graphql/types/label_type.rb b/app/graphql/types/label_type.rb
index 4848ee30950..d4fac949c93 100644
--- a/app/graphql/types/label_type.rb
+++ b/app/graphql/types/label_type.rb
@@ -18,6 +18,9 @@ module Types
description: 'Description of the label (Markdown rendered as HTML for caching).'
field :id, GraphQL::Types::ID, null: false,
description: 'Label ID.'
+ field :lock_on_merge, GraphQL::Types::Boolean, null: false,
+ description: 'Indicates this label is locked for merge requests ' \
+ 'that have been merged.'
field :text_color, GraphQL::Types::String, null: false,
description: 'Text color of the label.'
field :title, GraphQL::Types::String, null: false,
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 3fe8a05b311..4fd2b245de9 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -185,6 +185,8 @@ module Types
description: 'Users from whom a review has been requested.'
field :subscribed, GraphQL::Types::Boolean, method: :subscribed?, null: false, complexity: 5,
description: 'Indicates if the currently logged in user is subscribed to this merge request.'
+ field :supports_lock_on_merge, GraphQL::Types::Boolean, null: false, method: :supports_lock_on_merge?,
+ description: 'Indicates if the merge request supports locked labels.'
field :task_completion_status, Types::TaskCompletionStatus, null: false,
description: Types::TaskCompletionStatus.description
field :time_estimate, GraphQL::Types::Int, null: false,
@@ -231,6 +233,14 @@ module Types
field :prepared_at, Types::TimeType, null: true,
description: 'Timestamp of when the merge request was prepared.'
+ field :codequality_reports_comparer,
+ type: ::Types::Security::CodequalityReportsComparerType,
+ null: true,
+ alpha: { milestone: '16.4' },
+ description: 'Code quality reports comparison reported on the merge request. Returns `null` ' \
+ 'if `sast_reports_in_inline_diff` feature flag is disabled.',
+ resolver: ::Resolvers::CodequalityReportsComparerResolver
+
markdown_field :title_html, null: true
markdown_field :description_html, null: true
@@ -297,7 +307,7 @@ module Types
end
def security_auto_fix
- object.author == User.security_bot
+ object.author == ::Users::Internal.security_bot
end
def merge_user
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 957fd10690f..445f26e2fcf 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -181,6 +181,7 @@ module Types
mount_mutation Mutations::WorkItems::Export, alpha: { milestone: '15.10' }
mount_mutation Mutations::WorkItems::Convert, alpha: { milestone: '15.11' }
mount_mutation Mutations::WorkItems::LinkedItems::Add, alpha: { milestone: '16.3' }
+ mount_mutation Mutations::WorkItems::LinkedItems::Remove, alpha: { milestone: '16.3' }
mount_mutation Mutations::SavedReplies::Create
mount_mutation Mutations::SavedReplies::Update
mount_mutation Mutations::Pages::MarkOnboardingComplete
@@ -188,6 +189,7 @@ module Types
mount_mutation Mutations::Uploads::Delete
mount_mutation Mutations::Users::SetNamespaceCommitEmail
mount_mutation Mutations::WorkItems::Subscribe, alpha: { milestone: '16.3' }
+ mount_mutation Mutations::Admin::AbuseReportLabels::Create, alpha: { milestone: '16.4' }
end
end
diff --git a/app/graphql/types/organizations/group_sort_enum.rb b/app/graphql/types/organizations/group_sort_enum.rb
new file mode 100644
index 00000000000..8fb2f553539
--- /dev/null
+++ b/app/graphql/types/organizations/group_sort_enum.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Types
+ module Organizations
+ class GroupSortEnum < BaseEnum
+ graphql_name 'OrganizationGroupSort'
+ description 'Values for sorting organization groups'
+
+ sortable_fields = ['ID', 'Name', 'Path', 'Updated at', 'Created at']
+
+ sortable_fields.each do |field|
+ value "#{field.upcase.tr(' ', '_')}_ASC",
+ value: { field: field.downcase.tr(' ', '_'), direction: :asc },
+ description: "#{field} in ascending order.",
+ alpha: { milestone: '16.4' }
+
+ value "#{field.upcase.tr(' ', '_')}_DESC",
+ value: { field: field.downcase.tr(' ', '_'), direction: :desc },
+ description: "#{field} in descending order.",
+ alpha: { milestone: '16.4' }
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/organizations/organization_type.rb b/app/graphql/types/organizations/organization_type.rb
new file mode 100644
index 00000000000..cae0ef2232e
--- /dev/null
+++ b/app/graphql/types/organizations/organization_type.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Types
+ module Organizations
+ class OrganizationType < BaseObject
+ graphql_name 'Organization'
+
+ authorize :read_organization
+
+ field :groups,
+ Types::GroupType.connection_type,
+ null: false,
+ description: 'Groups within this organization that the user has access to.',
+ alpha: { milestone: '16.4' },
+ resolver: ::Resolvers::Organizations::GroupsResolver
+ field :id,
+ GraphQL::Types::ID,
+ null: false,
+ description: 'ID of the organization.',
+ alpha: { milestone: '16.4' }
+ field :name,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Name of the organization.',
+ alpha: { milestone: '16.4' }
+ field :organization_users,
+ null: false,
+ description: 'Users with access to the organization.',
+ alpha: { milestone: '16.4' },
+ resolver: ::Resolvers::Organizations::OrganizationUsersResolver
+ field :path,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Path of the organization.',
+ alpha: { milestone: '16.4' }
+ end
+ end
+end
diff --git a/app/graphql/types/organizations/organization_user_type.rb b/app/graphql/types/organizations/organization_user_type.rb
new file mode 100644
index 00000000000..41924586f38
--- /dev/null
+++ b/app/graphql/types/organizations/organization_user_type.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Types
+ module Organizations
+ class OrganizationUserType < BaseObject
+ graphql_name 'OrganizationUser'
+ description 'A user with access to the organization.'
+
+ include UsersHelper
+
+ authorize :read_organization_user
+
+ alias_method :organization_user, :object
+
+ field :badges,
+ [GraphQL::Types::String],
+ null: true,
+ description: 'Badges describing the user within the organization.',
+ alpha: { milestone: '16.4' }
+ field :id,
+ GraphQL::Types::ID,
+ null: false,
+ description: 'ID of the organization user.',
+ alpha: { milestone: '16.4' }
+ field :user,
+ ::Types::UserType,
+ null: false,
+ description: 'User that is associated with the organization.',
+ alpha: { milestone: '16.4' }
+
+ def badges
+ user_badges_in_admin_section(organization_user.user).pluck(:text) # rubocop:disable CodeReuse/ActiveRecord
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/permission_types/work_item.rb b/app/graphql/types/permission_types/work_item.rb
index d9946fc4ea6..40b708a7885 100644
--- a/app/graphql/types/permission_types/work_item.rb
+++ b/app/graphql/types/permission_types/work_item.rb
@@ -8,7 +8,7 @@ module Types
abilities :read_work_item, :update_work_item, :delete_work_item,
:admin_work_item, :admin_parent_link, :set_work_item_metadata,
- :create_note
+ :create_note, :admin_work_item_link
end
end
end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 38b8973034d..d02b3e4136f 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -96,6 +96,12 @@ module Types
required: true,
description: 'Global ID of the note.'
end
+ field :organization,
+ Types::Organizations::OrganizationType,
+ null: true,
+ resolver: Resolvers::Organizations::OrganizationResolver,
+ description: "Find an organization.",
+ alpha: { milestone: '16.4' }
field :package,
description: 'Find a package. This field can only be resolved for one query in any single request. Returns `null` if a package has no `default` status.',
resolver: Resolvers::PackageDetailsResolver
@@ -122,7 +128,8 @@ module Types
field :runners, Types::Ci::RunnerType.connection_type,
null: true,
resolver: Resolvers::Ci::RunnersResolver,
- description: "Find runners visible to the current user."
+ description: "Get all runners in the GitLab instance (project and shared). " \
+ "Access is restricted to users with administrator access."
field :snippets,
Types::SnippetType.connection_type,
null: true,
diff --git a/app/graphql/types/repository/blob_type.rb b/app/graphql/types/repository/blob_type.rb
index c5d6e26e94b..3959118631f 100644
--- a/app/graphql/types/repository/blob_type.rb
+++ b/app/graphql/types/repository/blob_type.rb
@@ -86,6 +86,9 @@ module Types
field :blame_path, GraphQL::Types::String, null: true,
description: 'Web path to blob blame page.'
+ field :blame, Types::Blame::BlameType, null: true,
+ description: 'Blob blame. Available only when feature flag `graphql_git_blame` is enabled.', alpha: { milestone: '16.3' }, resolver: Resolvers::BlameResolver
+
field :history_path, GraphQL::Types::String, null: true,
description: 'Web path to blob history page.'
diff --git a/app/graphql/types/security/codequality_reports_comparer/degradation_type.rb b/app/graphql/types/security/codequality_reports_comparer/degradation_type.rb
new file mode 100644
index 00000000000..fb7d722069f
--- /dev/null
+++ b/app/graphql/types/security/codequality_reports_comparer/degradation_type.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Types
+ module Security
+ module CodequalityReportsComparer
+ # rubocop: disable Graphql/AuthorizeTypes (The resolver authorizes the request)
+ class DegradationType < BaseObject
+ graphql_name 'CodequalityReportsComparerReportDegradation'
+
+ description 'Represents a degradation on the compared codequality report.'
+
+ field :description, GraphQL::Types::String,
+ null: false,
+ description: 'Description of the code quality degradation.'
+
+ field :fingerprint, GraphQL::Types::String,
+ null: false,
+ description: 'Unique fingerprint to identify the code quality degradation. For example, an MD5 hash.'
+
+ field :severity, Types::Ci::CodeQualityDegradationSeverityEnum,
+ null: false,
+ description:
+ "Severity of the code quality degradation " \
+ "(#{::Gitlab::Ci::Reports::CodequalityReports::SEVERITY_PRIORITIES.keys.map(&:upcase).join(', ')})."
+
+ field :file_path, GraphQL::Types::String,
+ null: false,
+ description: 'Relative path to the file containing the code quality degradation.'
+
+ field :line, GraphQL::Types::Int,
+ null: false,
+ description: 'Line on which the code quality degradation occurred.'
+
+ field :web_url, GraphQL::Types::String,
+ null: true,
+ description: 'URL to the file along with line number.'
+
+ field :engine_name, GraphQL::Types::String,
+ null: false,
+ description: 'Code quality plugin that reported the degradation.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+ end
+end
diff --git a/app/graphql/types/security/codequality_reports_comparer/report_type.rb b/app/graphql/types/security/codequality_reports_comparer/report_type.rb
new file mode 100644
index 00000000000..8a41160141a
--- /dev/null
+++ b/app/graphql/types/security/codequality_reports_comparer/report_type.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Types
+ module Security
+ module CodequalityReportsComparer
+ # rubocop: disable Graphql/AuthorizeTypes (Parent node applies authorization)
+ class ReportType < BaseObject
+ graphql_name 'CodequalityReportsComparerReport'
+
+ description 'Represents compared code quality report.'
+
+ field :status,
+ type: CodequalityReportsComparer::StatusEnum,
+ null: false,
+ description: 'Status of report.'
+
+ field :new_errors,
+ type: [CodequalityReportsComparer::DegradationType],
+ null: false,
+ description: 'New code quality degradations.'
+
+ field :resolved_errors,
+ type: [CodequalityReportsComparer::DegradationType],
+ null: true,
+ description: 'Resolved code quality degradations.'
+
+ field :existing_errors,
+ type: [CodequalityReportsComparer::DegradationType],
+ null: true,
+ description: 'All code quality degradations.'
+
+ field :summary,
+ type: CodequalityReportsComparer::SummaryType,
+ null: false,
+ description: 'Codequality report summary.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+ end
+end
diff --git a/app/graphql/types/security/codequality_reports_comparer/status_enum.rb b/app/graphql/types/security/codequality_reports_comparer/status_enum.rb
new file mode 100644
index 00000000000..9cab2664db8
--- /dev/null
+++ b/app/graphql/types/security/codequality_reports_comparer/status_enum.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Types
+ module Security
+ module CodequalityReportsComparer
+ class StatusEnum < BaseEnum
+ graphql_name 'CodequalityReportsComparerReportStatus'
+ description 'Report comparison status'
+
+ value 'SUCCESS', value: 'success', description: 'Report successfully generated.'
+ value 'FAILED', value: 'failed', description: 'Report failed to generate.'
+ value 'NOT_FOUND', value: 'not_found', description: 'Head report or base report not found.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/security/codequality_reports_comparer/summary_type.rb b/app/graphql/types/security/codequality_reports_comparer/summary_type.rb
new file mode 100644
index 00000000000..cd4a594c193
--- /dev/null
+++ b/app/graphql/types/security/codequality_reports_comparer/summary_type.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Types
+ module Security
+ module CodequalityReportsComparer
+ # rubocop: disable Graphql/AuthorizeTypes (The resolver authorizes the request)
+ class SummaryType < BaseObject
+ graphql_name 'CodequalityReportsComparerReportSummary'
+
+ description 'Represents a summary of the compared codequality report.'
+
+ field :total,
+ type: GraphQL::Types::Int,
+ null: true,
+ description: 'Total count of code quality degradations.'
+
+ field :resolved,
+ type: GraphQL::Types::Int,
+ null: true,
+ description: 'Count of resolved code quality degradations.'
+
+ field :errored,
+ type: GraphQL::Types::Int,
+ null: true,
+ description: 'Count of code quality errors.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+ end
+end
diff --git a/app/graphql/types/security/codequality_reports_comparer_type.rb b/app/graphql/types/security/codequality_reports_comparer_type.rb
new file mode 100644
index 00000000000..3b0f790af81
--- /dev/null
+++ b/app/graphql/types/security/codequality_reports_comparer_type.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Types
+ module Security
+ # rubocop: disable Graphql/AuthorizeTypes (The resolver authorizes the request)
+ class CodequalityReportsComparerType < BaseObject
+ graphql_name 'CodequalityReportsComparer'
+
+ description 'Represents reports comparison for code quality.'
+
+ field :report,
+ type: CodequalityReportsComparer::ReportType,
+ null: true,
+ hash_key: 'data',
+ description: 'Compared codequality report.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb
index a6f5b7e7456..170f28103eb 100644
--- a/app/graphql/types/user_type.rb
+++ b/app/graphql/types/user_type.rb
@@ -3,7 +3,7 @@
module Types
class UserType < ::Types::BaseObject
graphql_name 'UserCore'
- description 'Core represention of a GitLab user.'
+ description 'Core representation of a GitLab user.'
implements ::Types::UserInterface
authorize :read_user
diff --git a/app/graphql/types/work_items/award_emoji_update_action_enum.rb b/app/graphql/types/work_items/award_emoji_update_action_enum.rb
index 5b2512a215f..e068e231af3 100644
--- a/app/graphql/types/work_items/award_emoji_update_action_enum.rb
+++ b/app/graphql/types/work_items/award_emoji_update_action_enum.rb
@@ -8,6 +8,7 @@ module Types
value 'ADD', 'Adds the emoji.', value: :add
value 'REMOVE', 'Removes the emoji.', value: :remove
+ value 'TOGGLE', 'Toggles the status of the emoji.', value: :toggle
end
end
end
diff --git a/app/graphql/types/work_items/widgets/linked_items_type.rb b/app/graphql/types/work_items/widgets/linked_items_type.rb
index fa51742b9c1..2611c2456c5 100644
--- a/app/graphql/types/work_items/widgets/linked_items_type.rb
+++ b/app/graphql/types/work_items/widgets/linked_items_type.rb
@@ -13,7 +13,7 @@ module Types
field :linked_items, Types::WorkItems::LinkedItemType.connection_type,
null: true, complexity: 5,
alpha: { milestone: '16.3' },
- description: 'Linked items for the work item. Returns `null`' \
+ description: 'Linked items for the work item. Returns `null` ' \
'if `linked_work_items` feature flag is disabled.',
resolver: Resolvers::WorkItems::LinkedItemsResolver
end
diff --git a/app/helpers/admin/abuse_reports_helper.rb b/app/helpers/admin/abuse_reports_helper.rb
index 275bed406f1..015d4513091 100644
--- a/app/helpers/admin/abuse_reports_helper.rb
+++ b/app/helpers/admin/abuse_reports_helper.rb
@@ -18,7 +18,8 @@ module Admin
def abuse_report_data(report)
{
- abuse_report_data: Admin::AbuseReportDetailsSerializer.new.represent(report).to_json
+ abuse_report_data: Admin::AbuseReportDetailsSerializer.new.represent(report).to_json,
+ abuse_reports_list_path: admin_abuse_reports_path
}
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 2bf239979f7..e3a630024d9 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -315,7 +315,8 @@ module ApplicationHelper
class_names << 'issue-boards-page gl-overflow-auto' if current_controller?(:boards)
class_names << 'epic-boards-page gl-overflow-auto' if current_controller?(:epic_boards)
class_names << 'with-performance-bar' if performance_bar_enabled?
- class_names << 'with-top-bar' if show_super_sidebar? && !@hide_top_bar
+ class_names << 'with-header' if !show_super_sidebar? || !current_user
+ class_names << 'with-top-bar' if show_super_sidebar? && !@hide_top_bar_padding
class_names << system_message_class
class_names << 'logged-out-marketing-header' if !current_user && ::Gitlab.com? && !show_super_sidebar?
@@ -485,6 +486,15 @@ module ApplicationHelper
end
end
+ def controller_full_path
+ action = case controller.action_name
+ when 'create' then 'new'
+ when 'update' then 'edit'
+ else controller.action_name
+ end
+ "#{controller.controller_path}/#{action}"
+ end
+
private
def browser_id
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index a45425474b5..ef91915ce38 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -238,6 +238,7 @@ module ApplicationSettingsHelper
:container_expiration_policies_enable_historic_entries,
:container_registry_expiration_policies_caching,
:container_registry_token_expire_delay,
+ :decompress_archive_file_timeout,
:default_artifacts_expire_in,
:default_branch_name,
:default_branch_protection,
@@ -306,7 +307,6 @@ module ApplicationSettingsHelper
:housekeeping_optimize_repository_period,
:html_emails_enabled,
:import_sources,
- :in_product_marketing_emails_enabled,
:inactive_projects_delete_after_months,
:inactive_projects_min_size_mb,
:inactive_projects_send_warning_email_after_months,
@@ -413,6 +413,7 @@ module ApplicationSettingsHelper
:throttle_protected_paths_period_in_seconds,
:throttle_protected_paths_requests_per_period,
:protected_paths_raw,
+ :protected_paths_for_get_request_raw,
:time_tracking_limit_to_hours,
:two_factor_grace_period,
:update_runner_versions_enabled,
@@ -436,6 +437,7 @@ module ApplicationSettingsHelper
:mailgun_events_enabled,
:snowplow_collector_hostname,
:snowplow_cookie_domain,
+ :snowplow_database_collector_hostname,
:snowplow_enabled,
:snowplow_app_id,
:push_event_hooks_limit,
@@ -478,12 +480,14 @@ module ApplicationSettingsHelper
:sentry_dsn,
:sentry_clientside_dsn,
:sentry_environment,
+ :sentry_clientside_traces_sample_rate,
:sidekiq_job_limiter_mode,
:sidekiq_job_limiter_compression_threshold_bytes,
:sidekiq_job_limiter_limit_bytes,
:suggest_pipeline_enabled,
:search_rate_limit,
:search_rate_limit_unauthenticated,
+ :search_rate_limit_allowlist_raw,
:users_get_by_id_limit,
:users_get_by_id_limit_allowlist_raw,
:runner_token_expiration_interval,
diff --git a/app/helpers/artifacts_helper.rb b/app/helpers/artifacts_helper.rb
index f90d59409ed..10d2714840d 100644
--- a/app/helpers/artifacts_helper.rb
+++ b/app/helpers/artifacts_helper.rb
@@ -5,8 +5,7 @@ module ArtifactsHelper
{
project_path: project.full_path,
project_id: project.id,
- can_destroy_artifacts: can?(current_user, :destroy_artifacts, project).to_s,
- artifacts_management_feedback_image_path: image_path('illustrations/chat-bubble-sm.svg')
+ can_destroy_artifacts: can?(current_user, :destroy_artifacts, project).to_s
}
end
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index c928c6479de..b7acc562be5 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module AuthHelper
- PROVIDERS_WITH_ICONS = %w(
+ PROVIDERS_WITH_ICONS = %w[
alicloud
atlassian_oauth2
auth0
@@ -15,12 +15,11 @@ module AuthHelper
google_oauth2
jwt
openid_connect
- salesforce
shibboleth
twitter
- ).freeze
- LDAP_PROVIDER = /\Aldap/.freeze
- POPULAR_PROVIDERS = %w(google_oauth2 github).freeze
+ ].freeze
+ LDAP_PROVIDER = /\Aldap/
+ POPULAR_PROVIDERS = %w[google_oauth2 github].freeze
delegate :slack_app_id, to: :'Gitlab::CurrentSettings.current_application_settings'
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 6e0ba748d85..e6212ee7d8d 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -7,6 +7,15 @@ module ButtonHelper
# :text - Text to copy (optional)
# :gfm - GitLab Flavored Markdown to copy, if different from `text` (optional)
# :target - Selector for target element to copy from (optional)
+ # :class - CSS classes to be applied to the button (optional)
+ # :title - Button's title attribute (used for the tooltip) (optional)
+ # :button_text - Button's displayed label (optional)
+ # :hide_tooltip - Whether the tooltip should be hidden (optional, default: false)
+ # :hide_button_icon - Whether the icon should be hidden (optional, default: false)
+ # :item_prop - itemprop attribute
+ # :variant - Button variant (optional, default: :default)
+ # :category - Button category (optional, default: :tertiary)
+ # :size - Button size (optional, default: :small)
#
# Examples:
#
@@ -20,6 +29,65 @@ module ButtonHelper
#
# See http://clipboardjs.com/#usage
def clipboard_button(data = {})
+ css_class = data.delete(:class)
+ title = data.delete(:title) || _('Copy')
+ button_text = data[:button_text] || nil
+ hide_tooltip = data[:hide_tooltip] || false
+ hide_button_icon = data[:hide_button_icon] || false
+ item_prop = data[:itemprop] || nil
+ variant = data[:variant] || :default
+ category = data[:category] || :tertiary
+ size = data[:size] || :small
+
+ # This supports code in app/assets/javascripts/copy_to_clipboard.js that
+ # works around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM.
+ if text = data.delete(:text)
+ data[:clipboard_text] =
+ if gfm = data.delete(:gfm)
+ { text: text, gfm: gfm }
+ else
+ text
+ end
+ end
+
+ target = data.delete(:target)
+ data[:clipboard_target] = target if target
+
+ unless hide_tooltip
+ data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
+ end
+
+ render ::Pajamas::ButtonComponent.new(
+ icon: hide_button_icon ? nil : 'copy-to-clipboard',
+ variant: variant,
+ category: category,
+ size: size,
+ button_options: { class: css_class, title: title, aria: { label: title, live: 'polite' }, data: data, itemprop: item_prop }) do
+ button_text
+ end
+ end
+
+ # Output a "Copy to Clipboard" button
+ # Note: This is being replaced by a Pajamas-compliant helper that renders the button
+ # via ::Pajamas::ButtonComponent. Please use clipboard_button instead.
+ #
+ # data - Data attributes passed to `content_tag` (default: {}):
+ # :text - Text to copy (optional)
+ # :gfm - GitLab Flavored Markdown to copy, if different from `text` (optional)
+ # :target - Selector for target element to copy from (optional)
+ #
+ # Examples:
+ #
+ # # Define the clipboard's text
+ # clipboard_button(text: "Foo")
+ # # => "<button class='...' data-clipboard-text='Foo'>...</button>"
+ #
+ # # Define the target element
+ # clipboard_button(target: "div#foo")
+ # # => "<button class='...' data-clipboard-target='div#foo'>...</button>"
+ #
+ # See http://clipboardjs.com/#usage
+ def deprecated_clipboard_button(data = {})
css_class = data.delete(:class) || 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm'
title = data.delete(:title) || _('Copy')
button_text = data[:button_text] || nil
diff --git a/app/helpers/ci/status_helper.rb b/app/helpers/ci/status_helper.rb
index ea5b613cb78..5d526a6abb6 100644
--- a/app/helpers/ci/status_helper.rb
+++ b/app/helpers/ci/status_helper.rb
@@ -9,55 +9,6 @@
#
module Ci
module StatusHelper
- def ci_label_for_status(status)
- if detailed_status?(status)
- return status.label
- end
-
- label = case status
- when 'success'
- 'passed'
- when 'success-with-warnings'
- 'passed with warnings'
- when 'manual'
- 'waiting for manual action'
- when 'scheduled'
- 'waiting for delayed job'
- else
- status
- end
- translation = "CiStatusLabel|#{label}"
- s_(translation)
- end
-
- def ci_text_for_status(status)
- if detailed_status?(status)
- return status.text
- end
-
- case status
- when 'success'
- s_('CiStatusText|passed')
- when 'success-with-warnings'
- s_('CiStatusText|passed')
- when 'manual'
- s_('CiStatusText|blocked')
- when 'scheduled'
- s_('CiStatusText|delayed')
- else
- # All states are already being translated inside the detailed statuses:
- # :running => Gitlab::Ci::Status::Running
- # :skipped => Gitlab::Ci::Status::Skipped
- # :failed => Gitlab::Ci::Status::Failed
- # :success => Gitlab::Ci::Status::Success
- # :canceled => Gitlab::Ci::Status::Canceled
- # The following states are customized above:
- # :manual => Gitlab::Ci::Status::Manual
- status_translation = "CiStatusText|#{status}"
- s_(status_translation)
- end
- end
-
def ci_status_for_statuseable(subject)
status = subject.try(:status) || 'not found'
status.humanize
@@ -138,11 +89,34 @@ module Ci
end
end
+ private
+
def detailed_status?(status)
status.respond_to?(:text) &&
status.respond_to?(:group) &&
status.respond_to?(:label) &&
status.respond_to?(:icon)
end
+
+ def ci_label_for_status(status)
+ if detailed_status?(status)
+ return status.label
+ end
+
+ label = case status
+ when 'success'
+ 'passed'
+ when 'success-with-warnings'
+ 'passed with warnings'
+ when 'manual'
+ 'waiting for manual action'
+ when 'scheduled'
+ 'waiting for delayed job'
+ else
+ status
+ end
+ translation = "CiStatusLabel|#{label}"
+ s_(translation)
+ end
end
end
diff --git a/app/helpers/ci/variables_helper.rb b/app/helpers/ci/variables_helper.rb
index a492c48e58c..0dbd1adeb71 100644
--- a/app/helpers/ci/variables_helper.rb
+++ b/app/helpers/ci/variables_helper.rb
@@ -42,8 +42,8 @@ module Ci
def ci_variable_type_options
[
- %w(Variable env_var),
- %w(File file)
+ %w[Variable env_var],
+ %w[File file]
]
end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index 5c410a28229..1989d6ab3d5 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -92,7 +92,7 @@ module ClustersHelper
end
def cluster_created?(cluster)
- !cluster.status_name.in?(%i/scheduled creating/)
+ !cluster.status_name.in?(%i[scheduled creating])
end
def can_admin_cluster?(user, cluster)
diff --git a/app/helpers/colors_helper.rb b/app/helpers/colors_helper.rb
index 80cf6f197e5..3cd7263c39e 100644
--- a/app/helpers/colors_helper.rb
+++ b/app/helpers/colors_helper.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module ColorsHelper
- HEX_COLOR_PATTERN = /\A\#(?:[0-9A-Fa-f]{3}){1,2}\Z/.freeze
+ HEX_COLOR_PATTERN = /\A\#(?:[0-9A-Fa-f]{3}){1,2}\Z/
def hex_color_to_rgb_array(hex_color)
unless hex_color.is_a?(String) && HEX_COLOR_PATTERN.match?(hex_color)
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index c5df53ec606..9a78d4d9ad5 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -16,7 +16,7 @@ module DiffHelper
def diff_view
@diff_view ||= begin
- diff_views = %w(inline parallel)
+ diff_views = %w[inline parallel]
diff_view = params[:view] || cookies[:diff_view]
diff_view = diff_views.first unless diff_views.include?(diff_view)
diff_view.to_sym
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index af0f1bd6808..69b3fdc2271 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -16,7 +16,7 @@ module EmailsHelper
def action_title(url)
return unless url
- %w(merge_requests issues commit).each do |action|
+ %w[merge_requests issues commit].each do |action|
if url.split("/").include?(action)
return "View #{action.humanize.singularize}"
end
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
index 8140ee97291..6e9379a5926 100644
--- a/app/helpers/environment_helper.rb
+++ b/app/helpers/environment_helper.rb
@@ -62,7 +62,7 @@ module EnvironmentHelper
klass = "ci-status ci-#{status.dasherize} #{ci_icon_utilities}"
text = "#{ci_icon_for_status(status)} <span class=\"gl-ml-2\">#{status_text}</span>".html_safe
- if deployment.deployable
+ if deployment.deployable.instance_of?(::Ci::Build)
link_to(text, deployment_path(deployment), class: klass)
else
content_tag(:span, text, class: klass)
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index cd768ba8a7b..80a56493653 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -57,11 +57,9 @@ module EnvironmentsHelper
'default_branch' => project.default_branch,
'project_path' => project_path(project),
'tags_path' => project_tags_path(project),
- 'external_dashboard_url' => project.metrics_setting_external_dashboard_url,
'custom_metrics_path' => project_prometheus_metrics_path(project),
'validate_query_path' => validate_query_project_prometheus_metrics_path(project),
- 'custom_metrics_available' => custom_metrics_available?(project).to_s,
- 'dashboard_timezone' => project.metrics_setting_dashboard_timezone.to_s.upcase
+ 'custom_metrics_available' => custom_metrics_available?(project).to_s
}
end
@@ -93,8 +91,7 @@ module EnvironmentsHelper
'empty_loading_svg_path' => image_path('illustrations/monitoring/loading.svg'),
'empty_no_data_svg_path' => image_path('illustrations/monitoring/no_data.svg'),
'empty_no_data_small_svg_path' => image_path('illustrations/chart-empty-state-small.svg'),
- 'empty_unable_to_connect_svg_path' => image_path('illustrations/monitoring/unable_to_connect.svg'),
- 'custom_dashboard_base_path' => Gitlab::Metrics::Dashboard::RepoDashboardFinder::DASHBOARD_ROOT
+ 'empty_unable_to_connect_svg_path' => image_path('illustrations/monitoring/unable_to_connect.svg')
}
end
end
diff --git a/app/helpers/external_link_helper.rb b/app/helpers/external_link_helper.rb
index 53dacfe0566..40079c0803d 100644
--- a/app/helpers/external_link_helper.rb
+++ b/app/helpers/external_link_helper.rb
@@ -7,6 +7,6 @@ module ExternalLinkHelper
link = link_to url, { target: '_blank', rel: 'noopener noreferrer' }.merge(options) do
"#{body}#{sprite_icon('external-link', css_class: 'gl-ml-2')}".html_safe
end
- sanitize(link, tags: %w(a svg use), attributes: %w(target rel data-testid class href).concat(options.stringify_keys.keys))
+ sanitize(link, tags: %w[a svg use], attributes: %w[target rel data-testid class href].concat(options.stringify_keys.keys))
end
end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index bba3fac7468..ebebdfa56e6 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -29,9 +29,10 @@ module IconsHelper
ActionController::Base.helpers.image_path('file_icons/file_icons.svg', host: sprite_base_url)
end
- def sprite_icon(icon_name, size: DEFAULT_ICON_SIZE, css_class: nil)
+ def sprite_icon(icon_name, size: DEFAULT_ICON_SIZE, css_class: nil, file_icon: false)
memoized_icon("#{icon_name}_#{size}_#{css_class}") do
- if known_sprites&.exclude?(icon_name)
+ unknown_icon = file_icon ? unknown_file_icon_sprite(icon_name) : unknown_icon_sprite(icon_name)
+ if unknown_icon
exception = ArgumentError.new("#{icon_name} is not a known icon in @gitlab-org/gitlab-svg")
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception)
end
@@ -39,10 +40,11 @@ module IconsHelper
css_classes = []
css_classes << "s#{size}" if size
css_classes << css_class.to_s unless css_class.blank?
+ sprite_path = file_icon ? sprite_file_icons_path : sprite_icon_path
content_tag(
:svg,
- content_tag(:use, '', { 'href' => "#{sprite_icon_path}##{icon_name}" }),
+ content_tag(:use, '', { 'href' => "#{sprite_path}##{icon_name}" }),
class: css_classes.empty? ? nil : css_classes.join(' '),
data: { testid: "#{icon_name}-icon" }
)
@@ -123,61 +125,73 @@ module IconsHelper
def file_type_icon_class(type, mode, name)
if type == 'folder'
- icon_class = 'folder-o'
+ 'folder-o'
elsif type == 'archive'
- icon_class = 'archive'
+ 'archive'
elsif mode == '120000'
- icon_class = 'share'
+ 'share'
else
# Guess which icon to choose based on file extension.
# If you think a file extension is missing, feel free to add it on PR
case File.extname(name).downcase
when '.pdf'
- icon_class = 'document'
+ 'document'
when '.jpg', '.jpeg', '.jif', '.jfif',
- '.jp2', '.jpx', '.j2k', '.j2c',
- '.apng', '.png', '.gif', '.tif', '.tiff',
- '.svg', '.ico', '.bmp', '.webp'
- icon_class = 'doc-image'
+ '.jp2', '.jpx', '.j2k', '.j2c',
+ '.apng', '.png', '.gif', '.tif', '.tiff',
+ '.svg', '.ico', '.bmp', '.webp'
+ 'doc-image'
when '.zip', '.zipx', '.tar', '.gz', '.gzip', '.tgz', '.bz', '.bzip',
- '.bz2', '.bzip2', '.car', '.tbz', '.xz', 'txz', '.rar', '.7z',
- '.lz', '.lzma', '.tlz'
- icon_class = 'doc-compressed'
+ '.bz2', '.bzip2', '.car', '.tbz', '.xz', 'txz', '.rar', '.7z',
+ '.lz', '.lzma', '.tlz'
+ 'doc-compressed'
when '.mp3', '.wma', '.ogg', '.oga', '.wav', '.flac', '.aac', '.3ga',
- '.ac3', '.midi', '.m4a', '.ape', '.mpa'
- icon_class = 'volume-up'
+ '.ac3', '.midi', '.m4a', '.ape', '.mpa'
+ 'volume-up'
when '.mp4', '.m4p', '.m4v',
- '.mpg', '.mp2', '.mpeg', '.mpe', '.mpv',
- '.mpg', '.mpeg', '.m2v', '.m2ts',
- '.avi', '.mkv', '.flv', '.ogv', '.mov',
- '.3gp', '.3g2'
- icon_class = 'live-preview'
+ '.mpg', '.mp2', '.mpeg', '.mpe', '.mpv',
+ '.mpg', '.mpeg', '.m2v', '.m2ts',
+ '.avi', '.mkv', '.flv', '.ogv', '.mov',
+ '.3gp', '.3g2'
+ 'live-preview'
when '.doc', '.dot', '.docx', '.docm', '.dotx', '.dotm', '.docb',
- '.odt', '.ott', '.uot', '.rtf'
- icon_class = 'doc-text'
+ '.odt', '.ott', '.uot', '.rtf'
+ 'doc-text'
when '.xls', '.xlt', '.xlm', '.xlsx', '.xlsm', '.xltx', '.xltm',
- '.xlsb', '.xla', '.xlam', '.xll', '.xlw', '.ots', '.ods', '.uos'
- icon_class = 'document'
+ '.xlsb', '.xla', '.xlam', '.xll', '.xlw', '.ots', '.ods', '.uos'
+ 'document'
when '.ppt', '.pot', '.pps', '.pptx', '.pptm', '.potx', '.potm',
- '.ppam', '.ppsx', '.ppsm', '.sldx', '.sldm', '.odp', '.otp', '.uop'
- icon_class = 'doc-chart'
+ '.ppam', '.ppsx', '.ppsm', '.sldx', '.sldm', '.odp', '.otp', '.uop'
+ 'doc-chart'
else
- icon_class = 'doc-text'
+ 'doc-text'
end
end
-
- icon_class
end
private
+ def unknown_icon_sprite(icon_name)
+ known_sprites&.exclude?(icon_name)
+ end
+
+ def unknown_file_icon_sprite(icon_name)
+ known_file_icon_sprites&.exclude?(icon_name)
+ end
+
def known_sprites
return if Rails.env.production?
@known_sprites ||= Gitlab::Json.parse(File.read(Rails.root.join('node_modules/@gitlab/svgs/dist/icons.json')))['icons']
end
+ def known_file_icon_sprites
+ return if Rails.env.production?
+
+ @known_file_icon_sprites ||= Gitlab::Json.parse(File.read(Rails.root.join('node_modules/@gitlab/svgs/dist/file_icons/file_icons.json')))['icons']
+ end
+
def memoized_icon(key)
@rendered_icons ||= {}
diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb
index 645a08bfcc0..a88be976337 100644
--- a/app/helpers/integrations_helper.rb
+++ b/app/helpers/integrations_helper.rb
@@ -277,11 +277,11 @@ module IntegrationsHelper
s_("ProjectService|Trigger event for new comments.")
when "confidential_note", "confidential_note_events"
s_("ProjectService|Trigger event for new comments on confidential issues.")
- when "issue", "issue_events"
+ when "issue", "issue_events", "issues_events"
s_("ProjectService|Trigger event when an issue is created, updated, or closed.")
- when "confidential_issue", "confidential_issue_events"
+ when "confidential_issue", "confidential_issue_events", "confidential_issues_events"
s_("ProjectService|Trigger event when a confidential issue is created, updated, or closed.")
- when "merge_request", "merge_request_events"
+ when "merge_request", "merge_request_events", "merge_requests_events"
s_("ProjectService|Trigger event when a merge request is created, updated, or merged.")
when "pipeline", "pipeline_events"
s_("ProjectService|Trigger event when a pipeline status changes.")
@@ -289,16 +289,20 @@ module IntegrationsHelper
s_("ProjectService|Trigger event when a wiki page is created or updated.")
when "commit", "commit_events"
s_("ProjectService|Trigger event when a commit is created or updated.")
- when "deployment"
+ when "deployment", "deployment_events"
s_("ProjectService|Trigger event when a deployment starts or finishes.")
- when "alert"
+ when "alert", "alert_events"
s_("ProjectService|Trigger event when a new, unique alert is recorded.")
- when "incident"
+ 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"
+ s_('When enabled, job logs are collected by Datadog and displayed along with pipeline execution traces.')
end
end
# rubocop:enable Metrics/CyclomaticComplexity
@@ -323,12 +327,15 @@ module IntegrationsHelper
def serialize_integration(integration, group: nil, project: nil)
{
- active: integration.operating?,
+ id: integration.id,
+ active: integration.activated?,
+ configured: integration.persisted?,
title: integration.title,
description: integration.description,
updated_at: integration.updated_at,
edit_path: scoped_edit_integration_path(integration, group: group, project: project),
- name: integration.to_param
+ name: integration.to_param,
+ icon: integration.try(:avatar_url)
}
end
end
diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb
index 422380f3cc6..0443861903b 100644
--- a/app/helpers/invite_members_helper.rb
+++ b/app/helpers/invite_members_helper.rb
@@ -37,23 +37,13 @@ module InviteMembersHelper
# Overridden in EE
def common_invite_modal_dataset(source)
- dataset = {
+ {
id: source.id,
root_id: source.root_ancestor&.id,
name: source.name,
default_access_level: Gitlab::Access::GUEST,
full_path: source.full_path
}
-
- if current_user && show_invite_members_for_task?
- dataset.merge!(
- tasks_to_be_done_options: tasks_to_be_done_options.to_json,
- projects: projects_for_source(source).to_json,
- new_project_path: source.is_a?(Group) ? new_project_path(namespace_id: source.id) : ''
- )
- end
-
- dataset
end
private
@@ -70,19 +60,6 @@ module InviteMembersHelper
def users_filter_data(group)
{}
end
-
- def show_invite_members_for_task?
- params[:open_modal] == 'invite_members_for_task'
- end
-
- def tasks_to_be_done_options
- ::MemberTask::TASKS.keys.map { |task| { value: task, text: localized_tasks_to_be_done_choices[task] } }
- end
-
- def projects_for_source(source)
- projects = source.is_a?(Project) ? [source] : source.projects
- projects.map { |project| { id: project.id, title: project.title } }
- end
end
InviteMembersHelper.prepend_mod_with('InviteMembersHelper')
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index c83545fa7a7..7f948db2f71 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -33,22 +33,6 @@ module IssuablesHelper
end
end
- def sidebar_milestone_tooltip_label(milestone)
- return _('Milestone') unless milestone.present?
-
- [escape_once(milestone[:title]), sidebar_milestone_remaining_days(milestone) || _('Milestone')].join('<br/>')
- end
-
- def sidebar_milestone_remaining_days(milestone)
- due_date_with_remaining_days(milestone[:due_date], milestone[:start_date])
- end
-
- def due_date_with_remaining_days(due_date, start_date = nil)
- return unless due_date
-
- "#{due_date.to_fs(:medium)} (#{remaining_days_in_words(due_date, start_date)})"
- end
-
def multi_label_name(current_labels, default_label)
return default_label if current_labels.blank?
@@ -131,46 +115,6 @@ module IssuablesHelper
end
# rubocop: enable CodeReuse/ActiveRecord
- def issuable_meta_author_status(author)
- return "" unless author&.status&.customized? && status = user_status(author)
-
- status.to_s.html_safe
- end
-
- def issuable_meta(issuable, project)
- output = []
-
- if issuable.respond_to?(:work_item_type) && WorkItems::Type::WI_TYPES_WITH_CREATED_HEADER.include?(issuable.issue_type)
- output << content_tag(:span, sprite_icon(issuable.work_item_type.icon_name.to_s, css_class: 'gl-icon gl-vertical-align-middle gl-text-gray-500'), class: 'gl-mr-2', aria: { hidden: 'true' })
- output << content_tag(:span, s_('IssuableStatus|%{wi_type} created %{created_at} by ').html_safe % { wi_type: IntegrationsHelper.integration_issue_type(issuable.issue_type), created_at: time_ago_with_tooltip(issuable.created_at) }, class: 'gl-mr-2')
- else
- output << content_tag(:span, s_('IssuableStatus|Created %{created_at} by').html_safe % { created_at: time_ago_with_tooltip(issuable.created_at) }, class: 'gl-mr-2')
- end
-
- if issuable.is_a?(Issue) && issuable.service_desk_reply_to
- output << "#{html_escape(issuable.present(current_user: current_user).service_desk_reply_to)} via "
- end
-
- output << content_tag(:strong) do
- author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline-block")
- author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-inline d-sm-none")
-
- author_output << issuable_meta_author_status(issuable.author)
-
- author_output
- end
-
- if access = project.team.human_max_access(issuable.author_id)
- output << content_tag(:span, access, class: "user-access-role has-tooltip d-none d-xl-inline-block gl-ml-3 ", title: _("This user has the %{access} role in the %{name} project.") % { access: access.downcase, name: project.name })
- elsif project.team.contributor?(issuable.author_id)
- output << content_tag(:span, _("Contributor"), class: "user-access-role has-tooltip d-none d-xl-inline-block gl-ml-3", title: _("This user has previously committed to the %{name} project.") % { name: project.name })
- end
-
- output << content_tag(:span, (sprite_icon('first-contribution', css_class: 'gl-icon gl-vertical-align-middle') if issuable.first_contribution?), class: 'has-tooltip gl-ml-2', title: _('1st contribution!'))
-
- output.join.html_safe
- end
-
def issuables_state_counter_text(issuable_type, state, display_count)
titles = {
opened: _("Open"),
@@ -248,71 +192,6 @@ module IssuablesHelper
data
end
- def issue_only_initial_data(issuable)
- return {} unless issuable.is_a?(Issue)
-
- data = {
- authorId: issuable.author.id,
- authorName: issuable.author.name,
- authorUsername: issuable.author.username,
- authorWebUrl: url_for(user_path(issuable.author)),
- createdAt: issuable.created_at.to_time.iso8601,
- hasClosingMergeRequest: issuable.merge_requests_count(current_user) != 0,
- isFirstContribution: issuable.first_contribution?,
- issueType: issuable.issue_type,
- serviceDeskReplyTo: issuable.present(current_user: current_user).service_desk_reply_to,
- zoomMeetingUrl: ZoomMeeting.canonical_meeting_url(issuable),
- sentryIssueIdentifier: SentryIssue.find_by(issue: issuable)&.sentry_issue_identifier, # rubocop:disable CodeReuse/ActiveRecord
- iid: issuable.iid.to_s,
- isHidden: issue_hidden?(issuable),
- canCreateIncident: create_issue_type_allowed?(issuable.project, :incident),
- **incident_only_initial_data(issuable)
- }
-
- data.tap do |d|
- if issuable.duplicated? && can?(current_user, :read_issue, issuable.duplicated_to)
- d[:duplicatedToIssueUrl] = url_for([issuable.duplicated_to.project, issuable.duplicated_to, { only_path: false }])
- end
-
- if issuable.moved? && can?(current_user, :read_issue, issuable.moved_to)
- d[:movedToIssueUrl] = url_for([issuable.moved_to.project, issuable.moved_to, { only_path: false }])
- end
- end
- end
-
- def incident_only_initial_data(issue)
- return {} unless issue.incident_type_issue?
-
- {
- hasLinkedAlerts: issue.alert_management_alerts.any?,
- canUpdateTimelineEvent: can?(current_user, :admin_incident_management_timeline_event, issue),
- currentPath: url_for(safe_params),
- currentTab: safe_params[:incident_tab]
- }
- end
-
- def path_data(parent)
- return { groupPath: parent.path } if parent.is_a?(Group)
-
- {
- projectPath: ref_project.path,
- projectId: ref_project.id,
- projectNamespace: ref_project.namespace.full_path
- }
- end
-
- def updated_at_by(issuable)
- return {} unless issuable.edited?
-
- {
- updatedAt: issuable.last_edited_at.to_time.iso8601,
- updatedBy: {
- name: issuable.last_edited_by.name,
- path: user_path(issuable.last_edited_by)
- }
- }
- end
-
def issuables_count_for_state(issuable_type, state)
Gitlab::IssuablesCountForState.new(finder, fast_fail: true, store_in_redis_cache: true)[state]
end
@@ -333,15 +212,6 @@ module IssuablesHelper
issuable.author == current_user
end
- def issuable_display_type(issuable)
- case issuable
- when Issue
- issuable.issue_type.downcase
- when MergeRequest
- issuable.model_name.human.downcase
- end
- end
-
def has_filter_bar_param?
finder.class.scalar_params.any? { |p| params[p].present? }
end
@@ -353,12 +223,6 @@ module IssuablesHelper
end
end
- def reviewer_sidebar_data(reviewer, merge_request: nil)
- { avatar_url: reviewer.avatar_url, name: reviewer.name, username: reviewer.username }.tap do |data|
- data[:can_merge] = merge_request.can_be_merged_by?(reviewer) if merge_request
- end
- end
-
def issuable_squash_option?(issuable, project)
if issuable.persisted?
issuable.squash
@@ -428,27 +292,6 @@ module IssuablesHelper
cookies[:collapsed_gutter] == 'true'
end
- def issuable_todo_button_data(issuable, is_collapsed)
- {
- todo_text: _('Add a to do'),
- mark_text: _('Mark as done'),
- todo_icon: sprite_icon('todo-add'),
- mark_icon: sprite_icon('todo-done', css_class: 'todo-undone'),
- issuable_id: issuable[:id],
- issuable_type: issuable[:type],
- create_path: issuable[:create_todo_path],
- delete_path: issuable.dig(:current_user, :todo, :delete_path),
- placement: is_collapsed ? 'left' : nil,
- container: is_collapsed ? 'body' : nil,
- boundary: 'viewport',
- is_collapsed: is_collapsed,
- track_label: "right_sidebar",
- track_property: "update_todo",
- track_action: "click_button",
- track_value: ""
- }
- end
-
def close_reopen_params(issuable, action)
{
issuable.model_name.to_s.underscore => { state_event: action }
@@ -520,6 +363,86 @@ module IssuablesHelper
number_with_delimiter(count)
end
end
+
+ def issue_only_initial_data(issuable)
+ return {} unless issuable.is_a?(Issue)
+
+ {
+ canCreateIncident: create_issue_type_allowed?(issuable.project, :incident),
+ fullPath: issuable.project.full_path,
+ iid: issuable.iid,
+ 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),
+ **work_items_data
+ }
+ end
+
+ def incident_only_initial_data(issue)
+ return {} unless issue.incident_type_issue?
+
+ {
+ hasLinkedAlerts: issue.alert_management_alerts.any?,
+ canUpdateTimelineEvent: can?(current_user, :admin_incident_management_timeline_event, issue),
+ currentPath: url_for(safe_params),
+ currentTab: safe_params[:incident_tab]
+ }
+ end
+
+ def issue_header_data(issuable)
+ data = {
+ authorId: issuable.author.id,
+ authorName: issuable.author.name,
+ authorUsername: issuable.author.username,
+ authorWebUrl: url_for(user_path(issuable.author)),
+ createdAt: issuable.created_at.to_time.iso8601,
+ isFirstContribution: issuable.first_contribution?,
+ serviceDeskReplyTo: issuable.present(current_user: current_user).service_desk_reply_to
+ }
+
+ data.tap do |d|
+ if issuable.duplicated? && can?(current_user, :read_issue, issuable.duplicated_to)
+ d[:duplicatedToIssueUrl] = url_for([issuable.duplicated_to.project, issuable.duplicated_to, { only_path: false }])
+ end
+
+ if issuable.moved? && can?(current_user, :read_issue, issuable.moved_to)
+ d[:movedToIssueUrl] = url_for([issuable.moved_to.project, issuable.moved_to, { only_path: false }])
+ end
+ end
+ end
+
+ def work_items_data
+ {
+ registerPath: new_user_registration_path(redirect_to_referer: 'yes'),
+ signInPath: new_session_path(:user, redirect_to_referer: 'yes')
+ }
+ end
+
+ def path_data(parent)
+ return { groupPath: parent.path } if parent.is_a?(Group)
+
+ {
+ projectPath: ref_project.path,
+ projectId: ref_project.id,
+ projectNamespace: ref_project.namespace.full_path
+ }
+ end
+
+ def updated_at_by(issuable)
+ return {} unless issuable.edited?
+
+ {
+ updatedAt: issuable.last_edited_at.to_time.iso8601,
+ updatedBy: {
+ name: issuable.last_edited_by.name,
+ path: user_path(issuable.last_edited_by)
+ }
+ }
+ end
end
IssuablesHelper.prepend_mod_with('IssuablesHelper')
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index ed655b562c2..4419b573701 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -35,15 +35,6 @@ module IssuesHelper
end
end
- def issue_status_visibility(issue, status_box:)
- case status_box
- when :open
- 'hidden' if issue.closed?
- when :closed
- 'hidden' unless issue.closed?
- end
- end
-
def confidential_icon(issue)
sprite_icon('eye-slash', css_class: 'gl-vertical-align-text-bottom') if issue.confidential?
end
@@ -128,24 +119,6 @@ module IssuesHelper
can?(current_user, :create_merge_request_in, @project)
end
- def issue_closed_link(issue, current_user, css_class: '')
- if issue.moved? && can?(current_user, :read_issue, issue.moved_to)
- link_to(s_('IssuableStatus|moved'), issue.moved_to, class: css_class)
- elsif issue.duplicated? && can?(current_user, :read_issue, issue.duplicated_to)
- link_to(s_('IssuableStatus|duplicated'), issue.duplicated_to, class: css_class)
- end
- end
-
- def issue_closed_text(issue, current_user)
- link = issue_closed_link(issue, current_user, css_class: 'text-underline gl-reset-color!')
-
- if link
- s_('IssuableStatus|Closed (%{link})').html_safe % { link: link }
- else
- s_('IssuableStatus|Closed')
- end
- end
-
def show_moved_service_desk_issue_warning?(issue)
return false unless issue.moved_from
return false unless issue.from_service_desk?
@@ -167,11 +140,8 @@ module IssuesHelper
can_reopen_issue: can?(current_user, :reopen_issue, issuable).to_s,
can_report_spam: issuable.submittable_as_spam_by?(current_user).to_s,
can_update_issue: can?(current_user, :update_issue, issuable).to_s,
- iid: issuable.iid,
- issuable_id: issuable.id,
is_issue_author: (issuable.author == current_user).to_s,
issue_path: issuable_path(issuable),
- issue_type: issuable_display_type(issuable),
new_issue_path: new_project_issue_path(project, new_issuable_params),
project_path: project.full_path,
report_abuse_path: add_category_abuse_reports_path,
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 79bab0969d1..a6bc9bcf205 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -68,7 +68,7 @@ module LabelsHelper
# We need the `label` argument here for EE
def wrap_label_html(label_html, small:, label:)
- wrapper_classes = %w(gl-label)
+ wrapper_classes = %w[gl-label]
wrapper_classes << 'gl-label-sm' if small
%(<span class="#{wrapper_classes.join(' ')}">#{label_html}</span>).html_safe
@@ -220,10 +220,16 @@ module LabelsHelper
project || group&.subgroup?
end
+ def label_lock_on_merge_help_text
+ _('IMPORTANT: Use this setting only for VERY strict auditing purposes. ' \
+ 'When turned on, nobody will be able to remove the label from any merge requests after they are merged. ' \
+ 'In addition, nobody will be able to turn off this setting or delete this label.')
+ end
+
private
def render_label_link(label_html, link:, title:, dataset:)
- classes = %w(gl-link gl-label-link)
+ classes = %w[gl-link gl-label-link]
dataset ||= {}
if title.present?
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 1a44f3554b0..1bd5cc41961 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -63,7 +63,7 @@ module MarkupHelper
md = markdown_field(object, attribute, options.merge(post_process: false))
return unless md.present?
- tags = %w(a gl-emoji b strong i em pre code p span)
+ tags = %w[a gl-emoji b strong i em pre code p span]
context = markdown_field_render_context(object, attribute, options)
context.reverse_merge!(truncate_visible_max_chars: max_chars || md.length)
@@ -73,11 +73,11 @@ module MarkupHelper
text,
tags: tags,
attributes: Rails::Html::WhiteListSanitizer.allowed_attributes +
- %w(
+ %w[
style data-src data-name data-unicode-version data-html
data-reference-type data-project-path data-iid data-mr-title
data-user
- )
+ ]
)
render_links(text)
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index 42ffe338367..e4c1d7932aa 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -54,14 +54,6 @@ module MembersHelper
end
end
- def localized_tasks_to_be_done_choices
- {
- code: s_('TasksToBeDone|Create/import code into a project (repository)'),
- ci: s_('TasksToBeDone|Set up CI/CD pipelines to build, test, deploy, and monitor code'),
- issues: s_('TasksToBeDone|Create/import issues (tickets) to collaborate on ideas and plan work')
- }.freeze
- end
-
private
def source_text(member)
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 32a183d6cd8..a90a16e120c 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -226,6 +226,13 @@ module MergeRequestsHelper
}
end
+ def mr_compare_form_data(_, merge_request)
+ {
+ source_branch_url: project_new_merge_request_branch_from_path(merge_request.source_project),
+ target_branch_url: project_new_merge_request_branch_to_path(merge_request.source_project)
+ }
+ end
+
private
def review_requested_merge_requests_count
@@ -269,7 +276,7 @@ module MergeRequestsHelper
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: 'btn btn-default btn-sm gl-button btn-default-tertiary btn-icon gl-display-none! gl-md-display-inline-block! js-source-branch-copy')
+ 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'
diff --git a/app/helpers/nav/new_dropdown_helper.rb b/app/helpers/nav/new_dropdown_helper.rb
index 306c4d8694e..5274ace3d8a 100644
--- a/app/helpers/nav/new_dropdown_helper.rb
+++ b/app/helpers/nav/new_dropdown_helper.rb
@@ -157,7 +157,7 @@ module Nav
partial: partial,
component: 'invite_members',
data: {
- trigger_source: 'top-nav',
+ trigger_source: 'top_nav',
trigger_element: 'text-emoji'
}
)
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 4cbd5029ac9..d3707183964 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -79,11 +79,11 @@ module NavHelper
end
def admin_monitoring_nav_links
- %w(system_info background_migrations background_jobs health_check)
+ %w[system_info background_migrations background_jobs health_check]
end
def admin_analytics_nav_links
- %w(dev_ops_report usage_trends)
+ %w[dev_ops_report usage_trends]
end
def show_super_sidebar?(user = current_user)
diff --git a/app/helpers/organizations/organization_helper.rb b/app/helpers/organizations/organization_helper.rb
new file mode 100644
index 00000000000..6b5c4342c5c
--- /dev/null
+++ b/app/helpers/organizations/organization_helper.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Organizations
+ module OrganizationHelper
+ def organization_show_app_data(organization)
+ {
+ organization: organization.slice(:id, :name),
+ groups_and_projects_organization_path: groups_and_projects_organization_path(organization),
+ # TODO: Update counts to use real data
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/424531
+ association_counts: {
+ groups: 10,
+ projects: 5,
+ users: 1050
+ }
+ }.merge(shared_groups_and_projects_app_data).to_json
+ end
+
+ def organization_groups_and_projects_app_data
+ shared_groups_and_projects_app_data.to_json
+ end
+
+ private
+
+ def shared_groups_and_projects_app_data
+ {
+ projects_empty_state_svg_path: image_path('illustrations/empty-state/empty-projects-md.svg'),
+ groups_empty_state_svg_path: image_path('illustrations/empty-state/empty-groups-md.svg'),
+ new_group_path: new_group_path,
+ new_project_path: new_project_path
+ }
+ end
+ end
+end
diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb
index 05605394d57..8d260d5e455 100644
--- a/app/helpers/profiles_helper.rb
+++ b/app/helpers/profiles_helper.rb
@@ -34,7 +34,7 @@ module ProfilesHelper
def middle_dot_divider_classes(stacking, breakpoint)
['gl-mb-3'].tap do |classes|
if stacking
- classes.concat(%w(middle-dot-divider-sm gl-display-block gl-sm-display-inline-block))
+ classes.concat(%w[middle-dot-divider-sm gl-display-block gl-sm-display-inline-block])
else
classes << 'gl-display-inline-block'
classes << if breakpoint.nil?
diff --git a/app/helpers/projects/observability_helper.rb b/app/helpers/projects/observability_helper.rb
deleted file mode 100644
index 4515fdb1bc3..00000000000
--- a/app/helpers/projects/observability_helper.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module ObservabilityHelper
- def observability_tracing_view_model(project)
- Gitlab::Json.generate({
- tracingUrl: Gitlab::Observability.tracing_url(project),
- provisioningUrl: Gitlab::Observability.provisioning_url(project),
- oauthUrl: Gitlab::Observability.oauth_url
- })
- end
-
- def observability_tracing_details_model(project, trace_id)
- Gitlab::Json.generate({
- tracingIndexUrl: namespace_project_tracing_index_path(project.group, project),
- traceId: trace_id,
- tracingUrl: Gitlab::Observability.tracing_url(project),
- provisioningUrl: Gitlab::Observability.provisioning_url(project),
- oauthUrl: Gitlab::Observability.oauth_url
- })
- end
- end
-end
diff --git a/app/helpers/projects/pipeline_helper.rb b/app/helpers/projects/pipeline_helper.rb
index 42e8e44c94c..0c3b7d26fe2 100644
--- a/app/helpers/projects/pipeline_helper.rb
+++ b/app/helpers/projects/pipeline_helper.rb
@@ -18,7 +18,7 @@ module Projects
suite_endpoint: project_pipeline_test_path(project, pipeline, suite_name: 'suite', format: :json),
blob_path: project_blob_path(project, pipeline.sha),
has_test_report: pipeline.has_test_reports?,
- empty_state_image_path: image_path('illustrations/empty-state/empty-test-cases-lg.svg'),
+ empty_state_image_path: image_path('illustrations/empty-todos-md.svg'),
empty_dag_svg_path: image_path('illustrations/empty-state/empty-dag-md.svg'),
artifacts_expired_image_path: image_path('illustrations/pipeline.svg'),
tests_count: pipeline.test_report_summary.total[:count]
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 754e1b7c2a2..e45b38f2266 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -373,14 +373,6 @@ module ProjectsHelper
false
end
- def metrics_external_dashboard_url
- @project.metrics_setting_external_dashboard_url
- end
-
- def metrics_dashboard_timezone
- @project.metrics_setting_dashboard_timezone
- end
-
def grafana_integration_url
@project.grafana_integration&.grafana_url
end
diff --git a/app/helpers/registrations_helper.rb b/app/helpers/registrations_helper.rb
index 4acba9b68d7..c2c142bca4d 100644
--- a/app/helpers/registrations_helper.rb
+++ b/app/helpers/registrations_helper.rb
@@ -7,7 +7,7 @@ module RegistrationsHelper
min_length_message: s_('SignUp|Username is too short (minimum is %{min_length} characters).') % { min_length: User::MIN_USERNAME_LENGTH },
max_length: User::MAX_USERNAME_LENGTH,
max_length_message: s_('SignUp|Username is too long (maximum is %{max_length} characters).') % { max_length: User::MAX_USERNAME_LENGTH },
- qa_selector: 'new_user_username_field'
+ testid: 'new-user-username-field'
}
end
diff --git a/app/helpers/routing/projects_helper.rb b/app/helpers/routing/projects_helper.rb
index 9b4aafe49b4..06de9022be4 100644
--- a/app/helpers/routing/projects_helper.rb
+++ b/app/helpers/routing/projects_helper.rb
@@ -43,12 +43,11 @@ module Routing
end
def work_item_url(entity, *args)
- # TODO: we do not have a route to access group level work items yet.
- # That is to be done as part of view group level work item issue:
- # see https://gitlab.com/gitlab-org/gitlab/-/work_items/393987
- return unless entity.project.present?
-
- project_work_items_url(entity.project, entity.iid, *args)
+ if entity.project.present?
+ project_work_items_url(entity.project, entity.iid, *args)
+ else
+ group_work_item_url(entity.namespace, entity.iid, *args)
+ end
end
def merge_request_url(entity, *args)
@@ -94,6 +93,8 @@ module Routing
private
def use_work_items_path?(issue)
+ return true if issue.project.blank? && issue.namespace.present?
+
issue.issue_type == 'task'
end
end
diff --git a/app/helpers/safe_format_helper.rb b/app/helpers/safe_format_helper.rb
index d39a972f3f3..71bfc9ecb40 100644
--- a/app/helpers/safe_format_helper.rb
+++ b/app/helpers/safe_format_helper.rb
@@ -36,7 +36,7 @@ module SafeFormatHelper
# Returns an empty Hash if +tag+ is not a valid paired tag (e.g. <p>foo</p>).
# an empty Hash is returned.
#
- # @param [String] tag is a HTML-safe output from tag helper
+ # @param [String] html_tag is a HTML-safe output from tag helper
# @param [Symbol,Object] open_name name of opening tag
# @param [Symbol,Object] close_name name of closing tag
# @raise [ArgumentError] if +tag+ is not HTML-safe
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index cd32023adb6..f002a0c454d 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -242,7 +242,7 @@ module SearchHelper
elsif current_controller?(:commits)
'commits'
elsif current_controller?(:groups)
- if %w(issues merge_requests).include?(controller.action_name)
+ if %w[issues merge_requests].include?(controller.action_name)
controller.action_name
end
end
@@ -479,7 +479,7 @@ module SearchHelper
end.to_json
end
- def search_filter_input_options(type, placeholder = _('Search or filter results...'))
+ def search_filter_input_options(type, placeholder = _('Search or filter results…'))
opts =
{
id: "filtered-search-#{type}",
@@ -537,14 +537,14 @@ module SearchHelper
source,
count_tags: false,
count_tail: false,
- filtered_tags: %w(img),
+ filtered_tags: %w[img],
max_length: 200
)
end
def search_sanitize(html)
# Truncato's filtered_tags and filtered_attributes are not quite the same
- sanitize(html, tags: %w(a p ol ul li pre code))
+ sanitize(html, tags: %w[a p ol ul li pre code])
end
# _search_highlight is used in EE override
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index 1bd7da0a352..33ca5ad584e 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -64,7 +64,8 @@ module SidebarsHelper
gitlab_version: Gitlab.version_info,
gitlab_version_check: gitlab_version_check,
search: search_data,
- panel_type: panel_type
+ panel_type: panel_type,
+ shortcut_links: shortcut_links
}
end
@@ -106,7 +107,8 @@ module SidebarsHelper
update_pins_url: pins_path,
is_impersonating: impersonating?,
stop_impersonation_path: admin_impersonation_path,
- shortcut_links: shortcut_links(user, project: project)
+ shortcut_links: shortcut_links(user: user, project: project),
+ track_visits_path: track_namespace_visits_path
})
end
@@ -114,32 +116,43 @@ module SidebarsHelper
nav: nil, project: nil, user: nil, group: nil, current_ref: nil, ref_type: nil,
viewed_user: nil, organization: nil)
context_adds = { route_is_active: method(:active_nav_link?), is_super_sidebar: true }
- case nav
- when 'project'
- context = project_sidebar_context(project, user, current_ref, ref_type: ref_type, **context_adds)
- Sidebars::Projects::SuperSidebarPanel.new(context)
- when 'group'
- context = group_sidebar_context(group, user, **context_adds)
- Sidebars::Groups::SuperSidebarPanel.new(context)
- when 'profile'
- context = Sidebars::Context.new(current_user: user, container: user, **context_adds)
- Sidebars::UserSettings::Panel.new(context)
- when 'user_profile'
- context = Sidebars::Context.new(current_user: user, container: viewed_user, **context_adds)
- Sidebars::UserProfile::Panel.new(context)
- when 'explore'
- Sidebars::Explore::Panel.new(Sidebars::Context.new(current_user: user, container: nil, **context_adds))
- when 'search'
- context = Sidebars::Context.new(current_user: user, container: nil, **context_adds)
- Sidebars::Search::Panel.new(context)
- when 'admin'
- Sidebars::Admin::Panel.new(Sidebars::Context.new(current_user: user, container: nil, **context_adds))
- when 'organization'
- context = organization_sidebar_context(organization, user, **context_adds)
- Sidebars::Organizations::SuperSidebarPanel.new(context)
- else
+ panel = case nav
+ when 'project'
+ context = project_sidebar_context(project, user, current_ref, ref_type: ref_type, **context_adds)
+ Sidebars::Projects::SuperSidebarPanel.new(context)
+ when 'group'
+ context = group_sidebar_context(group, user, **context_adds)
+ Sidebars::Groups::SuperSidebarPanel.new(context)
+ when 'profile'
+ context = Sidebars::Context.new(current_user: user, container: user, **context_adds)
+ Sidebars::UserSettings::Panel.new(context)
+ when 'user_profile'
+ context = Sidebars::Context.new(current_user: user, container: viewed_user, **context_adds)
+ Sidebars::UserProfile::Panel.new(context)
+ when 'explore'
+ Sidebars::Explore::Panel.new(Sidebars::Context.new(current_user: user, container: nil, **context_adds))
+ when 'search'
+ context = Sidebars::Context.new(current_user: user, container: nil, **context_adds)
+ Sidebars::Search::Panel.new(context)
+ when 'admin'
+ Sidebars::Admin::Panel.new(Sidebars::Context.new(current_user: user, container: nil, **context_adds))
+ when 'organization'
+ context = organization_sidebar_context(organization, user, **context_adds)
+ Sidebars::Organizations::SuperSidebarPanel.new(context)
+ when 'your_work'
+ context = your_work_sidebar_context(user, **context_adds)
+ Sidebars::YourWork::Panel.new(context)
+ end
+
+ # We only return the panel if any menu item is rendered, otherwise fallback
+ return panel if panel&.render?
+
+ # Fallback menu "Your work" for logged-in users, "Explore" for logged-out
+ if user
context = your_work_sidebar_context(user, **context_adds)
Sidebars::YourWork::Panel.new(context)
+ else
+ Sidebars::Explore::Panel.new(Sidebars::Context.new(current_user: nil, container: nil, **context_adds))
end
end
@@ -387,7 +400,29 @@ module SidebarsHelper
!!session[:impersonator_id]
end
- def shortcut_links(user, project: nil)
+ def shortcut_links_anonymous
+ [
+ {
+ title: _('Snippets'),
+ href: explore_snippets_path,
+ css_class: 'dashboard-shortcuts-snippets'
+ },
+ {
+ title: _('Groups'),
+ href: explore_groups_path,
+ css_class: 'dashboard-shortcuts-groups'
+ },
+ {
+ title: _('Projects'),
+ href: explore_projects_path,
+ css_class: 'dashboard-shortcuts-projects'
+ }
+ ]
+ end
+
+ def shortcut_links(user: nil, project: nil)
+ return shortcut_links_anonymous unless user
+
shortcut_links = [
{
title: _('Milestones'),
@@ -403,6 +438,16 @@ module SidebarsHelper
title: _('Activity'),
href: activity_dashboard_path,
css_class: 'dashboard-shortcuts-activity'
+ },
+ {
+ title: _('Groups'),
+ href: dashboard_groups_path,
+ css_class: 'dashboard-shortcuts-groups'
+ },
+ {
+ title: _('Projects'),
+ href: dashboard_projects_path,
+ css_class: 'dashboard-shortcuts-projects'
}
]
diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb
index 07d83b8d850..21aa82aff1c 100644
--- a/app/helpers/sidekiq_helper.rb
+++ b/app/helpers/sidekiq_helper.rb
@@ -8,7 +8,7 @@ module SidekiqHelper
(?<state>[DIEKNRSTVWXZLpsl\+<>/\d]+)\s+
(?<start>.+?)\s+
(?<command>(?:ruby\d+:\s+)?sidekiq.*\].*)
- \z}x.freeze
+ \z}x
def parse_sidekiq_ps(line)
match = line.strip.match(SIDEKIQ_PS_REGEXP)
diff --git a/app/helpers/stat_anchors_helper.rb b/app/helpers/stat_anchors_helper.rb
index d9429f28be7..957985d6953 100644
--- a/app/helpers/stat_anchors_helper.rb
+++ b/app/helpers/stat_anchors_helper.rb
@@ -3,7 +3,7 @@
module StatAnchorsHelper
def stat_anchor_attrs(anchor)
{}.tap do |attrs|
- attrs[:class] = %w(nav-link gl-display-flex gl-align-items-center) << extra_classes(anchor)
+ attrs[:class] = %w[nav-link gl-display-flex gl-align-items-center] << extra_classes(anchor)
attrs[:itemprop] = anchor.itemprop if anchor.itemprop
attrs[:data] = anchor.data if anchor.data
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 4f17634f3e4..0d885621b6c 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -272,9 +272,9 @@ module TodosHelper
def show_todo_state?(todo)
case todo.target
when MergeRequest, Issue
- %w(closed merged).include?(todo.target.state)
+ %w[closed merged].include?(todo.target.state)
when AlertManagement::Alert
- %i(resolved).include?(todo.target.state)
+ %i[resolved].include?(todo.target.state)
else
false
end
diff --git a/app/helpers/users/callouts_helper.rb b/app/helpers/users/callouts_helper.rb
index 12f78d9bd16..1b5d0b276a3 100644
--- a/app/helpers/users/callouts_helper.rb
+++ b/app/helpers/users/callouts_helper.rb
@@ -14,7 +14,6 @@ module Users
PAGES_MOVED_CALLOUT = 'pages_moved_callout'
REGISTRATION_ENABLED_CALLOUT_ALLOWED_CONTROLLER_PATHS = [/^root/, /^dashboard\S*/, /^admin\S*/].freeze
WEB_HOOK_DISABLED = 'web_hook_disabled'
- ULTIMATE_FEATURE_REMOVAL_BANNER = 'ultimate_feature_removal_banner'
BRANCH_RULES_INFO_CALLOUT = 'branch_rules_info_callout'
NEW_NAVIGATION_CALLOUT = 'new_navigation_callout'
@@ -94,12 +93,6 @@ module Users
Gitlab.com? && current_user.created_at >= Date.new(2023, 6, 2)
end
- def ultimate_feature_removal_banner_dismissed?(project)
- return false unless project
-
- user_dismissed?(ULTIMATE_FEATURE_REMOVAL_BANNER, object: project)
- end
-
private
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil, object: nil)
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index ac279904fd2..30f8f6fdfe5 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -164,7 +164,7 @@ module UsersHelper
messageHtml: message,
actionPrimary: {
text: s_('AdminUsers|Confirm user'),
- attributes: [{ variant: 'confirm', 'data-qa-selector': 'confirm_user_confirm_button' }]
+ attributes: [{ variant: 'confirm', 'data-testid': 'confirm-user-confirm-button' }]
},
actionSecondary: {
text: _('Cancel'),
@@ -176,7 +176,7 @@ module UsersHelper
path: confirm_admin_user_path(user),
method: 'put',
modal_attributes: modal_attributes,
- qa_selector: 'confirm_user_button'
+ testid: 'confirm-user-button'
}
end
diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb
index dc8ef4e44be..45a4b292eb5 100644
--- a/app/helpers/version_check_helper.rb
+++ b/app/helpers/version_check_helper.rb
@@ -5,9 +5,8 @@ module VersionCheckHelper
def show_version_check?
return false unless Gitlab::CurrentSettings.version_check_enabled
- return false if User.single_user&.requires_usage_stats_consent?
- current_user&.can_read_all_resources?
+ current_user&.can_read_all_resources? && !User.single_user&.requires_usage_stats_consent?
end
def gitlab_version_check
diff --git a/app/helpers/vite_helper.rb b/app/helpers/vite_helper.rb
new file mode 100644
index 00000000000..4d1085a5169
--- /dev/null
+++ b/app/helpers/vite_helper.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module ViteHelper
+ def universal_javascript_include_tag(*args)
+ if vite_enabled
+ vite_javascript_tag(*args)
+ else
+ javascript_include_tag(*args)
+ end
+ end
+
+ def universal_asset_path(*args)
+ if vite_enabled
+ vite_asset_path(*args)
+ else
+ asset_path(*args)
+ end
+ end
+
+ private
+
+ def vite_enabled
+ Feature.enabled?(:vite) && !Rails.env.test? && vite_running
+ end
+
+ def vite_running
+ ViteRuby.instance.dev_server_running?
+ end
+end
diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb
index ba3c232bec4..92874168798 100644
--- a/app/helpers/webpack_helper.rb
+++ b/app/helpers/webpack_helper.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module WebpackHelper
+ include ViteHelper
+
def prefetch_link_tag(source)
href = asset_path(source)
@@ -14,7 +16,11 @@ module WebpackHelper
end
def webpack_bundle_tag(bundle)
- javascript_include_tag(*webpack_entrypoint_paths(bundle))
+ if vite_running
+ vite_javascript_tag bundle
+ else
+ javascript_include_tag(*webpack_entrypoint_paths(bundle))
+ end
end
def webpack_preload_asset_tag(asset, options = {})
@@ -32,6 +38,8 @@ module WebpackHelper
end
def webpack_controller_bundle_tags
+ return if Feature.enabled?(:vite) && !Rails.env.test?
+
chunks = []
action = case controller.action_name
diff --git a/app/helpers/work_items_helper.rb b/app/helpers/work_items_helper.rb
index 9036c7c8347..1969c98de8b 100644
--- a/app/helpers/work_items_helper.rb
+++ b/app/helpers/work_items_helper.rb
@@ -11,4 +11,10 @@ module WorkItemsHelper
report_abuse_path: add_category_abuse_reports_path
}
end
+
+ def work_items_list_data(group)
+ {
+ full_path: group.full_path
+ }
+ end
end
diff --git a/app/mailers/emails/in_product_marketing.rb b/app/mailers/emails/in_product_marketing.rb
index 972c1da065a..92743dc1926 100644
--- a/app/mailers/emails/in_product_marketing.rb
+++ b/app/mailers/emails/in_product_marketing.rb
@@ -12,15 +12,6 @@ module Emails
'X-Mailgun-Tag' => 'marketing'
}.freeze
- def in_product_marketing_email(recipient_id, group_id, track, series)
- group = Group.find(group_id)
- user = User.find(recipient_id)
- email = user.notification_email_for(group)
- @message = Gitlab::Email::Message::InProductMarketing.for(track).new(group: group, user: user, series: series)
-
- mail_to(to: email, subject: @message.subject_line)
- end
-
def build_ios_app_guide_email(recipient_email)
@message = ::Gitlab::Email::Message::BuildIosAppGuide.new
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index 25d68d47228..a9e1efbdd5d 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -2,6 +2,8 @@
module Emails
module Profile
+ include SafeFormatHelper
+
def new_user_email(user_id, token = nil)
@current_user = @user = User.find(user_id)
@target_url = user_url(@user)
@@ -58,6 +60,28 @@ module Emails
end
# rubocop: enable CodeReuse/ActiveRecord
+ def resource_access_tokens_about_to_expire_email(recipient, resource, token_names)
+ @user = recipient
+ @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
+
+ mail_with_locale(
+ to: recipient.notification_email_or_default,
+ subject: subject(
+ safe_format(
+ _("Your resource access tokens will expire in %{days_to_expire} or less"),
+ days_to_expire: pluralize(@days_to_expire, _('day'))
+ )
+ )
+ )
+ end
+
def access_token_created_email(user, token_name)
return unless user&.active?
@@ -155,7 +179,7 @@ module Emails
@user = user
@email = email
- mail_with_locale(to: @user.notification_email_or_default, subject: subject(_("New email address added")))
+ email_with_layout(to: @user.notification_email_or_default, subject: subject(_("New email address added")))
end
def new_achievement_email(user, achievement)
diff --git a/app/mailers/emails/service_desk.rb b/app/mailers/emails/service_desk.rb
index f609c9318da..9f3611df2cc 100644
--- a/app/mailers/emails/service_desk.rb
+++ b/app/mailers/emails/service_desk.rb
@@ -4,6 +4,7 @@ module Emails
module ServiceDesk
extend ActiveSupport::Concern
include MarkupHelper
+ include ::ServiceDesk::CustomEmails::Logger
EMAIL_ATTACHMENTS_SIZE_LIMIT = 10.megabytes.freeze
@@ -61,9 +62,10 @@ module Emails
def service_desk_custom_email_verification_email(service_desk_setting)
@service_desk_setting = service_desk_setting
+ @project = @service_desk_setting.project
email_sender = sender(
- User.support_bot.id,
+ Users::Internal.support_bot.id,
send_from_user_email: false,
sender_name: @service_desk_setting.outgoing_name,
sender_email: @service_desk_setting.custom_email
@@ -73,7 +75,7 @@ module Emails
subject = format(s_("Notify|Verify custom email address %{email} for %{project_name}"),
email: @service_desk_setting.custom_email,
- project_name: @service_desk_setting.project.name
+ project_name: @project.name
)
options = {
@@ -119,7 +121,7 @@ module Emails
def setup_service_desk_mail(issue_id)
@issue = Issue.find(issue_id)
@project = @issue.project
- @support_bot = User.support_bot
+ @support_bot = Users::Internal.support_bot
@service_desk_setting = @project.service_desk_setting
@@ -139,6 +141,11 @@ module Emails
return mail if !service_desk_custom_email_enabled? && !force
return mail unless @service_desk_setting.custom_email_credential.present?
+ # Only set custom email reply address if it's enabled, not when we force it.
+ inject_service_desk_custom_email_reply_address unless force
+
+ log_info(project: @project)
+
mail.delivery_method(::Mail::SMTP, @service_desk_setting.custom_email_credential.delivery_options)
end
@@ -146,6 +153,15 @@ module Emails
Feature.enabled?(:service_desk_custom_email, @project) && @service_desk_setting&.custom_email_enabled?
end
+ def inject_service_desk_custom_email_reply_address
+ return unless Feature.enabled?(:service_desk_custom_email_reply, @project)
+
+ reply_address = Gitlab::Email::ServiceDesk::CustomEmail.reply_address(@issue, reply_key)
+ headers['Reply-To'] = Mail::Address.new(reply_address).tap do |address|
+ address.display_name = reply_display_name(@issue)
+ end
+ end
+
def service_desk_sender_email_address
return unless service_desk_custom_email_enabled?
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 4180e76e1a0..77d32a55941 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -208,6 +208,7 @@ class Notify < ApplicationMailer
headers["#{prefix}-ID"] = object.id
headers["#{prefix}-IID"] = object.iid if object.respond_to?(:iid)
+ headers["#{prefix}-State"] = object.state if object.respond_to?(:state)
end
def add_project_headers
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index f43f4511913..638df56b770 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -64,6 +64,10 @@ class NotifyPreview < ActionMailer::Preview
end
end
+ def resource_access_token_about_to_expire_email
+ Notify.resource_access_tokens_about_to_expire_email(user, group, ['token_name'])
+ end
+
def access_token_created_email
Notify.access_token_created_email(user, 'token_name').message
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 4da4d113a7f..d8510524c1f 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -46,6 +46,7 @@ class Ability
issues.select { |issue| issue.visible_to_user?(user) }
end
end
+ alias_method :work_items_readable_by_user, :issues_readable_by_user
# Returns an Array of MergeRequests that can be read by the given user.
#
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index 75c90d370c3..bf25c539830 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -61,10 +61,11 @@ class AbuseReport < ApplicationRecord
validates :screenshot, file_size: { maximum: MAX_FILE_SIZE }
validate :validate_screenshot_is_image
- scope :by_user_id, ->(id) { where(user_id: id) }
- scope :by_reporter_id, ->(id) { where(reporter_id: id) }
+ scope :by_user_id, ->(user_id) { where(user_id: user_id) }
+ scope :by_reporter_id, ->(reporter_id) { where(reporter_id: reporter_id) }
scope :by_category, ->(category) { where(category: category) }
scope :with_users, -> { includes(:reporter, :user) }
+ scope :with_labels, -> { includes(:labels) }
enum category: {
spam: 1,
@@ -141,8 +142,14 @@ class AbuseReport < ApplicationRecord
end
end
- def other_reports_for_user
- user.abuse_reports.id_not_in(id)
+ def past_closed_reports_for_user
+ user.abuse_reports.closed.id_not_in(id)
+ end
+
+ def similar_open_reports_for_user
+ return AbuseReport.none unless open?
+
+ user.abuse_reports.open.by_category(category).id_not_in(id).includes(:reporter)
end
private
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
index 7d025fb7738..e42f9eeef23 100644
--- a/app/models/active_session.rb
+++ b/app/models/active_session.rb
@@ -102,17 +102,16 @@ class ActiveSession
# set marketing cookie when user has active session
def self.set_active_user_cookie(auth)
- auth.cookies[:about_gitlab_active_user] =
+ expiration_time = 2.weeks.from_now
+
+ auth.cookies[:gitlab_user] =
{
value: true,
- domain: Gitlab.config.gitlab.host
+ domain: Gitlab.config.gitlab.host,
+ expires: expiration_time
}
end
- def self.unset_active_user_cookie(auth)
- auth.cookies.delete :about_gitlab_active_user
- end
-
def self.list(user)
Gitlab::Redis::Sessions.with do |redis|
cleaned_up_lookup_entries(redis, user).map do |raw_session|
diff --git a/app/models/alerting/project_alerting_setting.rb b/app/models/alerting/project_alerting_setting.rb
index 34fa27eb29b..7e94d41137f 100644
--- a/app/models/alerting/project_alerting_setting.rb
+++ b/app/models/alerting/project_alerting_setting.rb
@@ -14,6 +14,8 @@ module Alerting
algorithm: 'aes-256-gcm'
before_validation :ensure_token
+ after_create :create_http_integration
+ after_update :sync_http_integration
private
@@ -24,5 +26,31 @@ module Alerting
def generate_token
SecureRandom.hex
end
+
+ # Remove in next required stop after %16.4
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/338838
+ def sync_http_integration
+ project.alert_management_http_integrations
+ .for_endpoint_identifier('legacy-prometheus')
+ .take
+ &.update_columns(
+ encrypted_token: encrypted_token,
+ encrypted_token_iv: encrypted_token_iv
+ )
+ end
+
+ # Remove in next required stop after %16.4
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/338838
+ def create_http_integration
+ AlertManagement::HttpIntegration.insert({
+ project_id: project_id,
+ encrypted_token: encrypted_token,
+ encrypted_token_iv: encrypted_token_iv,
+ active: true,
+ name: 'Prometheus',
+ endpoint_identifier: 'legacy-prometheus',
+ type_identifier: :prometheus
+ })
+ end
end
end
diff --git a/app/models/analytics/cycle_analytics/runtime_limiter.rb b/app/models/analytics/cycle_analytics/runtime_limiter.rb
new file mode 100644
index 00000000000..063377c3ddb
--- /dev/null
+++ b/app/models/analytics/cycle_analytics/runtime_limiter.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ class RuntimeLimiter
+ delegate :monotonic_time, to: :'Gitlab::Metrics::System'
+
+ DEFAULT_MAX_RUNTIME = 200.seconds
+
+ attr_reader :max_runtime, :start_time
+
+ def initialize(max_runtime = DEFAULT_MAX_RUNTIME)
+ @start_time = monotonic_time
+ @max_runtime = max_runtime
+ end
+
+ def elapsed_time
+ monotonic_time - start_time
+ end
+
+ def over_time?
+ @last_check = elapsed_time >= max_runtime
+ end
+
+ def was_over_time?
+ !!@last_check
+ end
+ end
+ end
+end
diff --git a/app/models/analytics/cycle_analytics/stage_event_hash.rb b/app/models/analytics/cycle_analytics/stage_event_hash.rb
index 6443a970945..7dcabd01ebf 100644
--- a/app/models/analytics/cycle_analytics/stage_event_hash.rb
+++ b/app/models/analytics/cycle_analytics/stage_event_hash.rb
@@ -13,7 +13,7 @@ module Analytics
# Atomic, safe insert without retrying
query = <<~SQL
- WITH insert_cte AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} (
+ WITH insert_cte AS MATERIALIZED (
INSERT INTO #{quoted_table_name} (hash_sha256) VALUES (#{casted_hash_code}) ON CONFLICT DO NOTHING RETURNING ID
)
SELECT ids.id FROM (
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index f67efaf4f58..153257636ba 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -14,38 +14,30 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
ignore_column :send_user_confirmation_email, remove_with: '15.8', remove_after: '2022-12-18'
ignore_column :web_ide_clientside_preview_enabled, remove_with: '15.11', remove_after: '2023-04-22'
ignore_columns %i[instance_administration_project_id instance_administrators_group_id], remove_with: '16.2', remove_after: '2023-06-22'
- ignore_columns %i[
- encrypted_tofa_access_token_expires_in
- encrypted_tofa_access_token_expires_in_iv
- encrypted_tofa_client_library_args
- encrypted_tofa_client_library_args_iv
- encrypted_tofa_client_library_class
- encrypted_tofa_client_library_class_iv
- encrypted_tofa_client_library_create_credentials_method
- encrypted_tofa_client_library_create_credentials_method_iv
- encrypted_tofa_client_library_fetch_access_token_method
- encrypted_tofa_client_library_fetch_access_token_method_iv
- encrypted_tofa_credentials
- encrypted_tofa_credentials_iv
- encrypted_tofa_host
- encrypted_tofa_host_iv
- encrypted_tofa_request_json_keys
- encrypted_tofa_request_json_keys_iv
- encrypted_tofa_request_payload
- encrypted_tofa_request_payload_iv
- encrypted_tofa_response_json_keys
- encrypted_tofa_response_json_keys_iv
- encrypted_tofa_url
- encrypted_tofa_url_iv
- vertex_project
- ], remove_with: '16.3', remove_after: '2023-07-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
+ ], remove_with: '16.6', remove_after: '2023-10-22'
+
+ ignore_columns %i[
+ encrypted_product_analytics_clickhouse_connection_string
+ encrypted_product_analytics_clickhouse_connection_string_iv
+ encrypted_jitsu_administrator_password
+ encrypted_jitsu_administrator_password_iv
+ jitsu_host
+ 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'
+
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
'Admin Area > Settings > Metrics and profiling > Metrics - Grafana'
@@ -244,6 +236,11 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
hostname: true,
if: :snowplow_enabled
+ validates :snowplow_database_collector_hostname,
+ allow_blank: true,
+ hostname: true,
+ length: { maximum: 255 }
+
validates :max_attachment_size,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -300,6 +297,10 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
presence: true,
numericality: { only_integer: true, greater_than: 0 }
+ validates :decompress_archive_file_timeout,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
validates :repository_storages, presence: true
validate :check_repository_storages
validate :check_repository_storages_weighted
@@ -310,7 +311,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
if: :auto_devops_enabled?
validates :enabled_git_access_protocol,
- inclusion: { in: %w(ssh http), allow_blank: true }
+ inclusion: { in: %w[ssh http], allow_blank: true }
validates :domain_denylist,
presence: { message: 'Domain denylist cannot be empty if denylist is enabled.' },
@@ -551,7 +552,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
if: :external_authorization_service_enabled
validates :spam_check_endpoint_url,
- addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS.merge({ schemes: %w(tls grpc) }), allow_blank: true
+ addressable_url: ADDRESSABLE_URL_VALIDATION_OPTIONS.merge({ schemes: %w[tls grpc] }), allow_blank: true
validates :spam_check_endpoint_url,
presence: true,
@@ -666,6 +667,10 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
validates :gitlab_shell_operation_limit
end
+ validates :search_rate_limit_allowlist,
+ length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
+ allow_nil: false
+
validates :notes_create_limit_allowlist,
length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
allow_nil: false
@@ -794,18 +799,20 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
attr_encrypted :arkose_labs_public_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
attr_encrypted :arkose_labs_private_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
attr_encrypted :cube_api_key, encryption_options_base_32_aes_256_gcm
- attr_encrypted :jitsu_administrator_password, encryption_options_base_32_aes_256_gcm
attr_encrypted :telesign_customer_xid, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
attr_encrypted :telesign_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
- attr_encrypted :product_analytics_clickhouse_connection_string, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
attr_encrypted :product_analytics_configurator_connection_string, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
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 :ai_access_token, 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)
+ # 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
+ # it here.
validates :disable_feed_token,
- inclusion: { in: [true, false], message: N_('must be a boolean value') }
+ inclusion: { in: [true, false], message: N_('must be a boolean value') }, on: :update
validates :disable_admin_oauth_scopes,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
@@ -962,7 +969,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
end
def parsed_kroki_url
- @parsed_kroki_url ||= Gitlab::UrlBlocker.validate!(kroki_url, schemes: %w(http https), enforce_sanitization: true)[0]
+ @parsed_kroki_url ||= Gitlab::UrlBlocker.validate!(kroki_url, schemes: %w[http https], enforce_sanitization: true)[0]
rescue Gitlab::UrlBlocker::BlockedUrlError => e
self.errors.add(
:kroki_url,
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index f6bf535158a..5a90e246499 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -9,12 +9,12 @@ module ApplicationSettingImplementation
\s # any whitespace character
| # or
[\r\n] # any number of newline characters
- }x.freeze
+ }x
# Setting a key restriction to `-1` means that all keys of this type are
# forbidden.
FORBIDDEN_KEY_VALUE = KeyRestrictionValidator::FORBIDDEN
- VALID_RUNNER_REGISTRAR_TYPES = %w(project group).freeze
+ VALID_RUNNER_REGISTRAR_TYPES = %w[project group].freeze
DEFAULT_PROTECTED_PATHS = [
'/users/password',
@@ -37,7 +37,6 @@ module ApplicationSettingImplementation
{
admin_mode: false,
after_sign_up_text: nil,
- ai_access_token: nil,
akismet_enabled: false,
akismet_api_key: nil,
allow_local_requests_from_system_hooks: true,
@@ -53,6 +52,7 @@ module ApplicationSettingImplementation
container_registry_vendor: '',
container_registry_version: '',
custom_http_clone_url_root: nil,
+ decompress_archive_file_timeout: 210,
default_artifacts_expire_in: '30 days',
default_branch_name: nil,
default_branch_protection: Settings.gitlab['default_branch_protection'],
@@ -171,6 +171,7 @@ module ApplicationSettingImplementation
snowplow_app_id: nil,
snowplow_collector_hostname: nil,
snowplow_cookie_domain: nil,
+ snowplow_database_collector_hostname: nil,
snowplow_enabled: false,
sourcegraph_enabled: false,
sourcegraph_public_only: true,
@@ -254,6 +255,7 @@ module ApplicationSettingImplementation
user_deactivation_emails_enabled: true,
search_rate_limit: 30,
search_rate_limit_unauthenticated: 10,
+ search_rate_limit_allowlist: [],
users_get_by_id_limit: 300,
users_get_by_id_limit_allowlist: [],
can_create_group: true,
@@ -380,6 +382,14 @@ module ApplicationSettingImplementation
self.protected_paths = strings_to_array(values)
end
+ def protected_paths_for_get_request_raw
+ array_to_string(protected_paths_for_get_request)
+ end
+
+ def protected_paths_for_get_request_raw=(values)
+ self.protected_paths_for_get_request = strings_to_array(values)
+ end
+
def notes_create_limit_allowlist_raw
array_to_string(notes_create_limit_allowlist)
end
@@ -396,6 +406,14 @@ module ApplicationSettingImplementation
self.users_get_by_id_limit_allowlist = strings_to_array(values).map(&:downcase)
end
+ def search_rate_limit_allowlist_raw
+ array_to_string(search_rate_limit_allowlist)
+ end
+
+ def search_rate_limit_allowlist_raw=(values)
+ self.search_rate_limit_allowlist = strings_to_array(values).map(&:downcase)
+ end
+
def asset_proxy_whitelist=(values)
values = strings_to_array(values) if values.is_a?(String)
diff --git a/app/models/approval.rb b/app/models/approval.rb
index 9ded44fe425..ecc15077c8d 100644
--- a/app/models/approval.rb
+++ b/app/models/approval.rb
@@ -3,10 +3,13 @@
class Approval < ApplicationRecord
include CreatedAtFilterable
include Importable
+ include ShaAttribute
belongs_to :user
belongs_to :merge_request
+ sha_attribute :patch_id_sha
+
validates :merge_request_id, presence: true, unless: :importing?
validates :user_id, presence: true, uniqueness: { scope: [:merge_request_id] }
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index ebc43b04b1b..73e3fa709b0 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -78,7 +78,7 @@ class AwardEmoji < ApplicationRecord
end
def broadcast_note_update
- awardable.expire_etag_cache
+ awardable.broadcast_noteable_notes_changed
awardable.trigger_note_subscription_update
end
diff --git a/app/models/badge.rb b/app/models/badge.rb
index 23e6f305c32..f4e719887ba 100644
--- a/app/models/badge.rb
+++ b/app/models/badge.rb
@@ -18,7 +18,7 @@ class Badge < ApplicationRecord
# This regex is built dynamically using the keys from the PLACEHOLDER struct.
# So, we can easily add new placeholder just by modifying the PLACEHOLDER hash.
# This regex will build the new PLACEHOLDER_REGEX with the new information
- PLACEHOLDERS_REGEX = /(#{PLACEHOLDERS.keys.join('|')})/.freeze
+ PLACEHOLDERS_REGEX = /(#{PLACEHOLDERS.keys.join('|')})/
default_scope { order_created_at_asc } # rubocop:disable Cop/DefaultScope
diff --git a/app/models/blob_viewer/binary_stl.rb b/app/models/blob_viewer/binary_stl.rb
index 425f72decae..6ccf75200e5 100644
--- a/app/models/blob_viewer/binary_stl.rb
+++ b/app/models/blob_viewer/binary_stl.rb
@@ -6,7 +6,7 @@ module BlobViewer
include ClientSide
self.partial_name = 'stl'
- self.extensions = %w(stl)
+ self.extensions = %w[stl]
self.binary = true
end
end
diff --git a/app/models/blob_viewer/cargo_toml.rb b/app/models/blob_viewer/cargo_toml.rb
index 2f1ebd25b4f..eb2a6f4433d 100644
--- a/app/models/blob_viewer/cargo_toml.rb
+++ b/app/models/blob_viewer/cargo_toml.rb
@@ -4,7 +4,7 @@ module BlobViewer
class CargoToml < DependencyManager
include Static
- self.file_types = %i(cargo_toml)
+ self.file_types = %i[cargo_toml]
def manager_name
'Cargo'
diff --git a/app/models/blob_viewer/cartfile.rb b/app/models/blob_viewer/cartfile.rb
index ea0494033bf..58fc97a9ffc 100644
--- a/app/models/blob_viewer/cartfile.rb
+++ b/app/models/blob_viewer/cartfile.rb
@@ -4,7 +4,7 @@ module BlobViewer
class Cartfile < DependencyManager
include Static
- self.file_types = %i(cartfile)
+ self.file_types = %i[cartfile]
def manager_name
'Carthage'
diff --git a/app/models/blob_viewer/changelog.rb b/app/models/blob_viewer/changelog.rb
index 8810bd25809..7992fbf542c 100644
--- a/app/models/blob_viewer/changelog.rb
+++ b/app/models/blob_viewer/changelog.rb
@@ -6,7 +6,7 @@ module BlobViewer
include Static
self.partial_name = 'changelog'
- self.file_types = %i(changelog)
+ self.file_types = %i[changelog]
self.binary = false
def render_error
diff --git a/app/models/blob_viewer/composer_json.rb b/app/models/blob_viewer/composer_json.rb
index aac7271242e..3449780f50f 100644
--- a/app/models/blob_viewer/composer_json.rb
+++ b/app/models/blob_viewer/composer_json.rb
@@ -4,7 +4,7 @@ module BlobViewer
class ComposerJson < DependencyManager
include ServerSide
- self.file_types = %i(composer_json)
+ self.file_types = %i[composer_json]
def manager_name
'Composer'
diff --git a/app/models/blob_viewer/contributing.rb b/app/models/blob_viewer/contributing.rb
index fa224309e31..524104f176a 100644
--- a/app/models/blob_viewer/contributing.rb
+++ b/app/models/blob_viewer/contributing.rb
@@ -6,7 +6,7 @@ module BlobViewer
include Static
self.partial_name = 'contributing'
- self.file_types = %i(contributing)
+ self.file_types = %i[contributing]
self.binary = false
end
end
diff --git a/app/models/blob_viewer/csv.rb b/app/models/blob_viewer/csv.rb
index 633e3bd63d8..97fa890653d 100644
--- a/app/models/blob_viewer/csv.rb
+++ b/app/models/blob_viewer/csv.rb
@@ -6,7 +6,7 @@ module BlobViewer
include ClientSide
self.binary = false
- self.extensions = %w(csv)
+ self.extensions = %w[csv]
self.partial_name = 'csv'
self.switcher_icon = 'table'
end
diff --git a/app/models/blob_viewer/gemfile.rb b/app/models/blob_viewer/gemfile.rb
index 77220cdbd08..84edacb32bd 100644
--- a/app/models/blob_viewer/gemfile.rb
+++ b/app/models/blob_viewer/gemfile.rb
@@ -4,7 +4,7 @@ module BlobViewer
class Gemfile < DependencyManager
include Static
- self.file_types = %i(gemfile gemfile_lock)
+ self.file_types = %i[gemfile gemfile_lock]
def manager_name
'Bundler'
diff --git a/app/models/blob_viewer/gemspec.rb b/app/models/blob_viewer/gemspec.rb
index 274859a7710..645458467f4 100644
--- a/app/models/blob_viewer/gemspec.rb
+++ b/app/models/blob_viewer/gemspec.rb
@@ -4,7 +4,7 @@ module BlobViewer
class Gemspec < DependencyManager
include ServerSide
- self.file_types = %i(gemspec)
+ self.file_types = %i[gemspec]
def manager_name
'RubyGems'
diff --git a/app/models/blob_viewer/gitlab_ci_yml.rb b/app/models/blob_viewer/gitlab_ci_yml.rb
index e255b6d15d2..9cee536d15b 100644
--- a/app/models/blob_viewer/gitlab_ci_yml.rb
+++ b/app/models/blob_viewer/gitlab_ci_yml.rb
@@ -7,7 +7,7 @@ module BlobViewer
self.partial_name = 'gitlab_ci_yml'
self.loading_partial_name = 'gitlab_ci_yml_loading'
- self.file_types = %i(gitlab_ci)
+ self.file_types = %i[gitlab_ci]
self.binary = false
def validation_message(opts)
diff --git a/app/models/blob_viewer/go_mod.rb b/app/models/blob_viewer/go_mod.rb
index d4d117f899c..eebf057c6dc 100644
--- a/app/models/blob_viewer/go_mod.rb
+++ b/app/models/blob_viewer/go_mod.rb
@@ -11,9 +11,9 @@ module BlobViewer
(?<name>.*?) (?# module name)
\s*(?://.*)? (?# comment)
(?:\n|\z) (?# newline or end of file)
- }x.freeze
+ }x
- self.file_types = %i(go_mod go_sum)
+ self.file_types = %i[go_mod go_sum]
def manager_name
'Go Modules'
diff --git a/app/models/blob_viewer/godeps_json.rb b/app/models/blob_viewer/godeps_json.rb
index 743c759aea5..37a133848a0 100644
--- a/app/models/blob_viewer/godeps_json.rb
+++ b/app/models/blob_viewer/godeps_json.rb
@@ -4,7 +4,7 @@ module BlobViewer
class GodepsJson < DependencyManager
include Static
- self.file_types = %i(godeps_json)
+ self.file_types = %i[godeps_json]
def manager_name
'godep'
diff --git a/app/models/blob_viewer/license.rb b/app/models/blob_viewer/license.rb
index 3427227ad26..489b29380d0 100644
--- a/app/models/blob_viewer/license.rb
+++ b/app/models/blob_viewer/license.rb
@@ -6,7 +6,7 @@ module BlobViewer
include Static
self.partial_name = 'license'
- self.file_types = %i(license)
+ self.file_types = %i[license]
self.binary = false
def license
diff --git a/app/models/blob_viewer/markup.rb b/app/models/blob_viewer/markup.rb
index 6f002a6b224..4b04d8425fd 100644
--- a/app/models/blob_viewer/markup.rb
+++ b/app/models/blob_viewer/markup.rb
@@ -7,7 +7,7 @@ module BlobViewer
self.partial_name = 'markup'
self.extensions = Gitlab::MarkupHelper::EXTENSIONS
- self.file_types = %i(readme)
+ self.file_types = %i[readme]
self.binary = false
def banzai_render_context
diff --git a/app/models/blob_viewer/notebook.rb b/app/models/blob_viewer/notebook.rb
index 351502d451f..e6f1988d7a6 100644
--- a/app/models/blob_viewer/notebook.rb
+++ b/app/models/blob_viewer/notebook.rb
@@ -6,7 +6,7 @@ module BlobViewer
include ClientSide
self.partial_name = 'notebook'
- self.extensions = %w(ipynb)
+ self.extensions = %w[ipynb]
self.binary = false
self.switcher_icon = 'doc-text'
self.switcher_title = 'notebook'
diff --git a/app/models/blob_viewer/open_api.rb b/app/models/blob_viewer/open_api.rb
index 0551f3bb1e3..5d9c5bea8dc 100644
--- a/app/models/blob_viewer/open_api.rb
+++ b/app/models/blob_viewer/open_api.rb
@@ -6,7 +6,7 @@ module BlobViewer
include ClientSide
self.partial_name = 'openapi'
- self.file_types = %i(openapi)
+ self.file_types = %i[openapi]
self.binary = false
self.switcher_icon = 'api'
end
diff --git a/app/models/blob_viewer/package_json.rb b/app/models/blob_viewer/package_json.rb
index 5350b6b0626..c205c10b536 100644
--- a/app/models/blob_viewer/package_json.rb
+++ b/app/models/blob_viewer/package_json.rb
@@ -4,7 +4,7 @@ module BlobViewer
class PackageJson < DependencyManager
include ServerSide
- self.file_types = %i(package_json)
+ self.file_types = %i[package_json]
def manager_name
yarn? ? 'yarn' : 'npm'
diff --git a/app/models/blob_viewer/pdf.rb b/app/models/blob_viewer/pdf.rb
index e3542b91d5c..61957ef4228 100644
--- a/app/models/blob_viewer/pdf.rb
+++ b/app/models/blob_viewer/pdf.rb
@@ -6,7 +6,7 @@ module BlobViewer
include ClientSide
self.partial_name = 'pdf'
- self.extensions = %w(pdf)
+ self.extensions = %w[pdf]
self.binary = true
self.switcher_icon = 'document'
self.switcher_title = 'PDF'
diff --git a/app/models/blob_viewer/podfile.rb b/app/models/blob_viewer/podfile.rb
index 73d714f48ca..dcabcfc4d57 100644
--- a/app/models/blob_viewer/podfile.rb
+++ b/app/models/blob_viewer/podfile.rb
@@ -4,7 +4,7 @@ module BlobViewer
class Podfile < DependencyManager
include Static
- self.file_types = %i(podfile)
+ self.file_types = %i[podfile]
def manager_name
'CocoaPods'
diff --git a/app/models/blob_viewer/podspec.rb b/app/models/blob_viewer/podspec.rb
index 2303471583d..50ca3f5bd16 100644
--- a/app/models/blob_viewer/podspec.rb
+++ b/app/models/blob_viewer/podspec.rb
@@ -4,7 +4,7 @@ module BlobViewer
class Podspec < DependencyManager
include ServerSide
- self.file_types = %i(podspec)
+ self.file_types = %i[podspec]
def manager_name
'CocoaPods'
diff --git a/app/models/blob_viewer/podspec_json.rb b/app/models/blob_viewer/podspec_json.rb
index d606f72376d..03e680e2a8b 100644
--- a/app/models/blob_viewer/podspec_json.rb
+++ b/app/models/blob_viewer/podspec_json.rb
@@ -2,7 +2,7 @@
module BlobViewer
class PodspecJson < Podspec
- self.file_types = %i(podspec_json)
+ self.file_types = %i[podspec_json]
def package_name
@package_name ||= fetch_from_json('name')
diff --git a/app/models/blob_viewer/readme.rb b/app/models/blob_viewer/readme.rb
index f1a5c6a6acc..ec84977d8c5 100644
--- a/app/models/blob_viewer/readme.rb
+++ b/app/models/blob_viewer/readme.rb
@@ -6,7 +6,7 @@ module BlobViewer
include Static
self.partial_name = 'readme'
- self.file_types = %i(readme)
+ self.file_types = %i[readme]
self.binary = false
def visible_to?(current_user)
diff --git a/app/models/blob_viewer/requirements_txt.rb b/app/models/blob_viewer/requirements_txt.rb
index 58161e83493..7322e416c4c 100644
--- a/app/models/blob_viewer/requirements_txt.rb
+++ b/app/models/blob_viewer/requirements_txt.rb
@@ -4,7 +4,7 @@ module BlobViewer
class RequirementsTxt < DependencyManager
include Static
- self.file_types = %i(requirements_txt)
+ self.file_types = %i[requirements_txt]
def manager_name
'pip'
diff --git a/app/models/blob_viewer/route_map.rb b/app/models/blob_viewer/route_map.rb
index 6731536dfe1..a8c64bd5e6a 100644
--- a/app/models/blob_viewer/route_map.rb
+++ b/app/models/blob_viewer/route_map.rb
@@ -7,7 +7,7 @@ module BlobViewer
self.partial_name = 'route_map'
self.loading_partial_name = 'route_map_loading'
- self.file_types = %i(route_map)
+ self.file_types = %i[route_map]
self.binary = false
def validation_message
diff --git a/app/models/blob_viewer/sketch.rb b/app/models/blob_viewer/sketch.rb
index 90bc9be29f4..b7b1d412eff 100644
--- a/app/models/blob_viewer/sketch.rb
+++ b/app/models/blob_viewer/sketch.rb
@@ -6,7 +6,7 @@ module BlobViewer
include ClientSide
self.partial_name = 'sketch'
- self.extensions = %w(sketch)
+ self.extensions = %w[sketch]
self.binary = true
self.switcher_icon = 'doc-image'
self.switcher_title = 'preview'
diff --git a/app/models/blob_viewer/svg.rb b/app/models/blob_viewer/svg.rb
index 60a11fbd97e..afcd3a7c735 100644
--- a/app/models/blob_viewer/svg.rb
+++ b/app/models/blob_viewer/svg.rb
@@ -6,7 +6,7 @@ module BlobViewer
include ServerSide
self.partial_name = 'svg'
- self.extensions = %w(svg)
+ self.extensions = %w[svg]
self.binary = false
self.switcher_icon = 'doc-image'
self.switcher_title = 'image'
diff --git a/app/models/blob_viewer/yarn_lock.rb b/app/models/blob_viewer/yarn_lock.rb
index 196d9f96f23..75369370602 100644
--- a/app/models/blob_viewer/yarn_lock.rb
+++ b/app/models/blob_viewer/yarn_lock.rb
@@ -4,7 +4,7 @@ module BlobViewer
class YarnLock < DependencyManager
include Static
- self.file_types = %i(yarn_lock)
+ self.file_types = %i[yarn_lock]
def manager_name
'Yarn'
diff --git a/app/models/bulk_imports/batch_tracker.rb b/app/models/bulk_imports/batch_tracker.rb
index 2e79d41d46e..eb7fe9f9913 100644
--- a/app/models/bulk_imports/batch_tracker.rb
+++ b/app/models/bulk_imports/batch_tracker.rb
@@ -18,6 +18,8 @@ module BulkImports
event :start do
transition created: :started
+ # To avoid errors when re-starting a pipeline in case of network errors
+ transition started: :started
end
event :retry do
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index 644673e249e..437118c36e8 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -196,6 +196,10 @@ class BulkImports::Entity < ApplicationRecord
update!(has_failures: true)
end
+ def source_version
+ @source_version ||= bulk_import.source_version_info
+ end
+
private
def validate_parent_is_a_group
@@ -240,7 +244,9 @@ class BulkImports::Entity < ApplicationRecord
errors.add(
:source_full_path,
- Gitlab::Regex.bulk_import_source_full_path_regex_message
+ s_('BulkImport|must have a relative path structure with no HTTP ' \
+ 'protocol characters, or leading or trailing forward slashes. Path segments must not start or ' \
+ 'end with a special character, and must not contain consecutive special characters')
)
end
end
diff --git a/app/models/bulk_imports/file_transfer/group_config.rb b/app/models/bulk_imports/file_transfer/group_config.rb
index 6766c00246b..67d53056444 100644
--- a/app/models/bulk_imports/file_transfer/group_config.rb
+++ b/app/models/bulk_imports/file_transfer/group_config.rb
@@ -3,7 +3,7 @@
module BulkImports
module FileTransfer
class GroupConfig < BaseConfig
- SKIPPED_RELATIONS = %w(members).freeze
+ SKIPPED_RELATIONS = %w[members].freeze
def import_export_yaml
::Gitlab::ImportExport.group_config_file
diff --git a/app/models/bulk_imports/file_transfer/project_config.rb b/app/models/bulk_imports/file_transfer/project_config.rb
index 8d4c68f7b5a..890a0fb6ee4 100644
--- a/app/models/bulk_imports/file_transfer/project_config.rb
+++ b/app/models/bulk_imports/file_transfer/project_config.rb
@@ -3,10 +3,10 @@
module BulkImports
module FileTransfer
class ProjectConfig < BaseConfig
- SKIPPED_RELATIONS = %w(
+ SKIPPED_RELATIONS = %w[
project_members
group_members
- ).freeze
+ ].freeze
LFS_OBJECTS_RELATION = 'lfs_objects'
REPOSITORY_BUNDLE_RELATION = 'repository'
diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb
index cda19273f52..d3fbfe3aa55 100644
--- a/app/models/chat_name.rb
+++ b/app/models/chat_name.rb
@@ -3,9 +3,6 @@
class ChatName < ApplicationRecord
LAST_USED_AT_INTERVAL = 1.hour
- include IgnorableColumns
- ignore_column :integration_id, remove_with: '16.0', remove_after: '2023-04-22'
-
belongs_to :user
validates :user, presence: true
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 7a623b0cefb..2abb8e4be48 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -165,7 +165,10 @@ module Ci
scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where("#{quoted_table_name}.id = #{Ci::BuildTraceChunk.quoted_table_name}.build_id").select(1)) }
scope :with_stale_live_trace, -> { with_live_trace.finished_before(12.hours.ago) }
scope :finished_before, -> (date) { finished.where('finished_at < ?', date) }
- scope :license_management_jobs, -> { where(name: %i(license_management license_scanning)) } # handle license rename https://gitlab.com/gitlab-org/gitlab/issues/8911
+ scope :license_management_jobs, -> { where(name: %i[license_management license_scanning]) } # handle license rename https://gitlab.com/gitlab-org/gitlab/issues/8911
+ # WARNING: This scope could lead to performance implications for large size of tables `ci_builds` and ci_runners`.
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123131
+ scope :with_runner_type, -> (runner_type) { joins(:runner).where(runner: { runner_type: runner_type }) }
scope :with_secure_reports_from_config_options, -> (job_types) do
joins(:metadata).where("#{Ci::BuildMetadata.quoted_table_name}.config_options -> 'artifacts' -> 'reports' ?| array[:job_types]", job_types: job_types)
@@ -388,6 +391,9 @@ module Ci
name == 'pages'
end
+ # overridden on EE
+ def pages_path_prefix; end
+
def runnable?
true
end
@@ -408,7 +414,7 @@ module Ci
end
def options_scheduled_at
- ChronicDuration.parse(options[:start_in])&.seconds&.from_now
+ ChronicDuration.parse(options[:start_in], use_complete_matcher: true)&.seconds&.from_now
end
def action?
@@ -487,10 +493,7 @@ module Ci
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables unless persisted? && persisted_environment.present?
- variables.concat(persisted_environment.predefined_variables)
-
- variables.append(key: 'CI_ENVIRONMENT_ACTION', value: environment_action)
- variables.append(key: 'CI_ENVIRONMENT_TIER', value: environment_tier)
+ variables.append(key: 'CI_ENVIRONMENT_SLUG', value: environment_slug)
# Here we're passing unexpanded environment_url for runner to expand,
# and we need to make sure that CI_ENVIRONMENT_NAME and
@@ -735,7 +738,7 @@ module Ci
def artifacts_expire_in=(value)
self.artifacts_expire_at =
if value
- ChronicDuration.parse(value)&.seconds&.from_now
+ ChronicDuration.parse(value, use_complete_matcher: true)&.seconds&.from_now
end
end
@@ -1039,6 +1042,13 @@ module Ci
end
end
+ def time_in_queue_seconds
+ return if queued_at.nil?
+
+ (::Time.current - queued_at).seconds.to_i
+ end
+ strong_memoize_attr :time_in_queue_seconds
+
protected
def run_status_commit_hooks!
diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb
index 317f2523f69..00241908644 100644
--- a/app/models/ci/build_need.rb
+++ b/app/models/ci/build_need.rb
@@ -7,15 +7,16 @@ module Ci
include SafelyChangeColumnDefault
include BulkInsertSafe
+ MAX_JOB_NAME_LENGTH = 128
+
columns_changing_default :partition_id
- ignore_column :id_convert_to_bigint, remove_with: '16.4', remove_after: '2023-09-22'
belongs_to :build, class_name: "Ci::Processable", foreign_key: :build_id, inverse_of: :needs
partitionable scope: :build
validates :build, presence: true
- validates :name, presence: true, length: { maximum: 128 }
+ validates :name, presence: true, length: { maximum: MAX_JOB_NAME_LENGTH }
validates :optional, inclusion: { in: [true, false] }
scope :scoped_build, -> { where("#{Ci::Build.quoted_table_name}.id = #{quoted_table_name}.build_id") }
diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb
index eaa2e1c428e..e197217bb70 100644
--- a/app/models/ci/build_runner_session.rb
+++ b/app/models/ci/build_runner_session.rb
@@ -20,7 +20,7 @@ module Ci
partitionable scope: :build
validates :build, presence: true
- validates :url, public_url: { schemes: %w(https) }
+ validates :url, public_url: { schemes: %w[https] }
def terminal_specification
wss_url = Gitlab::UrlHelpers.as_wss(Addressable::URI.escape(url))
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 3a5db04a687..5bf4e846304 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -162,7 +162,7 @@ module Ci
validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing?
- validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
+ validates :source, exclusion: { in: %w[unknown], unless: :importing? }, on: :create
after_create :keep_around_commits, unless: :importing?
after_find :observe_age_in_minutes, unless: :importing?
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 8d93429fd24..91c919dc662 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -52,7 +52,7 @@ module Ci
RUNNER_QUEUE_EXPIRY_TIME = 1.hour
# The `UPDATE_CONTACT_COLUMN_EVERY` defines how often the Runner DB entry can be updated
- UPDATE_CONTACT_COLUMN_EVERY = (40.minutes..55.minutes).freeze
+ UPDATE_CONTACT_COLUMN_EVERY = (40.minutes..55.minutes)
# The `STALE_TIMEOUT` constant defines the how far past the last contact or creation date a runner will be considered stale
STALE_TIMEOUT = 3.months
@@ -532,7 +532,9 @@ module Ci
'virtualbox' => :virtualbox,
'docker+machine' => :docker_machine,
'docker-ssh+machine' => :docker_ssh_machine,
- 'kubernetes' => :kubernetes
+ 'kubernetes' => :kubernetes,
+ 'docker-autoscaler' => :docker_autoscaler,
+ 'instance' => :instance
}.freeze
EXECUTOR_TYPE_TO_NAMES = EXECUTOR_NAME_TO_TYPES.invert.freeze
@@ -552,9 +554,7 @@ module Ci
end
def cleanup_runner_queue
- Gitlab::Redis::SharedState.with do |redis|
- redis.del(runner_queue_key)
- end
+ ::Gitlab::Workhorse.cleanup_key(runner_queue_key)
end
def runner_queue_key
diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb
index 8dc866929f3..cbea7efc70e 100644
--- a/app/models/clusters/agent.rb
+++ b/app/models/clusters/agent.rb
@@ -50,7 +50,7 @@ module Clusters
end
def connected?
- agent_tokens.active.where("last_used_at > ?", INACTIVE_AFTER.ago).exists?
+ agent_tokens.connected.exists?
end
def activity_event_deletion_cutoff
diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb
index b2b13f6cef7..f4c497a42cc 100644
--- a/app/models/clusters/agent_token.rb
+++ b/app/models/clusters/agent_token.rb
@@ -2,10 +2,15 @@
module Clusters
class AgentToken < ApplicationRecord
+ TOKEN_PREFIX = "glagent-"
+
include RedisCacheable
include TokenAuthenticatable
- add_authentication_token_field :token, encrypted: :required, token_generator: -> { Devise.friendly_token(50) }
+ add_authentication_token_field :token,
+ encrypted: :required,
+ token_generator: -> { Devise.friendly_token(50) },
+ format_with_prefix: :glagent_prefix
cached_attr_reader :last_used_at
self.table_name = 'cluster_agent_tokens'
@@ -21,6 +26,7 @@ module Clusters
scope :order_last_used_at_desc, -> { order(arel_table[:last_used_at].desc.nulls_last) }
scope :with_status, -> (status) { where(status: status) }
scope :active, -> { where(status: :active) }
+ scope :connected, -> { active.where("last_used_at > ?", Clusters::Agent::INACTIVE_AFTER.ago) }
enum status: {
active: 0,
@@ -30,5 +36,9 @@ module Clusters
def to_ability_name
:cluster
end
+
+ def glagent_prefix
+ TOKEN_PREFIX
+ end
end
end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 123ad0ebfaf..5efbec45561 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -8,7 +8,7 @@ module Clusters
include ReactiveCaching
include NullifyIfBlank
- RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze
+ RESERVED_NAMESPACES = %w[gitlab-managed-apps].freeze
REQUIRED_K8S_MIN_VERSION = 23
IGNORED_CONNECTION_EXCEPTIONS = [
diff --git a/app/models/commit.rb b/app/models/commit.rb
index d7aa66588d3..39e12b53f21 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -30,10 +30,10 @@ class Commit
MIN_SHA_LENGTH = Gitlab::Git::Commit::MIN_SHA_LENGTH
MAX_SHA_LENGTH = Gitlab::Git::Commit::MAX_SHA_LENGTH
- COMMIT_SHA_PATTERN = Gitlab::Git::Commit::SHA_PATTERN.freeze
- EXACT_COMMIT_SHA_PATTERN = /\A#{COMMIT_SHA_PATTERN}\z/.freeze
+ COMMIT_SHA_PATTERN = Gitlab::Git::Commit::SHA_PATTERN
+ EXACT_COMMIT_SHA_PATTERN = /\A#{COMMIT_SHA_PATTERN}\z/
# Used by GFM to match and present link extensions on node texts and hrefs.
- LINK_EXTENSION_PATTERN = /(patch)/.freeze
+ LINK_EXTENSION_PATTERN = /(patch)/
DEFAULT_MAX_DIFF_LINES_SETTING = 50_000
DEFAULT_MAX_DIFF_FILES_SETTING = 1_000
@@ -432,7 +432,7 @@ class Commit
end
def cherry_pick_message(user)
- %{#{message}\n\n#{cherry_pick_description(user)}}
+ %(#{message}\n\n#{cherry_pick_description(user)})
end
def revert_description(user)
@@ -444,7 +444,7 @@ class Commit
end
def revert_message(user)
- %{Revert "#{title.strip}"\n\n#{revert_description(user)}}
+ %(Revert "#{title.strip}"\n\n#{revert_description(user)})
end
def reverts_commit?(commit, user)
@@ -539,7 +539,7 @@ class Commit
# added by `git commit --fixup` which is used by some community members.
# https://gitlab.com/gitlab-org/gitlab/-/issues/342937#note_892065311
#
- DRAFT_REGEX = /\A\s*#{Gitlab::Regex.merge_request_draft}|(fixup!|squash!)\s/.freeze
+ DRAFT_REGEX = /\A\s*#{Gitlab::Regex.merge_request_draft}|(fixup!|squash!)\s/
def draft?
!!(title =~ DRAFT_REGEX)
@@ -554,10 +554,10 @@ class Commit
"commit:#{sha}"
end
- def expire_note_etag_cache
+ def broadcast_notes_changed
super
- expire_note_etag_cache_for_related_mrs
+ broadcast_notes_changed_for_related_mrs
end
def readable_by?(user)
@@ -614,8 +614,8 @@ class Commit
end
end
- def expire_note_etag_cache_for_related_mrs
- MergeRequest.includes(target_project: :namespace).by_commit_sha(id).find_each(&:expire_note_etag_cache)
+ def broadcast_notes_changed_for_related_mrs
+ MergeRequest.includes(target_project: :namespace).by_commit_sha(id).find_each(&:broadcast_notes_changed)
end
def commit_reference(from, referable_commit_id, full: false)
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index d882a185464..cb24297f2c8 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -28,11 +28,11 @@ class CommitRange
# The beginning and ending refs can be named or SHAs, and
# the range notation can be double- or triple-dot.
- REF_PATTERN = /[0-9a-zA-Z][0-9a-zA-Z_.-]*[0-9a-zA-Z\^]/.freeze
- PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/.freeze
+ REF_PATTERN = /[0-9a-zA-Z][0-9a-zA-Z_.-]*[0-9a-zA-Z\^]/
+ PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/
# In text references, the beginning and ending refs can only be valid SHAs.
- STRICT_PATTERN = /#{Gitlab::Git::Commit::RAW_SHA_PATTERN}\.{2,3}#{Gitlab::Git::Commit::RAW_SHA_PATTERN}/.freeze
+ STRICT_PATTERN = /#{Gitlab::Git::Commit::RAW_SHA_PATTERN}\.{2,3}#{Gitlab::Git::Commit::RAW_SHA_PATTERN}/
def self.reference_prefix
'@'
diff --git a/app/models/commit_signatures/gpg_signature.rb b/app/models/commit_signatures/gpg_signature.rb
index a9e8ca2dd33..45937b68691 100644
--- a/app/models/commit_signatures/gpg_signature.rb
+++ b/app/models/commit_signatures/gpg_signature.rb
@@ -3,6 +3,7 @@ module CommitSignatures
class GpgSignature < ApplicationRecord
include CommitSignature
include SignatureType
+ include EachBatch
sha_attribute :gpg_key_primary_keyid
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index c2425e9460a..3761aa81bf7 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -9,16 +9,19 @@ class CommitStatus < Ci::ApplicationRecord
include BulkInsertableAssociations
include TaggableQueries
- ROUTING_FEATURE_FLAG = :ci_partitioning_use_ci_builds_routing_table
+ def self.switch_table_names
+ if Gitlab::Utils.to_boolean(ENV['USE_CI_BUILDS_ROUTING_TABLE'])
+ :p_ci_builds
+ else
+ :ci_builds
+ end
+ end
- self.table_name = 'ci_builds'
- self.sequence_name = 'ci_builds_id_seq'
+ self.table_name = self.switch_table_names
+ self.sequence_name = :ci_builds_id_seq
self.primary_key = :id
- partitionable scope: :pipeline, through: {
- table: :p_ci_builds,
- flag: ROUTING_FEATURE_FLAG
- }
+ partitionable scope: :pipeline
belongs_to :user
belongs_to :project
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index f419fa8518e..e342939b3d6 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -48,12 +48,6 @@ module Avatarable
end
end
- class_methods do
- def bot_avatar(image:)
- Rails.root.join('lib', 'assets', 'images', 'bot_avatars', image).open
- end
- end
-
def avatar_type
unless self.avatar.image?
errors.add :avatar, "file format is not supported. Please try one of the following supported formats: #{AvatarUploader::SAFE_IMAGE_EXT.join(', ')}"
diff --git a/app/models/concerns/chronic_duration_attribute.rb b/app/models/concerns/chronic_duration_attribute.rb
index af4905115b1..7b7b61fdf06 100644
--- a/app/models/concerns/chronic_duration_attribute.rb
+++ b/app/models/concerns/chronic_duration_attribute.rb
@@ -17,7 +17,12 @@ module ChronicDurationAttribute
chronic_duration_attributes[virtual_attribute] = value.presence || parameters[:default].presence.to_s
begin
- new_value = value.present? ? ChronicDuration.parse(value).to_i : parameters[:default].presence
+ new_value = if value.present?
+ ChronicDuration.parse(value, use_complete_matcher: true).to_i
+ else
+ parameters[:default].presence
+ end
+
assign_attributes(source_attribute => new_value)
rescue ChronicDuration::DurationParseError
# ignore error as it will be caught by validation
diff --git a/app/models/concerns/ci/deployable.rb b/app/models/concerns/ci/deployable.rb
index b3b80989410..d25151f9a34 100644
--- a/app/models/concerns/ci/deployable.rb
+++ b/app/models/concerns/ci/deployable.rb
@@ -138,7 +138,11 @@ module Ci
end
def environment_url
- options&.dig(:environment, :url) || persisted_environment&.external_url
+ options&.dig(:environment, :url) || persisted_environment.try(:external_url)
+ end
+
+ def environment_slug
+ persisted_environment.try(:slug)
end
def environment_status
diff --git a/app/models/concerns/ci/has_runner_executor.rb b/app/models/concerns/ci/has_runner_executor.rb
index dc70cdb2018..6d4622945fe 100644
--- a/app/models/concerns/ci/has_runner_executor.rb
+++ b/app/models/concerns/ci/has_runner_executor.rb
@@ -17,7 +17,9 @@ module Ci
virtualbox: 8,
docker_machine: 9,
docker_ssh_machine: 10,
- kubernetes: 11
+ kubernetes: 11,
+ docker_autoscaler: 12,
+ instance: 13
}, _suffix: true
end
end
diff --git a/app/models/concerns/ci/maskable.rb b/app/models/concerns/ci/maskable.rb
index e2cef0981d1..15240385dd8 100644
--- a/app/models/concerns/ci/maskable.rb
+++ b/app/models/concerns/ci/maskable.rb
@@ -11,12 +11,12 @@ module Ci
# * Minimal length of 8 characters
# * Characters must be from the Base64 alphabet (RFC4648) with the addition of '@', ':', '.', and '~'
# * Absolutely no fun is allowed
- REGEX = %r{\A[a-zA-Z0-9_+=/@:.~-]{8,}\z}.freeze
+ REGEX = %r{\A[a-zA-Z0-9_+=/@:.~-]{8,}\z}
# * Single line
# * No spaces
# * Minimal length of 8 characters
# * Some fun is allowed
- MASK_AND_RAW_REGEX = %r{\A\S{8,}\z}.freeze
+ MASK_AND_RAW_REGEX = %r{\A\S{8,}\z}
included do
validates :masked, inclusion: { in: [true, false] }
diff --git a/app/models/concerns/ci/partitionable.rb b/app/models/concerns/ci/partitionable.rb
index ec6c85d888d..c4b1281fa72 100644
--- a/app/models/concerns/ci/partitionable.rb
+++ b/app/models/concerns/ci/partitionable.rb
@@ -107,7 +107,10 @@ module Ci
partitioned_by :partition_id,
strategy: :ci_sliding_list,
next_partition_if: proc { false },
- detach_partition_if: proc { false }
+ detach_partition_if: proc { false },
+ # Most of the db tasks are run in a weekly basis, e.g. execute_batched_migrations.
+ # Therefore, let's start with 1.week and see how it'd go.
+ analyze_interval: 1.week
end
end
end
diff --git a/app/models/concerns/clusters/agents/authorizations/ci_access/config_scopes.rb b/app/models/concerns/clusters/agents/authorizations/ci_access/config_scopes.rb
index eef68bfd349..9528a708ee1 100644
--- a/app/models/concerns/clusters/agents/authorizations/ci_access/config_scopes.rb
+++ b/app/models/concerns/clusters/agents/authorizations/ci_access/config_scopes.rb
@@ -17,7 +17,7 @@ module Clusters
class_methods do
def available_ci_access_fields(_project)
- %w(agent)
+ %w[agent]
end
end
end
diff --git a/app/models/concerns/cross_database_ignored_tables.rb b/app/models/concerns/cross_database_ignored_tables.rb
index c97e405cce4..14a9703a734 100644
--- a/app/models/concerns/cross_database_ignored_tables.rb
+++ b/app/models/concerns/cross_database_ignored_tables.rb
@@ -4,6 +4,12 @@ module CrossDatabaseIgnoredTables
extend ActiveSupport::Concern
class_methods do
+ def temporary_ignore_cross_database_tables(tables, url:, &blk)
+ Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
+ tables, url: url, &blk
+ )
+ end
+
def cross_database_ignore_tables(tables, options = {})
raise "missing issue url" if options[:url].blank?
@@ -40,8 +46,7 @@ module CrossDatabaseIgnoredTables
return yield unless options[:if].nil? || instance_eval(&options[:if])
url = options[:url]
- Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
- tables, url: url, &blk
- )
+
+ self.class.temporary_ignore_cross_database_tables(tables, url: url, &blk)
end
end
diff --git a/app/models/concerns/diff_positionable_note.rb b/app/models/concerns/diff_positionable_note.rb
index b10b318fb7c..2f64129b65f 100644
--- a/app/models/concerns/diff_positionable_note.rb
+++ b/app/models/concerns/diff_positionable_note.rb
@@ -14,7 +14,7 @@ module DiffPositionableNote
validates :position, json_schema: { filename: "position", hash_conversion: true }
end
- %i(original_position position change_position).each do |meth|
+ %i[original_position position change_position].each do |meth|
define_method "#{meth}=" do |new_position|
if new_position.is_a?(String)
new_position = begin
diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb
index 945d286a2fd..0c8cf861c38 100644
--- a/app/models/concerns/each_batch.rb
+++ b/app/models/concerns/each_batch.rb
@@ -54,7 +54,7 @@ module EachBatch
'the column: argument must be set to a column name to use for ordering rows'
end
- start = except(:select)
+ start = except(:select, :includes, :preload)
.select(column)
.reorder(column => order)
@@ -69,7 +69,7 @@ module EachBatch
1.step do |index|
start_cond = arel_table[column].gteq(start_id)
start_cond = arel_table[column].lteq(start_id) if order == :desc
- stop = except(:select)
+ stop = except(:select, :includes, :preload)
.select(column)
.where(start_cond)
.reorder(column => order)
diff --git a/app/models/concerns/editable.rb b/app/models/concerns/editable.rb
index 2e49e720ac9..be9858bf49b 100644
--- a/app/models/concerns/editable.rb
+++ b/app/models/concerns/editable.rb
@@ -8,6 +8,6 @@ module Editable
end
def last_edited_by
- super || User.ghost
+ super || Users::Internal.ghost
end
end
diff --git a/app/models/concerns/enums/prometheus_metric.rb b/app/models/concerns/enums/prometheus_metric.rb
index e65a01990a3..2cc765b7a3c 100644
--- a/app/models/concerns/enums/prometheus_metric.rb
+++ b/app/models/concerns/enums/prometheus_metric.rb
@@ -30,37 +30,37 @@ module Enums
# built-in groups
nginx_ingress_vts: {
group_title: _('Response metrics (NGINX Ingress VTS)'),
- required_metrics: %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg),
+ required_metrics: %w[nginx_upstream_responses_total nginx_upstream_response_msecs_avg],
priority: 10
}.freeze,
nginx_ingress: {
group_title: _('Response metrics (NGINX Ingress)'),
- required_metrics: %w(nginx_ingress_controller_requests nginx_ingress_controller_ingress_upstream_latency_seconds_sum),
+ required_metrics: %w[nginx_ingress_controller_requests nginx_ingress_controller_ingress_upstream_latency_seconds_sum],
priority: 10
}.freeze,
ha_proxy: {
group_title: _('Response metrics (HA Proxy)'),
- required_metrics: %w(haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total),
+ required_metrics: %w[haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total],
priority: 10
}.freeze,
aws_elb: {
group_title: _('Response metrics (AWS ELB)'),
- required_metrics: %w(aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum),
+ required_metrics: %w[aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum],
priority: 10
}.freeze,
nginx: {
group_title: _('Response metrics (NGINX)'),
- required_metrics: %w(nginx_server_requests nginx_server_requestMsec),
+ required_metrics: %w[nginx_server_requests nginx_server_requestMsec],
priority: 10
}.freeze,
kubernetes: {
group_title: _('System metrics (Kubernetes)'),
- required_metrics: %w(container_memory_usage_bytes container_cpu_usage_seconds_total),
+ required_metrics: %w[container_memory_usage_bytes container_cpu_usage_seconds_total],
priority: 5
}.freeze,
cluster_health: {
group_title: _('Cluster Health'),
- required_metrics: %w(container_memory_usage_bytes container_cpu_usage_seconds_total),
+ required_metrics: %w[container_memory_usage_bytes container_cpu_usage_seconds_total],
priority: 10
}.freeze
}.merge(custom_group_details).freeze
diff --git a/app/models/concerns/has_unique_internal_users.rb b/app/models/concerns/has_unique_internal_users.rb
deleted file mode 100644
index 25b56f6d70f..00000000000
--- a/app/models/concerns/has_unique_internal_users.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# frozen_string_literal: true
-
-module HasUniqueInternalUsers
- extend ActiveSupport::Concern
-
- class_methods do
- private
-
- def unique_internal(scope, username, email_pattern, &block)
- scope.first || create_unique_internal(scope, username, email_pattern, &block)
- end
-
- def create_unique_internal(scope, username, email_pattern, &creation_block)
- # Since we only want a single one of these in an instance, we use an
- # exclusive lease to ensure than this block is never run concurrently.
- lease_key = "user:unique_internal:#{username}"
- lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute.to_i)
-
- until uuid = lease.try_obtain
- # Keep trying until we obtain the lease. To prevent hammering Redis too
- # much we'll wait for a bit between retries.
- sleep(1)
- end
-
- # Recheck if the user is already present. One might have been
- # added between the time we last checked (first line of this method)
- # and the time we acquired the lock.
- existing_user = uncached { scope.first }
- return existing_user if existing_user.present?
-
- uniquify = Gitlab::Utils::Uniquify.new
-
- username = uniquify.string(username) { |s| User.find_by_username(s) }
-
- email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
- User.find_by_email(s)
- end
-
- user = scope.build(
- username: username,
- email: email,
- &creation_block
- )
-
- Users::UpdateService.new(user, user: user).execute(validate: false)
- user
- ensure
- Gitlab::ExclusiveLease.cancel(lease_key, uuid)
- end
- end
-end
diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb
index 2d0ff82e624..c3f702a4e69 100644
--- a/app/models/concerns/has_user_type.rb
+++ b/app/models/concerns/has_user_type.rb
@@ -74,4 +74,21 @@ module HasUserType
# https://gitlab.com/gitlab-org/gitlab/-/issues/346058
'****'
end
+
+ def resource_bot_resource
+ return unless project_bot?
+
+ projects&.first || groups&.first
+ end
+
+ def resource_bot_owners
+ return [] unless project_bot?
+
+ resource = resource_bot_resource
+ return [] unless resource
+
+ return resource.maintainers if resource.is_a?(Project)
+
+ resource.owners
+ end
end
diff --git a/app/models/concerns/integrations/enable_ssl_verification.rb b/app/models/concerns/integrations/enable_ssl_verification.rb
index 11dc8a76a2b..9735a9bf5f6 100644
--- a/app/models/concerns/integrations/enable_ssl_verification.rb
+++ b/app/models/concerns/integrations/enable_ssl_verification.rb
@@ -19,13 +19,16 @@ module Integrations
url_index = fields.index { |field| field[:name].ends_with?('_url') }
insert_index = url_index ? url_index + 1 : -1
- fields.insert(insert_index, {
- type: 'checkbox',
- name: 'enable_ssl_verification',
- title: s_('Integrations|SSL verification'),
- checkbox_label: s_('Integrations|Enable SSL verification'),
- help: s_('Integrations|Clear if using a self-signed certificate.')
- })
+ 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.')
+ )
+ )
end
end
end
diff --git a/app/models/concerns/integrations/reset_secret_fields.rb b/app/models/concerns/integrations/reset_secret_fields.rb
index f79c4392f19..24d716fe5dd 100644
--- a/app/models/concerns/integrations/reset_secret_fields.rb
+++ b/app/models/concerns/integrations/reset_secret_fields.rb
@@ -12,9 +12,7 @@ module Integrations
end
def exposing_secrets_fields
- # TODO: Once all integrations use `Integrations::Field` we can remove the `.try` here.
- # See: https://gitlab.com/groups/gitlab-org/-/epics/7652
- fields.select { _1.try(:exposes_secrets) }.pluck(:name)
+ fields.select(&:exposes_secrets).pluck(:name)
end
private
diff --git a/app/models/concerns/integrations/slack_mattermost_fields.rb b/app/models/concerns/integrations/slack_mattermost_fields.rb
new file mode 100644
index 00000000000..a8e63c4e405
--- /dev/null
+++ b/app/models/concerns/integrations/slack_mattermost_fields.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Integrations
+ module SlackMattermostFields
+ extend ActiveSupport::Concern
+
+ included do
+ field :webhook,
+ help: -> { webhook_help },
+ required: true,
+ if: -> { requires_webhook? }
+
+ field :username,
+ placeholder: 'GitLab-integration',
+ if: -> { requires_webhook? }
+
+ field :notify_only_broken_pipelines,
+ type: :checkbox,
+ section: Integration::SECTION_TYPE_CONFIGURATION,
+ help: 'Do not send notifications for successful pipelines.'
+
+ field :branches_to_be_notified,
+ type: :select,
+ section: Integration::SECTION_TYPE_CONFIGURATION,
+ title: -> { s_('Integration|Branches for which notifications are to be sent') },
+ choices: -> { branch_choices }
+
+ field :labels_to_be_notified,
+ section: Integration::SECTION_TYPE_CONFIGURATION,
+ placeholder: '~backend,~frontend',
+ help: 'Send notifications for issue, merge request, and comment events with the listed labels only. ' \
+ 'Leave blank to receive notifications for all events.'
+
+ field :labels_to_be_notified_behavior,
+ type: :select,
+ section: Integration::SECTION_TYPE_CONFIGURATION,
+ choices: [
+ ['Match any of the labels', Integrations::BaseChatNotification::MATCH_ANY_LABEL],
+ ['Match all of the labels', Integrations::BaseChatNotification::MATCH_ALL_LABELS]
+ ]
+ end
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 9a513ea0e5b..a9a00ab1c44 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -19,6 +19,7 @@ module Issuable
include Awardable
include Taskable
include Importable
+ include Transitionable
include Editable
include AfterCommitQueue
include Sortable
@@ -33,7 +34,7 @@ module Issuable
TITLE_HTML_LENGTH_MAX = 800
DESCRIPTION_LENGTH_MAX = 1.megabyte
DESCRIPTION_HTML_LENGTH_MAX = 5.megabytes
- SEARCHABLE_FIELDS = %w(title description).freeze
+ SEARCHABLE_FIELDS = %w[title description].freeze
MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS = 200
STATE_ID_MAP = {
@@ -225,6 +226,10 @@ module Issuable
false
end
+ def supports_lock_on_merge?
+ false
+ end
+
def severity
return IssuableSeverity::DEFAULT unless supports_severity?
@@ -235,6 +240,10 @@ module Issuable
super + [:notes]
end
+ def importing_or_transitioning?
+ importing? || transitioning?
+ end
+
private
def validate_description_length?
@@ -408,14 +417,14 @@ module Issuable
sort = sort.to_s
grouping_columns = [arel_table[:id]]
- if %w(milestone_due_desc milestone_due_asc milestone).include?(sort)
+ if %w[milestone_due_desc milestone_due_asc milestone].include?(sort)
milestone_table = Milestone.arel_table
grouping_columns << milestone_table[:id]
grouping_columns << milestone_table[:due_date]
- elsif %w(merged_at_desc merged_at_asc merged_at).include?(sort)
+ elsif %w[merged_at_desc merged_at_asc merged_at].include?(sort)
grouping_columns << MergeRequest::Metrics.arel_table[:id]
grouping_columns << MergeRequest::Metrics.arel_table[:merged_at]
- elsif %w(closed_at_desc closed_at_asc closed_at).include?(sort)
+ elsif %w[closed_at_desc closed_at_asc closed_at].include?(sort)
grouping_columns << MergeRequest::Metrics.arel_table[:id]
grouping_columns << MergeRequest::Metrics.arel_table[:latest_closed_at]
end
diff --git a/app/models/concerns/issue_available_features.rb b/app/models/concerns/issue_available_features.rb
index 3f65e701da7..2969f1e1928 100644
--- a/app/models/concerns/issue_available_features.rb
+++ b/app/models/concerns/issue_available_features.rb
@@ -10,10 +10,10 @@ module IssueAvailableFeatures
# EE only features are listed on EE::IssueAvailableFeatures
def available_features_for_issue_types
{
- assignee: %w(issue incident),
- confidentiality: %w(issue incident),
- time_tracking: %w(issue incident),
- move_and_clone: %w(issue incident)
+ assignee: %w[issue incident],
+ confidentiality: %w[issue incident objective key_result],
+ time_tracking: %w[issue incident],
+ move_and_clone: %w[issue incident]
}.with_indifferent_access
end
end
diff --git a/app/models/concerns/linkable_item.rb b/app/models/concerns/linkable_item.rb
index 135252727ab..c91e3615ba7 100644
--- a/app/models/concerns/linkable_item.rb
+++ b/app/models/concerns/linkable_item.rb
@@ -16,6 +16,7 @@ module LinkableItem
scope :for_source, ->(item) { where(source_id: item.id) }
scope :for_target, ->(item) { where(target_id: item.id) }
+ scope :for_source_and_target, ->(source, target) { where(source: source, target: target) }
scope :for_items, ->(source, target) do
where(source: source, target: target).or(where(source: target, target: source))
end
diff --git a/app/models/concerns/mentionable/reference_regexes.rb b/app/models/concerns/mentionable/reference_regexes.rb
index 0b6075fbeb8..b5634ba3b6d 100644
--- a/app/models/concerns/mentionable/reference_regexes.rb
+++ b/app/models/concerns/mentionable/reference_regexes.rb
@@ -28,7 +28,7 @@ module Mentionable
def self.external_pattern
strong_memoize(:external_pattern) do
issue_pattern = Integrations::BaseIssueTracker.base_reference_pattern
- link_patterns = URI::DEFAULT_PARSER.make_regexp(%w(http https))
+ link_patterns = URI::DEFAULT_PARSER.make_regexp(%w[http https])
reference_pattern(link_patterns, issue_pattern)
end
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 40a91c8ac94..06cee46645b 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -12,17 +12,17 @@ module Noteable
class_methods do
# `Noteable` class names that support replying to individual notes.
def replyable_types
- %w(Issue MergeRequest)
+ %w[Issue MergeRequest]
end
# `Noteable` class names that support resolvable notes.
def resolvable_types
- %w(Issue MergeRequest DesignManagement::Design)
+ %w[Issue MergeRequest DesignManagement::Design]
end
# `Noteable` class names that support creating/forwarding individual notes.
def email_creatable_types
- %w(Issue)
+ %w[Issue]
end
end
@@ -164,28 +164,15 @@ module Noteable
[MergeRequest, Issue].include?(self.class)
end
- def etag_caching_enabled?
+ def real_time_notes_enabled?
false
end
- def expire_note_etag_cache
+ def broadcast_notes_changed
return unless discussions_rendered_on_frontend?
- return unless etag_caching_enabled?
+ return unless real_time_notes_enabled?
- # TODO: We need to figure out a way to make ETag caching work for group-level work items
- Gitlab::EtagCaching::Store.new.touch(note_etag_key) unless is_a?(Issue) && project.nil?
-
- Noteable::NotesChannel.broadcast_to(self, event: 'updated') if Feature.enabled?(:action_cable_notes, project || try(:group))
- end
-
- def note_etag_key
- return Gitlab::Routing.url_helpers.designs_project_issue_path(project, issue, { vueroute: filename }) if self.is_a?(DesignManagement::Design)
-
- Gitlab::Routing.url_helpers.project_noteable_notes_path(
- project,
- target_type: noteable_target_type_name,
- target_id: id
- )
+ Noteable::NotesChannel.broadcast_to(self, event: 'updated')
end
def after_note_created(_note)
diff --git a/app/models/concerns/packages/nuget/version_normalizable.rb b/app/models/concerns/packages/nuget/version_normalizable.rb
index 473e5f07811..4bcfec89570 100644
--- a/app/models/concerns/packages/nuget/version_normalizable.rb
+++ b/app/models/concerns/packages/nuget/version_normalizable.rb
@@ -13,7 +13,7 @@ module Packages
private
def set_normalized_version
- return unless package && Feature.enabled?(:nuget_normalized_version, package.project)
+ return unless package
self.normalized_version = normalize
end
diff --git a/app/models/concerns/pg_full_text_searchable.rb b/app/models/concerns/pg_full_text_searchable.rb
index 562c8cf23f3..b7ca6f61573 100644
--- a/app/models/concerns/pg_full_text_searchable.rb
+++ b/app/models/concerns/pg_full_text_searchable.rb
@@ -21,11 +21,11 @@
module PgFullTextSearchable
extend ActiveSupport::Concern
- LONG_WORDS_REGEX = %r([A-Za-z0-9+/@]{50,}).freeze
+ LONG_WORDS_REGEX = %r([A-Za-z0-9+/@]{50,})
TSVECTOR_MAX_LENGTH = 1.megabyte.freeze
TEXT_SEARCH_DICTIONARY = 'english'
- URL_SCHEME_REGEX = %r{(?<=\A|\W)\w+://(?=\w+)}.freeze
- TSQUERY_DISALLOWED_CHARACTERS_REGEX = %r{[^a-zA-Z0-9 .@/\-_"]}.freeze
+ URL_SCHEME_REGEX = %r{(?<=\A|\W)\w+://(?=\w+)}
+ TSQUERY_DISALLOWED_CHARACTERS_REGEX = %r{[^a-zA-Z0-9 .@/\-_"]}
def update_search_data!
tsvector_sql_nodes = self.class.pg_full_text_searchable_columns.map do |column, weight|
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index a87eadb9332..ea8a1640bea 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -3,6 +3,8 @@
module ProtectedRef
extend ActiveSupport::Concern
+ include Importable
+
included do
belongs_to :project, touch: true
@@ -32,12 +34,13 @@ module ProtectedRef
# to fail.
has_many :"#{type}_access_levels", inverse_of: self.model_name.singular
+ # Overridden in EE with `if: -> { false }` so this validation does not apply on an EE instance.
validates :"#{type}_access_levels",
length: {
is: 1,
message: "are restricted to a single instance per #{self.model_name.human}."
},
- unless: -> { allow_multiple?(type) }
+ unless: -> { allow_multiple?(type) || importing? }
accepts_nested_attributes_for :"#{type}_access_levels", allow_destroy: true
end
diff --git a/app/models/concerns/redactable.rb b/app/models/concerns/redactable.rb
index 53ae300ee2d..5ad96d6cc46 100644
--- a/app/models/concerns/redactable.rb
+++ b/app/models/concerns/redactable.rb
@@ -10,7 +10,7 @@
module Redactable
extend ActiveSupport::Concern
- UNSUBSCRIBE_PATTERN = %r{/sent_notifications/\h{32}/unsubscribe}.freeze
+ UNSUBSCRIBE_PATTERN = %r{/sent_notifications/\h{32}/unsubscribe}
class_methods do
def redact_field(field)
diff --git a/app/models/concerns/require_email_verification.rb b/app/models/concerns/require_email_verification.rb
index d7182778b36..6581928f637 100644
--- a/app/models/concerns/require_email_verification.rb
+++ b/app/models/concerns/require_email_verification.rb
@@ -7,10 +7,7 @@ module RequireEmailVerification
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
- # This value is twice the amount we want it to be, because due to a bug in the devise-two-factor
- # gem every failed login attempt increments the value of failed_attempts by 2 instead of 1.
- # See: https://github.com/tinfoil/devise-two-factor/issues/127
- MAXIMUM_ATTEMPTS = 3 * 2
+ MAXIMUM_ATTEMPTS = 3
UNLOCK_IN = 24.hours
included do
diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb
index e967c78154d..5c2f0aa04ac 100644
--- a/app/models/concerns/resolvable_discussion.rb
+++ b/app/models/concerns/resolvable_discussion.rb
@@ -116,7 +116,7 @@ module ResolvableDiscussion
# Set the notes array to the updated notes
@notes = notes_relation.fresh.to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
- noteable.expire_note_etag_cache
+ noteable.broadcast_notes_changed
clear_memoized_values
end
diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb
index 7f9a7faa3f5..23abc5d5c22 100644
--- a/app/models/concerns/resolvable_note.rb
+++ b/app/models/concerns/resolvable_note.rb
@@ -4,7 +4,7 @@ module ResolvableNote
extend ActiveSupport::Concern
# Names of all subclasses of `Note` that can be resolvable.
- RESOLVABLE_TYPES = %w(DiffNote DiscussionNote).freeze
+ RESOLVABLE_TYPES = %w[DiffNote DiscussionNote].freeze
included do
belongs_to :resolved_by, class_name: "User"
diff --git a/app/models/concerns/restricted_signup.rb b/app/models/concerns/restricted_signup.rb
index cf97be21165..6af9ede5e8b 100644
--- a/app/models/concerns/restricted_signup.rb
+++ b/app/models/concerns/restricted_signup.rb
@@ -84,3 +84,5 @@ module RestrictedSignup
end
end
end
+
+::RestrictedSignup.prepend_mod
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index f2badfe48dd..ef14ff5fbe2 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -14,7 +14,17 @@ module Routable
# Routable.find_by_full_path('groupname/projectname') # -> Project
#
# Returns a single object, or nil.
- def self.find_by_full_path(path, follow_redirects: false, route_scope: Route, redirect_route_scope: RedirectRoute)
+
+ # 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?
+ )
+
return unless path.present?
# Convert path to string to prevent DB error: function lower(integer) does not exist
@@ -25,20 +35,50 @@ 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.
- Gitlab::Database.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046") do
+ if optimize_routable
+ path_condition = { path: path }
+
+ source_type_condition = if route_scope == Route
+ {}
+ else
+ { source_type: route_scope.klass.base_class }
+ end
+
route =
- route_scope.find_by(routes: { path: path }) ||
- route_scope.iwhere(Route.arel_table[:path] => path).take
+ Route.where(source_type_condition).find_by(path_condition) ||
+ Route.where(source_type_condition).iwhere(path_condition).take
if follow_redirects
- route ||= redirect_route_scope.iwhere(RedirectRoute.arel_table[:path] => path).take
+ route ||= RedirectRoute.where(source_type_condition).iwhere(path_condition).take
end
- next unless route
+ 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
- route.is_a?(Routable) ? route : route.source
+ next unless route
+
+ route.is_a?(Routable) ? route : route.source
+ end
end
end
+ # rubocop:enable Metrics/PerceivedComplexity
+ # rubocop:enable Metrics/CyclomaticComplexity
+
+ def self.optimize_routable_enabled?
+ Feature.enabled?(:optimize_routable)
+ end
included do
# Remove `inverse_of: source` when upgraded to rails 5.2
@@ -67,13 +107,22 @@ module Routable
#
# Returns a single object, or nil.
def find_by_full_path(path, follow_redirects: false)
- # TODO: Optimize these queries by avoiding joins
- # https://gitlab.com/gitlab-org/gitlab/-/issues/292252
+ 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
+
Routable.find_by_full_path(
path,
follow_redirects: follow_redirects,
- route_scope: includes(:route).references(:routes),
- redirect_route_scope: joins(:redirect_routes)
+ route_scope: route_scope,
+ redirect_route_scope: redirect_route_scope,
+ optimize_routable: optimize_routable
)
end
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index b73ed937b5d..5455a2159cd 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -17,8 +17,6 @@ module Storage
Namespace.find(parent_id_before_last_save) # raise NotFound early if needed
end
- move_repositories
-
if saved_change_to_parent?
former_parent_full_path = parent_was&.full_path
parent_full_path = parent&.full_path
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index bf645e99b5e..96f684522d2 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -11,8 +11,8 @@ require 'task_list/filter'
module Taskable
COMPLETED = 'completed'
INCOMPLETE = 'incomplete'
- COMPLETE_PATTERN = /\[[xX]\]/.freeze
- INCOMPLETE_PATTERN = /\[[[:space:]]\]/.freeze
+ COMPLETE_PATTERN = /\[[xX]\]/
+ INCOMPLETE_PATTERN = /\[[[:space:]]\]/
ITEM_PATTERN = %r{
^
(?:(?:>\s{0,4})*) # optional blockquote characters
@@ -22,7 +22,7 @@ module Taskable
#{COMPLETE_PATTERN}|#{INCOMPLETE_PATTERN}
)
(\s.+) # followed by whitespace and some text.
- }x.freeze
+ }x
ITEM_PATTERN_UNTRUSTED =
'^' \
diff --git a/app/models/concerns/transitionable.rb b/app/models/concerns/transitionable.rb
new file mode 100644
index 00000000000..70e1fc8b78a
--- /dev/null
+++ b/app/models/concerns/transitionable.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Transitionable
+ extend ActiveSupport::Concern
+
+ attr_accessor :transitioning
+
+ def transitioning?
+ return false unless transitioning && Feature.enabled?(:skip_validations_during_transitions, project)
+
+ true
+ end
+
+ def enable_transitioning
+ self.transitioning = true
+ end
+
+ def disable_transitioning
+ self.transitioning = false
+ end
+end
diff --git a/app/models/concerns/users/visitable.rb b/app/models/concerns/users/visitable.rb
new file mode 100644
index 00000000000..cb8e5fdc682
--- /dev/null
+++ b/app/models/concerns/users/visitable.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Users
+ module Visitable
+ extend ActiveSupport::Concern
+
+ included do
+ def self.visited_around?(entity_id:, user_id:, time:)
+ visits_around(entity_id: entity_id, user_id: user_id, time: time).any?
+ end
+
+ def self.visits_around(entity_id:, user_id:, time:)
+ time = time.to_datetime
+ where(entity_id: entity_id, user_id: user_id, visited_at: (time - 15.minutes)..(time + 15.minutes))
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb
index caaf2b33ef0..319509ea69a 100644
--- a/app/models/concerns/with_uploads.rb
+++ b/app/models/concerns/with_uploads.rb
@@ -22,7 +22,7 @@ module WithUploads
# Currently there is no simple way how to select only not-mounted
# uploads, it should be all FileUploaders so we select them by
# `uploader` class
- FILE_UPLOADERS = %w(PersonalFileUploader NamespaceFileUploader FileUploader).freeze
+ FILE_UPLOADERS = %w[PersonalFileUploader NamespaceFileUploader FileUploader].freeze
included do
around_destroy :ignore_uploads_table_in_transaction
diff --git a/app/models/container_expiration_policy.rb b/app/models/container_expiration_policy.rb
index aecb47f7a03..f643fa7730b 100644
--- a/app/models/container_expiration_policy.rb
+++ b/app/models/container_expiration_policy.rb
@@ -80,7 +80,9 @@ class ContainerExpirationPolicy < ApplicationRecord
end
def set_next_run_at
- self.next_run_at = Time.zone.now + ChronicDuration.parse(cadence).seconds
+ cadence_seconds = ChronicDuration.parse(cadence, use_complete_matcher: true).seconds
+
+ self.next_run_at = Time.zone.now + cadence_seconds
end
def disable!
diff --git a/app/models/container_registry/event.rb b/app/models/container_registry/event.rb
index dd2675e17d8..9f7724c052c 100644
--- a/app/models/container_registry/event.rb
+++ b/app/models/container_registry/event.rb
@@ -4,25 +4,25 @@ module ContainerRegistry
class Event
include Gitlab::Utils::StrongMemoize
- ALLOWED_ACTIONS = %w(push delete).freeze
+ ALLOWED_ACTIONS = %w[push delete].freeze
PUSH_ACTION = 'push'
DELETE_ACTION = 'delete'
EVENT_TRACKING_CATEGORY = 'container_registry:notification'
EVENT_PREFIX = 'i_container_registry'
- ALLOWED_ACTOR_TYPES = %w(
+ ALLOWED_ACTOR_TYPES = %w[
personal_access_token
build
gitlab_or_ldap
- ).freeze
+ ].freeze
- TRACKABLE_ACTOR_EVENTS = %w(
+ TRACKABLE_ACTOR_EVENTS = %w[
push_tag
delete_tag
push_repository
delete_repository
create_repository
- ).freeze
+ ].freeze
attr_reader :event
@@ -60,7 +60,7 @@ module ContainerRegistry
def target_tag?
# There is no clear indication in the event structure when we delete a top-level manifest
- # except existance of "tag" key
+ # except existence of "tag" key
event['target'].has_key?('tag')
end
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index 625d68925c6..c704795130b 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class CustomEmoji < ApplicationRecord
- NAME_REGEXP = /[a-z0-9_-]+/.freeze
+ NAME_REGEXP = /[a-z0-9_-]+/
belongs_to :namespace, inverse_of: :custom_emoji
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index f9fa4bd212c..de777b8ae53 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -43,7 +43,7 @@ class DeployKey < Key
end
def user
- super || User.ghost
+ super || Users::Internal.ghost
end
def audit_details
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index 498ca9c4f30..920321a1699 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -8,8 +8,8 @@ class DeployToken < ApplicationRecord
add_authentication_token_field :token, encrypted: :required
- AVAILABLE_SCOPES = %i(read_repository read_registry write_registry
- read_package_registry write_package_registry).freeze
+ AVAILABLE_SCOPES = %i[read_repository read_registry write_registry
+ read_package_registry write_package_registry].freeze
GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'
REQUIRED_DEPENDENCY_PROXY_SCOPES = %i[read_registry write_registry].freeze
diff --git a/app/models/description_version.rb b/app/models/description_version.rb
index fb61b7f5fde..05cca9f931f 100644
--- a/app/models/description_version.rb
+++ b/app/models/description_version.rb
@@ -9,7 +9,7 @@ class DescriptionVersion < ApplicationRecord
delegate :resource_parent, to: :issuable
def self.issuable_attrs
- %i(issue merge_request).freeze
+ %i[issue merge_request].freeze
end
def issuable
diff --git a/app/models/design_management.rb b/app/models/design_management.rb
index 81e170f7e59..20ada71755b 100644
--- a/app/models/design_management.rb
+++ b/app/models/design_management.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module DesignManagement
- DESIGN_IMAGE_SIZES = %w(v432x230).freeze
+ DESIGN_IMAGE_SIZES = %w[v432x230].freeze
def self.designs_directory
'designs'
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 02979d5f804..d680d0e334f 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -9,7 +9,7 @@ class DiffNote < Note
include Gitlab::Utils::StrongMemoize
def self.noteable_types
- %w(MergeRequest Commit DesignManagement::Design)
+ %w[MergeRequest Commit DesignManagement::Design]
end
validates :original_position, presence: true
diff --git a/app/models/discussion_note.rb b/app/models/discussion_note.rb
index 6621b30b645..a1dfa0e72ec 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]
end
validates :noteable_type, inclusion: { in: noteable_types }
diff --git a/app/models/draft_note.rb b/app/models/draft_note.rb
index ffc04f9bf90..f95eec742d8 100644
--- a/app/models/draft_note.rb
+++ b/app/models/draft_note.rb
@@ -5,8 +5,8 @@ class DraftNote < ApplicationRecord
include Sortable
include ShaAttribute
- PUBLISH_ATTRS = %i(noteable_id noteable_type type note).freeze
- DIFF_ATTRS = %i(position original_position change_position commit_id).freeze
+ PUBLISH_ATTRS = %i[noteable_id noteable_type type note].freeze
+ DIFF_ATTRS = %i[position original_position change_position commit_id].freeze
sha_attribute :commit_id
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 36445279b86..29394c37e2c 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -25,7 +25,6 @@ class Environment < ApplicationRecord
has_many :successful_deployments, -> { success }, class_name: 'Deployment'
has_many :active_deployments, -> { active }, class_name: 'Deployment'
has_many :prometheus_alerts, inverse_of: :environment
- has_many :self_managed_prometheus_alert_events, inverse_of: :environment
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :environment
# NOTE: If you preload multiple last deployments of environments, use Preloaders::Environments::DeploymentPreloader.
@@ -108,11 +107,11 @@ class Environment < ApplicationRecord
scope :deployed_and_updated_before, -> (project_id, before) do
# this query joins deployments and filters out any environment that has recent deployments
- joins = %{
+ joins = %(
LEFT JOIN "deployments" on "deployments".environment_id = "environments".id
AND "deployments".project_id = #{project_id}
AND "deployments".updated_at >= #{connection.quote(before)}
- }
+ )
Environment.joins(joins)
.where(project_id: project_id, updated_at: ...before)
.group('id', 'deployments.id')
@@ -193,7 +192,7 @@ class Environment < ApplicationRecord
end
event :stop_complete do
- transition %i(available stopping) => :stopped
+ transition %i[available stopping] => :stopped
end
state :available
diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb
index 7687bc2be60..f31615f2b3b 100644
--- a/app/models/environment_status.rb
+++ b/app/models/environment_status.rb
@@ -79,7 +79,7 @@ class EnvironmentStatus
private
- PAGE_EXTENSIONS = /\A\.(s?html?|php|asp|cgi|pl)\z/i.freeze
+ PAGE_EXTENSIONS = /\A\.(s?html?|php|asp|cgi|pl)\z/i
def deployment_metrics
@deployment_metrics ||= DeploymentMetrics.new(project, deployment)
@@ -102,7 +102,6 @@ class EnvironmentStatus
return [] unless pipeline
environments = pipeline.environments_in_self_and_project_descendants.includes(:project)
- environments = environments.available if Feature.disabled?(:review_apps_redeploy_mr_widget, mr.project)
environments.map do |environment|
next unless Ability.allowed?(user, :read_environment, environment)
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index c52f8a58c00..318538be645 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -19,7 +19,7 @@ module ErrorTracking
(?<project>[^/]+)/*
)?
\z
- }x.freeze
+ }x
self.reactive_cache_key = ->(setting) { [setting.class.model_name.singular, setting.project_id] }
self.reactive_cache_work_type = :external_dependency
diff --git a/app/models/event.rb b/app/models/event.rb
index 4547d7b9e60..9e4a662aaa5 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -10,6 +10,7 @@ class Event < ApplicationRecord
include UsageStatistics
include ShaAttribute
include IgnorableColumns
+ include EachBatch
ignore_column :target_id_convert_to_bigint, remove_with: '16.4', remove_after: '2023-09-22'
@@ -69,7 +70,7 @@ class Event < ApplicationRecord
# If the association for "target" defines an "author" association we want to
# eager-load this so Banzai & friends don't end up performing N+1 queries to
# get the authors of notes, issues, etc. (likewise for "noteable").
- incs = %i(author noteable work_item_type).select do |a|
+ incs = %i[author noteable work_item_type].select do |a|
reflections['events'].active_record.reflect_on_association(a)
end
@@ -137,7 +138,7 @@ class Event < ApplicationRecord
where(
'action IN (?) OR (target_type IN (?) AND action IN (?))',
[actions[:pushed], actions[:commented]],
- %w(MergeRequest Issue WorkItem), [actions[:created], actions[:closed], actions[:merged]]
+ %w[MergeRequest Issue WorkItem], [actions[:created], actions[:closed], actions[:merged]]
)
end
diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb
index 1bf35179393..f0cae9c88ca 100644
--- a/app/models/gpg_key.rb
+++ b/app/models/gpg_key.rb
@@ -10,7 +10,7 @@ class GpgKey < ApplicationRecord
sha_attribute :fingerprint
belongs_to :user
- has_many :gpg_signatures
+ has_many :gpg_signatures, class_name: 'CommitSignatures::GpgSignature'
has_many :subkeys, class_name: 'GpgKeySubkey'
scope :with_subkeys, -> { includes(:subkeys) }
diff --git a/app/models/group.rb b/app/models/group.rb
index 9df3c143e0c..9330ffef156 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -35,11 +35,12 @@ class Group < Namespace
foreign_key: :member_namespace_id, inverse_of: :group, class_name: 'GroupMember'
alias_method :members, :group_members
- has_many :users, through: :group_members
- has_many :owners,
- -> { where(members: { access_level: Gitlab::Access::OWNER }) },
- through: :all_group_members,
- source: :user
+ has_many :users, -> { allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/422405") },
+ through: :group_members
+ has_many :owners, -> {
+ where(members: { access_level: Gitlab::Access::OWNER })
+ .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/422405")
+ }, through: :all_group_members, source: :user
has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
has_many :namespace_requesters, -> { where.not(requested_at: nil).unscope(where: %i[source_id source_type]) },
@@ -785,8 +786,6 @@ class Group < Namespace
end
def execute_integrations(data, hooks_scope)
- return unless Feature.enabled?(:group_mentions, self)
-
integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend
integration.async_execute(data)
end
@@ -800,7 +799,9 @@ class Group < Namespace
end
def first_owner
- owners.first || parent&.first_owner || owner
+ first_owner_member = all_group_members.all_owners.order(:user_id).first
+
+ first_owner_member&.user || parent&.first_owner || owner
end
def default_branch_name
@@ -898,6 +899,10 @@ class Group < Namespace
feature_flag_enabled_for_self_or_ancestor?(:linked_work_items)
end
+ def supports_lock_on_merge?
+ feature_flag_enabled_for_self_or_ancestor?(:enforce_locked_labels_on_merge, type: :ops)
+ end
+
def usage_quotas_enabled?
::Feature.enabled?(:usage_quotas_for_all_editions, self) && root?
end
@@ -939,12 +944,12 @@ class Group < Namespace
private
- def feature_flag_enabled_for_self_or_ancestor?(feature_flag)
+ def feature_flag_enabled_for_self_or_ancestor?(feature_flag, type: :development)
actors = [root_ancestor]
actors << self if root_ancestor != self
actors.any? do |actor|
- ::Feature.enabled?(feature_flag, actor)
+ ::Feature.enabled?(feature_flag, actor, type: type)
end
end
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index d7a95363337..c0bfe31fb38 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -103,7 +103,7 @@ class WebHook < ApplicationRecord
end
# See app/validators/json_schemas/web_hooks_url_variables.json
- VARIABLE_REFERENCE_RE = /\{([A-Za-z]+[0-9]*(?:[._-][A-Za-z0-9]+)*)\}/.freeze
+ VARIABLE_REFERENCE_RE = /\{([A-Za-z]+[0-9]*(?:[._-][A-Za-z0-9]+)*)\}/
def interpolated_url(url = self.url, url_variables = self.url_variables)
return url unless url.include?('{')
diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb
index 4c35f699468..3e0c8e7c472 100644
--- a/app/models/hooks/web_hook_log.rb
+++ b/app/models/hooks/web_hook_log.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class WebHookLog < ApplicationRecord
- include SafeUrl
include Presentable
include DeleteWithLimit
include CreatedAtFilterable
@@ -58,10 +57,18 @@ class WebHookLog < ApplicationRecord
self[:request_headers].merge('X-Gitlab-Token' => _('[REDACTED]'))
end
+ def url_current?
+ # URL hash hasn't been set, so we must assume there's no prior value to
+ # compare to.
+ return true if url_hash.nil?
+
+ Gitlab::CryptoHelper.sha256(web_hook.interpolated_url) == url_hash
+ end
+
private
def obfuscate_basic_auth
- self.url = safe_url
+ self.url = Gitlab::UrlSanitizer.sanitize_masked_url(url)
end
def redact_user_emails
diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb
index 57638356362..7b2036a9def 100644
--- a/app/models/instance_configuration.rb
+++ b/app/models/instance_configuration.rb
@@ -3,7 +3,7 @@
require 'resolv'
class InstanceConfiguration
- SSH_ALGORITHMS = %w(DSA ECDSA ED25519 RSA).freeze
+ SSH_ALGORITHMS = %w[DSA ECDSA ED25519 RSA].freeze
SSH_ALGORITHMS_PATH = '/etc/ssh/'
CACHE_KEY = 'instance_configuration'
EXPIRATION_TIME = 24.hours
diff --git a/app/models/integration.rb b/app/models/integration.rb
index bc86b08018f..d4c76f743a3 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -63,6 +63,7 @@ class Integration < ApplicationRecord
encode: false,
encode_iv: false
+ alias_attribute :name, :title
# Handle assignment of props with symbol keys.
# To do this correctly, we need to call the method generated by attr_encrypted.
alias_method :attr_encrypted_props=, :properties=
@@ -468,11 +469,8 @@ class Integration < ApplicationRecord
[]
end
- # TODO: Once all integrations use `Integrations::Field` we can
- # use `#secret?` here.
- # See: https://gitlab.com/groups/gitlab-org/-/epics/7652
def secret_fields
- fields.select { |f| f[:type] == :password }.pluck(:name)
+ fields.select(&:secret?).pluck(:name)
end
# Expose a list of fields in the JSON endpoint.
diff --git a/app/models/integrations/apple_app_store.rb b/app/models/integrations/apple_app_store.rb
index 6f96626718f..ef12fc6bf6f 100644
--- a/app/models/integrations/apple_app_store.rb
+++ b/app/models/integrations/apple_app_store.rb
@@ -4,8 +4,8 @@ require 'app_store_connect'
module Integrations
class AppleAppStore < Integration
- ISSUER_ID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/.freeze
- KEY_ID_REGEX = /\A(?=.*[A-Z])(?=.*[0-9])[A-Z0-9]+\z/.freeze
+ ISSUER_ID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/
+ KEY_ID_REGEX = /\A(?=.*[A-Z])(?=.*[0-9])[A-Z0-9]+\z/
IS_KEY_CONTENT_BASE64 = "true"
SECTION_TYPE_APPLE_APP_STORE = 'apple_app_store'
diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb
index 7436c08aa38..859522670ef 100644
--- a/app/models/integrations/asana.rb
+++ b/app/models/integrations/asana.rb
@@ -12,8 +12,7 @@ module Integrations
help: -> { s_('AsanaService|User Personal Access Token. User must have access to the task. All comments are attributed to this user.') },
non_empty_password_title: -> { s_('ProjectService|Enter new API key') },
non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current API key.') },
- # Example Personal Access Token from Asana docs
- placeholder: '0/68a9e79b868c6789e79a124c30b0',
+ placeholder: '0/68a9e79b868c6789e79a124c30b0', # Example Personal Access Token from Asana docs
required: true
field :restrict_to_branch,
@@ -38,7 +37,7 @@ module Integrations
end
def self.supported_events
- %w(push)
+ %w[push]
end
def client
diff --git a/app/models/integrations/assembla.rb b/app/models/integrations/assembla.rb
index 6831fac32e6..1d3616b4c3b 100644
--- a/app/models/integrations/assembla.rb
+++ b/app/models/integrations/assembla.rb
@@ -28,7 +28,7 @@ module Integrations
end
def self.supported_events
- %w(push)
+ %w[push]
end
def execute(data)
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index 4d207574ca7..2c929dc2cb3 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -31,12 +31,12 @@ module Integrations
# Custom serialized properties initialization
prop_accessor(*SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] })
- boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
+ boolean_accessor :notify_only_default_branch
validates :webhook,
presence: true,
public_url: true,
- if: -> (integration) { integration.activated? && integration.requires_webhook? }
+ if: -> (integration) { integration.activated? && integration.class.requires_webhook? }
validates :labels_to_be_notified_behavior, inclusion: { in: LABEL_NOTIFICATION_BEHAVIOURS }, allow_blank: true, if: :activated?
validate :validate_channel_limit, if: :activated?
@@ -44,7 +44,7 @@ module Integrations
super
if properties.empty?
- self.notify_only_broken_pipelines = true
+ self.notify_only_broken_pipelines = true if self.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?
@@ -72,48 +72,7 @@ module Integrations
end
def fields
- default_fields + build_event_channels
- end
-
- def default_fields
- [
- {
- type: :checkbox,
- section: SECTION_TYPE_CONFIGURATION,
- name: 'notify_only_broken_pipelines',
- help: 'Do not send notifications for successful pipelines.'
- }.freeze,
- {
- type: :select,
- section: SECTION_TYPE_CONFIGURATION,
- name: 'branches_to_be_notified',
- title: s_('Integrations|Branches for which notifications are to be sent'),
- choices: self.class.branch_choices
- }.freeze,
- {
- type: :text,
- section: SECTION_TYPE_CONFIGURATION,
- name: 'labels_to_be_notified',
- placeholder: '~backend,~frontend',
- help: 'Send notifications for issue, merge request, and comment events with the listed labels only. Leave blank to receive notifications for all events.'
- }.freeze,
- {
- type: :select,
- section: SECTION_TYPE_CONFIGURATION,
- name: 'labels_to_be_notified_behavior',
- choices: [
- ['Match any of the labels', MATCH_ANY_LABEL],
- ['Match all of the labels', MATCH_ALL_LABELS]
- ]
- }.freeze
- ].tap do |fields|
- next unless requires_webhook?
-
- fields.unshift(
- { type: :text, name: 'webhook', help: webhook_help, required: true }.freeze,
- { type: :text, name: 'username', placeholder: 'GitLab-integration' }.freeze
- )
- end.freeze
+ self.class.fields + build_event_channels
end
def execute(data)
@@ -154,6 +113,15 @@ module Integrations
supported_events.map { |event| event_channel_name(event) }
end
+ override :api_field_names
+ def api_field_names
+ if mask_configurable_channels?
+ super - event_channel_names
+ else
+ super
+ end
+ end
+
def form_fields
super.reject { |field| field[:name].end_with?('channel') }
end
@@ -166,6 +134,10 @@ module Integrations
raise NotImplementedError
end
+ def help
+ raise NotImplementedError
+ end
+
# With some integrations the webhook is already tied to a specific channel,
# for others the channels are configurable for each event.
def configurable_channels?
@@ -181,7 +153,7 @@ module Integrations
self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend
end
- def requires_webhook?
+ def self.requires_webhook?
true
end
@@ -193,11 +165,32 @@ module Integrations
false
end
+ override :sections
+ def sections
+ [
+ {
+ type: SECTION_TYPE_CONNECTION,
+ title: s_('Integrations|Connection details'),
+ description: help
+ },
+ {
+ type: SECTION_TYPE_TRIGGER,
+ title: s_('Integrations|Trigger'),
+ description: s_('Integrations|An event will be triggered when one of the following items happen.')
+ },
+ {
+ type: SECTION_TYPE_CONFIGURATION,
+ title: s_('Integrations|Notification settings'),
+ description: s_('Integrations|Configure the scope of notifications.')
+ }
+ ]
+ end
+
private
def should_execute?(object_kind)
supported_events.include?(object_kind) &&
- (!requires_webhook? || webhook.present?)
+ (!self.class.requires_webhook? || webhook.present?)
end
def log_usage(_, _)
@@ -264,7 +257,7 @@ module Integrations
def build_event_channels
event_channel_names.map do |channel_field|
- { type: :text, name: channel_field, placeholder: default_channel_placeholder }
+ Field.new(name: channel_field, type: :text, placeholder: default_channel_placeholder, integration_class: self)
end
end
diff --git a/app/models/integrations/base_issue_tracker.rb b/app/models/integrations/base_issue_tracker.rb
index 7a54d354007..b59aee6743d 100644
--- a/app/models/integrations/base_issue_tracker.rb
+++ b/app/models/integrations/base_issue_tracker.rb
@@ -88,7 +88,7 @@ module Integrations
end
def self.supported_events
- %w(push)
+ %w[push]
end
def execute(data)
diff --git a/app/models/integrations/base_monitoring.rb b/app/models/integrations/base_monitoring.rb
index b0bebb5a859..12ea57f59a3 100644
--- a/app/models/integrations/base_monitoring.rb
+++ b/app/models/integrations/base_monitoring.rb
@@ -9,7 +9,7 @@ module Integrations
attribute :category, default: 'monitoring'
def self.supported_events
- %w()
+ %w[]
end
def can_query?
diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb
index 29a20419809..65aec8b278f 100644
--- a/app/models/integrations/base_slack_notification.rb
+++ b/app/models/integrations/base_slack_notification.rb
@@ -25,20 +25,24 @@ module Integrations
override :supported_events
def supported_events
- additional = %w[alert]
-
- if group_level? && Feature.enabled?(:group_mentions, group)
- additional += %w[group_mention group_confidential_mention]
- end
+ additional = group_level? ? %w[group_mention group_confidential_mention] : []
(super + additional).freeze
end
+ def self.supported_events
+ super + %w[alert]
+ end
+
override :configurable_channels?
def configurable_channels?
true
end
+ def help
+ # noop
+ end
+
private
override :log_usage
diff --git a/app/models/integrations/base_slash_commands.rb b/app/models/integrations/base_slash_commands.rb
index 7662da933ba..58821e5fb4e 100644
--- a/app/models/integrations/base_slash_commands.rb
+++ b/app/models/integrations/base_slash_commands.rb
@@ -13,7 +13,7 @@ module Integrations
end
def self.supported_events
- %w()
+ %w[]
end
def testable?
diff --git a/app/models/integrations/base_third_party_wiki.rb b/app/models/integrations/base_third_party_wiki.rb
index 8df172e9a53..dee3706c518 100644
--- a/app/models/integrations/base_third_party_wiki.rb
+++ b/app/models/integrations/base_third_party_wiki.rb
@@ -9,7 +9,7 @@ module Integrations
after_commit :cache_project_has_integration
def self.supported_events
- %w()
+ %w[]
end
private
diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb
index 6cd36e545a5..82a5142e8c2 100644
--- a/app/models/integrations/buildkite.rb
+++ b/app/models/integrations/buildkite.rb
@@ -29,7 +29,7 @@ module Integrations
validates :token, presence: true, if: :activated?
def self.supported_events
- %w(push merge_request tag_push)
+ %w[push merge_request tag_push]
end
# This is a stub method to work with deprecated API response
diff --git a/app/models/integrations/campfire.rb b/app/models/integrations/campfire.rb
index 007578e5830..8b5797a9d24 100644
--- a/app/models/integrations/campfire.rb
+++ b/app/models/integrations/campfire.rb
@@ -2,7 +2,7 @@
module Integrations
class Campfire < Integration
- SUBDOMAIN_REGEXP = %r{\A[a-z](?:[a-z0-9-]*[a-z0-9])?\z}i.freeze
+ SUBDOMAIN_REGEXP = %r{\A[a-z](?:[a-z0-9-]*[a-z0-9])?\z}i
validates :token, presence: true, if: :activated?
validates :room,
@@ -26,12 +26,9 @@ module Integrations
placeholder: '',
exposes_secrets: true,
help: -> do
- ERB::Util.html_escape(
+ format(ERB::Util.html_escape(
s_('CampfireService|The %{code_open}.campfirenow.com%{code_close} subdomain.')
- ) % {
- code_open: '<code>'.html_safe,
- code_close: '</code>'.html_safe
- }
+ ), code_open: '<code>'.html_safe, code_close: '</code>'.html_safe)
end
field :room,
@@ -48,13 +45,16 @@ module Integrations
end
def help
- docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('api/services', anchor: 'campfire'), target: '_blank', rel: 'noopener noreferrer'
-
- ERB::Util.html_escape(
+ docs_link = ActionController::Base.helpers.link_to(
+ _('Learn more.'),
+ Rails.application.routes.url_helpers.help_page_url('api/integrations', anchor: 'campfire'),
+ target: '_blank',
+ rel: 'noopener noreferrer'
+ )
+
+ format(ERB::Util.html_escape(
s_('CampfireService|Send notifications about push events to Campfire chat rooms. %{docs_link}')
- ) % {
- docs_link: docs_link.html_safe
- }
+ ), docs_link: docs_link.html_safe)
end
def self.to_param
@@ -62,14 +62,14 @@ module Integrations
end
def self.supported_events
- %w(push)
+ %w[push]
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
message = create_message(data)
- speak(self.room, message, auth)
+ speak(room, message, auth)
end
private
@@ -96,7 +96,7 @@ module Integrations
room = rooms(auth).find { |r| r["name"] == room_name }
return unless room
- path = "/room/#{room["id"]}/speak.json"
+ path = "/room/#{room['id']}/speak.json"
body = {
body: {
message: {
diff --git a/app/models/integrations/chat_message/base_message.rb b/app/models/integrations/chat_message/base_message.rb
index 501b214a769..600f07b97f1 100644
--- a/app/models/integrations/chat_message/base_message.rb
+++ b/app/models/integrations/chat_message/base_message.rb
@@ -3,7 +3,7 @@
module Integrations
module ChatMessage
class BaseMessage
- RELATIVE_LINK_REGEX = %r{!\[[^\]]*\]\((/uploads/[^\)]*)\)}.freeze
+ RELATIVE_LINK_REGEX = %r{!\[[^\]]*\]\((/uploads/[^\)]*)\)}
attr_reader :markdown
attr_reader :user_full_name
diff --git a/app/models/integrations/chat_message/deployment_message.rb b/app/models/integrations/chat_message/deployment_message.rb
index b28edeecb4d..0367459dfcb 100644
--- a/app/models/integrations/chat_message/deployment_message.rb
+++ b/app/models/integrations/chat_message/deployment_message.rb
@@ -26,8 +26,10 @@ module Integrations
end
def attachments
+ return description_message if markdown
+
[{
- text: "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{strip_markup(commit_title)}",
+ text: format(description_message),
color: color
}]
end
@@ -82,6 +84,10 @@ module Integrations
def running?
status == 'running'
end
+
+ def description_message
+ "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{strip_markup(commit_title)}"
+ end
end
end
end
diff --git a/app/models/integrations/confluence.rb b/app/models/integrations/confluence.rb
index 31e9a171d1b..eda8c37fc72 100644
--- a/app/models/integrations/confluence.rb
+++ b/app/models/integrations/confluence.rb
@@ -2,9 +2,9 @@
module Integrations
class Confluence < BaseThirdPartyWiki
- VALID_SCHEME_MATCH = %r{\Ahttps?\Z}.freeze
- VALID_HOST_MATCH = %r{\A.+\.atlassian\.net\Z}.freeze
- VALID_PATH_MATCH = %r{\A/wiki(/|\Z)}.freeze
+ VALID_SCHEME_MATCH = %r{\Ahttps?\Z}
+ VALID_HOST_MATCH = %r{\A.+\.atlassian\.net\Z}
+ VALID_PATH_MATCH = %r{\A/wiki(/|\Z)}
validates :confluence_url, presence: true, if: :activated?
validate :validate_confluence_url_is_cloud, if: :activated?
@@ -14,6 +14,10 @@ module Integrations
placeholder: 'https://example.atlassian.net/wiki',
required: true
+ def avatar_url
+ ActionController::Base.helpers.image_path('confluence.svg')
+ end
+
def self.to_param
'confluence'
end
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
index 1a56763fe57..b1f1361afcd 100644
--- a/app/models/integrations/datadog.rb
+++ b/app/models/integrations/datadog.rb
@@ -12,7 +12,7 @@ module Integrations
pipeline build archive_trace
].freeze
- TAG_KEY_VALUE_RE = %r{\A [\w-]+ : .*\S.* \z}x.freeze
+ TAG_KEY_VALUE_RE = %r{\A [\w-]+ : .*\S.* \z}x
field :datadog_site,
exposes_secrets: true,
@@ -40,7 +40,7 @@ module Integrations
ERB::Util.html_escape(
s_('DatadogIntegration|%{linkOpen}API key%{linkClose} used for authentication with Datadog.')
) % {
- linkOpen: %{<a href="#{URL_API_KEYS_DOCS}" target="_blank" rel="noopener noreferrer">}.html_safe,
+ linkOpen: %(<a href="#{URL_API_KEYS_DOCS}" target="_blank" rel="noopener noreferrer">).html_safe,
linkClose: '</a>'.html_safe
}
end,
diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb
index 7cae3ca20f9..815e3669d78 100644
--- a/app/models/integrations/discord.rb
+++ b/app/models/integrations/discord.rb
@@ -4,9 +4,7 @@ require "discordrb/webhooks"
module Integrations
class Discord < BaseChatNotification
- ATTACHMENT_REGEX = /: (?<entry>.*?)\n - (?<name>.*)\n*/.freeze
-
- undef :notify_only_broken_pipelines
+ ATTACHMENT_REGEX = /: (?<entry>.*?)\n - (?<name>.*)\n*/
field :webhook,
section: SECTION_TYPE_CONNECTION,
@@ -35,10 +33,6 @@ module Integrations
"discord"
end
- def fields
- self.class.fields + build_event_channels
- end
-
def help
docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/discord_notifications'), target: '_blank', rel: 'noopener noreferrer'
s_('Send notifications about project events to a Discord channel. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
@@ -52,26 +46,6 @@ module Integrations
%w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page]
end
- def sections
- [
- {
- type: SECTION_TYPE_CONNECTION,
- title: s_('Integrations|Connection details'),
- description: help
- },
- {
- type: SECTION_TYPE_TRIGGER,
- title: s_('Integrations|Trigger'),
- description: s_('Integrations|An event will be triggered when one of the following items happen.')
- },
- {
- type: SECTION_TYPE_CONFIGURATION,
- title: s_('Integrations|Notification settings'),
- description: s_('Integrations|Configure the scope of notifications.')
- }
- ]
- end
-
def configurable_channels?
true
end
diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb
index ac464c020dd..f6a12c4bb1a 100644
--- a/app/models/integrations/drone_ci.rb
+++ b/app/models/integrations/drone_ci.rb
@@ -43,7 +43,7 @@ module Integrations
end
def self.supported_events
- %w(push merge_request tag_push)
+ %w[push merge_request tag_push]
end
def commit_status_path(sha, ref)
diff --git a/app/models/integrations/emails_on_push.rb b/app/models/integrations/emails_on_push.rb
index eb893ae45d0..144d1a07b04 100644
--- a/app/models/integrations/emails_on_push.rb
+++ b/app/models/integrations/emails_on_push.rb
@@ -52,7 +52,7 @@ module Integrations
end
def self.supported_events
- %w(push tag_push)
+ %w[push tag_push]
end
def initialize_properties
diff --git a/app/models/integrations/external_wiki.rb b/app/models/integrations/external_wiki.rb
index 75fe6b6f164..acacab2528e 100644
--- a/app/models/integrations/external_wiki.rb
+++ b/app/models/integrations/external_wiki.rb
@@ -47,7 +47,7 @@ module Integrations
end
def self.supported_events
- %w()
+ %w[]
end
end
end
diff --git a/app/models/integrations/gitlab_slack_application.rb b/app/models/integrations/gitlab_slack_application.rb
index b0f54f39e8c..2d520eaf7e7 100644
--- a/app/models/integrations/gitlab_slack_application.rb
+++ b/app/models/integrations/gitlab_slack_application.rb
@@ -20,6 +20,8 @@ module Integrations
has_one :slack_integration, foreign_key: :integration_id, inverse_of: :integration
delegate :bot_access_token, :bot_user_id, to: :slack_integration, allow_nil: true
+ include SlackMattermostFields
+
def update_active_status
update(active: !!slack_integration)
end
@@ -66,18 +68,7 @@ module Integrations
def sections
return [] unless editable?
- [
- {
- type: SECTION_TYPE_TRIGGER,
- title: s_('Integrations|Trigger'),
- description: s_('Integrations|An event will be triggered when one of the following items happen.')
- },
- {
- type: SECTION_TYPE_CONFIGURATION,
- title: s_('Integrations|Notification settings'),
- description: s_('Integrations|Configure the scope of notifications.')
- }
- ]
+ super.drop(1)
end
override :configurable_events
@@ -88,7 +79,7 @@ module Integrations
end
override :requires_webhook?
- def requires_webhook?
+ def self.requires_webhook?
false
end
diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb
index 037c689c75e..680752c3d56 100644
--- a/app/models/integrations/hangouts_chat.rb
+++ b/app/models/integrations/hangouts_chat.rb
@@ -2,8 +2,6 @@
module Integrations
class HangoutsChat < BaseChatNotification
- undef :notify_only_broken_pipelines
-
field :webhook,
section: SECTION_TYPE_CONNECTION,
help: 'https://chat.googleapis.com/v1/spaces…',
@@ -36,10 +34,6 @@ module Integrations
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 fields
- self.class.fields + build_event_channels
- end
-
def default_channel_placeholder
end
diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb
index 7769ea7d2dd..0683c8408bc 100644
--- a/app/models/integrations/jenkins.rb
+++ b/app/models/integrations/jenkins.rb
@@ -66,7 +66,7 @@ module Integrations
end
def self.supported_events
- %w(push merge_request tag_push)
+ %w[push merge_request tag_push]
end
def title
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index faf0a378a17..d8d1f860e9a 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -126,7 +126,7 @@ module Integrations
# When these are false GitLab does not create cross reference
# comments on Jira except when an issue gets transitioned.
def self.supported_events
- %w(commit merge_request)
+ %w[commit merge_request]
end
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
diff --git a/app/models/integrations/mattermost.rb b/app/models/integrations/mattermost.rb
index e3c5c22ad3a..7e391b11d82 100644
--- a/app/models/integrations/mattermost.rb
+++ b/app/models/integrations/mattermost.rb
@@ -3,6 +3,7 @@
module Integrations
class Mattermost < BaseChatNotification
include SlackMattermostNotifier
+ include SlackMattermostFields
def title
_('Mattermost notifications')
@@ -25,7 +26,7 @@ module Integrations
'my-channel'
end
- def webhook_help
+ def self.webhook_help
'http://mattermost.example.com/hooks/'
end
diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb
index 25308948d51..208172d6303 100644
--- a/app/models/integrations/microsoft_teams.rb
+++ b/app/models/integrations/microsoft_teams.rb
@@ -2,8 +2,6 @@
module Integrations
class MicrosoftTeams < BaseChatNotification
- undef :notify_only_broken_pipelines
-
field :webhook,
section: SECTION_TYPE_CONNECTION,
help: 'https://outlook.office.com/webhook/…',
@@ -44,30 +42,6 @@ module Integrations
pipeline wiki_page]
end
- def fields
- self.class.fields + build_event_channels
- end
-
- def sections
- [
- {
- type: SECTION_TYPE_CONNECTION,
- title: s_('Integrations|Connection details'),
- description: help
- },
- {
- type: SECTION_TYPE_TRIGGER,
- title: s_('Integrations|Trigger'),
- description: s_('Integrations|An event will be triggered when one of the following items happen.')
- },
- {
- type: SECTION_TYPE_CONFIGURATION,
- title: s_('Integrations|Notification settings'),
- description: s_('Integrations|Configure the scope of notifications.')
- }
- ]
- end
-
private
def notify(message, opts)
diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb
index c9c08ec9771..c0acb6c87b4 100644
--- a/app/models/integrations/packagist.rb
+++ b/app/models/integrations/packagist.rb
@@ -42,7 +42,7 @@ module Integrations
end
def self.supported_events
- %w(push merge_request tag_push)
+ %w[push merge_request tag_push]
end
def execute(data)
diff --git a/app/models/integrations/pivotaltracker.rb b/app/models/integrations/pivotaltracker.rb
index 0d9a3f05a86..f42a872c49e 100644
--- a/app/models/integrations/pivotaltracker.rb
+++ b/app/models/integrations/pivotaltracker.rb
@@ -38,7 +38,7 @@ module Integrations
end
def self.supported_events
- %w(push)
+ %w[push]
end
def execute(data)
diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb
index 736318ed707..8474a5b7adf 100644
--- a/app/models/integrations/prometheus.rb
+++ b/app/models/integrations/prometheus.rb
@@ -41,6 +41,7 @@ module Integrations
before_save :synchronize_service_state
after_save :clear_reactive_cache!
+ after_commit :sync_http_integration!
after_commit :track_events
@@ -180,5 +181,16 @@ module Integrations
nil
end
strong_memoize_attr :iap_client
+
+ # 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?
+
+ project.alert_management_http_integrations
+ .for_endpoint_identifier('legacy-prometheus')
+ .take
+ &.update_columns(active: manual_configuration)
+ end
end
end
diff --git a/app/models/integrations/pumble.rb b/app/models/integrations/pumble.rb
index 8f0dddcc5c5..09e011023ed 100644
--- a/app/models/integrations/pumble.rb
+++ b/app/models/integrations/pumble.rb
@@ -2,8 +2,6 @@
module Integrations
class Pumble < BaseChatNotification
- undef :notify_only_broken_pipelines
-
field :webhook,
section: SECTION_TYPE_CONNECTION,
help: 'https://api.pumble.com/workspaces/x/...',
@@ -52,10 +50,6 @@ module Integrations
pipeline wiki_page]
end
- def fields
- self.class.fields + build_event_channels
- end
-
private
def notify(message, opts)
diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb
index 006b731c6c2..e97c7e5e738 100644
--- a/app/models/integrations/pushover.rb
+++ b/app/models/integrations/pushover.rb
@@ -47,19 +47,19 @@ module Integrations
[
['Device default sound', nil],
['Pushover (default)', 'pushover'],
- %w(Bike bike),
- %w(Bugle bugle),
+ %w[Bike bike],
+ %w[Bugle bugle],
['Cash Register', 'cashregister'],
- %w(Classical classical),
- %w(Cosmic cosmic),
- %w(Falling falling),
- %w(Gamelan gamelan),
- %w(Incoming incoming),
- %w(Intermission intermission),
- %w(Magic magic),
- %w(Mechanical mechanical),
+ %w[Classical classical],
+ %w[Cosmic cosmic],
+ %w[Falling falling],
+ %w[Gamelan gamelan],
+ %w[Incoming incoming],
+ %w[Intermission intermission],
+ %w[Magic magic],
+ %w[Mechanical mechanical],
['Piano Bar', 'pianobar'],
- %w(Siren siren),
+ %w[Siren siren],
['Space Alarm', 'spacealarm'],
['Tug Boat', 'tugboat'],
['Alien Alarm (long)', 'alien'],
@@ -84,7 +84,7 @@ module Integrations
end
def self.supported_events
- %w(push)
+ %w[push]
end
def execute(data)
diff --git a/app/models/integrations/shimo.rb b/app/models/integrations/shimo.rb
index f5b6595fff2..227fdca5c91 100644
--- a/app/models/integrations/shimo.rb
+++ b/app/models/integrations/shimo.rb
@@ -8,6 +8,10 @@ module Integrations
title: -> { s_('Shimo|Shimo Workspace URL') },
required: true
+ def avatar_url
+ ActionController::Base.helpers.image_path('logos/shimo.svg')
+ end
+
def render?
valid? && activated?
end
diff --git a/app/models/integrations/slack.rb b/app/models/integrations/slack.rb
index 07d2d802915..f70376e2f0d 100644
--- a/app/models/integrations/slack.rb
+++ b/app/models/integrations/slack.rb
@@ -3,6 +3,7 @@
module Integrations
class Slack < BaseSlackNotification
include SlackMattermostNotifier
+ include SlackMattermostFields
def title
'Slack notifications'
@@ -16,8 +17,7 @@ module Integrations
'slack'
end
- override :webhook_help
- def webhook_help
+ def self.webhook_help
'https://hooks.slack.com/services/…'
end
diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb
index c74e0aab030..575c3b8a334 100644
--- a/app/models/integrations/teamcity.rb
+++ b/app/models/integrations/teamcity.rb
@@ -6,7 +6,7 @@ module Integrations
include ReactivelyCached
prepend EnableSslVerification
- TEAMCITY_SAAS_HOSTNAME = /\A[^\.]+\.teamcity\.com\z/i.freeze
+ TEAMCITY_SAAS_HOSTNAME = /\A[^\.]+\.teamcity\.com\z/i
field :teamcity_url,
title: -> { s_('ProjectService|TeamCity server URL') },
@@ -43,7 +43,7 @@ module Integrations
end
def supported_events
- %w(push merge_request)
+ %w[push merge_request]
end
end
diff --git a/app/models/integrations/telegram.rb b/app/models/integrations/telegram.rb
index 9af12c712c6..7c196720386 100644
--- a/app/models/integrations/telegram.rb
+++ b/app/models/integrations/telegram.rb
@@ -21,6 +21,11 @@ module Integrations
placeholder: '@channelusername',
required: true
+ field :notify_only_broken_pipelines,
+ type: :checkbox,
+ section: SECTION_TYPE_CONFIGURATION,
+ help: 'If selected, successful pipelines do not trigger a notification event.'
+
with_options if: :activated? do
validates :token, :room, presence: true
end
@@ -51,34 +56,10 @@ module Integrations
)
end
- def fields
- self.class.fields + build_event_channels
- end
-
def self.supported_events
super - ['deployment']
end
- def sections
- [
- {
- type: SECTION_TYPE_CONNECTION,
- title: s_('Integrations|Connection details'),
- description: help
- },
- {
- type: SECTION_TYPE_TRIGGER,
- title: s_('Integrations|Trigger'),
- description: s_('Integrations|An event will be triggered when one of the following items happen.')
- },
- {
- type: SECTION_TYPE_CONFIGURATION,
- title: s_('Integrations|Notification settings'),
- description: s_('Integrations|Configure the scope of notifications.')
- }
- ]
- end
-
private
def set_webhook
diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb
index 6de693b5278..3b4bcfa28d3 100644
--- a/app/models/integrations/unify_circuit.rb
+++ b/app/models/integrations/unify_circuit.rb
@@ -2,8 +2,6 @@
module Integrations
class UnifyCircuit < BaseChatNotification
- undef :notify_only_broken_pipelines
-
field :webhook,
section: SECTION_TYPE_CONNECTION,
help: 'https://yourcircuit.com/rest/v2/webhooks/incoming/…',
@@ -31,10 +29,6 @@ module Integrations
'unify_circuit'
end
- def fields
- self.class.fields + build_event_channels
- end
-
def help
docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/unify_circuit'), target: '_blank', rel: 'noopener noreferrer'
s_('Integrations|Send notifications about project events to a Unify Circuit conversation. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb
index 21c65cc2b32..3ef8ab39352 100644
--- a/app/models/integrations/webex_teams.rb
+++ b/app/models/integrations/webex_teams.rb
@@ -2,8 +2,6 @@
module Integrations
class WebexTeams < BaseChatNotification
- undef :notify_only_broken_pipelines
-
field :webhook,
section: SECTION_TYPE_CONNECTION,
help: 'https://api.ciscospark.com/v1/webhooks/incoming/...',
@@ -31,10 +29,6 @@ module Integrations
'webex_teams'
end
- def fields
- self.class.fields + build_event_channels
- end
-
def help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/webex_teams'), target: '_blank', rel: 'noopener noreferrer'
s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe }
diff --git a/app/models/integrations/zentao.rb b/app/models/integrations/zentao.rb
index fd2c741bd6b..58ec4abf30c 100644
--- a/app/models/integrations/zentao.rb
+++ b/app/models/integrations/zentao.rb
@@ -34,6 +34,10 @@ module Integrations
validates :api_token, presence: true, if: :activated?
validates :zentao_product_xid, presence: true, if: :activated?
+ def avatar_url
+ ActionController::Base.helpers.image_path('logos/zentao.svg')
+ end
+
def self.issues_license_available?(project)
project&.licensed_feature_available?(:zentao_issues_integration)
end
@@ -82,7 +86,7 @@ module Integrations
end
def self.supported_events
- %w()
+ %w[]
end
private
diff --git a/app/models/issuable_severity.rb b/app/models/issuable_severity.rb
index cd7e5fafb60..08984bbb723 100644
--- a/app/models/issuable_severity.rb
+++ b/app/models/issuable_severity.rb
@@ -11,11 +11,11 @@ class IssuableSeverity < ApplicationRecord
}.freeze
SEVERITY_QUICK_ACTION_PARAMS = {
- unknown: %w(Unknown 0),
- low: %w(Low S4 4),
- medium: %w(Medium S3 3),
- high: %w(High S2 2),
- critical: %w(Critical S1 1)
+ unknown: %w[Unknown 0],
+ low: %w[Low S4 4],
+ medium: %w[Medium S3 3],
+ high: %w[High S2 2],
+ critical: %w[Critical S1 1]
}.freeze
belongs_to :issue
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d227448961a..58383a6a329 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -14,7 +14,6 @@ class Issue < ApplicationRecord
include TimeTrackable
include ThrottledTouch
include LabelEventable
- include IgnorableColumns
include MilestoneEventable
include WhereComposite
include StateEventable
@@ -48,16 +47,14 @@ class Issue < ApplicationRecord
#
# This should be kept consistent with the enums used for the GraphQL issue list query in
# https://gitlab.com/gitlab-org/gitlab/-/blob/1379c2d7bffe2a8d809f23ac5ef9b4114f789c07/app/assets/javascripts/issues/list/constants.js#L154-158
- TYPES_FOR_LIST = %w(issue incident test_case task objective key_result).freeze
+ TYPES_FOR_LIST = %w[issue incident test_case task objective key_result].freeze
# Types of issues that should be displayed on issue board lists
- TYPES_FOR_BOARD_LIST = %w(issue incident).freeze
+ TYPES_FOR_BOARD_LIST = %w[issue incident].freeze
# This default came from the enum `issue_type` column. Defined as default in the DB
DEFAULT_ISSUE_TYPE = :issue
- ignore_column :issue_type, remove_with: '16.4', remove_after: '2023-08-22'
-
belongs_to :project
belongs_to :namespace, inverse_of: :issues
@@ -112,7 +109,6 @@ class Issue < ApplicationRecord
has_one :sentry_issue
has_one :alert_management_alert, class_name: 'AlertManagement::Alert'
has_one :incident_management_issuable_escalation_status, class_name: 'IncidentManagement::IssuableEscalationStatus'
- has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_and_belongs_to_many :prometheus_alert_events, join_table: :issues_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :issue, validate: false
has_many :prometheus_alerts, through: :prometheus_alert_events
@@ -190,7 +186,6 @@ class Issue < ApplicationRecord
scope :preload_awardable, -> { preload(:award_emoji) }
scope :with_alert_management_alerts, -> { joins(:alert_management_alert) }
scope :with_prometheus_alert_events, -> { joins(:issues_prometheus_alert_events) }
- scope :with_self_managed_prometheus_alert_events, -> { joins(:issues_self_managed_prometheus_alert_events) }
scope :with_api_entity_associations, -> {
preload(:work_item_type, :timelogs, :closed_by, :assignees, :author, :labels, :issuable_severity,
namespace: [{ parent: :route }, :route], milestone: { project: [:route, { namespace: :route }] },
@@ -223,8 +218,11 @@ class Issue < ApplicationRecord
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
- scope :service_desk, -> { where(author: ::User.support_bot) }
- scope :inc_relations_for_view, -> { includes(author: :status, assignees: :status) }
+ scope :service_desk, -> { where(author: ::Users::Internal.support_bot) }
+ scope :inc_relations_for_view, -> do
+ includes(author: :status, assignees: :status)
+ .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/422155')
+ end
# An issue can be uniquely identified by project_id and iid
# Takes one or more sets of composite IDs, expressed as hash-like records of
@@ -546,18 +544,14 @@ class Issue < ApplicationRecord
end
def related_issues(current_user, preload: nil)
- related_issues = self.class
- .select(['issues.*', 'issue_links.id AS issue_link_id',
- 'issue_links.link_type as issue_link_type_value',
- 'issue_links.target_id as issue_link_source_id',
- 'issue_links.created_at as issue_link_created_at',
- 'issue_links.updated_at as issue_link_updated_at'])
- .joins("INNER JOIN issue_links ON
- (issue_links.source_id = issues.id AND issue_links.target_id = #{id})
- OR
- (issue_links.target_id = issues.id AND issue_links.source_id = #{id})")
- .preload(preload)
- .reorder('issue_link_id')
+ related_issues =
+ linked_issues_select
+ .joins("INNER JOIN issue_links ON
+ (issue_links.source_id = issues.id AND issue_links.target_id = #{id})
+ OR
+ (issue_links.target_id = issues.id AND issue_links.source_id = #{id})")
+ .preload(preload)
+ .reorder('issue_link_id')
related_issues = yield related_issues if block_given?
@@ -607,7 +601,7 @@ class Issue < ApplicationRecord
end
end
- def etag_caching_enabled?
+ def real_time_notes_enabled?
true
end
@@ -642,7 +636,7 @@ class Issue < ApplicationRecord
end
def from_service_desk?
- author.id == User.support_bot.id
+ author.id == Users::Internal.support_bot.id
end
def issue_link_type
@@ -716,8 +710,8 @@ class Issue < ApplicationRecord
end
def expire_etag_cache
- # TODO: Fix this for the case when issues is created at group level
- # TODO: https://gitlab.com/gitlab-org/gitlab/-/work_items/395814
+ # We don't expire the cache for issues that don't have a project, since they are created at the group level
+ # and they are only displayed in the new work item view that uses GraphQL subscriptions for real-time updates
return unless project
key = Gitlab::Routing.url_helpers.realtime_changes_project_issue_path(project, self)
@@ -789,7 +783,7 @@ class Issue < ApplicationRecord
# TODO: https://gitlab.com/gitlab-org/gitlab/-/work_items/393126
return unless project
- Issues::SearchData.upsert({ namespace_id: namespace_id, project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i(project_id issue_id))
+ Issues::SearchData.upsert({ namespace_id: namespace_id, project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i[project_id issue_id])
end
def ensure_metrics!
@@ -833,6 +827,14 @@ class Issue < ApplicationRecord
errors.add(:work_item_type_id, format(_('can not be changed to %{new_type}'), new_type: work_item_type&.name))
end
+
+ def linked_issues_select
+ self.class.select(['issues.*', 'issue_links.id AS issue_link_id',
+ 'issue_links.link_type as issue_link_type_value',
+ 'issue_links.target_id as issue_link_source_id',
+ 'issue_links.created_at as issue_link_created_at',
+ 'issue_links.updated_at as issue_link_updated_at'])
+ end
end
Issue.prepend_mod_with('Issue')
diff --git a/app/models/label_link.rb b/app/models/label_link.rb
index d326b07ad31..0c2d205c641 100644
--- a/app/models/label_link.rb
+++ b/app/models/label_link.rb
@@ -24,3 +24,5 @@ class LabelLink < ApplicationRecord
relation
end
end
+
+LabelLink.prepend_mod_with('LabelLink')
diff --git a/app/models/lfs_download_object.rb b/app/models/lfs_download_object.rb
index 3df6742fbc9..046e47262dd 100644
--- a/app/models/lfs_download_object.rb
+++ b/app/models/lfs_download_object.rb
@@ -9,7 +9,7 @@ class LfsDownloadObject
validates :oid, format: { with: /\A\h{64}\z/ }
validates :size, numericality: { greater_than_or_equal_to: 0 }
- validates :link, public_url: { protocols: %w(http https) }
+ validates :link, public_url: { protocols: %w[http https] }
validate :headers_must_be_hash
def initialize(oid:, size:, link:, headers: {})
diff --git a/app/models/license_template.rb b/app/models/license_template.rb
index 548066107c1..bfe2a8d379e 100644
--- a/app/models/license_template.rb
+++ b/app/models/license_template.rb
@@ -5,12 +5,12 @@ class LicenseTemplate
%r{[\<\{\[]
(project|description|
one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
- [\>\}\]]}xi.freeze
- YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
+ [\>\}\]]}xi
+ YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i
FULLNAME_TEMPLATE_REGEX =
%r{[\<\{\[]
(fullname|name\sof\s(author|copyright\sowner))
- [\>\}\]]}xi.freeze
+ [\>\}\]]}xi
attr_reader :key, :name, :project, :category, :nickname, :url, :meta
diff --git a/app/models/loose_foreign_keys/modification_tracker.rb b/app/models/loose_foreign_keys/modification_tracker.rb
index 72a596d2114..eec9b8ad285 100644
--- a/app/models/loose_foreign_keys/modification_tracker.rb
+++ b/app/models/loose_foreign_keys/modification_tracker.rb
@@ -2,10 +2,6 @@
module LooseForeignKeys
class ModificationTracker
- MAX_DELETES = 100_000
- MAX_UPDATES = 50_000
- MAX_RUNTIME = 30.seconds # must be less than the scheduling frequency of the LooseForeignKeys::CleanupWorker cron worker
-
delegate :monotonic_time, to: :'Gitlab::Metrics::System'
def initialize
@@ -22,6 +18,18 @@ module LooseForeignKeys
)
end
+ def max_runtime
+ 30.seconds
+ end
+
+ def max_deletes
+ 100_000
+ end
+
+ def max_updates
+ 50_000
+ end
+
def add_deletions(table, count)
@delete_count_by_table[table] += count
@deletes_counter.increment({ table: table }, count)
@@ -33,9 +41,9 @@ module LooseForeignKeys
end
def over_limit?
- @delete_count_by_table.values.sum >= MAX_DELETES ||
- @update_count_by_table.values.sum >= MAX_UPDATES ||
- monotonic_time - @start_time >= MAX_RUNTIME
+ @delete_count_by_table.values.sum >= max_deletes ||
+ @update_count_by_table.values.sum >= max_updates ||
+ monotonic_time - @start_time >= max_runtime
end
def stats
diff --git a/app/models/loose_foreign_keys/turbo_modification_tracker.rb b/app/models/loose_foreign_keys/turbo_modification_tracker.rb
new file mode 100644
index 00000000000..5229b17e971
--- /dev/null
+++ b/app/models/loose_foreign_keys/turbo_modification_tracker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module LooseForeignKeys
+ # This is a modification tracker with the additional limits that can be enabled
+ # for some database via an OPS Feature Flag.
+
+ class TurboModificationTracker < ModificationTracker
+ extend ::Gitlab::Utils::Override
+
+ override :max_runtime
+ def max_runtime
+ 45.seconds
+ end
+
+ override :max_deletes
+ def max_deletes
+ 200_000
+ end
+
+ override :max_updates
+ def max_updates
+ 150_000
+ end
+ end
+end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index ada89345a7f..52b9c3a80e3 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -5,11 +5,10 @@ class GroupMember < Member
include CreatedAtFilterable
SOURCE_TYPE = 'Namespace'
- SOURCE_TYPE_FORMAT = /\ANamespace\z/.freeze
+ SOURCE_TYPE_FORMAT = /\ANamespace\z/
belongs_to :group, foreign_key: 'source_id'
alias_attribute :namespace_id, :source_id
- delegate :update_two_factor_requirement, to: :user, allow_nil: true
# Make sure group member points only to group as it source
attribute :source_type, default: SOURCE_TYPE
@@ -26,6 +25,16 @@ class GroupMember < Member
attr_accessor :last_owner
+ def update_two_factor_requirement
+ return unless user
+
+ Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
+ %w[users user_details user_preferences], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424288'
+ ) do
+ user.update_two_factor_requirement
+ end
+ end
+
# For those who get to see a modal with a role dropdown, here are the options presented
def self.permissible_access_level_roles(_, _)
# This method is a stopgap in preparation for https://gitlab.com/gitlab-org/gitlab/-/issues/364087
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index e0fecf702de..d07e4f9e298 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -2,7 +2,7 @@
class ProjectMember < Member
SOURCE_TYPE = 'Project'
- SOURCE_TYPE_FORMAT = /\AProject\z/.freeze
+ SOURCE_TYPE_FORMAT = /\AProject\z/
belongs_to :project, foreign_key: 'source_id'
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 469dba42952..6a72ed6476e 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -66,7 +66,7 @@ class MergeRequest < ApplicationRecord
belongs_to :latest_merge_request_diff, class_name: 'MergeRequestDiff'
manual_inverse_association :latest_merge_request_diff, :merge_request
- # method overriden in EE
+ # method overridden in EE
def suggested_reviewer_users
User.none
end
@@ -162,7 +162,7 @@ class MergeRequest < ApplicationRecord
# Keep states definition to be evaluated before the state_machine block to
# avoid spec failures. If this gets evaluated after, the `merged` and `locked`
- # states (which are overriden) can be nil.
+ # states (which are overridden) can be nil.
#
def self.available_state_names
super + [:merged, :locked]
@@ -279,6 +279,12 @@ class MergeRequest < ApplicationRecord
def check_state?(merge_status)
[:unchecked, :cannot_be_merged_recheck, :checking, :cannot_be_merged_rechecking].include?(merge_status.to_sym)
end
+
+ # rubocop: disable Style/SymbolProc
+ before_transition { |merge_request| merge_request.enable_transitioning }
+
+ after_transition { |merge_request| merge_request.disable_transitioning }
+ # rubocop: enable Style/SymbolProc
end
# Returns current merge_status except it returns `cannot_be_merged_rechecking` as `checking`
@@ -292,10 +298,14 @@ class MergeRequest < ApplicationRecord
validates :target_project, presence: true
validates :target_branch, presence: true
validates :merge_user, presence: true, if: :auto_merge_enabled?, unless: :importing?
- validate :validate_branches, unless: [:allow_broken, :importing?, :closed_or_merged_without_fork?]
+ validate :validate_branches, unless: [
+ :allow_broken,
+ :importing_or_transitioning?,
+ :closed_or_merged_without_fork?
+ ]
validate :validate_fork, unless: :closed_or_merged_without_fork?
- validate :validate_target_project, on: :create, unless: :importing?
- validate :validate_reviewer_size_length, unless: :importing?
+ validate :validate_target_project, on: :create, unless: :importing_or_transitioning?
+ validate :validate_reviewer_size_length, unless: :importing_or_transitioning?
scope :by_source_or_target_branch, ->(branch_name) do
where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
@@ -371,6 +381,7 @@ class MergeRequest < ApplicationRecord
scope :with_csv_entity_associations, -> { preload(:assignees, :approved_by_users, :author, :milestone, metrics: [:merged_by]) }
scope :with_jira_integration_associations, -> { preload_routables.preload(:metrics, :assignees, :author) }
+ scope :recently_unprepared, -> { where(prepared_at: nil).where(created_at: 2.hours.ago..).order(:created_at, :id) } # id is the tie-breaker
scope :by_target_branch_wildcard, ->(wildcard_branch_name) do
where("target_branch LIKE ?", ApplicationRecord.sanitize_sql_like(wildcard_branch_name).tr('*', '%'))
@@ -550,13 +561,9 @@ class MergeRequest < ApplicationRecord
end
def merge_pipeline
- return unless merged?
-
- # When the merge_method is :merge there will be a merge_commit_sha, however
- # when it is fast-forward there is no merge commit, so we must fall back to
- # either the squash commit (if the MR was squashed) or the diff head commit.
- sha = merge_commit_sha || squash_commit_sha || diff_head_sha
- target_project.latest_pipeline(target_branch, sha)
+ if sha = merged_commit_sha
+ target_project.latest_pipeline(target_branch, sha)
+ end
end
def head_pipeline_active?
@@ -632,7 +639,7 @@ class MergeRequest < ApplicationRecord
end
end
- DRAFT_REGEX = /\A*#{Gitlab::Regex.merge_request_draft}+\s*/i.freeze
+ DRAFT_REGEX = /\A*#{Gitlab::Regex.merge_request_draft}+\s*/i
def self.draft?(title)
!!(title =~ DRAFT_REGEX)
@@ -734,6 +741,12 @@ class MergeRequest < ApplicationRecord
true
end
+ def supports_lock_on_merge?
+ return false unless merged?
+
+ project.supports_lock_on_merge?
+ end
+
# Calls `MergeWorker` to proceed with the merge process and
# updates `merge_jid` with the MergeWorker#jid.
# This helps tracking enqueued and ongoing merge jobs.
@@ -1218,7 +1231,7 @@ class MergeRequest < ApplicationRecord
}
end
- def mergeable?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, check_mergeability_retry_lease: false)
+ def mergeable?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, check_mergeability_retry_lease: false, skip_rebase_check: false)
return false unless mergeable_state?(
skip_ci_check: skip_ci_check,
skip_discussions_check: skip_discussions_check,
@@ -1227,7 +1240,7 @@ class MergeRequest < ApplicationRecord
check_mergeability(sync_retry_lease: check_mergeability_retry_lease)
- can_be_merged? && !should_be_rebased?
+ can_be_merged? && (!should_be_rebased? || skip_rebase_check)
end
def mergeability_checks
@@ -1593,7 +1606,7 @@ class MergeRequest < ApplicationRecord
# Since another process checks for matching merge request, we need
# to make it possible to detect whether the query should go to the
# primary.
- target_project.mark_primary_write_location
+ target_project.sticking.stick(:project, target_project.id)
end
def diverged_commits_count
@@ -1654,6 +1667,7 @@ class MergeRequest < ApplicationRecord
variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee_username_list) if assignees.present?
variables.append(key: 'CI_MERGE_REQUEST_MILESTONE', value: milestone.title) if milestone
variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present?
+ variables.append(key: 'CI_MERGE_REQUEST_SQUASH_ON_MERGE', value: squash_on_merge?.to_s)
variables.concat(source_project_variables)
end
end
@@ -1831,7 +1845,7 @@ class MergeRequest < ApplicationRecord
def merged_commit_sha
return unless merged?
- sha = merge_commit_sha || squash_commit_sha || diff_head_sha
+ sha = super || merge_commit_sha || squash_commit_sha || diff_head_sha
sha.presence
end
@@ -1996,7 +2010,7 @@ class MergeRequest < ApplicationRecord
all_pipelines.for_sha_or_source_sha(diff_head_sha).first
end
- def etag_caching_enabled?
+ def real_time_notes_enabled?
true
end
@@ -2097,6 +2111,10 @@ class MergeRequest < ApplicationRecord
spammable_attribute_changed? && project.public?
end
+ def missing_required_squash?
+ !squash && target_project.squash_always?
+ end
+
private
attr_accessor :skip_fetch_ref
@@ -2141,6 +2159,7 @@ class MergeRequest < ApplicationRecord
variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH', value: source_project.full_path)
variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL', value: source_project.web_url)
variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME', value: source_branch.to_s)
+ variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_PROTECTED', value: ProtectedBranch.protected?(source_project, source_branch).to_s)
end
end
diff --git a/app/models/metrics/dashboard/annotation.rb b/app/models/metrics/dashboard/annotation.rb
deleted file mode 100644
index ac0fcb41089..00000000000
--- a/app/models/metrics/dashboard/annotation.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-module Metrics
- module Dashboard
- class Annotation < ApplicationRecord
- include DeleteWithLimit
-
- self.table_name = 'metrics_dashboard_annotations'
-
- validates :starting_at, presence: true
- validates :description, presence: true, length: { maximum: 255 }
- validates :dashboard_path, presence: true, length: { maximum: 255 }
- validates :panel_xid, length: { maximum: 255 }
- validate :ending_at_after_starting_at
-
- scope :after, ->(after) { where('starting_at >= ?', after) }
- scope :before, ->(before) { where('starting_at <= ?', before) }
-
- scope :for_dashboard, ->(dashboard_path) { where(dashboard_path: dashboard_path) }
- scope :ending_before, ->(timestamp) { where('COALESCE(ending_at, starting_at) < ?', timestamp) }
-
- private
-
- # If annotation has NULL in ending_at column that indicates, that this annotation IS TIED TO SINGLE POINT
- # IN TIME designated by starting_at timestamp. It does NOT mean that annotation is ever going starting from
- # stating_at timestamp
- def ending_at_after_starting_at
- return if ending_at.blank? || starting_at.blank? || starting_at <= ending_at
-
- errors.add(:ending_at, s_("MetricsDashboardAnnotation|can't be before starting_at time"))
- end
- end
- end
-end
diff --git a/app/models/metrics/users_starred_dashboard.rb b/app/models/metrics/users_starred_dashboard.rb
deleted file mode 100644
index 07748eb1431..00000000000
--- a/app/models/metrics/users_starred_dashboard.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module Metrics
- class UsersStarredDashboard < ApplicationRecord
- self.table_name = 'metrics_users_starred_dashboards'
-
- belongs_to :user, inverse_of: :metrics_users_starred_dashboards
- belongs_to :project, inverse_of: :metrics_users_starred_dashboards
-
- validates :user_id, presence: true
- validates :project_id, presence: true
- validates :dashboard_path, presence: true, length: { maximum: 255 }
- validates :dashboard_path, uniqueness: { scope: %i[user_id project_id] }
-
- scope :for_project, ->(project) { where(project: project) }
- scope :for_project_dashboard, ->(project, path) { for_project(project).where(dashboard_path: path) }
- end
-end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 8de717fb61d..eb0da368c7b 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -134,7 +134,9 @@ class Milestone < ApplicationRecord
end
def participants
- User.joins(assigned_issues: :milestone).where(milestones: { id: id }).distinct
+ User.joins(assigned_issues: :milestone)
+ .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/422155')
+ .where(milestones: { id: id }).distinct
end
def self.sort_by_attribute(method)
diff --git a/app/models/ml/model_version.rb b/app/models/ml/model_version.rb
index 6d0e7c35865..e7fcde2cb5c 100644
--- a/app/models/ml/model_version.rb
+++ b/app/models/ml/model_version.rb
@@ -14,7 +14,7 @@ module Ml
belongs_to :model, class_name: 'Ml::Model'
belongs_to :project
- belongs_to :package, class_name: 'Packages::Package', optional: true
+ belongs_to :package, class_name: 'Packages::MlModel::Package', optional: true
delegate :name, to: :model
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index a7d03c3688a..ea0ea4de5b5 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -17,6 +17,9 @@ class Namespace < ApplicationRecord
include BlocksUnsafeSerialization
include Ci::NamespaceSettings
include Referable
+ include CrossDatabaseIgnoredTables
+
+ cross_database_ignore_tables %w[routes redirect_routes], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424277'
# Tells ActiveRecord not to store the full class name, in order to save some space
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69794
@@ -145,7 +148,6 @@ class Namespace < ApplicationRecord
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_destroy :rm_dir
after_save :reload_namespace_details
@@ -155,7 +157,6 @@ class Namespace < ApplicationRecord
# Legacy Storage specific hooks
- before_destroy(prepend: true) { prepare_for_destroy }
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?
@@ -166,7 +167,9 @@ class Namespace < ApplicationRecord
scope :sort_by_type, -> { order(arel_table[:type].asc.nulls_first) }
scope :include_route, -> { includes(:route) }
scope :by_parent, -> (parent) { where(parent_id: parent) }
+ scope :by_root_id, -> (root_id) { where('traversal_ids[1] IN (?)', root_id) }
scope :filter_by_path, -> (query) { where('lower(path) = :query', query: query.downcase) }
+ scope :in_organization, -> (organization) { where(organization: organization) }
scope :with_statistics, -> do
joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id')
@@ -231,16 +234,26 @@ class Namespace < ApplicationRecord
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
- def search(query, include_parents: false, use_minimum_char_limit: true)
+ def search(query, include_parents: false, use_minimum_char_limit: true, exact_matches_first: false)
if include_parents
- without_project_namespaces
+ route_columns = [Route.arel_table[:path], Route.arel_table[:name]]
+ namespaces = without_project_namespaces
.where(id: Route.for_routable_type(Namespace.name)
.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
- .fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name]],
+ .fuzzy_search(query, route_columns,
use_minimum_char_limit: use_minimum_char_limit)
.select(:source_id))
+
+ if exact_matches_first
+ namespaces = namespaces
+ .joins(:route)
+ .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
+ .order(exact_matches_first_sql(query, route_columns))
+ end
+
+ namespaces
else
- without_project_namespaces.fuzzy_search(query, [:path, :name], use_minimum_char_limit: use_minimum_char_limit)
+ without_project_namespaces.fuzzy_search(query, [:path, :name], use_minimum_char_limit: use_minimum_char_limit, exact_matches_first: exact_matches_first)
end
end
@@ -465,7 +478,7 @@ class Namespace < ApplicationRecord
return { scope: :group, status: auto_devops_enabled } unless auto_devops_enabled.nil?
strong_memoize(:first_auto_devops_config) do
- if has_parent?
+ if parent.present?
Rails.cache.fetch(first_auto_devops_config_cache_key_for(id), expires_in: 1.day) do
parent.first_auto_devops_config
end
@@ -751,7 +764,7 @@ class Namespace < ApplicationRecord
end
def reload_namespace_details
- return unless !project_namespace? && (previous_changes.keys & %w(description description_html cached_markdown_version)).any? && namespace_details.present?
+ return unless !project_namespace? && (previous_changes.keys & %w[description description_html cached_markdown_version]).any? && namespace_details.present?
namespace_details.reset
end
diff --git a/app/models/namespace/detail.rb b/app/models/namespace/detail.rb
index 6c825b5364f..a65027733e9 100644
--- a/app/models/namespace/detail.rb
+++ b/app/models/namespace/detail.rb
@@ -3,7 +3,6 @@
class Namespace::Detail < ApplicationRecord
include IgnorableColumns
- ignore_column :free_user_cap_over_limt_notified_at, remove_with: '15.7', remove_after: '2022-11-22'
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'
diff --git a/app/models/namespace/root_storage_statistics.rb b/app/models/namespace/root_storage_statistics.rb
index 8af0cf2767c..1d11bcb574c 100644
--- a/app/models/namespace/root_storage_statistics.rb
+++ b/app/models/namespace/root_storage_statistics.rb
@@ -2,7 +2,7 @@
class Namespace::RootStorageStatistics < ApplicationRecord
SNIPPETS_SIZE_STAT_NAME = 'snippets_size'
- STATISTICS_ATTRIBUTES = %W(
+ STATISTICS_ATTRIBUTES = %W[
storage_size
repository_size
wiki_size
@@ -12,7 +12,7 @@ class Namespace::RootStorageStatistics < ApplicationRecord
#{SNIPPETS_SIZE_STAT_NAME}
pipeline_artifacts_size
uploads_size
- ).freeze
+ ].freeze
self.primary_key = :namespace_id
@@ -36,7 +36,7 @@ class Namespace::RootStorageStatistics < ApplicationRecord
end
def self.namespace_statistics_attributes
- %w(storage_size dependency_proxy_size)
+ %w[storage_size dependency_proxy_size]
end
private
diff --git a/app/models/namespace/traversal_hierarchy.rb b/app/models/namespace/traversal_hierarchy.rb
index d2de85b5dd4..86fb562f4f4 100644
--- a/app/models/namespace/traversal_hierarchy.rb
+++ b/app/models/namespace/traversal_hierarchy.rb
@@ -39,9 +39,16 @@ class Namespace
AND namespaces.traversal_ids::bigint[] <> cte.traversal_ids
SQL
- Namespace.transaction do
- @root.lock!("FOR NO KEY UPDATE")
- Namespace.connection.exec_query(sql)
+ # Hint: when a user is created, it also creates a Namespaces::UserNamespace in
+ # `ensure_namespace_correct`. This method is then called within the same
+ # transaction of the user INSERT.
+ Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
+ %w[namespaces], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424279'
+ ) do
+ Namespace.transaction do
+ @root.lock!("FOR NO KEY UPDATE")
+ Namespace.connection.exec_query(sql)
+ end
end
rescue ActiveRecord::Deadlocked
db_deadlock_counter.increment(source: 'Namespace#sync_traversal_ids!')
diff --git a/app/models/namespaces/randomized_suffix_path.rb b/app/models/namespaces/randomized_suffix_path.rb
index 586d7bff5c3..b22ba789688 100644
--- a/app/models/namespaces/randomized_suffix_path.rb
+++ b/app/models/namespaces/randomized_suffix_path.rb
@@ -3,7 +3,7 @@
module Namespaces
class RandomizedSuffixPath
MAX_TRIES = 4
- LEADING_ZEROS = /^0+/.freeze
+ LEADING_ZEROS = /^0+/
def initialize(path)
@path = path
diff --git a/app/models/note.rb b/app/models/note.rb
index f1760a8dc4a..8fc45436dc7 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -28,7 +28,7 @@ class Note < ApplicationRecord
ignore_column :id_convert_to_bigint, remove_with: '16.3', remove_after: '2023-08-22'
- ISSUE_TASK_SYSTEM_NOTE_PATTERN = /\A.*marked\sthe\stask.+as\s(completed|incomplete).*\z/.freeze
+ ISSUE_TASK_SYSTEM_NOTE_PATTERN = /\A.*marked\sthe\stask.+as\s(completed|incomplete).*\z/
cache_markdown_field :note, pipeline: :note, issuable_reference_expansion_enabled: true
@@ -74,6 +74,7 @@ class Note < ApplicationRecord
attr_mentionable :note, pipeline: :note
participant :author
+ belongs_to :namespace
belongs_to :project
belongs_to :noteable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :author, class_name: "User"
@@ -104,6 +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
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
@@ -169,7 +171,7 @@ class Note < ApplicationRecord
end
end
- scope :diff_notes, -> { where(type: %w(LegacyDiffNote DiffNote)) }
+ scope :diff_notes, -> { where(type: %w[LegacyDiffNote DiffNote]) }
scope :new_diff_notes, -> { where(type: 'DiffNote') }
scope :non_diff_notes, -> { where(type: NON_DIFF_NOTE_TYPES) }
@@ -193,7 +195,7 @@ class Note < ApplicationRecord
scope :for_note_or_capitalized_note, ->(text) { where(note: [text, text.capitalize]) }
scope :like_note_or_capitalized_note, ->(text) { where('(note LIKE ? OR note LIKE ?)', text, text.capitalize) }
- before_validation :nullify_blank_type, :nullify_blank_line_code
+ before_validation :ensure_namespace_id, :nullify_blank_type, :nullify_blank_line_code
# Syncs `confidential` with `internal` as we rename the column.
# https://gitlab.com/gitlab-org/gitlab/-/issues/367923
before_create :set_internal_flag
@@ -205,7 +207,7 @@ class Note < ApplicationRecord
after_commit :trigger_note_subscription_create, on: :create
after_commit :trigger_note_subscription_update, on: :update
after_commit :trigger_note_subscription_destroy, on: :destroy
- after_commit :expire_etag_cache, unless: :importing?
+ after_commit :broadcast_noteable_notes_changed, unless: :importing?
def trigger_note_subscription_create
return unless trigger_note_subscription?
@@ -589,8 +591,8 @@ class Note < ApplicationRecord
update_columns(attributes_to_update)
end
- def expire_etag_cache
- noteable&.expire_note_etag_cache
+ def broadcast_noteable_notes_changed
+ noteable&.broadcast_notes_changed
end
def touch(*args, **kwargs)
@@ -825,6 +827,16 @@ class Note < ApplicationRecord
project.repository.keep_around(self.commit_id)
end
+ def ensure_namespace_id
+ return if namespace_id.present? && !noteable_changed? && !project_changed?
+
+ self.namespace_id = if for_project_noteable?
+ project&.project_namespace_id
+ elsif for_personal_snippet?
+ noteable&.author&.namespace&.id
+ end
+ end
+
def nullify_blank_type
self.type = nil if self.type.blank?
end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index cde7b92e74a..eb4fa9ac474 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -60,7 +60,7 @@ class NotificationSetting < ApplicationRecord
end
def self.allowed_fields(source = nil)
- NotificationSetting.email_events(source).dup + %i(level notification_email)
+ NotificationSetting.email_events(source).dup + %i[level notification_email]
end
def email_events
diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb
index 01db0a5cf8b..b93537e0d1e 100644
--- a/app/models/operations/feature_flag.rb
+++ b/app/models/operations/feature_flag.rb
@@ -52,7 +52,7 @@ module Operations
class << self
def preload_relations
- preload(strategies: :scopes)
+ preload(strategies: [:scopes, :user_list])
end
def for_unleash_client(project, environment)
diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb
index 9f2119949fb..893b08d7872 100644
--- a/app/models/organizations/organization.rb
+++ b/app/models/organizations/organization.rb
@@ -10,6 +10,7 @@ module Organizations
has_many :namespaces
has_many :groups
+ has_many :projects
has_one :settings, class_name: "OrganizationSetting"
@@ -38,7 +39,7 @@ module Organizations
end
def user?(user)
- users.exists?(user.id)
+ organization_users.exists?(user: user)
end
private
diff --git a/app/models/packages/debian.rb b/app/models/packages/debian.rb
index 2b8d0a4f51e..1fe4e28146e 100644
--- a/app/models/packages/debian.rb
+++ b/app/models/packages/debian.rb
@@ -4,13 +4,13 @@ module Packages
module Debian
TEMPORARY_PACKAGE_NAME = 'debian-temporary-package'
- DISTRIBUTION_REGEX = %r{[a-z0-9][a-z0-9.-]*}i.freeze
+ DISTRIBUTION_REGEX = %r{[a-z0-9][a-z0-9.-]*}i
COMPONENT_REGEX = DISTRIBUTION_REGEX.freeze
- ARCHITECTURE_REGEX = %r{[a-z0-9][-a-z0-9]*}.freeze
+ ARCHITECTURE_REGEX = %r{[a-z0-9][-a-z0-9]*}
- LETTER_REGEX = %r{(lib)?[a-z0-9]}.freeze
+ LETTER_REGEX = %r{(lib)?[a-z0-9]}
- EMPTY_FILE_SHA256 = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'.freeze
+ EMPTY_FILE_SHA256 = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
INCOMING_PACKAGE_NAME = 'incoming'
diff --git a/app/models/packages/debian/file_entry.rb b/app/models/packages/debian/file_entry.rb
index 7ea0dfe8765..4ac621dcbd4 100644
--- a/app/models/packages/debian/file_entry.rb
+++ b/app/models/packages/debian/file_entry.rb
@@ -6,7 +6,7 @@ module Packages
include ActiveModel::Model
DIGESTS = %i[md5 sha1 sha256].freeze
- FILENAME_REGEX = %r{\A[a-zA-Z0-9][a-zA-Z0-9_.~+-]*\z}.freeze
+ FILENAME_REGEX = %r{\A[a-zA-Z0-9][a-zA-Z0-9_.~+-]*\z}
attr_accessor :filename,
:size,
diff --git a/app/models/packages/dependency_link.rb b/app/models/packages/dependency_link.rb
index 51018602bdc..400b4cce208 100644
--- a/app/models/packages/dependency_link.rb
+++ b/app/models/packages/dependency_link.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
class Packages::DependencyLink < ApplicationRecord
+ include EachBatch
+
belongs_to :package, inverse_of: :dependency_links
belongs_to :dependency, inverse_of: :dependency_links, class_name: 'Packages::Dependency'
has_one :nuget_metadatum, inverse_of: :dependency_link, class_name: 'Packages::Nuget::DependencyLinkMetadatum'
@@ -14,6 +16,32 @@ class Packages::DependencyLink < ApplicationRecord
scope :with_dependency_type, ->(dependency_type) { where(dependency_type: dependency_type) }
scope :includes_dependency, -> { includes(:dependency) }
scope :for_package, ->(package) { where(package_id: package.id) }
+ scope :for_packages, ->(packages) { where(package: packages) }
scope :preload_dependency, -> { preload(:dependency) }
scope :preload_nuget_metadatum, -> { preload(:nuget_metadatum) }
+ scope :select_dependency_id, -> { select(:dependency_id) }
+
+ def self.dependency_ids_grouped_by_type(packages)
+ inner_query = where(package_id: packages)
+ .select('
+ package_id,
+ dependency_type,
+ ARRAY_AGG(dependency_id) as dependency_ids
+ ')
+ .group(:package_id, :dependency_type)
+
+ cte = Gitlab::SQL::CTE.new(:dependency_links_cte, inner_query)
+ cte_alias = cte.table.alias(table_name)
+
+ with(cte.to_arel)
+ .select('
+ package_id,
+ JSON_OBJECT_AGG(
+ dependency_type,
+ dependency_ids
+ ) AS dependency_ids_by_type
+ ')
+ .from(cte_alias)
+ .group(:package_id)
+ end
end
diff --git a/app/models/packages/ml_model/package.rb b/app/models/packages/ml_model/package.rb
new file mode 100644
index 00000000000..de2b5f8f2a8
--- /dev/null
+++ b/app/models/packages/ml_model/package.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Packages
+ module MlModel
+ class Package < Packages::Package
+ has_one :model_version, class_name: "Ml::ModelVersion", inverse_of: :package
+
+ validates :name,
+ format: Gitlab::Regex.ml_model_name_regex,
+ presence: true,
+ length: { maximum: 255 }
+
+ validates :version,
+ format: Gitlab::Regex.semver_regex,
+ presence: true,
+ length: { maximum: 255 }
+ end
+ end
+end
diff --git a/app/models/packages/nuget/metadatum.rb b/app/models/packages/nuget/metadatum.rb
index e7cf4528f16..1025af0fd24 100644
--- a/app/models/packages/nuget/metadatum.rb
+++ b/app/models/packages/nuget/metadatum.rb
@@ -15,8 +15,7 @@ class Packages::Nuget::Metadatum < ApplicationRecord
validates :icon_url, public_url: { allow_blank: true }, length: { maximum: MAX_URL_LENGTH }
validates :authors, presence: true, length: { maximum: MAX_AUTHORS_LENGTH }
validates :description, presence: true, length: { maximum: MAX_DESCRIPTION_LENGTH }
- validates :normalized_version, presence: true,
- if: -> { Feature.enabled?(:nuget_normalized_version, package&.project) }
+ validates :normalized_version, presence: true
validate :ensure_nuget_package_type
diff --git a/app/models/packages/nuget/symbol.rb b/app/models/packages/nuget/symbol.rb
new file mode 100644
index 00000000000..643b5552d84
--- /dev/null
+++ b/app/models/packages/nuget/symbol.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Packages
+ module Nuget
+ class Symbol < ApplicationRecord
+ include FileStoreMounter
+
+ belongs_to :package, -> { where(package_type: :nuget) }, inverse_of: :nuget_symbols
+
+ delegate :project_id, to: :package
+
+ validates :package, :file, :file_path, :signature, :object_storage_key, :size, presence: true
+ validates :signature, uniqueness: { scope: :file_path }
+ validates :object_storage_key, uniqueness: true
+
+ mount_file_store_uploader SymbolUploader
+
+ before_validation :set_object_storage_key, on: :create
+
+ private
+
+ def set_object_storage_key
+ return unless project_id && signature
+
+ self.object_storage_key = Gitlab::HashedPath.new(
+ 'packages', 'nuget', package_id, 'symbols', OpenSSL::Digest::SHA256.hexdigest(signature),
+ root_hash: project_id
+ ).to_s
+ end
+ end
+ end
+end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index b09911f4216..02e3908b3bf 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -7,6 +7,7 @@ class Packages::Package < ApplicationRecord
include Gitlab::Utils::StrongMemoize
include Packages::Installable
include Packages::Downloadable
+ include EnumInheritance
DISPLAYABLE_STATUSES = [:default, :error].freeze
INSTALLABLE_STATUSES = [:default, :hidden].freeze
@@ -48,6 +49,7 @@ class Packages::Package < ApplicationRecord
has_one :pypi_metadatum, inverse_of: :package, class_name: 'Packages::Pypi::Metadatum'
has_one :maven_metadatum, inverse_of: :package, class_name: 'Packages::Maven::Metadatum'
has_one :nuget_metadatum, inverse_of: :package, class_name: 'Packages::Nuget::Metadatum'
+ has_many :nuget_symbols, inverse_of: :package, class_name: 'Packages::Nuget::Symbol'
has_one :composer_metadatum, inverse_of: :package, class_name: 'Packages::Composer::Metadatum'
has_one :rubygems_metadatum, inverse_of: :package, class_name: 'Packages::Rubygems::Metadatum'
has_one :rpm_metadatum, inverse_of: :package, class_name: 'Packages::Rpm::Metadatum'
@@ -179,11 +181,7 @@ class Packages::Package < ApplicationRecord
scope :preload_conan_metadatum, -> { preload(:conan_metadatum) }
scope :with_npm_scope, ->(scope) do
- if Feature.enabled?(:npm_package_registry_fix_group_path_validation)
- npm.where("position('/' in packages_packages.name) > 0 AND split_part(packages_packages.name, '/', 1) = :package_scope", package_scope: "@#{sanitize_sql_like(scope)}")
- else
- npm.where("name ILIKE :package_name", package_name: "@#{sanitize_sql_like(scope)}/%")
- end
+ npm.where("position('/' in packages_packages.name) > 0 AND split_part(packages_packages.name, '/', 1) = :package_scope", package_scope: "@#{sanitize_sql_like(scope)}")
end
scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
@@ -220,6 +218,12 @@ class Packages::Package < ApplicationRecord
joins(:project).reorder(keyset_order)
end
+ def self.inheritance_column = 'package_type'
+
+ def self.inheritance_column_to_class_map = {
+ ml_model: 'Packages::MlModel::Package'
+ }.freeze
+
def self.only_maven_packages_with_path(path, use_cte: false)
if use_cte
# This is an optimization fence which assumes that looking up the Metadatum record by path (globally)
diff --git a/app/models/packages/protection.rb b/app/models/packages/protection.rb
new file mode 100644
index 00000000000..ebaecf89992
--- /dev/null
+++ b/app/models/packages/protection.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Packages
+ module Protection
+ def self.table_name_prefix
+ 'packages_protection_'
+ end
+ end
+end
diff --git a/app/models/packages/protection/rule.rb b/app/models/packages/protection/rule.rb
new file mode 100644
index 00000000000..bb65be92b90
--- /dev/null
+++ b/app/models/packages/protection/rule.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Packages
+ module Protection
+ class Rule < ApplicationRecord
+ enum package_type: Packages::Package.package_types.slice(:npm)
+
+ 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
+ ] }
+ end
+ end
+end
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index 2ffb2e84cbf..e8becc833ca 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -35,7 +35,7 @@ module Pages
{
type: 'zip',
path: deployment.file.url_or_file_path(
- expire_at: ::Gitlab::Pages::CacheControl::DEPLOYMENT_EXPIRATION.from_now
+ expire_at: ::Gitlab::Pages::DEPLOYMENT_EXPIRATION.from_now
),
global_id: global_id,
sha256: deployment.file_sha256,
diff --git a/app/models/pages/virtual_domain.rb b/app/models/pages/virtual_domain.rb
index fafbe449c8c..0a64e91bf60 100644
--- a/app/models/pages/virtual_domain.rb
+++ b/app/models/pages/virtual_domain.rb
@@ -2,9 +2,8 @@
module Pages
class VirtualDomain
- def initialize(projects:, cache: nil, trim_prefix: nil, domain: nil)
+ def initialize(projects:, trim_prefix: nil, domain: nil)
@projects = projects
- @cache = cache
@trim_prefix = trim_prefix
@domain = domain
end
@@ -18,23 +17,19 @@ module Pages
end
def lookup_paths
- paths = projects.map do |project|
- project.pages_lookup_path(trim_prefix: trim_prefix, domain: domain)
- end
-
- # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/328715
- paths = paths.select(&:source)
-
- paths.sort_by(&:prefix).reverse
- end
-
- # cache_key is required by #present_cached in ::API::Internal::Pages
- def cache_key
- @cache_key ||= cache&.cache_key
+ projects
+ .map { |project| lookup_paths_for(project) }
+ .select(&:source) # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/328715
+ .sort_by(&:prefix)
+ .reverse
end
private
- attr_reader :projects, :trim_prefix, :domain, :cache
+ attr_reader :projects, :trim_prefix, :domain
+
+ def lookup_paths_for(project)
+ Pages::LookupPath.new(project, trim_prefix: trim_prefix, domain: domain)
+ end
end
end
diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb
index ec2293fa032..de7b2416258 100644
--- a/app/models/pages_deployment.rb
+++ b/app/models/pages_deployment.rb
@@ -11,13 +11,16 @@ class PagesDeployment < ApplicationRecord
attribute :file_store, :integer, default: -> { ::Pages::DeploymentUploader.default_store }
belongs_to :project, optional: false
+
+ # ci_build is optional, because PagesDeployment must live even if its build/pipeline is removed.
belongs_to :ci_build, class_name: 'Ci::Build', optional: true
- scope :older_than, -> (id) { where('id < ?', id) }
+ 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) }
validates :file, presence: true
validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES }
@@ -32,6 +35,14 @@ class PagesDeployment < ApplicationRecord
skip_callback :save, :after, :store_file!
after_commit :store_file_after_commit!, on: [:create, :update]
+ def self.deactivate_deployments_older_than(deployment, time: nil)
+ now = Time.now.utc
+ active
+ .older_than(deployment.id)
+ .where(project_id: deployment.project_id, path_prefix: deployment.path_prefix)
+ .update_all(updated_at: now, deleted_at: time || now)
+ end
+
def migrated?
file.filename == MIGRATED_FILE_NAME
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 88d7f0f972a..b86bc761cc1 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -9,6 +9,8 @@ class PagesDomain < ApplicationRecord
VERIFICATION_THRESHOLD = 3.days.freeze
SSL_RENEWAL_THRESHOLD = 30.days.freeze
+ MAX_CERTIFICATE_KEY_LENGTH = 8192
+
enum certificate_source: { user_provided: 0, gitlab_provided: 1 }, _prefix: :certificate
enum scope: { instance: 0, group: 1, project: 2 }, _prefix: :scope, _default: :project
enum usage: { pages: 0, serverless: 1 }, _prefix: :usage, _default: :pages
@@ -34,6 +36,7 @@ class PagesDomain < ApplicationRecord
validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? }
validate :validate_intermediates, if: ->(domain) { domain.certificate.present? && domain.certificate_changed? }
validate :validate_custom_domain_count_per_project, on: :create
+ validate :max_certificate_key_length, if: ->(domain) { domain.key.present? }
attribute :auto_ssl_enabled, default: -> { ::Gitlab::LetsEncrypt.enabled? }
attribute :wildcard, default: false
@@ -234,6 +237,16 @@ class PagesDomain < ApplicationRecord
private
+ def max_certificate_key_length
+ return unless pkey.is_a?(OpenSSL::PKey::RSA)
+ return if pkey.to_s.bytesize <= MAX_CERTIFICATE_KEY_LENGTH
+
+ errors.add(
+ :key,
+ s_("PagesDomain|Certificate Key is too long. (Max %d bytes)") % MAX_CERTIFICATE_KEY_LENGTH
+ )
+ end
+
def set_verification_code
return if self.verification_code.present?
diff --git a/app/models/performance_monitoring/prometheus_metric.rb b/app/models/performance_monitoring/prometheus_metric.rb
deleted file mode 100644
index d67b1809d93..00000000000
--- a/app/models/performance_monitoring/prometheus_metric.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-module PerformanceMonitoring
- class PrometheusMetric
- include ActiveModel::Model
-
- attr_accessor :id, :unit, :label, :query, :query_range
-
- validates :unit, presence: true
- validates :query, presence: true, unless: :query_range
- validates :query_range, presence: true, unless: :query
-
- class << self
- def from_json(json_content)
- build_from_hash(json_content).tap(&:validate!)
- end
-
- private
-
- def build_from_hash(attributes)
- return new unless attributes.is_a?(Hash)
-
- new(
- id: attributes['id'],
- unit: attributes['unit'],
- label: attributes['label'],
- query: attributes['query'],
- query_range: attributes['query_range']
- )
- end
- end
- end
-end
diff --git a/app/models/performance_monitoring/prometheus_panel.rb b/app/models/performance_monitoring/prometheus_panel.rb
deleted file mode 100644
index b33c09001ae..00000000000
--- a/app/models/performance_monitoring/prometheus_panel.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-module PerformanceMonitoring
- class PrometheusPanel
- include ActiveModel::Model
-
- attr_accessor :type, :title, :y_label, :weight, :metrics, :y_axis, :max_value
-
- validates :title, presence: true
- validates :metrics, array_members: { member_class: PerformanceMonitoring::PrometheusMetric }
-
- class << self
- def from_json(json_content)
- build_from_hash(json_content).tap(&:validate!)
- end
-
- private
-
- def build_from_hash(attributes)
- return new unless attributes.is_a?(Hash)
-
- new(
- type: attributes['type'],
- title: attributes['title'],
- y_label: attributes['y_label'],
- weight: attributes['weight'],
- metrics: initialize_children_collection(attributes['metrics'])
- )
- end
-
- def initialize_children_collection(children)
- return unless children.is_a?(Array)
-
- children.map { |metrics| PerformanceMonitoring::PrometheusMetric.from_json(metrics) }
- end
- end
-
- def id(group_title)
- Digest::SHA2.hexdigest([group_title, type, title].join)
- end
- end
-end
diff --git a/app/models/performance_monitoring/prometheus_panel_group.rb b/app/models/performance_monitoring/prometheus_panel_group.rb
deleted file mode 100644
index 7f3d2a1b8f4..00000000000
--- a/app/models/performance_monitoring/prometheus_panel_group.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-module PerformanceMonitoring
- class PrometheusPanelGroup
- include ActiveModel::Model
-
- attr_accessor :group, :priority, :panels
-
- validates :group, presence: true
- validates :panels, array_members: { member_class: PerformanceMonitoring::PrometheusPanel }
-
- class << self
- def from_json(json_content)
- build_from_hash(json_content).tap(&:validate!)
- end
-
- private
-
- def build_from_hash(attributes)
- return new unless attributes.is_a?(Hash)
-
- new(
- group: attributes['group'],
- priority: attributes['priority'],
- panels: initialize_children_collection(attributes['panels'])
- )
- end
-
- def initialize_children_collection(children)
- return unless children.is_a?(Array)
-
- children.map { |panels| PerformanceMonitoring::PrometheusPanel.from_json(panels) }
- end
- end
- end
-end
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 08f725de980..4dfe7252a0c 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -14,7 +14,7 @@ class PersonalAccessToken < ApplicationRecord
format_with_prefix: :prefix_from_application_current_settings
# PATs are 20 characters + optional configurable settings prefix (0..20)
- TOKEN_LENGTH_RANGE = (20..40).freeze
+ TOKEN_LENGTH_RANGE = (20..40)
MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS = 365
serialize :scopes, Array # rubocop:disable Cop/ActiveRecordSerialize
diff --git a/app/models/plan.rb b/app/models/plan.rb
index 22c1201421c..9ab22bc045a 100644
--- a/app/models/plan.rb
+++ b/app/models/plan.rb
@@ -5,6 +5,8 @@ class Plan < MainClusterwide::ApplicationRecord
has_one :limits, class_name: 'PlanLimits'
+ scope :by_name, ->(name) { where(name: name) }
+
ALL_PLANS = [DEFAULT].freeze
DEFAULT_PLANS = [DEFAULT].freeze
private_constant :ALL_PLANS, :DEFAULT_PLANS
diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb
index bc3898fafe7..7d043bae91c 100644
--- a/app/models/pool_repository.rb
+++ b/app/models/pool_repository.rb
@@ -8,15 +8,15 @@ class PoolRepository < ApplicationRecord
include AfterCommitQueue
belongs_to :source_project, class_name: 'Project'
- validates :source_project, presence: true
has_many :member_projects, class_name: 'Project'
after_create :set_disk_path
scope :by_source_project, ->(project) { where(source_project: project) }
- scope :by_source_project_and_shard_name, ->(project, shard_name) do
- by_source_project(project)
+ scope :by_disk_path, ->(disk_path) { where(disk_path: disk_path) }
+ scope :by_disk_path_and_shard_name, ->(disk_path, shard_name) do
+ by_disk_path(disk_path)
.for_repository_storage(shard_name)
end
@@ -101,8 +101,8 @@ class PoolRepository < ApplicationRecord
@object_pool ||= Gitlab::Git::ObjectPool.new(
shard.name,
disk_path + '.git',
- source_project.repository.raw,
- source_project.full_path
+ source_project&.repository&.raw,
+ source_project&.full_path
)
end
diff --git a/app/models/project.rb b/app/models/project.rb
index ad8757880fd..68196f0a757 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -44,9 +44,12 @@ class Project < ApplicationRecord
include IssueParent
include UpdatedAtFilterable
include IgnorableColumns
+ include CrossDatabaseIgnoredTables
ignore_column :emails_disabled, remove_with: '16.3', remove_after: '2023-08-22'
+ cross_database_ignore_tables %w[routes redirect_routes], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424277'
+
extend Gitlab::Cache::RequestCache
extend Gitlab::Utils::Override
@@ -68,10 +71,10 @@ class Project < ApplicationRecord
}.freeze
VALID_IMPORT_PORTS = [80, 443].freeze
- VALID_IMPORT_PROTOCOLS = %w(http https git).freeze
+ VALID_IMPORT_PROTOCOLS = %w[http https git].freeze
VALID_MIRROR_PORTS = [22, 80, 443].freeze
- VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze
+ VALID_MIRROR_PROTOCOLS = %w[http https ssh git].freeze
SORTING_PREFERENCE_FIELD = :projects_sort
MAX_BUILD_TIMEOUT = 1.month
@@ -81,6 +84,8 @@ class Project < ApplicationRecord
MAX_SUGGESTIONS_TEMPLATE_LENGTH = 255
MAX_COMMIT_TEMPLATE_LENGTH = 500
+ INSTANCE_RUNNER_RUNNING_JOBS_MAX_BUCKET = 5
+
DEFAULT_MERGE_COMMIT_TEMPLATE = <<~MSG.rstrip.freeze
Merge branch '%{source_branch}' into '%{target_branch}'
@@ -163,6 +168,7 @@ class Project < ApplicationRecord
# Relations
belongs_to :pool_repository
belongs_to :creator, class_name: 'User'
+ belongs_to :organization, class_name: 'Organizations::Organization'
belongs_to :group, -> { where(type: Group.sti_name) }, foreign_key: 'namespace_id'
belongs_to :namespace
# Sync deletion via DB Trigger to ensure we do not have
@@ -265,6 +271,9 @@ class Project < ApplicationRecord
dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :npm_metadata_caches, class_name: 'Packages::Npm::MetadataCache'
has_one :packages_cleanup_policy, class_name: 'Packages::Cleanup::Policy', inverse_of: :project
+ has_many :package_protection_rules,
+ class_name: 'Packages::Protection::Rule',
+ inverse_of: :project
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -273,7 +282,6 @@ class Project < ApplicationRecord
has_one :project_repository, inverse_of: :project
has_one :incident_management_setting, inverse_of: :project, class_name: 'IncidentManagement::ProjectIncidentManagementSetting'
has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting'
- has_one :metrics_setting, inverse_of: :project, class_name: 'ProjectMetricsSetting'
has_one :grafana_integration, inverse_of: :project
has_one :project_setting, inverse_of: :project, autosave: true
has_one :alerting_setting, inverse_of: :project, class_name: 'Alerting::ProjectAlertingSetting'
@@ -336,7 +344,15 @@ class Project < ApplicationRecord
primary_key: :project_namespace_id, foreign_key: :member_namespace_id, inverse_of: :project,
class_name: 'ProjectMember'
- has_many :users, through: :project_members
+ has_many :users, -> { allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/422405") },
+ through: :project_members
+ has_many :maintainers,
+ -> do
+ allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/422405")
+ .where(members: { access_level: Gitlab::Access::MAINTAINER })
+ end,
+ through: :project_members,
+ source: :user
has_many :project_callouts, class_name: 'Users::ProjectCallout', foreign_key: :project_id
@@ -370,8 +386,6 @@ class Project < ApplicationRecord
has_many :prometheus_metrics
has_many :prometheus_alerts, inverse_of: :project
has_many :prometheus_alert_events, inverse_of: :project
- has_many :self_managed_prometheus_alert_events, inverse_of: :project
- has_many :metrics_users_starred_dashboards, class_name: 'Metrics::UsersStarredDashboard', inverse_of: :project
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
@@ -476,7 +490,6 @@ class Project < ApplicationRecord
accepts_nested_attributes_for :incident_management_setting, update_only: true
accepts_nested_attributes_for :error_tracking_setting, update_only: true
- accepts_nested_attributes_for :metrics_setting, update_only: true, allow_destroy: true
accepts_nested_attributes_for :grafana_integration, update_only: true, allow_destroy: true
accepts_nested_attributes_for :prometheus_integration, update_only: true
accepts_nested_attributes_for :alerting_setting, update_only: true
@@ -492,11 +505,6 @@ class Project < ApplicationRecord
delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_owner, :add_role
end
- with_options to: :metrics_setting, allow_nil: true, prefix: true do
- delegate :external_dashboard_url
- delegate :dashboard_timezone
- end
-
with_options to: :namespace do
delegate :actual_limits, :actual_plan_name, :actual_plan, :root_ancestor, allow_nil: true
delegate :maven_package_requests_forwarding, :pypi_package_requests_forwarding, :npm_package_requests_forwarding
@@ -1282,7 +1290,7 @@ class Project < ApplicationRecord
def design_repository
strong_memoize(:design_repository) do
- Gitlab::GlRepository::DESIGN.repository_for(self)
+ find_or_create_design_management_repository.repository
end
end
@@ -1665,7 +1673,7 @@ class Project < ApplicationRecord
return unless Gitlab::Email::IncomingEmail.supports_issue_creation? && author
# check since this can come from a request parameter
- return unless %w(issue merge_request).include?(address_type)
+ return unless %w[issue merge_request].include?(address_type)
author.ensure_incoming_email_token!
@@ -2757,10 +2765,6 @@ class Project < ApplicationRecord
[]
end
- def mark_primary_write_location
- self.class.sticking.mark_primary_write_location(:project, self.id)
- end
-
def toggle_ci_cd_settings!(settings_attribute)
ci_cd_settings.toggle!(settings_attribute)
end
@@ -2842,7 +2846,7 @@ class Project < ApplicationRecord
return if old_pool_repository.blank?
return if pool_repository_shard_matches_repository?(old_pool_repository)
- new_pool_repository = PoolRepository.by_source_project_and_shard_name(old_pool_repository.source_project, repository_storage).take!
+ new_pool_repository = PoolRepository.by_disk_path_and_shard_name(old_pool_repository.disk_path, repository_storage).take!
update!(pool_repository: new_pool_repository)
old_pool_repository.unlink_repository(repository, disconnect: !pending_delete?)
@@ -2871,10 +2875,6 @@ class Project < ApplicationRecord
recipients
end
- def pages_lookup_path(trim_prefix: nil, domain: nil)
- Pages::LookupPath.new(self, trim_prefix: trim_prefix, domain: domain)
- end
-
def closest_setting(name)
setting = read_attribute(name)
setting = closest_namespace_setting(name) if setting.nil?
@@ -2954,10 +2954,6 @@ class Project < ApplicationRecord
jira_imports.last
end
- def metrics_setting
- super || build_metrics_setting
- end
-
def service_desk_enabled
Gitlab::ServiceDesk.enabled?(project: self)
end
@@ -2965,7 +2961,11 @@ class Project < ApplicationRecord
alias_method :service_desk_enabled?, :service_desk_enabled
def service_desk_address
- service_desk_custom_address || service_desk_incoming_address
+ service_desk_custom_address || service_desk_system_address
+ end
+
+ def service_desk_system_address
+ service_desk_alias_address || service_desk_incoming_address
end
def service_desk_incoming_address
@@ -2977,7 +2977,7 @@ class Project < ApplicationRecord
config.address&.gsub(wildcard, "#{full_path_slug}-#{default_service_desk_suffix}")
end
- def service_desk_custom_address
+ def service_desk_alias_address
return unless Gitlab::Email::ServiceDeskEmail.enabled?
key = service_desk_setting&.project_key || default_service_desk_suffix
@@ -2985,6 +2985,13 @@ class Project < ApplicationRecord
Gitlab::Email::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}")
end
+ def service_desk_custom_address
+ return unless Feature.enabled?(:service_desk_custom_email, self)
+ return unless service_desk_setting&.custom_email_enabled?
+
+ service_desk_setting.custom_email
+ end
+
def default_service_desk_suffix
"#{id}-issue-"
end
@@ -3261,6 +3268,10 @@ class Project < ApplicationRecord
group.crm_enabled?
end
+ def supports_lock_on_merge?
+ group&.supports_lock_on_merge? || ::Feature.enabled?(:enforce_locked_labels_on_merge, self, type: :ops)
+ end
+
def path_availability
base, _, host = path.partition('.')
@@ -3270,6 +3281,13 @@ class Project < ApplicationRecord
errors.add(:path, s_('Project|already in use'))
end
+ def instance_runner_running_jobs_count
+ # excluding currently started job
+ ::Ci::RunningBuild.instance_type.where(project_id: self.id)
+ .limit(INSTANCE_RUNNER_RUNNING_JOBS_MAX_BUCKET + 1).count - 1
+ end
+ strong_memoize_attr :instance_runner_running_jobs_count
+
private
# overridden in EE
@@ -3483,11 +3501,11 @@ class Project < ApplicationRecord
end
def sync_project_namespace?
- (changes.keys & %w(name path namespace_id namespace visibility_level shared_runners_enabled)).any? && project_namespace.present?
+ (changes.keys & %w[name path namespace_id namespace visibility_level shared_runners_enabled]).any? && project_namespace.present?
end
def reload_project_namespace_details
- return unless (previous_changes.keys & %w(description description_html cached_markdown_version)).any? && project_namespace.namespace_details.present?
+ return unless (previous_changes.keys & %w[description description_html cached_markdown_version]).any? && project_namespace.namespace_details.present?
project_namespace.namespace_details.reset
end
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
index 99128d3cddf..c328e7d37c8 100644
--- a/app/models/project_authorization.rb
+++ b/app/models/project_authorization.rb
@@ -11,6 +11,11 @@ class ProjectAuthorization < ApplicationRecord
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validates :user, uniqueness: { scope: :project }, presence: true
+ scope :non_guests, -> { where('access_level > ?', ::Gitlab::Access::GUEST) }
+
+ # TODO: To be removed after https://gitlab.com/gitlab-org/gitlab/-/issues/418205
+ before_create :assign_is_unique
+
def self.select_from_union(relations)
from_union(relations)
.select(['project_id', 'MAX(access_level) AS access_level'])
@@ -25,6 +30,12 @@ class ProjectAuthorization < ApplicationRecord
def self.insert_all(attributes)
super(attributes, unique_by: connection.schema_cache.primary_keys(table_name))
end
+
+ private
+
+ def assign_is_unique
+ self.is_unique = true
+ end
end
ProjectAuthorization.prepend_mod_with('ProjectAuthorization')
diff --git a/app/models/project_authorizations/changes.rb b/app/models/project_authorizations/changes.rb
index 1d717950c1c..1f0cec1a50c 100644
--- a/app/models/project_authorizations/changes.rb
+++ b/app/models/project_authorizations/changes.rb
@@ -90,6 +90,8 @@ module ProjectAuthorizations
log_details(entire_size: attributes.size, batch_size: BATCH_SIZE) if add_delay
attributes.each_slice(BATCH_SIZE) do |attributes_batch|
+ attributes_batch.each { |attrs| attrs[:is_unique] = true }
+
ProjectAuthorization.insert_all(attributes_batch)
perform_delay if add_delay
end
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index cc9003423be..8d049b8d1b1 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -19,6 +19,7 @@ class ProjectCiCdSetting < ApplicationRecord
attribute :forward_deployment_enabled, default: true
attribute :separated_caches, default: true
+ validates :merge_trains_skip_train_allowed, inclusion: { in: [true, false] }
chronic_duration_attr :runner_token_expiration_interval_human_readable, :runner_token_expiration_interval
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 92ba02ec777..36f1e09b2ba 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -173,6 +173,10 @@ class ProjectFeature < ApplicationRecord
package_registry_access_level == PUBLIC || project.public?
end
+ def private?(feature)
+ access_level(feature) == PRIVATE
+ end
+
private
def set_pages_access_level
@@ -201,11 +205,11 @@ class ProjectFeature < ApplicationRecord
self.errors.add(field, "cannot have higher visibility level than repository access level") if not_allowed
end
- %i(merge_requests_access_level builds_access_level).each(&validator)
+ %i[merge_requests_access_level builds_access_level].each(&validator)
end
def feature_validation_exclusion
- %i(pages package_registry)
+ %i[pages package_registry]
end
override :resource_member?
diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb
index f16d661d4bb..a7b2c40557a 100644
--- a/app/models/project_import_state.rb
+++ b/app/models/project_import_state.rb
@@ -132,10 +132,17 @@ class ProjectImportState < ApplicationRecord
alias_method :no_import?, :none?
+ # This method is coupled to the repository mirror domain.
+ # Use with caution in the importers domain. As an alternative, use the `#completed?` method.
+ # See EE-override and https://gitlab.com/gitlab-org/gitlab/-/merge_requests/4697
def in_progress?
scheduled? || started?
end
+ def completed?
+ finished? || failed? || canceled?
+ end
+
def started?
# import? does SQL work so only run it if it looks like there's an import running
status == 'started' && project.import?
diff --git a/app/models/project_metrics_setting.rb b/app/models/project_metrics_setting.rb
deleted file mode 100644
index c66d0f52f4c..00000000000
--- a/app/models/project_metrics_setting.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-class ProjectMetricsSetting < ApplicationRecord
- belongs_to :project
-
- validates :external_dashboard_url,
- allow_nil: true,
- length: { maximum: 255 },
- addressable_url: { enforce_sanitization: true, ascii_only: true }
-
- enum dashboard_timezone: { local: 0, utc: 1 }
-
- def dashboard_timezone=(val)
- super(val&.downcase)
- end
-end
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index fec951eb7fe..69d1a9f4aeb 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -3,28 +3,25 @@
class ProjectSetting < ApplicationRecord
include ::Gitlab::Utils::StrongMemoize
include EachBatch
+ include IgnorableColumns
- ALLOWED_TARGET_PLATFORMS = %w(ios osx tvos watchos android).freeze
+ ALLOWED_TARGET_PLATFORMS = %w[ios osx tvos watchos android].freeze
belongs_to :project, inverse_of: :project_setting
scope :for_projects, ->(projects) { where(project_id: projects) }
- attr_encrypted :cube_api_key,
- mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_32,
- algorithm: 'aes-256-gcm',
- encode: false,
- encode_iv: false
+ ignore_columns %i[
+ encrypted_product_analytics_clickhouse_connection_string
+ encrypted_product_analytics_clickhouse_connection_string_iv
+ encrypted_jitsu_administrator_password
+ encrypted_jitsu_administrator_password_iv
+ jitsu_host
+ jitsu_project_xid
+ jitsu_administrator_email
+ ], remove_with: '16.5', remove_after: '2023-09-22'
- attr_encrypted :jitsu_administrator_password,
- mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_32,
- algorithm: 'aes-256-gcm',
- encode: false,
- encode_iv: false
-
- attr_encrypted :product_analytics_clickhouse_connection_string,
+ attr_encrypted :cube_api_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm',
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 3b9b82ee094..34754f4fc95 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -80,6 +80,7 @@ class ProjectTeam
# so we filter out only members of project or project's group
def members_in_project_and_ancestors
members.where(id: member_user_ids)
+ .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/422405')
end
def members_with_access_levels(access_levels = [])
diff --git a/app/models/releases/link.rb b/app/models/releases/link.rb
index 67d765a15c0..e088fe81f6e 100644
--- a/app/models/releases/link.rb
+++ b/app/models/releases/link.rb
@@ -8,10 +8,10 @@ module Releases
# See https://gitlab.com/gitlab-org/gitlab/-/issues/218753
# Regex modified to prevent catastrophic backtracking
- FILEPATH_REGEX = %r{\A\/[^\/](?!.*\/\/.*)[\-\.\w\/]+[\da-zA-Z]+\z}.freeze
+ FILEPATH_REGEX = %r{\A\/[^\/](?!.*\/\/.*)[\-\.\w\/]+[\da-zA-Z]+\z}
FILEPATH_MAX_LENGTH = 128
- validates :url, presence: true, addressable_url: { schemes: %w(http https ftp) }, uniqueness: { scope: :release }
+ validates :url, presence: true, addressable_url: { schemes: %w[http https ftp] }, uniqueness: { scope: :release }
validates :name, presence: true, uniqueness: { scope: :release }
validates :filepath, uniqueness: { scope: :release }, allow_blank: true
validate :filepath_format_valid?
diff --git a/app/models/repository.rb b/app/models/repository.rb
index b8a46f80bc7..1c27a7a64cf 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -47,27 +47,26 @@ class Repository
#
# For example, for entry `:commit_count` there's a method called `commit_count` which
# stores its data in the `commit_count` cache key.
- CACHED_METHODS = %i(size recent_objects_size commit_count readme_path contribution_guide
+ CACHED_METHODS = %i[size recent_objects_size commit_count readme_path contribution_guide
changelog license_blob license_gitaly gitignore
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? root_ref merged_branch_names
has_visible_content? issue_template_names_hash merge_request_template_names_hash
- user_defined_metrics_dashboard_paths xcode_project? has_ambiguous_refs?).freeze
+ xcode_project? has_ambiguous_refs?].freeze
# Certain method caches should be refreshed when certain types of files are
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
# the corresponding methods to call for refreshing caches.
METHOD_CACHES_FOR_FILE_TYPES = {
- readme: %i(readme_path),
+ readme: %i[readme_path],
changelog: :changelog,
- license: %i(license_blob license_gitaly),
+ license: %i[license_blob license_gitaly],
contributing: :contribution_guide,
gitignore: :gitignore,
gitlab_ci: :gitlab_ci_yml,
avatar: :avatar,
issue_template: :issue_template_names_hash,
merge_request_template: :merge_request_template_names_hash,
- metrics_dashboard: :user_defined_metrics_dashboard_paths,
xcode_config: :xcode_project?
}.freeze
@@ -344,13 +343,13 @@ class Repository
end
def expire_tags_cache
- expire_method_caches(%i(tag_names tag_count has_ambiguous_refs?))
+ expire_method_caches(%i[tag_names tag_count has_ambiguous_refs?])
@tags = nil
@tag_names_include = nil
end
def expire_branches_cache
- expire_method_caches(%i(branch_names merged_branch_names branch_count has_visible_content? has_ambiguous_refs?))
+ expire_method_caches(%i[branch_names merged_branch_names branch_count has_visible_content? has_ambiguous_refs?])
expire_protected_branches_cache
@local_branches = nil
@@ -363,7 +362,7 @@ class Repository
end
def expire_statistics_caches
- expire_method_caches(%i(size recent_objects_size commit_count))
+ expire_method_caches(%i[size recent_objects_size commit_count])
end
def expire_all_method_caches
@@ -371,7 +370,7 @@ class Repository
end
def expire_avatar_cache
- expire_method_caches(%i(avatar))
+ expire_method_caches(%i[avatar])
end
# Refreshes the method caches of this repository.
@@ -412,19 +411,19 @@ class Repository
end
def expire_root_ref_cache
- expire_method_caches(%i(root_ref))
+ expire_method_caches(%i[root_ref])
end
# Expires the cache(s) used to determine if a repository is empty or not.
def expire_emptiness_caches
return unless empty?
- expire_method_caches(%i(has_visible_content?))
+ expire_method_caches(%i[has_visible_content?])
raw_repository.expire_has_local_branches_cache
end
def expire_exists_cache
- expire_method_caches(%i(exists?))
+ expire_method_caches(%i[exists?])
end
# expire cache that doesn't depend on repository data (when expiring)
@@ -628,11 +627,6 @@ class Repository
end
cache_method :merge_request_template_names_hash, fallback: {}
- def user_defined_metrics_dashboard_paths
- Gitlab::Metrics::Dashboard::RepoDashboardFinder.list_dashboards(project)
- end
- cache_method :user_defined_metrics_dashboard_paths, fallback: []
-
def readme
head_tree&.readme
end
@@ -1250,6 +1244,8 @@ class Repository
def get_patch_id(old_revision, new_revision)
raw_repository.get_patch_id(old_revision, new_revision)
+ rescue Gitlab::Git::CommandError
+ nil
end
def object_pool
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index 13610d37a74..d5c839724d4 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -13,8 +13,7 @@ class ResourceLabelEvent < ResourceEvent
validates :label, presence: { unless: :importing? }, on: :create
validate :exactly_one_issuable, unless: :importing?
- after_destroy :expire_etag_cache
- after_save :expire_etag_cache
+ after_commit :broadcast_notes_changed, unless: :importing?
enum action: {
add: 1,
@@ -22,7 +21,7 @@ class ResourceLabelEvent < ResourceEvent
}
def self.issuable_attrs
- %i(issue merge_request).freeze
+ %i[issue merge_request].freeze
end
def self.preload_label_subjects(events)
@@ -97,8 +96,8 @@ class ResourceLabelEvent < ResourceEvent
issuable.is_a?(MergeRequest) ? :project_merge_requests_url : :project_issues_url
end
- def expire_etag_cache
- issuable.expire_note_etag_cache
+ def broadcast_notes_changed
+ issuable.broadcast_notes_changed
end
def local_label?
diff --git a/app/models/resource_state_event.rb b/app/models/resource_state_event.rb
index 134f71e35ad..88a86258b0a 100644
--- a/app/models/resource_state_event.rb
+++ b/app/models/resource_state_event.rb
@@ -14,7 +14,7 @@ class ResourceStateEvent < ResourceEvent
after_create :issue_usage_metrics
def self.issuable_attrs
- %i(issue merge_request).freeze
+ %i[issue merge_request].freeze
end
def issuable
diff --git a/app/models/resource_timebox_event.rb b/app/models/resource_timebox_event.rb
index 1cc77501d8d..644ffae5749 100644
--- a/app/models/resource_timebox_event.rb
+++ b/app/models/resource_timebox_event.rb
@@ -16,7 +16,7 @@ class ResourceTimeboxEvent < ResourceEvent
after_create :issue_usage_metrics
def self.issuable_attrs
- %i(issue merge_request).freeze
+ %i[issue merge_request].freeze
end
def issuable
diff --git a/app/models/review.rb b/app/models/review.rb
index d47aaf027ce..98e9a314df7 100644
--- a/app/models/review.rb
+++ b/app/models/review.rb
@@ -31,6 +31,10 @@ class Review < ApplicationRecord
def user_mentions
merge_request.user_mentions.where.not(note_id: nil)
end
+
+ def from_merge_request_author?
+ merge_request.author_id == author_id
+ end
end
Review.prepend_mod
diff --git a/app/models/self_managed_prometheus_alert_event.rb b/app/models/self_managed_prometheus_alert_event.rb
deleted file mode 100644
index cf26563e92d..00000000000
--- a/app/models/self_managed_prometheus_alert_event.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-class SelfManagedPrometheusAlertEvent < ApplicationRecord
- include AlertEventLifecycle
-
- belongs_to :project, validate: true, inverse_of: :self_managed_prometheus_alert_events
- belongs_to :environment, validate: true, inverse_of: :self_managed_prometheus_alert_events
- has_and_belongs_to_many :related_issues, class_name: 'Issue', join_table: :issues_self_managed_prometheus_alert_events # rubocop:disable Rails/HasAndBelongsToMany
-
- validates :started_at, presence: true
- validates :payload_key, uniqueness: { scope: :project_id }
-
- def self.find_or_initialize_by_payload_key(project, payload_key)
- find_or_initialize_by(project: project, payload_key: payload_key) do |event|
- yield event if block_given?
- end
- end
-end
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index f3a0479d3b7..30c53b978f8 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
class SentNotification < ApplicationRecord
+ include IgnorableColumns
+
+ ignore_column %i[id_convert_to_bigint], remove_with: '16.5', remove_after: '2023-09-22'
+
belongs_to :project
belongs_to :noteable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :recipient, class_name: "User"
diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb
index 9139dc22a94..a262802c8af 100644
--- a/app/models/snippet_repository.rb
+++ b/app/models/snippet_repository.rb
@@ -5,7 +5,7 @@ class SnippetRepository < ApplicationRecord
include Shardable
DEFAULT_EMPTY_FILE_NAME = 'snippetfile'
- EMPTY_FILE_PATTERN = /^#{DEFAULT_EMPTY_FILE_NAME}(\d+)\.txt$/.freeze
+ EMPTY_FILE_PATTERN = /^#{DEFAULT_EMPTY_FILE_NAME}(\d+)\.txt$/
CommitError = Class.new(StandardError)
InvalidPathError = Class.new(CommitError)
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index ecd3e27a9c4..7caf3a1040b 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -5,7 +5,7 @@ module Terraform
include UsageStatistics
include AfterCommitQueue
- HEX_REGEXP = %r{\A\h+\z}.freeze
+ HEX_REGEXP = %r{\A\h+\z}
UUID_LENGTH = 32
self.locking_column = :activerecord_lock_version
diff --git a/app/models/user.rb b/app/models/user.rb
index 9f85d41b133..c4e867ab571 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -22,7 +22,6 @@ class User < MainClusterwide::ApplicationRecord
include FromUnion
include BatchDestroyDependentAssociations
include BatchNullifyDependentAssociations
- include HasUniqueInternalUsers
include IgnorableColumns
include UpdateHighestRole
include HasUserType
@@ -31,7 +30,28 @@ class User < MainClusterwide::ApplicationRecord
include RestrictedSignup
include StripAttribute
include EachBatch
- include SafelyChangeColumnDefault
+ include CrossDatabaseIgnoredTables
+ include IgnorableColumns
+
+ ignore_column %i[
+ email_opted_in
+ email_opted_in_ip
+ email_opted_in_source_id
+ email_opted_in_at
+ ], remove_with: '16.6', remove_after: '2023-10-22'
+
+ # `ensure_namespace_correct` needs to be moved to an after_commit (?)
+ cross_database_ignore_tables %w[namespaces namespace_settings], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424279'
+
+ # `notification_settings_for` is called, and elsewhere `save` is then called.
+ cross_database_ignore_tables %w[notification_settings], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424284'
+
+ # Associations with dependent: option
+ cross_database_ignore_tables(
+ %w[namespaces projects project_authorizations issues merge_requests merge_requests issues issues merge_requests],
+ url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424285',
+ on: :destroy
+ )
DEFAULT_NOTIFICATION_LEVEL = :participating
@@ -55,13 +75,11 @@ class User < MainClusterwide::ApplicationRecord
:public_email
].freeze
- FORBIDDEN_SEARCH_STATES = %w(blocked banned ldap_blocked).freeze
+ FORBIDDEN_SEARCH_STATES = %w[blocked banned ldap_blocked].freeze
INCOMING_MAIL_TOKEN_PREFIX = 'glimt-'
FEED_TOKEN_PREFIX = 'glft-'
- columns_changing_default :project_view
-
# lib/tasks/tokens.rake needs to be updated when changing mail and feed tokens
add_authentication_token_field :incoming_email_token, token_generator: -> { self.generate_incoming_mail_token }
add_authentication_token_field :feed_token, format_with_prefix: :prefix_for_feed_token
@@ -262,8 +280,6 @@ class User < MainClusterwide::ApplicationRecord
has_many :organization_users, class_name: 'Organizations::OrganizationUser', inverse_of: :user
has_many :organizations, through: :organization_users, class_name: 'Organizations::Organization', inverse_of: :users
- has_many :metrics_users_starred_dashboards, class_name: 'Metrics::UsersStarredDashboard', inverse_of: :user
-
has_one :status, class_name: 'UserStatus'
has_one :user_preference
has_one :user_detail
@@ -346,7 +362,9 @@ class User < MainClusterwide::ApplicationRecord
email_to_confirm.confirm
end
else
- add_primary_email_to_emails!
+ ignore_cross_database_tables_if_factory_bot(%w[emails]) do
+ add_primary_email_to_emails!
+ end
end
end
after_commit(on: :update) do
@@ -378,6 +396,7 @@ class User < MainClusterwide::ApplicationRecord
:gitpod_enabled, :gitpod_enabled=,
:setup_for_company, :setup_for_company=,
:project_shortcut_buttons, :project_shortcut_buttons=,
+ :keyboard_shortcuts_enabled, :keyboard_shortcuts_enabled=,
:render_whitespace_in_code, :render_whitespace_in_code=,
:markdown_surround_selection, :markdown_surround_selection=,
:markdown_automatic_lists, :markdown_automatic_lists=,
@@ -501,11 +520,19 @@ class User < MainClusterwide::ApplicationRecord
end
after_transition any => :active do |user|
- user.starred_projects.update_counters(star_count: 1)
+ user.class.temporary_ignore_cross_database_tables(
+ %w[projects], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424278'
+ ) do
+ user.starred_projects.update_counters(star_count: 1)
+ end
end
after_transition active: any do |user|
- user.starred_projects.update_counters(star_count: -1)
+ user.class.temporary_ignore_cross_database_tables(
+ %w[projects], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424278'
+ ) do
+ user.starred_projects.update_counters(star_count: -1)
+ end
end
end
@@ -884,92 +911,6 @@ class User < MainClusterwide::ApplicationRecord
}x
end
- # Return (create if necessary) the ghost user. The ghost user
- # owns records previously belonging to deleted users.
- def ghost
- email = 'ghost%s@example.com'
- unique_internal(where(user_type: :ghost), 'ghost', email) do |u|
- u.bio = _('This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.')
- u.name = 'Ghost User'
- end
- end
-
- def alert_bot
- email_pattern = "alert%s@#{Settings.gitlab.host}"
-
- unique_internal(where(user_type: :alert_bot), 'alert-bot', email_pattern) do |u|
- u.bio = 'The GitLab alert bot'
- u.name = 'GitLab Alert Bot'
- u.avatar = bot_avatar(image: 'alert-bot.png')
- end
- end
-
- def migration_bot
- email_pattern = "noreply+gitlab-migration-bot%s@#{Settings.gitlab.host}"
-
- unique_internal(where(user_type: :migration_bot), 'migration-bot', email_pattern) do |u|
- u.bio = 'The GitLab migration bot'
- u.name = 'GitLab Migration Bot'
- u.confirmed_at = Time.zone.now
- end
- end
-
- def security_bot
- email_pattern = "security-bot%s@#{Settings.gitlab.host}"
-
- unique_internal(where(user_type: :security_bot), 'GitLab-Security-Bot', email_pattern) do |u|
- u.bio = 'System bot that monitors detected vulnerabilities for solutions and creates merge requests with the fixes.'
- u.name = 'GitLab Security Bot'
- u.website_url = Gitlab::Routing.url_helpers.help_page_url('user/application_security/security_bot/index.md')
- u.avatar = bot_avatar(image: 'security-bot.png')
- u.confirmed_at = Time.zone.now
- end
- end
-
- def support_bot
- email_pattern = "support%s@#{Settings.gitlab.host}"
-
- unique_internal(where(user_type: :support_bot), 'support-bot', email_pattern) do |u|
- u.bio = 'The GitLab support bot used for Service Desk'
- u.name = 'GitLab Support Bot'
- u.avatar = bot_avatar(image: 'support-bot.png')
- u.confirmed_at = Time.zone.now
- end
- end
-
- def automation_bot
- email_pattern = "automation%s@#{Settings.gitlab.host}"
-
- unique_internal(where(user_type: :automation_bot), 'automation-bot', email_pattern) do |u|
- u.bio = 'The GitLab automation bot used for automated workflows and tasks'
- u.name = 'GitLab Automation Bot'
- u.avatar = bot_avatar(image: 'support-bot.png') # todo: add an avatar for automation-bot
- end
- end
-
- def llm_bot
- email_pattern = "llm-bot%s@#{Settings.gitlab.host}"
-
- unique_internal(where(user_type: :llm_bot), 'GitLab-Llm-Bot', email_pattern) do |u|
- u.bio = 'The Gitlab LLM bot used for fetching LLM-generated content'
- u.name = 'GitLab LLM Bot'
- u.avatar = bot_avatar(image: 'support-bot.png') # todo: add an avatar for llm-bot
- u.confirmed_at = Time.zone.now
- end
- end
-
- def admin_bot
- email_pattern = "admin-bot%s@#{Settings.gitlab.host}"
-
- unique_internal(where(user_type: :admin_bot), 'GitLab-Admin-Bot', email_pattern) do |u|
- u.bio = 'Admin bot used for tasks that require admin privileges'
- u.name = 'GitLab Admin Bot'
- u.avatar = bot_avatar(image: 'admin-bot.png')
- u.admin = true
- u.confirmed_at = Time.zone.now
- end
- end
-
# Return true if there is only single non-internal user in the deployment,
# ghost user is ignored.
def single_user?
@@ -2009,7 +1950,7 @@ class User < MainClusterwide::ApplicationRecord
def access_level=(new_level)
new_level = new_level.to_s
- return unless %w(admin regular).include?(new_level)
+ return unless %w[admin regular].include?(new_level)
self.admin = (new_level == 'admin')
end
@@ -2175,16 +2116,6 @@ class User < MainClusterwide::ApplicationRecord
[last_activity, last_sign_in].compact.max
end
- REQUIRES_ROLE_VALUE = 99
-
- def role_required?
- role_before_type_cast == REQUIRES_ROLE_VALUE
- end
-
- def set_role_required!
- update_column(:role, REQUIRES_ROLE_VALUE)
- end
-
def dismissed_callout?(feature_name:, ignore_dismissal_earlier_than: nil)
callout = callouts_by_feature_name[feature_name]
@@ -2354,7 +2285,7 @@ class User < MainClusterwide::ApplicationRecord
def ban_and_report
msg = 'Potential spammer account deletion'
- attrs = { user_id: id, reporter: User.security_bot, category: 'spam' }
+ attrs = { user_id: id, reporter: Users::Internal.security_bot, category: 'spam' }
abuse_report = AbuseReport.find_by(attrs)
if abuse_report.nil?
@@ -2519,7 +2450,7 @@ class User < MainClusterwide::ApplicationRecord
def update_highest_role?
return false unless persisted?
- (previous_changes.keys & %w(state user_type)).any?
+ (previous_changes.keys & %w[state user_type]).any?
end
def update_highest_role_attribute
diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb
index 425f2cc062b..15d50071bf6 100644
--- a/app/models/user_custom_attribute.rb
+++ b/app/models/user_custom_attribute.rb
@@ -15,6 +15,7 @@ class UserCustomAttribute < ApplicationRecord
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'
IDENTITY_VERIFICATION_PHONE_EXEMPT = 'identity_verification_phone_exempt'
@@ -45,6 +46,14 @@ class UserCustomAttribute < ApplicationRecord
upsert_custom_attributes([custom_attribute])
end
+ def set_banned_by_spam_log(spam_log)
+ 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
+
private
def blocked_users
diff --git a/app/models/user_interacted_project.rb b/app/models/user_interacted_project.rb
index 1c7515894fe..73bca362960 100644
--- a/app/models/user_interacted_project.rb
+++ b/app/models/user_interacted_project.rb
@@ -24,7 +24,7 @@ class UserInteractedProject < ApplicationRecord
}
cached_exists?(**attributes) do
- where(attributes).exists? || UserInteractedProject.insert_all([attributes], unique_by: %w(project_id user_id))
+ where(attributes).exists? || UserInteractedProject.insert_all([attributes], unique_by: %w[project_id user_id])
true
end
end
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index eac66905d0c..8fc9f4617d0 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -35,6 +35,7 @@ class UserPreference < MainClusterwide::ApplicationRecord
attribute :time_display_relative, default: true
attribute :render_whitespace_in_code, default: false
attribute :project_shortcut_buttons, default: true
+ attribute :keyboard_shortcuts_enabled, default: true
enum visibility_pipeline_id_type: { id: 0, iid: 1 }
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index 0d3262b2474..def0765560e 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -62,7 +62,7 @@ module Users
project_quality_summary_feedback: 59, # EE-only
merge_request_settings_moved_callout: 60,
new_top_level_group_alert: 61,
- artifacts_management_page_feedback_banner: 62,
+ # 62, removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131314
# 63 and 64 were removed with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120233
branch_rules_info_callout: 65,
create_runner_workflow_banner: 66,
@@ -71,9 +71,11 @@ module Users
project_repository_limit_alert_alert_threshold: 69, # EE-only
project_repository_limit_alert_error_threshold: 70, # EE-only
new_navigation_callout: 71,
- code_suggestions_third_party_callout: 72, # EE-only
+ # 72 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129022
namespace_over_storage_users_combined_alert: 73, # EE-only
- rich_text_editor: 74
+ rich_text_editor: 74,
+ vsd_feedback_banner: 75, # EE-only
+ security_policy_protected_branch_modification: 76 # EE-only
}
validates :feature_name,
diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb
index 1b0fd8682db..086943884a5 100644
--- a/app/models/users/credit_card_validation.rb
+++ b/app/models/users/credit_card_validation.rb
@@ -16,6 +16,12 @@ module Users
greater_than_or_equal_to: 0, less_than_or_equal_to: 9999
}
+ validates :last_digits_hash, length: { maximum: 44 }
+ validates :holder_name_hash, length: { maximum: 44 }
+ validates :expiration_date_hash, length: { maximum: 44 }
+ validates :network_hash, length: { maximum: 44 }
+
+ 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?
@@ -32,6 +38,11 @@ module Users
)
end
+ before_save :set_last_digits_hash, if: -> { last_digits.present? }
+ before_save :set_holder_name_hash, if: -> { holder_name.present? }
+ before_save :set_network_hash, if: -> { network.present? }
+ before_save :set_expiration_date_hash, if: -> { expiration_date.present? }
+
def similar_records
self.class.similar_to(self).order(credit_card_validated_at: :desc).includes(:user)
end
@@ -43,5 +54,21 @@ module Users
def used_by_banned_user?
self.class.by_banned_user.similar_to(self).similar_by_holder_name(holder_name).exists?
end
+
+ def set_last_digits_hash
+ self.last_digits_hash = Gitlab::CryptoHelper.sha256(last_digits)
+ end
+
+ def set_holder_name_hash
+ self.holder_name_hash = Gitlab::CryptoHelper.sha256(holder_name.downcase)
+ end
+
+ def set_network_hash
+ self.network_hash = Gitlab::CryptoHelper.sha256(network.downcase)
+ end
+
+ def set_expiration_date_hash
+ self.expiration_date_hash = Gitlab::CryptoHelper.sha256(expiration_date.to_s)
+ end
end
end
diff --git a/app/models/users/group_visit.rb b/app/models/users/group_visit.rb
new file mode 100644
index 00000000000..0bcfda049fc
--- /dev/null
+++ b/app/models/users/group_visit.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Users
+ class GroupVisit < ApplicationRecord
+ include Users::Visitable
+ include PartitionedTable
+
+ self.table_name = "groups_visits"
+ self.primary_key = :id
+
+ partitioned_by :visited_at, strategy: :monthly, retain_for: 3.months
+
+ validates :entity_id, presence: true
+ validates :user_id, presence: true
+ validates :visited_at, presence: true
+ end
+end
diff --git a/app/models/users/project_callout.rb b/app/models/users/project_callout.rb
index 3964f202be6..6affe5b5030 100644
--- a/app/models/users/project_callout.rb
+++ b/app/models/users/project_callout.rb
@@ -11,10 +11,10 @@ module Users
enum feature_name: {
awaiting_members_banner: 1, # EE-only
web_hook_disabled: 2,
- ultimate_feature_removal_banner: 3,
+ # 3 was removed https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129703,
+ # and cleaned up https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129924, it can be replaced
namespace_storage_pre_enforcement_banner: 4, # EE-only
- # 5,6,7 were unused and removed with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118330,
- # they can be replaced.
+ # 5,6,7 were removed https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118330, they can be replaced
license_check_deprecation_alert: 8 # EE-only
}
diff --git a/app/models/users/project_visit.rb b/app/models/users/project_visit.rb
new file mode 100644
index 00000000000..1d076e0be56
--- /dev/null
+++ b/app/models/users/project_visit.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Users
+ class ProjectVisit < ApplicationRecord
+ include Users::Visitable
+ include PartitionedTable
+
+ self.table_name = "projects_visits"
+ self.primary_key = :id
+
+ partitioned_by :visited_at, strategy: :monthly, retain_for: 3.months
+
+ validates :entity_id, presence: true
+ validates :user_id, presence: true
+ validates :visited_at, presence: true
+ end
+end
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 73156b2f040..62b837eeeb6 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -4,7 +4,8 @@ class WorkItem < Issue
include Gitlab::Utils::StrongMemoize
COMMON_QUICK_ACTIONS_COMMANDS = [
- :title, :reopen, :close, :cc, :tableflip, :shrug, :type, :promote_to
+ :title, :reopen, :close, :cc, :tableflip, :shrug, :type, :promote_to, :checkin_reminder,
+ :subscribe, :unsubscribe, :confidential, :award
].freeze
self.table_name = 'issues'
@@ -146,6 +147,18 @@ class WorkItem < Issue
{ common: common_params, widgets: widget_params }
end
+ def linked_work_items(current_user = nil, authorize: true, preload: nil, link_type: nil)
+ linked_work_items = linked_work_items_query(link_type).preload(preload).reorder('issue_link_id')
+ return linked_work_items unless authorize
+
+ cross_project_filter = ->(work_items) { work_items.where(project: project) }
+ Ability.work_items_readable_by_user(
+ linked_work_items,
+ current_user,
+ filters: { read_cross_project: cross_project_filter }
+ )
+ end
+
private
override :parent_link_confidentiality
@@ -241,6 +254,21 @@ class WorkItem < Issue
errors.add(:work_item_type_id, _('reached maximum depth'))
end
end
+
+ def linked_work_items_query(link_type)
+ type_condition =
+ if link_type == WorkItems::RelatedWorkItemLink::TYPE_RELATES_TO
+ " AND issue_links.link_type = #{WorkItems::RelatedWorkItemLink.link_types[link_type]}"
+ else
+ ""
+ end
+
+ linked_issues_select
+ .joins("INNER JOIN issue_links ON
+ (issue_links.source_id = issues.id AND issue_links.target_id = #{id}#{type_condition})
+ OR
+ (issue_links.target_id = issues.id AND issue_links.source_id = #{id}#{type_condition})")
+ end
end
WorkItem.prepend_mod
diff --git a/app/models/work_items/parent_link.rb b/app/models/work_items/parent_link.rb
index d9e3690b6fc..ea7755b03b4 100644
--- a/app/models/work_items/parent_link.rb
+++ b/app/models/work_items/parent_link.rb
@@ -7,7 +7,6 @@ module WorkItems
self.table_name = 'work_item_parent_links'
MAX_CHILDREN = 100
- PARENT_TYPES = [:issue, :incident].freeze
belongs_to :work_item
belongs_to :work_item_parent, class_name: 'WorkItem'
@@ -122,3 +121,5 @@ module WorkItems
end
end
end
+
+WorkItems::ParentLink.prepend_mod
diff --git a/app/models/work_items/related_work_item_link.rb b/app/models/work_items/related_work_item_link.rb
index 4de197d3d35..a911ef5f05d 100644
--- a/app/models/work_items/related_work_item_link.rb
+++ b/app/models/work_items/related_work_item_link.rb
@@ -6,9 +6,13 @@ module WorkItems
self.table_name = 'issue_links'
+ MAX_LINKS_COUNT = 100
+
belongs_to :source, class_name: 'WorkItem'
belongs_to :target, class_name: 'WorkItem'
+ validate :validate_max_number_of_links, on: :create
+
class << self
extend ::Gitlab::Utils::Override
@@ -23,5 +27,15 @@ module WorkItems
'work item'
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
+
+ return unless target && target.linked_work_items(authorize: false).size >= MAX_LINKS_COUNT
+
+ errors.add :target, s_('WorkItems|This work item would exceed the maximum number of linked items.')
+ end
end
end
diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb
index 369ffc660aa..b7ceeecbc7f 100644
--- a/app/models/work_items/type.rb
+++ b/app/models/work_items/type.rb
@@ -44,8 +44,6 @@ module WorkItems
# where it's possible to switch between issue and incident.
CHANGEABLE_BASE_TYPES = %w[issue incident test_case].freeze
- WI_TYPES_WITH_CREATED_HEADER = %w[issue incident ticket].freeze
-
cache_markdown_field :description, pipeline: :single_line
enum base_type: BASE_TYPES.transform_values { |value| value[:enum_value] }
diff --git a/app/models/work_items/widgets/linked_items.rb b/app/models/work_items/widgets/linked_items.rb
index 06a0f6db964..b405555c038 100644
--- a/app/models/work_items/widgets/linked_items.rb
+++ b/app/models/work_items/widgets/linked_items.rb
@@ -3,7 +3,7 @@
module WorkItems
module Widgets
class LinkedItems < Base
- delegate :related_issues, to: :work_item
+ delegate :linked_work_items, to: :work_item
end
end
end
diff --git a/app/models/x509_certificate.rb b/app/models/x509_certificate.rb
index 7c2581b8bb2..90f3bd69c47 100644
--- a/app/models/x509_certificate.rb
+++ b/app/models/x509_certificate.rb
@@ -17,8 +17,6 @@ class X509Certificate < ApplicationRecord
# rfc 5280 - 4.2.1.2 Subject Key Identifier
validates :subject_key_identifier, presence: true, format: { with: Gitlab::Regex.x509_subject_key_identifier_regex }
- # rfc 5280 - 4.1.2.6 Subject
- validates :subject, presence: true
# rfc 5280 - 4.1.2.6 Subject (subjectAltName contains the email address)
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
# rfc 5280 - 4.1.2.2 Serial number
diff --git a/app/models/x509_issuer.rb b/app/models/x509_issuer.rb
index 81491d8e507..769d56a9838 100644
--- a/app/models/x509_issuer.rb
+++ b/app/models/x509_issuer.rb
@@ -6,13 +6,16 @@ class X509Issuer < ApplicationRecord
# rfc 5280 - 4.2.1.1 Authority Key Identifier
validates :subject_key_identifier, presence: true, format: { with: Gitlab::Regex.x509_subject_key_identifier_regex }
# rfc 5280 - 4.1.2.4 Issuer
- validates :subject, presence: true
# rfc 5280 - 4.2.1.13 CRL Distribution Points
# cRLDistributionPoints extension using URI:http
- validates :crl_url, presence: true, public_url: true
+ validates :crl_url, allow_nil: true, public_url: true
def self.safe_create!(attributes)
create_with(attributes)
.safe_find_or_create_by!(subject_key_identifier: attributes[:subject_key_identifier])
end
+
+ def self.with_crl_url
+ where.not(crl_url: nil)
+ end
end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index d6aaa3e983d..1ec2495a661 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -57,6 +57,12 @@ class BasePolicy < DeclarativePolicy::Base
with_options scope: :user, score: 0
condition(:can_create_group) { @user&.can_create_group }
+ # TODO: update to check application setting
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/423302
+ desc 'User can create an organization'
+ with_options scope: :user, score: 0
+ condition(:can_create_organization) { true }
+
desc "The application is restricted from public visibility"
condition(:restricted_public_level, scope: :global) do
Gitlab::CurrentSettings.current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
diff --git a/app/policies/ci/bridge_policy.rb b/app/policies/ci/bridge_policy.rb
index 5f9e8eab08a..9cf3a017b39 100644
--- a/app/policies/ci/bridge_policy.rb
+++ b/app/policies/ci/bridge_policy.rb
@@ -5,8 +5,12 @@ module Ci
include Ci::DeployablePolicy
condition(:can_update_downstream_branch) do
- ::Gitlab::UserAccess.new(@user, container: @subject.downstream_project)
- .can_update_branch?(@subject.target_revision_ref)
+ # `bridge.downstream_project` could be `nil` if the downstream project was removed after the pipeline creation,
+ # which raises an error in `UserAccess` class because `container` arg must be present.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/424145 for more information.
+ @subject.downstream_project.present? &&
+ ::Gitlab::UserAccess.new(@user, container: @subject.downstream_project)
+ .can_update_branch?(@subject.target_revision_ref)
end
rule { can_update_downstream_branch }.enable :play_job
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index 4d21da0226b..1d60b1e79de 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -18,6 +18,10 @@ module Ci
@subject.triggered_by?(@user)
end
+ condition(:project_allows_read_dependency) do
+ can?(:read_dependency, @subject.project)
+ end
+
# Disallow users without permissions from accessing internal pipelines
rule { ~can?(:read_build) & ~external_pipeline }.policy do
prevent :read_pipeline
@@ -41,6 +45,10 @@ module Ci
enable :read_pipeline_variable
end
+ rule { project_allows_read_dependency }.policy do
+ enable :read_dependency
+ end
+
def ref_protected?(user, project, tag, ref)
access = ::Gitlab::UserAccess.new(user, container: project)
diff --git a/app/policies/design_management/repository_policy.rb b/app/policies/design_management/repository_policy.rb
new file mode 100644
index 00000000000..404f363a03a
--- /dev/null
+++ b/app/policies/design_management/repository_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ class RepositoryPolicy < ::BasePolicy
+ delegate { @subject.project }
+ end
+end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index bf7bfe36254..7594360a91c 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -29,6 +29,7 @@ class GlobalPolicy < BasePolicy
prevent :receive_notifications
prevent :use_quick_actions
prevent :create_group
+ prevent :create_organization
prevent :execute_graphql_mutation
end
@@ -93,6 +94,10 @@ class GlobalPolicy < BasePolicy
enable :create_group
end
+ rule { can_create_organization }.policy do
+ enable :create_organization
+ end
+
rule { can?(:create_group) }.policy do
enable :create_group_with_default_branch_protection
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index c50f74f2b35..faa83019bda 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -217,6 +217,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :update_cluster
enable :admin_cluster
enable :read_deploy_token
+ enable :read_group_runners
enable :create_jira_connect_subscription
enable :maintainer_access
enable :read_upload
diff --git a/app/policies/organizations/organization_policy.rb b/app/policies/organizations/organization_policy.rb
index 1c0d996c7d4..401aa45786d 100644
--- a/app/policies/organizations/organization_policy.rb
+++ b/app/policies/organizations/organization_policy.rb
@@ -14,10 +14,12 @@ module Organizations
rule { admin }.policy do
enable :admin_organization
enable :read_organization
+ enable :read_organization_user
end
rule { organization_user }.policy do
enable :read_organization
+ enable :read_organization_user
end
end
end
diff --git a/app/policies/organizations/organization_user_policy.rb b/app/policies/organizations/organization_user_policy.rb
new file mode 100644
index 00000000000..91d542378c8
--- /dev/null
+++ b/app/policies/organizations/organization_user_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Organizations
+ class OrganizationUserPolicy < BasePolicy
+ delegate :organization
+ end
+end
diff --git a/app/policies/project_member_policy.rb b/app/policies/project_member_policy.rb
index ace74dca448..9d6a8c22e6d 100644
--- a/app/policies/project_member_policy.rb
+++ b/app/policies/project_member_policy.rb
@@ -37,3 +37,5 @@ class ProjectMemberPolicy < BasePolicy
enable :withdraw_member_access_request
end
end
+
+ProjectMemberPolicy.prepend_mod_with('ProjectMemberPolicy')
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 25495bb0221..38e6360f81d 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -862,7 +862,11 @@ class ProjectPolicy < BasePolicy
enable :set_pipeline_variables
end
- rule { ~security_and_compliance_disabled & can?(:developer_access) }.policy do
+ rule { security_and_compliance_disabled }.policy do
+ prevent :access_security_and_compliance
+ end
+
+ rule { can?(:developer_access) }.policy do
enable :access_security_and_compliance
end
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index bc12d210334..6f32f4de62c 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -76,7 +76,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
end
def pipeline_editor_path
- project_ci_pipeline_editor_path(project, branch_name: blob.commit_id) if can_collaborate_with_project?(project) && blob.path == project.ci_config_path_or_default
+ project_ci_pipeline_editor_path(project, branch_name: commit_id) if can_collaborate_with_project?(project) && blob.path == project.ci_config_path_or_default
end
def gitpod_blob_url
diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb
index 79c1946f3d2..838196e96ac 100644
--- a/app/presenters/ci/build_runner_presenter.rb
+++ b/app/presenters/ci/build_runner_presenter.rb
@@ -61,6 +61,16 @@ module Ci
end
# rubocop: enable CodeReuse/ActiveRecord
+ def project_jobs_running_on_instance_runners_count
+ # if not instance runner we don't care about that value and present `+Inf` as a placeholder for Prometheus
+ return '+Inf' unless runner.instance_type?
+
+ return project.instance_runner_running_jobs_count.to_s if
+ project.instance_runner_running_jobs_count < Project::INSTANCE_RUNNER_RUNNING_JOBS_MAX_BUCKET
+
+ "#{Project::INSTANCE_RUNNER_RUNNING_JOBS_MAX_BUCKET}+"
+ end
+
private
def create_archive(artifacts)
diff --git a/app/presenters/dev_ops_report/metric_presenter.rb b/app/presenters/dev_ops_report/metric_presenter.rb
index 1a5b12fa408..75dbbb63f76 100644
--- a/app/presenters/dev_ops_report/metric_presenter.rb
+++ b/app/presenters/dev_ops_report/metric_presenter.rb
@@ -93,52 +93,52 @@ module DevOpsReport
IdeaToProductionStep.new(
metric: metric,
title: 'Idea',
- features: %w(issues)
+ features: %w[issues]
),
IdeaToProductionStep.new(
metric: metric,
title: 'Issue',
- features: %w(issues notes)
+ features: %w[issues notes]
),
IdeaToProductionStep.new(
metric: metric,
title: 'Plan',
- features: %w(milestones boards)
+ features: %w[milestones boards]
),
IdeaToProductionStep.new(
metric: metric,
title: 'Code',
- features: %w(merge_requests)
+ features: %w[merge_requests]
),
IdeaToProductionStep.new(
metric: metric,
title: 'Commit',
- features: %w(merge_requests)
+ features: %w[merge_requests]
),
IdeaToProductionStep.new(
metric: metric,
title: 'Test',
- features: %w(ci_pipelines)
+ features: %w[ci_pipelines]
),
IdeaToProductionStep.new(
metric: metric,
title: 'Review',
- features: %w(ci_pipelines environments)
+ features: %w[ci_pipelines environments]
),
IdeaToProductionStep.new(
metric: metric,
title: 'Staging',
- features: %w(environments deployments)
+ features: %w[environments deployments]
),
IdeaToProductionStep.new(
metric: metric,
title: 'Production',
- features: %w(deployments)
+ features: %w[deployments]
),
IdeaToProductionStep.new(
metric: metric,
title: 'Feedback',
- features: %w(projects_prometheus_active service_desk_issues)
+ features: %w[projects_prometheus_active service_desk_issues]
)
]
end
diff --git a/app/presenters/event_presenter.rb b/app/presenters/event_presenter.rb
index a098db7fbbc..8f7b2d5868e 100644
--- a/app/presenters/event_presenter.rb
+++ b/app/presenters/event_presenter.rb
@@ -54,4 +54,29 @@ class EventPresenter < Gitlab::View::Presenter::Delegated
target.noteable_type.titleize
end.downcase
end
+
+ def push_activity_description
+ return unless push_action?
+
+ if batch_push?
+ "#{action_name} #{ref_count} #{ref_type.pluralize(ref_count)}"
+ else
+ "#{action_name} #{ref_type}"
+ end
+ end
+
+ def batch_push?
+ push_action? && ref_count.to_i > 0
+ end
+
+ def linked_to_reference?
+ return false unless push_action?
+ return false if event.project.nil?
+
+ if tag?
+ project.repository.tag_exists?(ref_name)
+ else
+ project.repository.branch_exists?(ref_name)
+ end
+ end
end
diff --git a/app/presenters/gitlab/blame_presenter.rb b/app/presenters/gitlab/blame_presenter.rb
index 6230e61d2be..0a2738bea5f 100644
--- a/app/presenters/gitlab/blame_presenter.rb
+++ b/app/presenters/gitlab/blame_presenter.rb
@@ -40,6 +40,10 @@ module Gitlab
@commits[commit.id] ||= get_commit_data(commit, previous_path)
end
+ def groups_commit_data
+ groups.each { |group| group[:commit_data] = commit_data(group[:commit]) }
+ end
+
private
# Huge source files with a high churn rate (e.g. 'locale/gitlab.pot') could have
@@ -86,5 +90,17 @@ module Gitlab
def mail_to(*args, &block)
ActionController::Base.helpers.mail_to(*args, &block)
end
+
+ def project
+ return super.project if defined?(super.project)
+
+ blame.commit.repository.project
+ end
+
+ def page
+ return super.page if defined?(super.page)
+
+ nil
+ end
end
end
diff --git a/app/presenters/issue_presenter.rb b/app/presenters/issue_presenter.rb
index 42ecbc9988e..9403dd0814b 100644
--- a/app/presenters/issue_presenter.rb
+++ b/app/presenters/issue_presenter.rb
@@ -28,6 +28,9 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
Gitlab::Utils::Email.obfuscated_email(super, deform: true)
end
+ delegator_override :external_author
+ alias_method :external_author, :service_desk_reply_to
+
delegator_override :issue_email_participants
def issue_email_participants
issue.issue_email_participants.present(current_user: current_user)
diff --git a/app/presenters/projects/security/configuration_presenter.rb b/app/presenters/projects/security/configuration_presenter.rb
index 8a6569e7bf3..f248652befc 100644
--- a/app/presenters/projects/security/configuration_presenter.rb
+++ b/app/presenters/projects/security/configuration_presenter.rb
@@ -25,7 +25,8 @@ module Projects
auto_fix_enabled: autofix_enabled,
can_toggle_auto_fix_settings: can_toggle_autofix,
auto_fix_user_path: auto_fix_user_path,
- security_training_enabled: project.security_training_available?
+ security_training_enabled: project.security_training_available?,
+ continuous_vulnerability_scans_enabled: continuous_vulnerability_scans_enabled
}
end
@@ -83,7 +84,8 @@ module Projects
configuration_path: scan.configuration_path,
available: scan.available?,
can_enable_by_merge_request: scan.can_enable_by_merge_request?,
- meta_info_path: scan.meta_info_path
+ meta_info_path: scan.meta_info_path,
+ on_demand_available: scan.on_demand_available?
}
end
@@ -94,6 +96,8 @@ module Projects
def project_settings
project.security_setting
end
+
+ def continuous_vulnerability_scans_enabled; end
end
end
end
diff --git a/app/presenters/search_service_presenter.rb b/app/presenters/search_service_presenter.rb
index 91e67c379c4..d79736a6e52 100644
--- a/app/presenters/search_service_presenter.rb
+++ b/app/presenters/search_service_presenter.rb
@@ -17,7 +17,7 @@ class SearchServicePresenter < Gitlab::View::Presenter::Delegated
blobs: :with_web_entity_associations
}.freeze
- SORT_ENABLED_SCOPES = %w(issues merge_requests epics).freeze
+ SORT_ENABLED_SCOPES = %w[issues merge_requests epics].freeze
delegator_override :search_objects
def search_objects
diff --git a/app/serializers/activity_pub/activity_streams_serializer.rb b/app/serializers/activity_pub/activity_streams_serializer.rb
new file mode 100644
index 00000000000..39caa4a6d10
--- /dev/null
+++ b/app/serializers/activity_pub/activity_streams_serializer.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ class ActivityStreamsSerializer < ::BaseSerializer
+ MissingIdentifierError = Class.new(StandardError)
+ MissingTypeError = Class.new(StandardError)
+ MissingOutboxError = Class.new(StandardError)
+
+ alias_method :base_represent, :represent
+
+ def represent(resource, opts = {}, entity_class = nil)
+ response = if respond_to?(:paginated?) && paginated?
+ represent_paginated(resource, opts, entity_class)
+ else
+ represent_whole(resource, opts, entity_class)
+ end
+
+ validate_response(HashWithIndifferentAccess.new(response))
+ end
+
+ private
+
+ def validate_response(response)
+ unless response[:id].present?
+ raise MissingIdentifierError, "The serializer does not provide the mandatory 'id' field."
+ end
+
+ unless response[:type].present?
+ raise MissingTypeError, "The serializer does not provide the mandatory 'type' field."
+ end
+
+ response
+ end
+
+ def represent_whole(resource, opts, entity_class)
+ raise MissingOutboxError, 'Please provide an :outbox option for this actor' unless opts[:outbox].present?
+
+ serialized = base_represent(resource, opts, entity_class)
+
+ {
+ :@context => "https://www.w3.org/ns/activitystreams",
+ inbox: opts[:inbox],
+ outbox: opts[:outbox]
+ }.merge(serialized)
+ end
+
+ def represent_paginated(resources, opts, entity_class)
+ if paginator.params['page'].present?
+ represent_page(resources, resources.current_page, opts, entity_class)
+ else
+ represent_pagination_index(resources)
+ end
+ end
+
+ def represent_page(resources, page, opts, entity_class)
+ opts[:page] = page
+ serialized = base_represent(resources, opts, entity_class)
+
+ {
+ :@context => 'https://www.w3.org/ns/activitystreams',
+ type: 'OrderedCollectionPage',
+ id: collection_url(page),
+ prev: page > 1 ? collection_url(page - 1) : nil,
+ next: page < resources.total_pages ? collection_url(page + 1) : nil,
+ partOf: collection_url,
+ orderedItems: serialized
+ }
+ end
+
+ def represent_pagination_index(resources)
+ {
+ :@context => 'https://www.w3.org/ns/activitystreams',
+ type: 'OrderedCollection',
+ id: collection_url,
+ totalItems: resources.total_count,
+ first: collection_url(1),
+ last: collection_url(resources.total_pages)
+ }
+ end
+
+ def collection_url(page = nil)
+ uri = URI.parse(paginator.request.url)
+ uri.query ||= ""
+ parts = uri.query.split('&').reject { |part| part =~ /^page=/ }
+ parts << "page=#{page}" if page
+ uri.query = parts.join('&')
+ uri.to_s.sub(/\?$/, '')
+ end
+ end
+end
diff --git a/app/serializers/activity_pub/project_entity.rb b/app/serializers/activity_pub/project_entity.rb
new file mode 100644
index 00000000000..02ed0cdc047
--- /dev/null
+++ b/app/serializers/activity_pub/project_entity.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ class ProjectEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id do |project|
+ project_url(project)
+ end
+
+ expose :type do |*|
+ "Application"
+ end
+
+ expose :name
+
+ expose :description, as: :summary
+
+ expose :url do |project|
+ project_url(project)
+ end
+ end
+end
diff --git a/app/serializers/activity_pub/release_entity.rb b/app/serializers/activity_pub/release_entity.rb
new file mode 100644
index 00000000000..9e3e5397034
--- /dev/null
+++ b/app/serializers/activity_pub/release_entity.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ class ReleaseEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id do |release, opts|
+ "#{opts[:url]}##{release.tag}"
+ end
+
+ expose :type do |*|
+ "Create"
+ end
+
+ expose :to do |*|
+ 'https://www.w3.org/ns/activitystreams#Public'
+ end
+
+ expose :author, as: :actor, using: UserEntity
+
+ expose :object do
+ expose :id do |release|
+ project_release_url(release.project, release)
+ end
+
+ expose :type do |*|
+ "Application"
+ end
+
+ expose :name
+
+ expose :url do |release|
+ project_release_url(release.project, release)
+ end
+
+ expose :description, as: :content
+ expose :project, as: :context, using: ProjectEntity
+ end
+ end
+end
diff --git a/app/serializers/activity_pub/releases_actor_entity.rb b/app/serializers/activity_pub/releases_actor_entity.rb
new file mode 100644
index 00000000000..c52741c73a5
--- /dev/null
+++ b/app/serializers/activity_pub/releases_actor_entity.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ class ReleasesActorEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id do |project|
+ project_releases_url(project)
+ end
+
+ expose :type do |*|
+ "Application"
+ end
+
+ expose :path, as: :preferredUsername do |project|
+ "#{project.path}-releases"
+ end
+
+ expose :name do |project|
+ "#{_('Releases')} - #{project.name}"
+ end
+
+ expose :description, as: :content
+
+ expose nil, using: ProjectEntity, as: :context do |project|
+ project
+ end
+ end
+end
diff --git a/app/serializers/activity_pub/releases_actor_serializer.rb b/app/serializers/activity_pub/releases_actor_serializer.rb
new file mode 100644
index 00000000000..5bae83f2dc7
--- /dev/null
+++ b/app/serializers/activity_pub/releases_actor_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ class ReleasesActorSerializer < ActivityStreamsSerializer
+ entity ReleasesActorEntity
+ end
+end
diff --git a/app/serializers/activity_pub/releases_outbox_serializer.rb b/app/serializers/activity_pub/releases_outbox_serializer.rb
new file mode 100644
index 00000000000..b6d4e633fb0
--- /dev/null
+++ b/app/serializers/activity_pub/releases_outbox_serializer.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ class ReleasesOutboxSerializer < ActivityStreamsSerializer
+ include WithPagination
+
+ entity ReleaseEntity
+ end
+end
diff --git a/app/serializers/activity_pub/user_entity.rb b/app/serializers/activity_pub/user_entity.rb
new file mode 100644
index 00000000000..bd0886db5b2
--- /dev/null
+++ b/app/serializers/activity_pub/user_entity.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ class UserEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id do |user|
+ user_url(user)
+ end
+
+ expose :type do |*|
+ 'Person'
+ end
+
+ expose :name
+ expose :username, as: :preferredUsername
+
+ expose :url do |user|
+ user_url(user)
+ end
+ end
+end
diff --git a/app/serializers/admin/abuse_report_details_entity.rb b/app/serializers/admin/abuse_report_details_entity.rb
index 3efb8508e5e..8a67aabda9e 100644
--- a/app/serializers/admin/abuse_report_details_entity.rb
+++ b/app/serializers/admin/abuse_report_details_entity.rb
@@ -8,17 +8,21 @@ module Admin
expose :details, merge: true do |report|
UserEntity.represent(report.user, only: [:name, :username, :avatar_url, :email, :created_at, :last_activity_on])
end
+
expose :path do |report|
user_path(report.user)
end
+
expose :admin_path do |report|
admin_user_path(report.user)
end
+
expose :plan do |report|
if Gitlab::CurrentSettings.current_application_settings.try(:should_check_namespace_plan?)
report.user.namespace&.actual_plan&.title
end
end
+
expose :verification_state do
expose :email do |report|
report.user.confirmed?
@@ -30,6 +34,7 @@ module Admin
report.user.credit_card_validation.present?
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
@@ -41,55 +46,38 @@ module Admin
card_match_admin_user_path(report.user) if Gitlab.ee?
end
end
- expose :other_reports do |report|
- AbuseReportEntity.represent(report.other_reports_for_user, only: [:created_at, :category, :report_path])
+
+ expose :past_closed_reports do |report|
+ AbuseReportEntity.represent(report.past_closed_reports_for_user, only: [:created_at, :category, :report_path])
+ end
+
+ expose :similar_open_reports, if: ->(report) { report.open? } do |report|
+ ReportedContentEntity.represent(report.similar_open_reports_for_user)
end
+
expose :most_used_ip do |report|
AuthenticationEvent.most_used_ip_address_for_user(report.user)
end
+
expose :last_sign_in_ip do |report|
report.user.last_sign_in_ip
end
+
expose :snippets_count do |report|
report.user.snippets.count
end
+
expose :groups_count do |report|
report.user.groups.count
end
+
expose :notes_count do |report|
report.user.notes.count
end
end
- expose :reporter, if: ->(report) { report.reporter } do
- expose :details, merge: true do |report|
- UserEntity.represent(report.reporter, only: [:name, :username, :avatar_url])
- end
- expose :path do |report|
- user_path(report.reporter)
- end
- end
-
- expose :report do
- expose :status
- expose :message
- expose :created_at, as: :reported_at
- expose :category
- expose :report_type, as: :type
- expose :reported_content, as: :content
- expose :reported_from_url, as: :url
- expose :screenshot_path, as: :screenshot
-
- # Kept for backwards compatibility.
- # TODO: See https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/167?work_item_iid=443
- # In 16.4 remove or re-use this field after frontend has migrated to using moderate_user_path
- expose :update_path do |report|
- admin_abuse_report_path(report)
- end
-
- expose :moderate_user_path do |report|
- moderate_user_admin_abuse_report_path(report)
- end
+ expose :report do |report|
+ ReportedContentEntity.represent(report)
end
end
end
diff --git a/app/serializers/admin/abuse_report_entity.rb b/app/serializers/admin/abuse_report_entity.rb
index 22395a2fe91..f8bd851cd1e 100644
--- a/app/serializers/admin/abuse_report_entity.rb
+++ b/app/serializers/admin/abuse_report_entity.rb
@@ -8,6 +8,7 @@ module Admin
expose :created_at
expose :updated_at
expose :count
+ expose :labels, using: LabelEntity, if: ->(*) { Feature.enabled?(:abuse_report_labels) }
expose :reported_user do |report|
UserEntity.represent(report.user, only: [:name])
diff --git a/app/serializers/admin/reported_content_entity.rb b/app/serializers/admin/reported_content_entity.rb
new file mode 100644
index 00000000000..bf690647672
--- /dev/null
+++ b/app/serializers/admin/reported_content_entity.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Admin
+ class ReportedContentEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id
+ expose :global_id do |report|
+ Gitlab::GlobalId.build(report, id: report.id).to_s
+ end
+ expose :status
+ expose :message
+ expose :created_at, as: :reported_at
+ expose :category
+ expose :report_type, as: :type
+ expose :reported_content, as: :content
+ expose :reported_from_url, as: :url
+ expose :screenshot_path, as: :screenshot
+
+ expose :reporter, if: ->(report) { report.reporter } do
+ expose :details, merge: true do |report|
+ UserEntity.represent(report.reporter, only: [:name, :username, :avatar_url])
+ end
+
+ expose :path do |report|
+ user_path(report.reporter)
+ end
+ end
+
+ expose :update_path do |report|
+ admin_abuse_report_path(report)
+ end
+
+ expose :moderate_user_path do |report|
+ moderate_user_admin_abuse_report_path(report)
+ end
+ end
+end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index a34f329e9ec..741643f7989 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -57,6 +57,10 @@ class BuildDetailsEntity < Ci::JobEntity
using: JobArtifactReportEntity,
if: -> (*) { can?(current_user, :read_build, build) }
+ expose :job_annotations,
+ as: :annotations,
+ using: Ci::JobAnnotationEntity
+
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
erase_project_job_path(project, build)
diff --git a/app/serializers/ci/job_annotation_entity.rb b/app/serializers/ci/job_annotation_entity.rb
new file mode 100644
index 00000000000..8d7b2e21460
--- /dev/null
+++ b/app/serializers/ci/job_annotation_entity.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Ci
+ class JobAnnotationEntity < Grape::Entity
+ expose :name
+ expose :data
+ end
+end
diff --git a/app/serializers/codequality_degradation_entity.rb b/app/serializers/codequality_degradation_entity.rb
index 15a26739c51..9f90a30bd2d 100644
--- a/app/serializers/codequality_degradation_entity.rb
+++ b/app/serializers/codequality_degradation_entity.rb
@@ -2,6 +2,9 @@
class CodequalityDegradationEntity < Grape::Entity
expose :description
+ expose :fingerprint, if: ->(_, options) do
+ Feature.enabled?(:sast_reports_in_inline_diff, options[:request]&.project)
+ end
expose :severity do |degradation|
degradation.dig(:severity)&.downcase
end
diff --git a/app/serializers/issue_serializer.rb b/app/serializers/issue_serializer.rb
index 0fa76f098cd..b459997cc69 100644
--- a/app/serializers/issue_serializer.rb
+++ b/app/serializers/issue_serializer.rb
@@ -5,18 +5,23 @@ class IssueSerializer < BaseSerializer
# to serialize the `issue` based on `serializer` key in `opts` param.
# Hence, `entity` doesn't need to be declared on the class scope.
def represent(issue, opts = {})
- entity =
- case opts[:serializer]
- when 'sidebar'
- IssueSidebarBasicEntity
- when 'sidebar_extras'
- IssueSidebarExtrasEntity
- when 'board'
- IssueBoardEntity
- else
- IssueEntity
- end
+ entity = choose_entity(opts)
super(issue, opts, entity)
end
+
+ def choose_entity(opts)
+ case opts[:serializer]
+ when 'sidebar'
+ IssueSidebarBasicEntity
+ when 'sidebar_extras'
+ IssueSidebarExtrasEntity
+ when 'board'
+ IssueBoardEntity
+ else
+ IssueEntity
+ end
+ end
end
+
+IssueSerializer.prepend_mod_with('IssueSerializer')
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index b738438a78f..a3e842d348e 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -50,7 +50,7 @@ class PipelineSerializer < BaseSerializer
{
manual_actions: :metadata,
scheduled_actions: :metadata,
- failed_builds: %i(project metadata),
+ failed_builds: %i[project metadata],
merge_request: {
source_project: [:route, { namespace: :route }],
target_project: [:route, { namespace: :route }]
diff --git a/app/serializers/profile/event_entity.rb b/app/serializers/profile/event_entity.rb
index f3c1a927084..93c5be32de3 100644
--- a/app/serializers/profile/event_entity.rb
+++ b/app/serializers/profile/event_entity.rb
@@ -47,23 +47,26 @@ module Profile
end
end
- expose :target, if: ->(event) { event.target && event.visible_to_user?(current_user) } do
- expose(:id) { |event| event.target.id }
+ expose :target, if: ->(event) { event.visible_to_user?(current_user) } do
expose(:target_type, as: :type)
- expose(:target_title, as: :title)
- expose(:issue_type, if: ->(event) { event.work_item? || event.issue? }) do |event|
- event.target.issue_type
- end
- expose :reference_link_text, if: ->(event) { event.target.respond_to?(:reference_link_text) } do |event|
- event.target.reference_link_text
- end
+ with_options if: ->(event) { event.target } do
+ expose(:id) { |event| event.target.id }
+ expose(:target_title, as: :title)
+ expose(:issue_type, if: ->(event) { event.work_item? || event.issue? }) do |event|
+ event.target.issue_type
+ end
+
+ expose :reference_link_text, if: ->(event) { event.target.respond_to?(:reference_link_text) } do |event|
+ event.target.reference_link_text
+ end
- expose :web_url do |event|
- if event.wiki_page?
- event_wiki_page_target_url(event)
- else
- Gitlab::UrlBuilder.build(event.target)
+ expose :web_url do |event|
+ if event.wiki_page?
+ event_wiki_page_target_url(event)
+ else
+ Gitlab::UrlBuilder.build(event.target)
+ end
end
end
end
diff --git a/app/services/admin/abuse_report_labels/create_service.rb b/app/services/admin/abuse_report_labels/create_service.rb
new file mode 100644
index 00000000000..937890a9f51
--- /dev/null
+++ b/app/services/admin/abuse_report_labels/create_service.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Admin
+ module AbuseReportLabels
+ class CreateService < Labels::BaseService
+ def initialize(params = {})
+ @params = params
+ end
+
+ def execute
+ params[:color] = convert_color_name_to_hex if params[:color].present?
+
+ ::Admin::AbuseReportLabel.create(params)
+ end
+ 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 da61a4dc8f6..823568d9db8 100644
--- a/app/services/admin/abuse_reports/moderate_user_service.rb
+++ b/app/services/admin/abuse_reports/moderate_user_service.rb
@@ -61,10 +61,17 @@ module Admin
def close_report
return error('Report already closed') if abuse_report.closed?
+ close_similar_open_reports
abuse_report.closed!
success
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
+ abuse_report.similar_open_reports_for_user.update_all(status: 'closed')
+ end
+
def close_report_and_record_event
event = action
diff --git a/app/services/admin/abuse_reports/update_service.rb b/app/services/admin/abuse_reports/update_service.rb
new file mode 100644
index 00000000000..36992e1aa25
--- /dev/null
+++ b/app/services/admin/abuse_reports/update_service.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Admin
+ module AbuseReports
+ class UpdateService < BaseService
+ attr_reader :abuse_report, :params, :current_user
+
+ def initialize(abuse_report, current_user, params)
+ @abuse_report = abuse_report
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ return ServiceResponse.error(message: 'Admin is required') unless current_user&.can_admin_all_resources?
+
+ abuse_report.label_ids = label_ids
+
+ ServiceResponse.success
+ end
+
+ private
+
+ def label_ids
+ params[:label_ids].filter_map do |id|
+ GitlabSchema.parse_gid(id, expected_type: ::Admin::AbuseReportLabel).model_id
+ rescue Gitlab::Graphql::Errors::ArgumentError
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb
index 6d484c4fa22..a46ecc3eee6 100644
--- a/app/services/application_settings/update_service.rb
+++ b/app/services/application_settings/update_service.rb
@@ -6,7 +6,7 @@ module ApplicationSettings
attr_reader :params, :application_setting
- MARKDOWN_CACHE_INVALIDATING_PARAMS = %w(asset_proxy_enabled asset_proxy_url asset_proxy_secret_key asset_proxy_whitelist).freeze
+ MARKDOWN_CACHE_INVALIDATING_PARAMS = %w[asset_proxy_enabled asset_proxy_url asset_proxy_secret_key asset_proxy_whitelist].freeze
def execute
result = update_settings
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index eaee5ce70fc..9b010272995 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -39,11 +39,11 @@ module Auth
end
def self.full_access_token(*names)
- access_token(%w(*), names)
+ access_token(%w[*], names)
end
def self.import_access_token
- access_token(%w(*), ['import'], 'registry')
+ access_token(%w[*], ['import'], 'registry')
end
def self.pull_access_token(*names)
diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb
index 1660ddb934f..77ed0369624 100644
--- a/app/services/auto_merge/base_service.rb
+++ b/app/services/auto_merge/base_service.rb
@@ -20,6 +20,10 @@ module AutoMerge
:failed
end
+ def process(_)
+ raise NotImplementedError
+ end
+
def update(merge_request)
assign_allowed_merge_params(merge_request, params.merge(auto_merge_strategy: strategy))
@@ -87,16 +91,21 @@ module AutoMerge
merge_request.auto_merge_enabled = false
merge_request.merge_user = nil
- merge_request.merge_params&.except!(
- 'should_remove_source_branch',
- 'commit_message',
- 'squash_commit_message',
- 'auto_merge_strategy'
- )
+ merge_request.merge_params&.except!(*clearable_auto_merge_parameters)
merge_request.save!
end
+ # Overridden in EE child classes
+ def clearable_auto_merge_parameters
+ %w[
+ should_remove_source_branch
+ commit_message
+ squash_commit_message
+ auto_merge_strategy
+ ]
+ end
+
def track_exception(error, merge_request)
Gitlab::ErrorTracking.track_exception(error, merge_request_id: merge_request&.id)
end
diff --git a/app/services/boards/update_service.rb b/app/services/boards/update_service.rb
index 6ba8f68a4cb..c702398e89e 100644
--- a/app/services/boards/update_service.rb
+++ b/app/services/boards/update_service.rb
@@ -2,7 +2,7 @@
module Boards
class UpdateService < Boards::BaseService
- PERMITTED_PARAMS = %i(name hide_backlog_list hide_closed_list).freeze
+ PERMITTED_PARAMS = %i[name hide_backlog_list hide_closed_list].freeze
def execute(board)
filter_params
diff --git a/app/services/bulk_imports/create_pipeline_trackers_service.rb b/app/services/bulk_imports/create_pipeline_trackers_service.rb
deleted file mode 100644
index 7fa62e0ce8a..00000000000
--- a/app/services/bulk_imports/create_pipeline_trackers_service.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-# frozen_string_literal: true
-
-module BulkImports
- class CreatePipelineTrackersService
- def initialize(entity)
- @entity = entity
- end
-
- def execute!
- entity.class.transaction do
- entity.pipelines.each do |pipeline|
- status = skip_pipeline?(pipeline) ? -2 : 0
-
- entity.trackers.create!(
- stage: pipeline[:stage],
- pipeline_name: pipeline[:pipeline],
- status: status
- )
- end
- end
- end
-
- private
-
- attr_reader :entity
-
- def skip_pipeline?(pipeline)
- return false unless source_version.valid?
-
- minimum_version, maximum_version = pipeline.values_at(:minimum_source_version, :maximum_source_version)
-
- if minimum_version && non_patch_source_version < Gitlab::VersionInfo.parse(minimum_version)
- log_skipped_pipeline(pipeline, minimum_version, maximum_version)
- return true
- end
-
- if maximum_version && non_patch_source_version > Gitlab::VersionInfo.parse(maximum_version)
- log_skipped_pipeline(pipeline, minimum_version, maximum_version)
- return true
- end
-
- false
- end
-
- def source_version
- @source_version ||= entity.bulk_import.source_version_info
- end
-
- def non_patch_source_version
- source_version.without_patch
- end
-
- def log_skipped_pipeline(pipeline, 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: 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/create_service.rb b/app/services/bulk_imports/create_service.rb
index 7fc3511a253..d58620eb089 100644
--- a/app/services/bulk_imports/create_service.rb
+++ b/app/services/bulk_imports/create_service.rb
@@ -118,11 +118,6 @@ module BulkImports
end
client.get("/#{entity_type}/#{source_entity_identifier}/export_relations/status")
- rescue BulkImports::NetworkError => e
- # the source instance will return a 404 if the feature is disabled as the endpoint won't be available
- return if e.cause.is_a?(Gitlab::HTTP::BlockedUrlError)
-
- raise ::BulkImports::Error.setting_not_enabled
end
def track_access_level(entity_params)
diff --git a/app/services/bulk_imports/file_download_service.rb b/app/services/bulk_imports/file_download_service.rb
index 48adb90fb4c..1f2437d783d 100644
--- a/app/services/bulk_imports/file_download_service.rb
+++ b/app/services/bulk_imports/file_download_service.rb
@@ -15,7 +15,7 @@ module BulkImports
ServiceError = Class.new(StandardError)
- DEFAULT_ALLOWED_CONTENT_TYPES = %w(application/gzip application/octet-stream).freeze
+ DEFAULT_ALLOWED_CONTENT_TYPES = %w[application/gzip application/octet-stream].freeze
def initialize(
configuration:,
@@ -83,6 +83,8 @@ module BulkImports
end
def raise_error(message)
+ logger.warn(message: message, response_headers: response_headers, importer: 'gitlab_migration')
+
raise ServiceError, message
end
@@ -109,12 +111,16 @@ module BulkImports
@filename.presence || remote_filename
end
+ def logger
+ @logger ||= Gitlab::Import::Logger.build
+ end
+
def validate_url
::Gitlab::UrlBlocker.validate!(
http_client.resource_url(relative_url),
allow_localhost: allow_local_requests?,
allow_local_network: allow_local_requests?,
- schemes: %w(http https)
+ schemes: %w[http https]
)
end
diff --git a/app/services/ci/create_commit_status_service.rb b/app/services/ci/create_commit_status_service.rb
new file mode 100644
index 00000000000..e5b446a07e2
--- /dev/null
+++ b/app/services/ci/create_commit_status_service.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: true
+
+module Ci
+ class CreateCommitStatusService < BaseService
+ include ::Gitlab::ExclusiveLeaseHelpers
+ include ::Gitlab::Utils::StrongMemoize
+ include ::Services::ReturnServiceResponses
+
+ delegate :sha, to: :commit
+
+ def execute(optional_commit_status_params:)
+ in_lock(pipeline_lock_key, **pipeline_lock_params) do
+ @optional_commit_status_params = optional_commit_status_params
+ unsafe_execute
+ end
+ end
+
+ private
+
+ attr_reader :pipeline, :stage, :commit_status, :optional_commit_status_params
+
+ def unsafe_execute
+ return not_found('Commit') if commit.blank?
+ return bad_request('State is required') if params[:state].blank?
+ return not_found('References for commit') if ref.blank?
+
+ @pipeline = first_matching_pipeline || create_pipeline
+ return forbidden unless ::Ability.allowed?(current_user, :update_pipeline, pipeline)
+
+ @stage = find_or_create_external_stage
+ @commit_status = find_or_build_external_commit_status
+
+ return bad_request(commit_status.errors.messages) if commit_status.invalid?
+
+ response = add_or_update_external_job
+
+ return bad_request(response.message) if response.error?
+
+ update_merge_request_head_pipeline
+ response
+ end
+
+ def ref
+ params[:ref] || first_matching_pipeline&.ref ||
+ repository.branch_names_contains(sha).first
+ end
+ strong_memoize_attr :ref
+
+ def commit
+ project.commit(params[:sha])
+ end
+ strong_memoize_attr :commit
+
+ def first_matching_pipeline
+ pipelines = project.ci_pipelines.newest_first(sha: sha)
+ pipelines = pipelines.for_ref(params[:ref]) if params[:ref]
+ pipelines = pipelines.id_in(params[:pipeline_id]) if params[:pipeline_id]
+ pipelines.first
+ end
+ strong_memoize_attr :first_matching_pipeline
+
+ def name
+ params[:name] || params[:context] || 'default'
+ end
+
+ def create_pipeline
+ project.ci_pipelines.build(
+ source: :external,
+ sha: sha,
+ ref: ref,
+ user: current_user,
+ protected: project.protected_for?(ref)
+ ).tap do |new_pipeline|
+ new_pipeline.ensure_project_iid!
+ new_pipeline.save!
+ end
+ end
+
+ def find_or_create_external_stage
+ pipeline.stages.safe_find_or_create_by!(name: 'external') do |stage| # rubocop:disable Performance/ActiveRecordSubtransactionMethods
+ stage.position = ::GenericCommitStatus::EXTERNAL_STAGE_IDX
+ stage.project = project
+ end
+ end
+
+ def find_or_build_external_commit_status
+ ::GenericCommitStatus.running_or_pending.find_or_initialize_by( # rubocop:disable CodeReuse/ActiveRecord
+ project: project,
+ pipeline: pipeline,
+ name: name,
+ ref: ref,
+ user: current_user,
+ protected: project.protected_for?(ref),
+ ci_stage: stage,
+ stage_idx: stage.position,
+ stage: 'external'
+ ).tap do |new_commit_status|
+ new_commit_status.assign_attributes(optional_commit_status_params)
+ end
+ end
+
+ def add_or_update_external_job
+ ::Ci::Pipelines::AddJobService.new(pipeline).execute!(commit_status) do |job|
+ apply_job_state!(job)
+ end
+ end
+
+ def update_merge_request_head_pipeline
+ return unless pipeline.latest?
+
+ ::MergeRequest
+ .from_project(project).from_source_branches(ref)
+ .update_all(head_pipeline_id: pipeline.id)
+ end
+
+ def apply_job_state!(job)
+ case params[:state]
+ when 'pending'
+ job.enqueue!
+ when 'running'
+ job.enqueue
+ job.run!
+ when 'success'
+ job.success!
+ when 'failed'
+ job.drop!(:api_failure)
+ when 'canceled'
+ job.cancel!
+ else
+ raise('invalid state')
+ end
+ end
+
+ def pipeline_lock_key
+ "api:commit_statuses:project:#{project.id}:sha:#{params[:sha]}"
+ end
+
+ def pipeline_lock_params
+ {
+ ttl: 5.seconds,
+ sleep_sec: 0.1.seconds,
+ retries: 20
+ }
+ end
+
+ def not_found(message)
+ error("404 #{message} Not Found", :not_found)
+ end
+
+ def bad_request(message)
+ error(message, :bad_request)
+ end
+
+ def forbidden
+ error("403 Forbidden", :forbidden)
+ end
+ end
+end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index fe0e842f542..2231b1dd6bd 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -154,13 +154,6 @@ module Ci
duration >= LOG_MAX_CREATION_THRESHOLD
end
-
- l.log_when do |observations|
- pipeline_includes_count = observations['pipeline_includes_count']
- next false unless pipeline_includes_count
-
- pipeline_includes_count.to_i > Gitlab::Ci::Config::External::Context::TEMP_MAX_INCLUDES
- 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 953432a9dd3..05cd20a152b 100644
--- a/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb
+++ b/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb
@@ -105,11 +105,7 @@ module Ci
end
def pipelines_created_after
- if Feature.enabled?(:lower_interval_for_canceling_redundant_pipelines, project)
- 3.days.ago
- else
- 1.week.ago
- end
+ 3.days.ago
end
# Finding the pipelines to cancel is an expensive task that is not well
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 68ebb376ccd..470a1d3951b 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -10,7 +10,7 @@ module Ci
TEMPORARY_LOCK_TIMEOUT = 3.seconds
- Result = Struct.new(:build, :build_json, :valid?)
+ Result = Struct.new(:build, :build_json, :build_presented, :valid?)
##
# The queue depth limit number has been determined by observing 95
@@ -25,8 +25,8 @@ module Ci
end
def execute(params = {})
- db_all_caught_up =
- ::Ci::Runner.sticking.all_caught_up?(:runner, runner.id)
+ replica_caught_up =
+ ::Ci::Runner.sticking.find_caught_up_replica(:runner, runner.id, use_primary_on_failure: false)
@metrics.increment_queue_operation(:queue_attempt)
@@ -40,10 +40,10 @@ module Ci
# we might still have some CI builds to be picked. Instead we should say to runner:
# "Hi, we don't have any more builds now, but not everything is right anyway, so try again".
# Runner will retry, but again, against replica, and again will check if replication lag did catch-up.
- if !db_all_caught_up && !result.build
+ if !replica_caught_up && !result.build
metrics.increment_queue_operation(:queue_replication_lag)
- ::Ci::RegisterJobService::Result.new(nil, nil, false) # rubocop:disable Cop/AvoidReturnFromBlocks
+ ::Ci::RegisterJobService::Result.new(nil, nil, nil, false) # rubocop:disable Cop/AvoidReturnFromBlocks
else
result
end
@@ -86,7 +86,7 @@ module Ci
next unless result
if result.valid?
- @metrics.register_success(result.build)
+ @metrics.register_success(result.build_presented)
@metrics.observe_queue_depth(:found, depth)
return result # rubocop:disable Cop/AvoidReturnFromBlocks
@@ -102,7 +102,7 @@ module Ci
@metrics.observe_queue_depth(:not_found, depth) if valid
@metrics.register_failure
- Result.new(nil, nil, valid)
+ Result.new(nil, nil, nil, valid)
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -159,7 +159,7 @@ module Ci
# this operation.
#
if ::Ci::UpdateBuildQueueService.new.remove!(build)
- return Result.new(nil, nil, false)
+ return Result.new(nil, nil, nil, false)
end
return
@@ -190,11 +190,11 @@ module Ci
# to make sure that this is properly handled by runner.
@metrics.increment_queue_operation(:build_conflict_lock)
- Result.new(nil, nil, false)
+ Result.new(nil, nil, nil, false)
rescue StateMachines::InvalidTransition
@metrics.increment_queue_operation(:build_conflict_transition)
- Result.new(nil, nil, false)
+ Result.new(nil, nil, nil, false)
rescue StandardError => ex
@metrics.increment_queue_operation(:build_conflict_exception)
@@ -221,7 +221,7 @@ module Ci
log_build_dependencies_size(presented_build)
build_json = Gitlab::Json.dump(::API::Entities::Ci::JobRequest::Response.new(presented_build))
- Result.new(build, build_json, true)
+ Result.new(build, build_json, presented_build, true)
end
def log_build_dependencies_size(presented_build)
diff --git a/app/services/ci/update_instance_variables_service.rb b/app/services/ci/update_instance_variables_service.rb
index ee513647d08..2f941118a1c 100644
--- a/app/services/ci/update_instance_variables_service.rb
+++ b/app/services/ci/update_instance_variables_service.rb
@@ -5,7 +5,7 @@
module Ci
class UpdateInstanceVariablesService
- UNASSIGNABLE_KEYS = %w(id _destroy).freeze
+ UNASSIGNABLE_KEYS = %w[id _destroy].freeze
def initialize(params)
@params = params[:variables_attributes]
diff --git a/app/services/clusters/agent_tokens/track_usage_service.rb b/app/services/clusters/agent_tokens/track_usage_service.rb
index fdc79ac0f8b..18fe236c44d 100644
--- a/app/services/clusters/agent_tokens/track_usage_service.rb
+++ b/app/services/clusters/agent_tokens/track_usage_service.rb
@@ -4,7 +4,7 @@ module Clusters
module AgentTokens
class TrackUsageService
# The `UPDATE_USED_COLUMN_EVERY` defines how often the token DB entry can be updated
- UPDATE_USED_COLUMN_EVERY = (40.minutes..55.minutes).freeze
+ UPDATE_USED_COLUMN_EVERY = (40.minutes..55.minutes)
delegate :agent, to: :token
diff --git a/app/services/clusters/kubernetes/create_or_update_service_account_service.rb b/app/services/clusters/kubernetes/create_or_update_service_account_service.rb
index e87640f4c76..94781422686 100644
--- a/app/services/clusters/kubernetes/create_or_update_service_account_service.rb
+++ b/app/services/clusters/kubernetes/create_or_update_service_account_service.rb
@@ -137,9 +137,9 @@ module Clusters
name: Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME,
namespace: service_account_namespace,
rules: [{
- apiGroups: %w(serving.knative.dev),
- resources: %w(configurations configurationgenerations routes revisions revisionuids autoscalers services),
- verbs: %w(get list create update delete patch watch)
+ apiGroups: %w[serving.knative.dev],
+ resources: %w[configurations configurationgenerations routes revisions revisionuids autoscalers services],
+ verbs: %w[get list create update delete patch watch]
}]
).generate
end
@@ -159,9 +159,9 @@ module Clusters
name: Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_NAME,
namespace: service_account_namespace,
rules: [{
- apiGroups: %w(database.crossplane.io),
- resources: %w(postgresqlinstances),
- verbs: %w(get list create watch)
+ apiGroups: %w[database.crossplane.io],
+ resources: %w[postgresqlinstances],
+ verbs: %w[get list create watch]
}]
).generate
end
diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb
index a498d39d34e..89370bd8abb 100644
--- a/app/services/commits/create_service.rb
+++ b/app/services/commits/create_service.rb
@@ -39,7 +39,12 @@ module Commits
Gitlab::Git::PreReceiveError,
Gitlab::Git::CommandError => ex
Gitlab::ErrorTracking.log_exception(ex)
- error(ex.message)
+
+ if Feature.enabled?(:errors_utf_8_encoding)
+ error(Gitlab::EncodingHelper.encode_utf8_no_detect(ex.message))
+ else
+ error(ex.message)
+ end
end
private
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index 569b91de73e..b02cfea151d 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -17,9 +17,6 @@ class CompareService
return unless raw_compare && raw_compare.base && raw_compare.head
- Compare.new(raw_compare,
- start_project,
- base_sha: base_sha,
- straight: straight)
+ Compare.new(raw_compare, start_project, base_sha: base_sha, straight: straight)
end
end
diff --git a/app/services/concerns/alert_management/alert_processing.rb b/app/services/concerns/alert_management/alert_processing.rb
index 9fe82507edd..5d35658a2b3 100644
--- a/app/services/concerns/alert_management/alert_processing.rb
+++ b/app/services/concerns/alert_management/alert_processing.rb
@@ -36,7 +36,7 @@ module AlertManagement
SystemNoteService.log_resolving_alert(alert, alert_source)
if alert.resolve(incoming_payload.ends_at)
- SystemNoteService.change_alert_status(alert, User.alert_bot)
+ SystemNoteService.change_alert_status(alert, Users::Internal.alert_bot)
close_issue(alert.issue_id) if auto_close_incident?
end
diff --git a/app/services/concerns/rate_limited_service.rb b/app/services/concerns/rate_limited_service.rb
index fa366c1ccd0..79be952ac14 100644
--- a/app/services/concerns/rate_limited_service.rb
+++ b/app/services/concerns/rate_limited_service.rb
@@ -61,9 +61,11 @@ module RateLimitedService
cattr_accessor :rate_limiter_scoped_and_keyed
def self.rate_limit(key:, opts:, rate_limiter: ::Gitlab::ApplicationRateLimiter)
- self.rate_limiter_scoped_and_keyed = RateLimiterScopedAndKeyed.new(key: key,
- opts: opts,
- rate_limiter: rate_limiter)
+ self.rate_limiter_scoped_and_keyed = RateLimiterScopedAndKeyed.new(
+ key: key,
+ opts: opts,
+ rate_limiter: rate_limiter
+ )
end
end
diff --git a/app/services/concerns/service_desk/custom_emails/logger.rb b/app/services/concerns/service_desk/custom_emails/logger.rb
new file mode 100644
index 00000000000..1817933c3d0
--- /dev/null
+++ b/app/services/concerns/service_desk/custom_emails/logger.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module ServiceDesk
+ module CustomEmails
+ module Logger
+ private
+
+ def log_warning(error_message: nil)
+ with_context do
+ Gitlab::AppLogger.warn(build_log_message(error_message: error_message))
+ end
+ end
+
+ def log_info(error_message: nil, project: nil)
+ with_context(project: project) do
+ Gitlab::AppLogger.info(build_log_message(error_message: error_message))
+ end
+ end
+
+ def with_context(project: nil, &block)
+ Gitlab::ApplicationContext.with_context(
+ related_class: self.class.to_s,
+ user: current_user,
+ project: project || self.project,
+ &block
+ )
+ end
+
+ def log_category
+ 'custom_email'
+ end
+
+ def build_log_message(error_message: nil)
+ {
+ category: log_category,
+ error_message: error_message
+ }.compact
+ end
+ end
+ end
+end
diff --git a/app/services/concerns/update_repository_storage_methods.rb b/app/services/concerns/update_repository_storage_methods.rb
index dca38abf7af..f14c79ecd7e 100644
--- a/app/services/concerns/update_repository_storage_methods.rb
+++ b/app/services/concerns/update_repository_storage_methods.rb
@@ -69,7 +69,17 @@ module UpdateRepositoryStorageMethods
raise Error, s_('UpdateRepositoryStorage|Timeout waiting for %{type} repository pushes') % { type: type.name }
end
- repository = type.repository_for(container)
+ # `Projects::UpdateRepositoryStorageService`` expects the repository it is
+ # moving to have a `Project` as a container.
+ # This hack allows design repos to also be moved as part of a project move
+ # as before.
+ # The alternative to this hack is to setup a service like
+ # `Snippets::UpdateRepositoryStorageService' and a corresponding worker like
+ # `Snippets::UpdateRepositoryStorageWorker` for snippets.
+ #
+ # Gitlab issue: https://gitlab.com/gitlab-org/gitlab/-/issues/423429
+
+ repository = type.repository_for(type.design? ? container.design_management_repository : container)
full_path = repository.full_path
raw_repository = repository.raw
checksum = repository.checksum
diff --git a/app/services/design_management/copy_design_collection/copy_service.rb b/app/services/design_management/copy_design_collection/copy_service.rb
index 8074a193bbf..d391c13696f 100644
--- a/app/services/design_management/copy_design_collection/copy_service.rb
+++ b/app/services/design_management/copy_design_collection/copy_service.rb
@@ -58,8 +58,8 @@ module DesignManagement
private
attr_reader :designs, :event_enum_map, :git_user, :sha_attribute, :shas,
- :temporary_branch, :target_design_collection, :target_issue,
- :target_repository, :target_project, :versions
+ :temporary_branch, :target_design_collection, :target_issue,
+ :target_repository, :target_project, :versions
alias_method :merge_branch, :target_branch
diff --git a/app/services/design_management/delete_designs_service.rb b/app/services/design_management/delete_designs_service.rb
index 921c904d8de..a6a0f5e0252 100644
--- a/app/services/design_management/delete_designs_service.rb
+++ b/app/services/design_management/delete_designs_service.rb
@@ -16,8 +16,10 @@ module DesignManagement
version = delete_designs!
EventCreateService.new.destroy_designs(designs, current_user)
- Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_designs_removed_action(author: current_user,
- project: project)
+ Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_designs_removed_action(
+ author: current_user,
+ project: project
+ )
TodosDestroyer::DestroyedDesignsWorker.perform_async(designs.map(&:id))
success(version: version)
diff --git a/app/services/design_management/runs_design_actions.rb b/app/services/design_management/runs_design_actions.rb
index 267ed6bf29f..62db7824592 100644
--- a/app/services/design_management/runs_design_actions.rb
+++ b/app/services/design_management/runs_design_actions.rb
@@ -15,10 +15,12 @@ module DesignManagement
def run_actions(actions, skip_system_notes: false)
raise NoActions if actions.empty?
- sha = repository.commit_files(current_user,
- branch_name: target_branch,
- message: commit_message,
- actions: actions.map(&:gitaly_action))
+ sha = repository.commit_files(
+ current_user,
+ branch_name: target_branch,
+ message: commit_message,
+ actions: actions.map(&:gitaly_action)
+ )
::DesignManagement::Version
.create_for_designs(actions, sha, current_user)
diff --git a/app/services/design_management/save_designs_service.rb b/app/services/design_management/save_designs_service.rb
index ea5675c6ddd..4c4e34862e8 100644
--- a/app/services/design_management/save_designs_service.rb
+++ b/app/services/design_management/save_designs_service.rb
@@ -131,11 +131,11 @@ module DesignManagement
def track_usage_metrics(action)
if action == :update
- ::Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_designs_modified_action(author: current_user,
- project: project)
+ ::Gitlab::UsageDataCounters::IssueActivityUniqueCounter
+ .track_issue_designs_modified_action(author: current_user, project: project)
else
- ::Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_designs_added_action(author: current_user,
- project: project)
+ ::Gitlab::UsageDataCounters::IssueActivityUniqueCounter
+ .track_issue_designs_added_action(author: current_user, project: project)
end
::Gitlab::UsageDataCounters::DesignsCounter.count(action)
diff --git a/app/services/error_tracking/base_service.rb b/app/services/error_tracking/base_service.rb
index 8458eb1f3b8..e95d4eec3c8 100644
--- a/app/services/error_tracking/base_service.rb
+++ b/app/services/error_tracking/base_service.rb
@@ -17,8 +17,7 @@ module ErrorTracking
private
def perform
- raise NotImplementedError,
- "#{self.class} does not implement #{__method__}"
+ raise NotImplementedError, "#{self.class} does not implement #{__method__}"
end
def compose_response(response, &block)
@@ -33,8 +32,7 @@ module ErrorTracking
end
def parse_response(response)
- raise NotImplementedError,
- "#{self.class} does not implement #{__method__}"
+ raise NotImplementedError, "#{self.class} does not implement #{__method__}"
end
def unauthorized
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 1893cfcfcff..b755f512772 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -190,10 +190,14 @@ class EventCreateService
private
def create_record_event(record, current_user, status, fingerprint = nil)
- create_event(record.resource_parent, current_user, status,
- fingerprint: fingerprint,
- target_id: record.id,
- target_type: record.class.name)
+ create_event(
+ record.resource_parent,
+ current_user,
+ status,
+ fingerprint: fingerprint,
+ target_id: record.id,
+ target_type: record.class.name
+ )
end
# If creating several events, this method will insert them all in a single
diff --git a/app/services/feature_flags/base_service.rb b/app/services/feature_flags/base_service.rb
index 834409bf3c4..961e8d54efa 100644
--- a/app/services/feature_flags/base_service.rb
+++ b/app/services/feature_flags/base_service.rb
@@ -4,7 +4,7 @@ module FeatureFlags
class BaseService < ::BaseService
include Gitlab::Utils::StrongMemoize
- AUDITABLE_ATTRIBUTES = %w(name description active).freeze
+ AUDITABLE_ATTRIBUTES = %w[name description active].freeze
def success(**args)
sync_to_jira(args[:feature_flag])
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index dd09ecafb4f..a7be73aa04c 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -2,7 +2,7 @@
module Files
class MultiService < Files::BaseService
- UPDATE_FILE_ACTIONS = %w(update move delete chmod).freeze
+ UPDATE_FILE_ACTIONS = %w[update move delete chmod].freeze
def create_commit!
transformer = Lfs::FileTransformer.new(project, repository, @branch_name)
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index 9fa966bb8a8..c11917b92ec 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -3,15 +3,19 @@
module Files
class UpdateService < Files::BaseService
def create_commit!
- repository.update_file(current_user, @file_path, @file_content,
- message: @commit_message,
- branch_name: @branch_name,
- previous_path: @previous_path,
- author_email: @author_email,
- author_name: @author_name,
- start_project: @start_project,
- start_branch_name: @start_branch,
- execute_filemode: @execute_filemode)
+ repository.update_file(
+ current_user,
+ @file_path,
+ @file_content,
+ message: @commit_message,
+ branch_name: @branch_name,
+ previous_path: @previous_path,
+ author_email: @author_email,
+ author_name: @author_name,
+ start_project: @start_project,
+ start_branch_name: @start_branch,
+ execute_filemode: @execute_filemode
+ )
end
private
diff --git a/app/services/google_cloud/create_cloudsql_instance_service.rb b/app/services/google_cloud/create_cloudsql_instance_service.rb
index 8d040c6c908..9a1263f0796 100644
--- a/app/services/google_cloud/create_cloudsql_instance_service.rb
+++ b/app/services/google_cloud/create_cloudsql_instance_service.rb
@@ -17,26 +17,30 @@ module GoogleCloud
private
def create_cloud_instance
- google_api_client.create_cloudsql_instance(gcp_project_id,
- instance_name,
- root_password,
- database_version,
- region,
- tier)
+ google_api_client.create_cloudsql_instance(
+ gcp_project_id,
+ instance_name,
+ root_password,
+ database_version,
+ region,
+ tier
+ )
end
def trigger_instance_setup_worker
- GoogleCloud::CreateCloudsqlInstanceWorker.perform_in(WORKER_INTERVAL,
- current_user.id,
- project.id,
- {
- 'google_oauth2_token': google_oauth2_token,
- 'gcp_project_id': gcp_project_id,
- 'instance_name': instance_name,
- 'database_version': database_version,
- 'environment_name': environment_name,
- 'is_protected': protected?
- })
+ GoogleCloud::CreateCloudsqlInstanceWorker.perform_in(
+ WORKER_INTERVAL,
+ current_user.id,
+ project.id,
+ {
+ 'google_oauth2_token': google_oauth2_token,
+ 'gcp_project_id': gcp_project_id,
+ 'instance_name': instance_name,
+ 'database_version': database_version,
+ 'environment_name': environment_name,
+ 'is_protected': protected?
+ }
+ )
end
def protected?
diff --git a/app/services/google_cloud/fetch_google_ip_list_service.rb b/app/services/google_cloud/fetch_google_ip_list_service.rb
index f7739971603..54af841d002 100644
--- a/app/services/google_cloud/fetch_google_ip_list_service.rb
+++ b/app/services/google_cloud/fetch_google_ip_list_service.rb
@@ -18,9 +18,11 @@ module GoogleCloud
subnets = fetch_and_update_cache!
- Gitlab::AppJsonLogger.info(class: self.class.name,
- message: 'Successfully retrieved Google IP list',
- subnet_count: subnets.count)
+ Gitlab::AppJsonLogger.info(
+ class: self.class.name,
+ message: 'Successfully retrieved Google IP list',
+ subnet_count: subnets.count
+ )
success({ subnets: subnets })
rescue IpListNotRetrievedError => err
diff --git a/app/services/google_cloud/generate_pipeline_service.rb b/app/services/google_cloud/generate_pipeline_service.rb
index 95de1fa21b7..30c358687aa 100644
--- a/app/services/google_cloud/generate_pipeline_service.rb
+++ b/app/services/google_cloud/generate_pipeline_service.rb
@@ -71,8 +71,12 @@ module GoogleCloud
end
def pipeline_content(include_path)
- gitlab_ci_yml = ::Gitlab::Ci::Config::Yaml.load!(default_branch_gitlab_ci_yml || '{}')
- append_remote_include(gitlab_ci_yml, "https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/#{include_path}")
+ gitlab_ci_yml = ::Gitlab::Ci::Config::Yaml::Loader.new(default_branch_gitlab_ci_yml || '{}').load
+
+ append_remote_include(
+ gitlab_ci_yml.content,
+ "https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/#{include_path}"
+ )
end
def append_remote_include(gitlab_ci_yml, include_url)
diff --git a/app/services/gpg_keys/destroy_service.rb b/app/services/gpg_keys/destroy_service.rb
index 2e82509897e..c8ee90db9f6 100644
--- a/app/services/gpg_keys/destroy_service.rb
+++ b/app/services/gpg_keys/destroy_service.rb
@@ -2,9 +2,24 @@
module GpgKeys
class DestroyService < Keys::BaseService
+ BATCH_SIZE = 1000
+
def execute(key)
+ nullify_signatures(key)
key.destroy
end
+
+ private
+
+ # When a GPG key is deleted, the related signatures have their gpg_key_id column nullified
+ # However, when the number of signatures is large, then a timeout may happen
+ # The signatures are processed in batches before GPG key delete is attempted in order to
+ # avoid timeouts
+ def nullify_signatures(key)
+ key.gpg_signatures.each_batch(of: BATCH_SIZE) do |batch|
+ batch.update_all(gpg_key_id: nil)
+ end
+ end
end
end
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
index 0f74b2d9349..21d3c6499a0 100644
--- a/app/services/groups/create_service.rb
+++ b/app/services/groups/create_service.rb
@@ -35,10 +35,14 @@ module Groups
@group.build_chat_team(name: response['name'], team_id: response['id'])
end
- Group.transaction do
- if @group.save
- @group.add_owner(current_user)
- Integration.create_from_active_default_integrations(@group, :group_id)
+ Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
+ %w[routes redirect_routes], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424281'
+ ) do
+ Group.transaction do
+ if @group.save
+ @group.add_owner(current_user)
+ Integration.create_from_active_default_integrations(@group, :group_id)
+ end
end
end
diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb
index 45e8972213e..a896ca5cabc 100644
--- a/app/services/groups/destroy_service.rb
+++ b/app/services/groups/destroy_service.rb
@@ -14,8 +14,6 @@ module Groups
# TODO - add a policy check here https://gitlab.com/gitlab-org/gitlab/-/issues/353082
raise DestroyError, "You can't delete this group because you're blocked." if current_user.blocked?
- group.prepare_for_destroy
-
group.projects.includes(:project_feature).each do |project|
# Execute the destruction of the models immediately to ensure atomic cleanup.
success = ::Projects::DestroyService.new(project, current_user).execute
@@ -83,7 +81,10 @@ module Groups
# rubocop:disable CodeReuse/ActiveRecord
def destroy_group_bots
- bot_ids = group.members_and_requesters.joins(:user).merge(User.project_bot).pluck(:user_id)
+ bot_ids = group.members_and_requesters.joins(:user)
+ .merge(User.project_bot)
+ .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/422405')
+ .pluck(:user_id)
current_user_id = current_user.id
group.run_after_commit do
diff --git a/app/services/groups/ssh_certificates/create_service.rb b/app/services/groups/ssh_certificates/create_service.rb
new file mode 100644
index 00000000000..6890901c306
--- /dev/null
+++ b/app/services/groups/ssh_certificates/create_service.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Groups
+ module SshCertificates
+ class CreateService
+ def initialize(group, params)
+ @group = group
+ @params = params
+ end
+
+ def execute
+ key = params[:key]
+ fingerprint = generate_fingerprint(key)
+
+ return ServiceResponse.error(message: 'Group', reason: :forbidden) if group.has_parent?
+
+ # return a key error instead of fingerprint error, as the user has no knowledge of fingerprint.
+ unless fingerprint
+ return ServiceResponse.error(message: 'Validation failed: Invalid key',
+ reason: :unprocessable_entity)
+ end
+
+ result = group.ssh_certificates.create!(
+ key: key,
+ title: params[:title],
+ fingerprint: fingerprint
+ )
+
+ # title and key attributes are returned as [FILTERED]
+ # by config/application.rb#L181-233
+ # make attributes unfiltered by running find
+ ssh_certificate = group.ssh_certificates.find(result.id)
+ ServiceResponse.success(payload: ssh_certificate)
+
+ rescue ActiveRecord::RecordInvalid, ArgumentError => e
+ ServiceResponse.error(
+ message: e.message,
+ reason: :unprocessable_entity
+ )
+ end
+
+ private
+
+ attr_reader :group, :params
+
+ def generate_fingerprint(key)
+ Gitlab::SSHPublicKey.new(key).fingerprint_sha256&.delete_prefix('SHA256:')
+ end
+ end
+ end
+end
diff --git a/app/services/groups/ssh_certificates/destroy_service.rb b/app/services/groups/ssh_certificates/destroy_service.rb
new file mode 100644
index 00000000000..7a450d5bee6
--- /dev/null
+++ b/app/services/groups/ssh_certificates/destroy_service.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Groups
+ module SshCertificates
+ class DestroyService
+ def initialize(group, params)
+ @group = group
+ @params = params
+ end
+
+ def execute
+ ssh_certificate = group.ssh_certificates.find(params[:ssh_certificates_id])
+
+ ssh_certificate.destroy!
+ ServiceResponse.success
+
+ rescue ActiveRecord::RecordNotFound
+ ServiceResponse.error(
+ message: 'SSH Certificate not found',
+ reason: :not_found
+ )
+
+ rescue ActiveRecord::RecordNotDestroyed
+ ServiceResponse.error(
+ message: 'SSH Certificate could not be deleted',
+ reason: :method_not_allowed
+ )
+ end
+
+ private
+
+ attr_reader :group, :params
+ end
+ end
+end
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index 64256e43ce3..6b979308d26 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -60,13 +60,17 @@ module Groups
old_root_ancestor_id = @group.root_ancestor.id
was_root_group = @group.root?
- Group.transaction do
- update_group_attributes
- ensure_ownership
- update_integrations
- remove_issue_contacts(old_root_ancestor_id, was_root_group)
- update_crm_objects(was_root_group)
- remove_namespace_commit_emails(was_root_group)
+ Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
+ %w[routes redirect_routes], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424280'
+ ) do
+ Group.transaction do
+ update_group_attributes
+ ensure_ownership
+ update_integrations
+ remove_issue_contacts(old_root_ancestor_id, was_root_group)
+ update_crm_objects(was_root_group)
+ remove_namespace_commit_emails(was_root_group)
+ end
end
post_update_hooks(@updated_project_ids, old_root_ancestor_id)
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 7d0142fc067..d91e09d212a 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -47,10 +47,6 @@ module Groups
private
def valid_path_change?
- unless Feature.enabled?(:npm_package_registry_fix_group_path_validation)
- return valid_path_change_with_npm_packages?
- end
-
return true unless group.packages_feature_enabled?
return true if params[:path].blank?
return true if group.has_parent?
@@ -68,21 +64,6 @@ module Groups
false
end
- # TODO: delete this function along with npm_package_registry_fix_group_path_validation
- def valid_path_change_with_npm_packages?
- return true unless group.packages_feature_enabled?
- return true if params[:path].blank?
- return true if !group.has_parent? && group.path == params[:path]
-
- npm_packages = ::Packages::GroupPackagesFinder.new(current_user, group, package_type: :npm).execute
- if npm_packages.exists?
- group.errors.add(:path, s_('GroupSettings|cannot change when group contains projects with NPM packages'))
- return
- end
-
- true
- end
-
def before_assignment_hook(group, params)
@full_path_before = group.full_path
@path_before = group.path
diff --git a/app/services/import/bitbucket_server_service.rb b/app/services/import/bitbucket_server_service.rb
index 5d496dc7cc3..3d961780889 100644
--- a/app/services/import/bitbucket_server_service.rb
+++ b/app/services/import/bitbucket_server_service.rb
@@ -83,7 +83,7 @@ module Import
url,
allow_localhost: allow_local_requests?,
allow_local_network: allow_local_requests?,
- schemes: %w(http https)
+ schemes: %w[http https]
)
end
diff --git a/app/services/import/fogbugz_service.rb b/app/services/import/fogbugz_service.rb
index 9a8def43312..2f63e4e6fb7 100644
--- a/app/services/import/fogbugz_service.rb
+++ b/app/services/import/fogbugz_service.rb
@@ -88,7 +88,7 @@ module Import
url,
allow_localhost: allow_local_requests?,
allow_local_network: allow_local_requests?,
- schemes: %w(http https)
+ schemes: %w[http https]
)
end
diff --git a/app/services/import/github/cancel_project_import_service.rb b/app/services/import/github/cancel_project_import_service.rb
index 62cd0c95eaf..740b9e5c2e7 100644
--- a/app/services/import/github/cancel_project_import_service.rb
+++ b/app/services/import/github/cancel_project_import_service.rb
@@ -7,13 +7,13 @@ module Import
return error('Not Found', :not_found) unless authorized_to_read?
return error('Unauthorized access', :forbidden) unless authorized_to_cancel?
- if project.import_in_progress?
+ if project.import_state.completed?
+ error(cannot_cancel_error_message, :bad_request)
+ else
project.import_state.cancel
metrics.track_canceled_import
success(project: project)
- else
- error(cannot_cancel_error_message, :bad_request)
end
end
diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb
index df255a7ae24..73e0c229a9c 100644
--- a/app/services/import/github_service.rb
+++ b/app/services/import/github_service.rb
@@ -91,7 +91,7 @@ module Import
url,
allow_localhost: allow_local_requests?,
allow_local_network: allow_local_requests?,
- schemes: %w(http https)
+ schemes: %w[http https]
)
end
diff --git a/app/services/import/gitlab_projects/file_acquisition_strategies/remote_file.rb b/app/services/import/gitlab_projects/file_acquisition_strategies/remote_file.rb
index e30818cc5d2..ed99d20d67f 100644
--- a/app/services/import/gitlab_projects/file_acquisition_strategies/remote_file.rb
+++ b/app/services/import/gitlab_projects/file_acquisition_strategies/remote_file.rb
@@ -11,7 +11,7 @@ module Import
end
validates :file_url, addressable_url: {
- schemes: %w(https),
+ schemes: %w[https],
allow_localhost: allow_local_requests?,
allow_local_network: allow_local_requests?,
dns_rebind_protection: true
diff --git a/app/services/import/gitlab_projects/file_acquisition_strategies/remote_file_s3.rb b/app/services/import/gitlab_projects/file_acquisition_strategies/remote_file_s3.rb
index 7599343d4e1..57ed717b966 100644
--- a/app/services/import/gitlab_projects/file_acquisition_strategies/remote_file_s3.rb
+++ b/app/services/import/gitlab_projects/file_acquisition_strategies/remote_file_s3.rb
@@ -13,7 +13,7 @@ module Import
validates_presence_of :region, :bucket_name, :file_key, :access_key_id, :secret_access_key
validates :file_url, addressable_url: {
- schemes: %w(https),
+ schemes: %w[https],
allow_localhost: allow_local_requests?,
allow_local_network: allow_local_requests?,
dns_rebind_protection: true
diff --git a/app/services/import/validate_remote_git_endpoint_service.rb b/app/services/import/validate_remote_git_endpoint_service.rb
index 2886bd5c9b7..a994072c4aa 100644
--- a/app/services/import/validate_remote_git_endpoint_service.rb
+++ b/app/services/import/validate_remote_git_endpoint_service.rb
@@ -8,7 +8,7 @@ module Import
GIT_SERVICE_NAME = "git-upload-pack"
GIT_EXPECTED_FIRST_PACKET_LINE = "# service=#{GIT_SERVICE_NAME}"
- GIT_BODY_MESSAGE_REGEXP = /^[0-9a-f]{4}#{GIT_EXPECTED_FIRST_PACKET_LINE}/.freeze
+ GIT_BODY_MESSAGE_REGEXP = /^[0-9a-f]{4}#{GIT_EXPECTED_FIRST_PACKET_LINE}/
# https://github.com/git/git/blob/master/Documentation/technical/protocol-common.txt#L56-L59
GIT_PROTOCOL_PKT_LEN = 4
GIT_MINIMUM_RESPONSE_LENGTH = GIT_PROTOCOL_PKT_LEN + GIT_EXPECTED_FIRST_PACKET_LINE.length
diff --git a/app/services/import_export_clean_up_service.rb b/app/services/import_export_clean_up_service.rb
index 567ac065cf7..5ee2f70ec4c 100644
--- a/app/services/import_export_clean_up_service.rb
+++ b/app/services/import_export_clean_up_service.rb
@@ -60,7 +60,7 @@ class ImportExportCleanUpService
end
def directories_cmd
- %W(find #{path} -mindepth #{DIR_DEPTH} -maxdepth #{DIR_DEPTH} -type d -not -path #{path} -mmin +#{mmin})
+ %W[find #{path} -mindepth #{DIR_DEPTH} -maxdepth #{DIR_DEPTH} -type d -not -path #{path} -mmin +#{mmin}]
end
def logger
diff --git a/app/services/incident_management/pager_duty/create_incident_issue_service.rb b/app/services/incident_management/pager_duty/create_incident_issue_service.rb
index 0c9ca2c0add..58c3a062910 100644
--- a/app/services/incident_management/pager_duty/create_incident_issue_service.rb
+++ b/app/services/incident_management/pager_duty/create_incident_issue_service.rb
@@ -6,7 +6,7 @@ module IncidentManagement
include IncidentManagement::Settings
def initialize(project, incident_payload)
- super(project, User.alert_bot, incident_payload)
+ super(project, Users::Internal.alert_bot, incident_payload)
end
def execute
diff --git a/app/services/incident_management/pager_duty/process_webhook_service.rb b/app/services/incident_management/pager_duty/process_webhook_service.rb
index 3ce2674616e..6f779bfdb18 100644
--- a/app/services/incident_management/pager_duty/process_webhook_service.rb
+++ b/app/services/incident_management/pager_duty/process_webhook_service.rb
@@ -10,7 +10,7 @@ module IncidentManagement
PAGER_DUTY_PAYLOAD_SIZE_LIMIT = 55.kilobytes
# https://developer.pagerduty.com/docs/db0fa8c8984fc-overview#event-types
- PAGER_DUTY_PROCESSABLE_EVENT_TYPES = %w(incident.triggered).freeze
+ PAGER_DUTY_PROCESSABLE_EVENT_TYPES = %w[incident.triggered].freeze
def initialize(project, payload)
super(project: project)
diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb
index 166452968f4..e996aecdf97 100644
--- a/app/services/issuable/bulk_update_service.rb
+++ b/app/services/issuable/bulk_update_service.rb
@@ -43,7 +43,7 @@ module Issuable
end
def permitted_attrs(type)
- attrs = %i(state_event milestone_id add_label_ids remove_label_ids subscription_event)
+ attrs = %i[state_event milestone_id add_label_ids remove_label_ids subscription_event]
if type == 'issue'
attrs.push(:assignee_ids, :confidential)
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 3b007d4dba7..27cfaef2db2 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -169,7 +169,7 @@ class IssuableBaseService < ::BaseContainerService
params[:incident_management_issuable_escalation_status_attributes] = result[:escalation_status]
end
- def process_label_ids(attributes, existing_label_ids: nil, extra_label_ids: [])
+ def process_label_ids(attributes, issuable:, existing_label_ids: nil, extra_label_ids: []) # rubocop:disable Lint/UnusedMethodArgument
label_ids = attributes.delete(:label_ids)
add_label_ids = attributes.delete(:add_label_ids)
remove_label_ids = attributes.delete(:remove_label_ids)
@@ -180,15 +180,29 @@ class IssuableBaseService < ::BaseContainerService
new_label_ids |= add_label_ids if add_label_ids
new_label_ids -= remove_label_ids if remove_label_ids
- new_label_ids.uniq
+ filter_locked_labels(issuable, new_label_ids.uniq, existing_label_ids)
+ end
+
+ # Filter out any locked labels that are attempting to be removed
+ def filter_locked_labels(issuable, ids, existing_label_ids)
+ return ids unless issuable.supports_lock_on_merge?
+ return ids unless existing_label_ids.present?
+
+ removed_label_ids = existing_label_ids - ids
+ removed_locked_label_ids = labels_service.filter_locked_label_ids(removed_label_ids)
+
+ ids + removed_locked_label_ids
end
def process_assignee_ids(attributes, existing_assignee_ids: nil, extra_assignee_ids: [])
- process = Issuable::ProcessAssignees.new(assignee_ids: attributes.delete(:assignee_ids),
- add_assignee_ids: attributes.delete(:add_assignee_ids),
- remove_assignee_ids: attributes.delete(:remove_assignee_ids),
- existing_assignee_ids: existing_assignee_ids,
- extra_assignee_ids: extra_assignee_ids)
+ process = Issuable::ProcessAssignees.new(
+ assignee_ids: attributes.delete(:assignee_ids),
+ add_assignee_ids: attributes.delete(:add_assignee_ids),
+ remove_assignee_ids: attributes.delete(:remove_assignee_ids),
+ existing_assignee_ids: existing_assignee_ids,
+ extra_assignee_ids: extra_assignee_ids
+ )
+
process.execute
end
@@ -221,7 +235,7 @@ class IssuableBaseService < ::BaseContainerService
params.delete(:state_event)
params[:author] ||= current_user
- params[:label_ids] = process_label_ids(params, extra_label_ids: issuable.label_ids.to_a)
+ params[:label_ids] = process_label_ids(params, issuable: issuable, extra_label_ids: issuable.label_ids.to_a)
if issuable.respond_to?(:assignee_ids)
params[:assignee_ids] = process_assignee_ids(params, extra_assignee_ids: issuable.assignee_ids.to_a)
@@ -373,9 +387,11 @@ class IssuableBaseService < ::BaseContainerService
filter_params(issuable)
if issuable.changed? || params.present?
- issuable.assign_attributes(params.merge(updated_by: current_user,
- last_edited_at: Time.current,
- last_edited_by: current_user))
+ issuable.assign_attributes(params.merge(
+ updated_by: current_user,
+ last_edited_at: Time.current,
+ last_edited_by: current_user
+ ))
before_update(issuable, skip_spam_check: true)
@@ -404,10 +420,13 @@ class IssuableBaseService < ::BaseContainerService
update_task_params = params.delete(:update_task)
return unless update_task_params
- tasklist_toggler = TaskListToggleService.new(issuable.description, issuable.description_html,
- line_source: update_task_params[:line_source],
- line_number: update_task_params[:line_number].to_i,
- toggle_as_checked: update_task_params[:checked])
+ tasklist_toggler = TaskListToggleService.new(
+ issuable.description,
+ issuable.description_html,
+ line_source: update_task_params[:line_source],
+ line_number: update_task_params[:line_number].to_i,
+ toggle_as_checked: update_task_params[:checked]
+ )
unless tasklist_toggler.execute
# if we make it here, the data is much newer than we thought it was - fail fast
@@ -469,7 +488,7 @@ class IssuableBaseService < ::BaseContainerService
# rubocop: enable CodeReuse/ActiveRecord
def assign_requested_labels(issuable)
- label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
+ label_ids = process_label_ids(params, issuable: issuable, existing_label_ids: issuable.label_ids)
return unless ids_changing?(issuable.label_ids, label_ids)
params[:label_ids] = label_ids
diff --git a/app/services/issuable_links/create_service.rb b/app/services/issuable_links/create_service.rb
index 533e92f6225..761ba3f74aa 100644
--- a/app/services/issuable_links/create_service.rb
+++ b/app/services/issuable_links/create_service.rb
@@ -76,13 +76,13 @@ module IssuableLinks
target_issuables.map do |referenced_object|
link = relate_issuables(referenced_object)
- if link.valid?
- after_create_for(link)
- else
+ if link.errors.any?
@errors << _("%{ref} cannot be added: %{error}") % {
ref: referenced_object.to_reference,
error: link.errors.messages.values.flatten.to_sentence
}
+ else
+ after_create_for(link)
end
link
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index b9b7cd08b68..a5ae5854e33 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -21,7 +21,7 @@ module Issues
Issues::CloseService
end
- NO_REBALANCING_NEEDED = ((RelativePositioning::MIN_POSITION * 0.9999)..(RelativePositioning::MAX_POSITION * 0.9999)).freeze
+ NO_REBALANCING_NEEDED = ((RelativePositioning::MIN_POSITION * 0.9999)..(RelativePositioning::MAX_POSITION * 0.9999))
def rebalance_if_needed(issue)
return unless issue
@@ -111,9 +111,6 @@ module Issues
issue.namespace.execute_integrations(issue_data, hooks_scope)
execute_incident_hooks(issue, issue_data) if issue.work_item_type&.incident?
-
- return unless Feature.enabled?(:group_mentions, issue.project)
-
execute_group_mention_hooks(issue, issue_data) if action == 'open'
end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index f848a8db12a..ef43e707a21 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -6,10 +6,12 @@ module Issues
def execute(issue, commit: nil, notifications: true, system_note: true, skip_authorization: false)
return issue unless can_close?(issue, skip_authorization: skip_authorization)
- close_issue(issue,
- closed_via: commit,
- notifications: notifications,
- system_note: system_note)
+ close_issue(
+ issue,
+ closed_via: commit,
+ notifications: notifications,
+ system_note: system_note
+ )
end
# Closes the supplied issue without checking if the user is authorized to
@@ -86,7 +88,7 @@ module Issues
issue = alert.issue
if alert.resolve
- SystemNoteService.change_alert_status(alert, User.alert_bot, " because #{current_user.to_reference} closed incident #{issue.to_reference(project)}")
+ SystemNoteService.change_alert_status(alert, Users::Internal.alert_bot, " because #{current_user.to_reference} closed incident #{issue.to_reference(project)}")
else
Gitlab::AppLogger.warn(
message: 'Cannot resolve an associated Alert Management alert',
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index e1ddfe47439..c828c156d50 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -7,7 +7,7 @@ module Issues
include ::Services::ReturnServiceResponses
rate_limit key: :issues_create,
- opts: { scope: [:project, :current_user, :external_author] }
+ opts: { scope: [:project, :current_user, :external_author] }
def initialize(container:, current_user: nil, params: {}, build_service: nil, perform_spam_check: true)
@extra_params = params.delete(:extra_params) || {}
@@ -90,9 +90,12 @@ module Issues
def resolve_discussions_with_issue(issue)
return if discussions_to_resolve.empty?
- Discussions::ResolveService.new(project, current_user,
- one_or_more_discussions: discussions_to_resolve,
- follow_up_issue: issue).execute
+ Discussions::ResolveService.new(
+ project,
+ current_user,
+ one_or_more_discussions: discussions_to_resolve,
+ follow_up_issue: issue
+ ).execute
end
private
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index e26e3d0835b..c3ddf7b6709 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -141,15 +141,23 @@ module Issues
end
def add_note_from
- SystemNoteService.noteable_moved(new_entity, target_project,
- original_entity, current_user,
- direction: :from)
+ SystemNoteService.noteable_moved(
+ new_entity,
+ target_project,
+ original_entity,
+ current_user,
+ direction: :from
+ )
end
def add_note_to
- SystemNoteService.noteable_moved(original_entity, old_project,
- new_entity, current_user,
- direction: :to)
+ SystemNoteService.noteable_moved(
+ original_entity,
+ old_project,
+ new_entity,
+ current_user,
+ direction: :to
+ )
end
end
end
diff --git a/app/services/issues/relative_position_rebalancing_service.rb b/app/services/issues/relative_position_rebalancing_service.rb
index a8d0ae01176..e165cb36634 100644
--- a/app/services/issues/relative_position_rebalancing_service.rb
+++ b/app/services/issues/relative_position_rebalancing_service.rb
@@ -141,7 +141,7 @@ module Issues
def run_update_query(values, query_name)
Issue.connection.exec_query(<<~SQL, query_name)
- WITH cte(cte_id, new_pos) AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} (
+ WITH cte(cte_id, new_pos) AS MATERIALIZED (
SELECT *
FROM (VALUES #{values}) as t (id, pos)
)
diff --git a/app/services/labels/available_labels_service.rb b/app/services/labels/available_labels_service.rb
index 21f92eeaf09..7809f263eb6 100644
--- a/app/services/labels/available_labels_service.rb
+++ b/app/services/labels/available_labels_service.rb
@@ -40,19 +40,8 @@ module Labels
ids.map(&:to_i) & existing_ids
end
- def filter_locked_labels_ids_in_param(key)
- ids = Array.wrap(params[key])
- return [] if ids.empty?
-
- params = finder_params
- params[:locked_labels] = true
- existing_labels = LabelsFinder.new(current_user, params).execute
-
- # rubocop:disable CodeReuse/ActiveRecord
- existing_ids = existing_labels.id_in(ids).pluck(:id)
- # rubocop:enable CodeReuse/ActiveRecord
-
- ids.map(&:to_i) & existing_ids
+ def filter_locked_label_ids(ids)
+ available_labels.with_lock_on_merge.id_in(ids).pluck(:id) # rubocop:disable CodeReuse/ActiveRecord
end
def available_labels
diff --git a/app/services/labels/create_service.rb b/app/services/labels/create_service.rb
index 675439b2f64..c69b9bd8de7 100644
--- a/app/services/labels/create_service.rb
+++ b/app/services/labels/create_service.rb
@@ -13,9 +13,7 @@ module Labels
project_or_group = target_params[:project] || target_params[:group]
if project_or_group.present?
- if Feature.disabled?(:enforce_locked_labels_on_merge, project_or_group, type: :ops)
- params.delete(:lock_on_merge)
- end
+ params.delete(:lock_on_merge) unless project_or_group.supports_lock_on_merge?
project_or_group.labels.create(params)
elsif target_params[:template]
diff --git a/app/services/labels/update_service.rb b/app/services/labels/update_service.rb
index 4ac54959e84..0ffb0fabf21 100644
--- a/app/services/labels/update_service.rb
+++ b/app/services/labels/update_service.rb
@@ -21,8 +21,12 @@ module Labels
def allow_lock_on_merge?(label)
return if label.template?
return unless label.respond_to?(:parent_container)
+ return unless label.parent_container.supports_lock_on_merge?
- Feature.enabled?(:enforce_locked_labels_on_merge, label.parent_container, type: :ops)
+ # If we've made it here, then we're allowed to turn it on. However, we do _not_
+ # want to allow it to be turned off. So if it's already set, then don't allow the possibility
+ # that it could be turned off.
+ !label.lock_on_merge
end
end
end
diff --git a/app/services/loose_foreign_keys/process_deleted_records_service.rb b/app/services/loose_foreign_keys/process_deleted_records_service.rb
index 8700276c982..e0c9c19f5b9 100644
--- a/app/services/loose_foreign_keys/process_deleted_records_service.rb
+++ b/app/services/loose_foreign_keys/process_deleted_records_service.rb
@@ -4,13 +4,13 @@ module LooseForeignKeys
class ProcessDeletedRecordsService
BATCH_SIZE = 1000
- def initialize(connection:)
+ def initialize(connection:, modification_tracker: LooseForeignKeys::ModificationTracker.new)
@connection = connection
+ @modification_tracker = modification_tracker
end
def execute
raised_error = false
- modification_tracker = ModificationTracker.new
tracked_tables.cycle do |table|
records = load_batch_for_table(table)
@@ -54,7 +54,7 @@ module LooseForeignKeys
private
- attr_reader :connection
+ attr_reader :connection, :modification_tracker
def db_config_name
::Gitlab::Database.db_config_name(connection)
diff --git a/app/services/members/creator_service.rb b/app/services/members/creator_service.rb
index a6fff3003ac..cc18aae7446 100644
--- a/app/services/members/creator_service.rb
+++ b/app/services/members/creator_service.rb
@@ -39,31 +39,34 @@ module Members
sources = Array.wrap(sources) if sources.is_a?(ApplicationRecord) # For single source
- Member.transaction do
- sources.flat_map do |source|
- # If this user is attempting to manage Owner members and doesn't have permission, do not allow
- if managing_owners?(args[:current_user], access_level) && cannot_manage_owners?(source, args[:current_user])
- next []
+ Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
+ %w[users user_preferences user_details emails identities], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424276'
+ ) do
+ Member.transaction do
+ sources.flat_map do |source|
+ # If this user is attempting to manage Owner members and doesn't have permission, do not allow
+ current_user = args[:current_user]
+ next [] if managing_owners?(current_user, access_level) && cannot_manage_owners?(source, current_user)
+
+ emails, users, existing_members = parse_users_list(source, invitees)
+
+ common_arguments = {
+ source: source,
+ access_level: access_level,
+ existing_members: existing_members,
+ tasks_to_be_done: args[:tasks_to_be_done] || []
+ }.merge(parsed_args(args))
+
+ members = emails.map do |email|
+ new(invitee: email, builder: InviteMemberBuilder, **common_arguments).execute
+ end
+
+ members += users.map do |user|
+ new(invitee: user, **common_arguments).execute
+ end
+
+ members
end
-
- emails, users, existing_members = parse_users_list(source, invitees)
-
- common_arguments = {
- source: source,
- access_level: access_level,
- existing_members: existing_members,
- tasks_to_be_done: args[:tasks_to_be_done] || []
- }.merge(parsed_args(args))
-
- members = emails.map do |email|
- new(invitee: email, builder: InviteMemberBuilder, **common_arguments).execute
- end
-
- members += users.map do |user|
- new(invitee: user, **common_arguments).execute
- end
-
- members
end
end
end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index e432016795d..d4cc60c6de0 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -47,6 +47,10 @@ module Members
def enqueue_jobs_that_needs_to_be_run_only_once_per_hierarchy(member, unassign_issuables)
return if recursive_call?
+ enqueue_cleanup_jobs_once_per_heirarchy(member, unassign_issuables)
+ end
+
+ def enqueue_cleanup_jobs_once_per_heirarchy(member, unassign_issuables)
enqueue_delete_todos(member)
enqueue_unassign_issuables(member) if unassign_issuables
end
diff --git a/app/services/merge_requests/approval_service.rb b/app/services/merge_requests/approval_service.rb
index 8560a15b7c4..dbe5567cbc5 100644
--- a/app/services/merge_requests/approval_service.rb
+++ b/app/services/merge_requests/approval_service.rb
@@ -5,12 +5,17 @@ module MergeRequests
def execute(merge_request)
return unless eligible_for_approval?(merge_request)
- approval = merge_request.approvals.new(user: current_user)
+ approval = merge_request.approvals.new(
+ user: current_user,
+ patch_id_sha: fetch_patch_id_sha(merge_request)
+ )
return success unless save_approval(approval)
reset_approvals_cache(merge_request)
+
merge_request_activity_counter.track_approve_mr_action(user: current_user, merge_request: merge_request)
+
trigger_merge_request_merge_status_updated(merge_request)
trigger_merge_request_reviewers_updated(merge_request)
trigger_merge_request_approval_state_updated(merge_request)
@@ -31,6 +36,17 @@ 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/base_service.rb b/app/services/merge_requests/base_service.rb
index 0fc85675e49..f36cad7139a 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -36,10 +36,7 @@ module MergeRequests
merge_request.project.execute_integrations(merge_data, :merge_request_hooks)
execute_external_hooks(merge_request, merge_data)
-
- if action == 'open' && Feature.enabled?(:group_mentions, merge_request.project)
- execute_group_mention_hooks(merge_request, merge_data)
- end
+ execute_group_mention_hooks(merge_request, merge_data) if action == 'open'
enqueue_jira_connect_messages_for(merge_request)
end
@@ -113,7 +110,7 @@ module MergeRequests
# Don't try to print expensive instance variables.
def inspect
- return "#<#{self.class}>" unless respond_to?(:merge_request)
+ return "#<#{self.class}>" unless respond_to?(:merge_request) && merge_request
"#<#{self.class} #{merge_request.to_reference(full: true)}>"
end
@@ -176,21 +173,10 @@ module MergeRequests
params.delete(:allow_collaboration)
end
- filter_locked_labels(merge_request)
filter_reviewer(merge_request)
filter_suggested_reviewers
end
- # Filter out any locked labels that are requested to be removed.
- # Only supported for merged MRs.
- def filter_locked_labels(merge_request)
- return unless params[:remove_label_ids].present?
- return unless merge_request.merged?
- return unless Feature.enabled?(:enforce_locked_labels_on_merge, merge_request.project, type: :ops)
-
- params[:remove_label_ids] -= labels_service.filter_locked_labels_ids_in_param(:remove_label_ids)
- end
-
def filter_reviewer(merge_request)
return if params[:reviewer_ids].blank?
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index b8853e8bcbc..bb347096274 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -74,7 +74,7 @@ module MergeRequests
# IssuableBaseService#process_label_ids and
# IssuableBaseService#process_assignee_ids take care
# of the removal.
- params[:label_ids] = process_label_ids(params, extra_label_ids: merge_request.label_ids.to_a)
+ params[:label_ids] = process_label_ids(params, issuable: merge_request, extra_label_ids: merge_request.label_ids.to_a)
params[:assignee_ids] = process_assignee_ids(params, extra_assignee_ids: merge_request.assignee_ids.to_a)
@@ -130,10 +130,14 @@ module MergeRequests
if source_branch_default? && !target_branch_specified?
merge_request.target_branch = nil
else
- merge_request.target_branch ||= target_project.default_branch
+ merge_request.target_branch ||= get_target_branch
end
end
+ def get_target_branch
+ target_project.default_branch
+ end
+
def source_branch_specified?
params[:source_branch].present?
end
diff --git a/app/services/merge_requests/create_ref_service.rb b/app/services/merge_requests/create_ref_service.rb
index e0f10183bac..eae6845335a 100644
--- a/app/services/merge_requests/create_ref_service.rb
+++ b/app/services/merge_requests/create_ref_service.rb
@@ -9,101 +9,117 @@ module MergeRequests
CreateRefError = Class.new(StandardError)
def initialize(
- current_user:, merge_request:, target_ref:, first_parent_ref:,
- source_sha: nil, merge_commit_message: nil)
-
+ current_user:, merge_request:, target_ref:, first_parent_ref:, source_sha: nil
+ )
@current_user = current_user
@merge_request = merge_request
- @initial_source_sha = source_sha
+ @source_sha = source_sha
@target_ref = target_ref
- @merge_commit_message = merge_commit_message
+ @first_parent_ref = first_parent_ref
@first_parent_sha = target_project.commit(first_parent_ref)&.sha
end
def execute
- commit_sha = initial_source_sha # the SHA to be at HEAD of target_ref
- source_sha = initial_source_sha # the SHA to be the merged result of the source (minus the merge commit)
- expected_old_oid = "" # the SHA we expect target_ref to be at prior to an update (an optimistic lock)
-
# TODO: Update this message with the removal of FF merge_trains_create_ref_service and update tests
# This is for compatibility with MergeToRefService during the rollout.
return ServiceResponse.error(message: '3:Invalid merge source') unless first_parent_sha.present?
- commit_sha, source_sha, expected_old_oid = maybe_squash!(commit_sha, source_sha, expected_old_oid)
- commit_sha, source_sha, expected_old_oid = maybe_rebase!(commit_sha, source_sha, expected_old_oid)
- commit_sha, source_sha = maybe_merge!(commit_sha, source_sha, expected_old_oid)
-
- ServiceResponse.success(
- payload: {
- commit_sha: commit_sha,
- target_sha: first_parent_sha,
- source_sha: source_sha
- }
- )
+ result = {
+ commit_sha: source_sha, # the SHA to be at HEAD of target_ref
+ expected_old_oid: "", # the SHA we expect target_ref to be at prior to an update (an optimistic lock)
+ source_sha: source_sha, # for pipeline.source_sha
+ target_sha: first_parent_sha # for pipeline.target_sha
+ }
+
+ result = maybe_squash!(**result)
+ result = maybe_rebase!(**result)
+ result = maybe_merge!(**result)
+
+ update_merge_request!(merge_request, result)
+
+ ServiceResponse.success(payload: result)
rescue CreateRefError => error
ServiceResponse.error(message: error.message)
end
private
- attr_reader :current_user, :merge_request, :target_ref, :first_parent_sha, :initial_source_sha
+ attr_reader :current_user, :merge_request, :target_ref, :first_parent_ref, :first_parent_sha, :source_sha
delegate :target_project, to: :merge_request
delegate :repository, to: :target_project
- def maybe_squash!(commit_sha, source_sha, expected_old_oid)
+ def maybe_squash!(commit_sha:, **rest)
if merge_request.squash_on_merge?
squash_result = MergeRequests::SquashService.new(
merge_request: merge_request,
current_user: current_user,
commit_message: squash_commit_message
).execute
+
raise CreateRefError, squash_result[:message] if squash_result[:status] == :error
commit_sha = squash_result[:squash_sha]
- source_sha = commit_sha
+ squash_commit_sha = commit_sha
end
# squash does not overwrite target_ref, so expected_old_oid remains the same
- [commit_sha, source_sha, expected_old_oid]
+ rest.merge(
+ commit_sha: commit_sha,
+ squash_commit_sha: squash_commit_sha
+ ).compact
end
- def maybe_rebase!(commit_sha, source_sha, expected_old_oid)
+ def maybe_rebase!(commit_sha:, expected_old_oid:, squash_commit_sha: nil, **rest)
if target_project.ff_merge_must_be_possible?
commit_sha = safe_gitaly_operation do
repository.rebase_to_ref(
current_user,
- source_sha: source_sha,
+ source_sha: commit_sha,
target_ref: target_ref,
- first_parent_ref: first_parent_sha
+ first_parent_ref: first_parent_sha,
+ expected_old_oid: expected_old_oid || ""
)
end
- source_sha = commit_sha
+ squash_commit_sha = commit_sha if squash_commit_sha # rebase rewrites commit SHAs after first_parent_sha
expected_old_oid = commit_sha
end
- [commit_sha, source_sha, expected_old_oid]
+ rest.merge(
+ commit_sha: commit_sha,
+ squash_commit_sha: squash_commit_sha,
+ expected_old_oid: expected_old_oid
+ ).compact
end
- def maybe_merge!(commit_sha, source_sha, expected_old_oid)
+ def maybe_merge!(commit_sha:, expected_old_oid:, **rest)
unless target_project.merge_requests_ff_only_enabled
commit_sha = safe_gitaly_operation do
repository.merge_to_ref(
current_user,
- source_sha: source_sha,
+ source_sha: commit_sha,
target_ref: target_ref,
message: merge_commit_message,
first_parent_ref: first_parent_sha,
branch: nil,
- expected_old_oid: expected_old_oid
+ expected_old_oid: expected_old_oid || ""
)
end
- commit = target_project.commit(commit_sha)
- _, source_sha = commit.parent_ids
+
+ expected_old_oid = commit_sha
+ merge_commit_sha = commit_sha
end
- [commit_sha, source_sha]
+ rest.merge(
+ commit_sha: commit_sha,
+ merge_commit_sha: merge_commit_sha,
+ expected_old_oid: expected_old_oid
+ ).compact
+ end
+
+ def update_merge_request!(merge_request, result)
+ # overridden in EE
end
def safe_gitaly_operation
@@ -119,12 +135,10 @@ module MergeRequests
strong_memoize_attr :squash_commit_message
def merge_commit_message
- return @merge_commit_message if @merge_commit_message.present?
-
- @merge_commit_message = (
- merge_request.merge_params['commit_message'].presence ||
+ merge_request.merge_params['commit_message'].presence ||
merge_request.default_merge_commit_message(user: current_user)
- )
end
end
end
+
+MergeRequests::CreateRefService.prepend_mod_with('MergeRequests::CreateRefService')
diff --git a/app/services/merge_requests/ff_merge_service.rb b/app/services/merge_requests/ff_merge_service.rb
deleted file mode 100644
index 1a83bbf9de6..00000000000
--- a/app/services/merge_requests/ff_merge_service.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-module MergeRequests
- # MergeService class
- #
- # Do git fast-forward merge and in case of success
- # mark merge request as merged and execute all hooks and notifications
- # Executed when you do fast-forward merge via GitLab UI
- #
- class FfMergeService < MergeRequests::MergeService
- extend ::Gitlab::Utils::Override
-
- private
-
- override :execute_git_merge
- def execute_git_merge
- repository.ff_merge(
- current_user,
- source,
- merge_request.target_branch,
- merge_request: merge_request
- )
- end
-
- override :merge_success_data
- def merge_success_data(commit_id)
- # There is no merge commit to update, so this is just blank.
- {}
- end
- end
-end
diff --git a/app/services/merge_requests/merge_base_service.rb b/app/services/merge_requests/merge_base_service.rb
index fa0a4f808e2..0c8795cfd61 100644
--- a/app/services/merge_requests/merge_base_service.rb
+++ b/app/services/merge_requests/merge_base_service.rb
@@ -18,24 +18,8 @@ module MergeRequests
# No-op
end
- def source
- strong_memoize(:source) do
- if merge_request.squash_on_merge?
- squash_sha!
- else
- merge_request.diff_head_sha
- end
- end
- end
-
private
- def check_source
- unless source
- raise_error('No source for merge')
- end
- end
-
# Overridden in EE.
def check_size_limit
# No-op
@@ -53,26 +37,6 @@ module MergeRequests
def handle_merge_error(*args)
# No-op
end
-
- def commit_message
- params[:commit_message] ||
- merge_request.default_merge_commit_message(user: current_user)
- end
-
- def squash_sha!
- squash_result = ::MergeRequests::SquashService.new(
- merge_request: merge_request,
- current_user: current_user,
- commit_message: params[:squash_commit_message]
- ).execute
-
- case squash_result[:status]
- when :success
- squash_result[:squash_sha]
- when :error
- raise ::MergeRequests::MergeService::MergeError, squash_result[:message]
- end
- end
end
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 1398a6dbb67..29aba3c8679 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -16,38 +16,6 @@ module MergeRequests
delegate :merge_jid, :state, to: :@merge_request
def execute(merge_request, options = {})
- return execute_v2(merge_request, options) if Feature.enabled?(:refactor_merge_service, project)
-
- if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService)
- FfMergeService.new(project: project, current_user: current_user, params: params).execute(merge_request)
- return
- end
-
- return if merge_request.merged?
- return unless exclusive_lease(merge_request.id).try_obtain
-
- @merge_request = merge_request
- @options = options
- jid = merge_jid
-
- validate!
-
- merge_request.in_locked_state do
- if commit
- after_merge
- clean_merge_jid
- success
- end
- end
-
- log_info("Merge process finished on JID #{jid} with state #{state}")
- rescue MergeError => e
- handle_merge_error(log_message: e.message, save_message_on_model: true)
- ensure
- exclusive_lease(merge_request.id).cancel
- end
-
- def execute_v2(merge_request, options = {})
return if merge_request.merged?
return unless exclusive_lease(merge_request.id).try_obtain
@@ -61,7 +29,7 @@ module MergeRequests
validate!
merge_request.in_locked_state do
- if commit_v2
+ if commit
after_merge
clean_merge_jid
success
@@ -90,28 +58,8 @@ module MergeRequests
end
end
- # Can remove this entire method when :refactor_merge_service is enabled
- def error_check!
- super
-
- return if Feature.enabled?(:refactor_merge_service, project)
-
- check_source
-
- error =
- if @merge_request.should_be_rebased?
- 'Only fast-forward merge is allowed for your project. Please update your source branch'
- elsif !@merge_request.mergeable?(skip_discussions_check: @options[:skip_discussions_check], check_mergeability_retry_lease: @options[:check_mergeability_retry_lease])
- 'Merge request is not mergeable'
- elsif !@merge_request.squash && project.squash_always?
- 'This project requires squashing commits when merge requests are accepted.'
- end
-
- raise_error(error) if error
- end
-
def validate_strategy!
- @merge_strategy.validate! if Feature.enabled?(:refactor_merge_service, project)
+ @merge_strategy.validate!
end
def updated_check!
@@ -121,7 +69,7 @@ module MergeRequests
end
end
- def commit_v2
+ def commit
log_info("Git merge started on JID #{merge_jid}")
merge_result = try_merge { @merge_strategy.execute_git_merge! }
@@ -131,7 +79,11 @@ module MergeRequests
log_info("Git merge finished on JID #{merge_jid} commit #{commit_sha}")
- new_merge_request_attributes = merge_result.slice(:merge_commit_sha, :squash_commit_sha)
+ new_merge_request_attributes = {
+ merged_commit_sha: commit_sha,
+ merge_commit_sha: merge_result[:merge_commit_sha],
+ squash_commit_sha: merge_result[:squash_commit_sha]
+ }.compact
merge_request.update!(new_merge_request_attributes) if new_merge_request_attributes.present?
commit_sha
@@ -140,35 +92,6 @@ module MergeRequests
log_info("Merge request marked in progress")
end
- def commit
- log_info("Git merge started on JID #{merge_jid}")
- commit_id = try_merge { execute_git_merge }
-
- if commit_id
- log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
- else
- raise_error(GENERIC_ERROR_MESSAGE)
- end
-
- update_merge_sha_metadata(commit_id)
-
- commit_id
- ensure
- merge_request.update_and_mark_in_progress_merge_commit_sha(nil)
- log_info("Merge request marked in progress")
- end
-
- def update_merge_sha_metadata(commit_id)
- data_to_update = merge_success_data(commit_id)
- data_to_update[:squash_commit_sha] = source if merge_request.squash_on_merge?
-
- merge_request.update!(**data_to_update) if data_to_update.present?
- end
-
- def merge_success_data(commit_id)
- { merge_commit_sha: commit_id }
- end
-
def try_merge
yield
rescue Gitlab::Git::PreReceiveError => e
@@ -178,10 +101,6 @@ module MergeRequests
raise_error(GENERIC_ERROR_MESSAGE)
end
- def execute_git_merge
- repository.merge(current_user, source, merge_request, commit_message)
- end
-
def after_merge
log_info("Post merge started on JID #{merge_jid} with state #{state}")
MergeRequests::PostMergeService.new(project: project, current_user: current_user).execute(merge_request)
diff --git a/app/services/merge_requests/merge_strategies/from_source_branch.rb b/app/services/merge_requests/merge_strategies/from_source_branch.rb
index 9fe5fc5160b..fe0e4d8a90c 100644
--- a/app/services/merge_requests/merge_strategies/from_source_branch.rb
+++ b/app/services/merge_requests/merge_strategies/from_source_branch.rb
@@ -28,7 +28,7 @@ module MergeRequests
check_mergeability_retry_lease: @options[:check_mergeability_retry_lease]
)
'Merge request is not mergeable'
- elsif !merge_request.squash && project.squash_always?
+ elsif merge_request.missing_required_squash?
'This project requires squashing commits when merge requests are accepted.'
end
@@ -110,3 +110,5 @@ module MergeRequests
end
end
end
+
+::MergeRequests::MergeStrategies::FromSourceBranch.prepend_mod
diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb
index 8b79feb5e0f..6e1b56d9651 100644
--- a/app/services/merge_requests/merge_to_ref_service.rb
+++ b/app/services/merge_requests/merge_to_ref_service.rb
@@ -31,14 +31,13 @@ module MergeRequests
private
- override :source
def source
merge_request.diff_head_sha
end
override :error_check!
def error_check!
- check_source
+ raise_error('No source for merge') unless source
end
##
@@ -55,6 +54,11 @@ module MergeRequests
params[:first_parent_ref] || merge_request.target_branch_ref
end
+ def commit_message
+ params[:commit_message] ||
+ merge_request.default_merge_commit_message(user: current_user)
+ end
+
def extracted_merge_to_ref
repository.merge_to_ref(current_user,
source_sha: source,
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 447f4f9428c..7a7d0dbfef2 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -94,7 +94,9 @@ module MergeRequests
)
merge_requests.each do |merge_request|
- merge_request.merge_commit_sha = analyzer.get_merge_commit(merge_request.diff_head_sha)
+ sha = analyzer.get_merge_commit(merge_request.diff_head_sha)
+ merge_request.merge_commit_sha = sha
+ merge_request.merged_commit_sha = sha
MergeRequests::PostMergeService
.new(project: merge_request.target_project, current_user: @current_user)
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 598dbaf93a9..c435048e343 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -94,7 +94,7 @@ module MergeRequests
end
def track_title_and_desc_edits(changed_fields)
- tracked_fields = %w(title description)
+ tracked_fields = %w[title description]
return unless changed_fields.any? { |field| tracked_fields.include?(field) }
diff --git a/app/services/metrics/global_metrics_update_service.rb b/app/services/metrics/global_metrics_update_service.rb
deleted file mode 100644
index 356de58ba2e..00000000000
--- a/app/services/metrics/global_metrics_update_service.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-module Metrics
- # Update metrics regarding GitLab instance wide
- #
- # Anything that is not specific to a machine, process, request or any other context
- # can be updated from this services.
- #
- # Examples of metrics that qualify:
- # * Global counters (instance users, instance projects...)
- # * State of settings stored in the database (whether a feature is active or not, tuning values...)
- #
- class GlobalMetricsUpdateService
- def execute
- return unless ::Gitlab::Metrics.prometheus_metrics_enabled?
-
- maintenance_mode_metric.set({}, (::Gitlab.maintenance_mode? ? 1 : 0))
- end
-
- def maintenance_mode_metric
- ::Gitlab::Metrics.gauge(:gitlab_maintenance_mode, 'Is GitLab Maintenance Mode enabled?')
- end
- end
-end
diff --git a/app/services/metrics/sample_metrics_service.rb b/app/services/metrics/sample_metrics_service.rb
deleted file mode 100644
index 9bf32b295e2..00000000000
--- a/app/services/metrics/sample_metrics_service.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-module Metrics
- class SampleMetricsService
- DIRECTORY = "sample_metrics"
-
- attr_reader :identifier, :range_minutes
-
- def initialize(identifier, range_start:, range_end:)
- @identifier = identifier
- @range_minutes = convert_range_minutes(range_start, range_end)
- end
-
- def query
- return unless identifier && File.exist?(file_location)
-
- query_interval
- end
-
- private
-
- def file_location
- sanitized_string = identifier.gsub(/[^0-9A-Za-z_]/, '')
- File.join(Rails.root, DIRECTORY, "#{sanitized_string}.yml")
- end
-
- def query_interval
- result = YAML.load_file(File.expand_path(file_location, __dir__))
- result[range_minutes]
- end
-
- def convert_range_minutes(range_start, range_end)
- ((range_end.to_time - range_start.to_time) / 1.minute).to_i
- end
- end
-end
diff --git a/app/services/namespaces/in_product_marketing_emails_service.rb b/app/services/namespaces/in_product_marketing_emails_service.rb
index 1ce7e4cae16..14e670126c6 100644
--- a/app/services/namespaces/in_product_marketing_emails_service.rb
+++ b/app/services/namespaces/in_product_marketing_emails_service.rb
@@ -44,108 +44,6 @@ module Namespaces
interval_days = TRACKS.dig(track.to_sym, :interval_days)
interval_days&.count || 0
end
-
- def self.send_for_all_tracks_and_intervals
- TRACKS.each_key do |track|
- TRACKS[track][:interval_days].each do |interval|
- new(track, interval).execute
- end
- end
- end
-
- def initialize(track, interval)
- @track = track
- @interval = interval
- @sent_email_records = ::Users::InProductMarketingEmailRecords.new
- end
-
- def execute
- raise ArgumentError, "Track #{track} not defined" unless TRACKS.key?(track)
-
- groups_for_track.each_batch do |groups|
- groups.each do |group|
- send_email_for_group(group)
- end
- end
- end
-
- private
-
- attr_reader :track, :interval, :sent_email_records
-
- def send_email(user, group)
- NotificationService.new.in_product_marketing(user.id, group.id, track, series)
- end
-
- def send_email_for_group(group)
- users_for_group(group).each do |user|
- if can_perform_action?(user, group)
- send_email(user, group)
- sent_email_records.add(user, track: track, series: series)
- end
- end
-
- sent_email_records.save!
- end
-
- def groups_for_track
- onboarding_progress_scope = Onboarding::Progress
- .completed_actions_with_latest_in_range(completed_actions, range)
- .incomplete_actions(incomplete_actions)
-
- # Filtering out sub-groups is a temporary fix to prevent calling
- # `.root_ancestor` on groups that are not root groups.
- # See https://gitlab.com/groups/gitlab-org/-/epics/5594 for more information.
- Group
- .top_most
- .with_onboarding_progress
- .merge(onboarding_progress_scope)
- .merge(subscription_scope)
- end
-
- def subscription_scope
- {}
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def users_for_group(group)
- group.users
- .where(email_opted_in: true)
- .merge(Users::InProductMarketingEmail.without_track_and_series(track, series))
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def can_perform_action?(user, group)
- case track
- when :create, :verify
- user.can?(:create_projects, group)
- when :trial, :trial_short
- user.can?(:start_trial, group)
- when :team, :team_short
- user.can?(:admin_group_member, group)
- when :admin_verify
- user.can?(:admin_group, group)
- when :experience
- true
- end
- end
-
- def completed_actions
- TRACKS[track][:completed_actions]
- end
-
- def range
- date = (interval + 1).days.ago
- date.beginning_of_day..date.end_of_day
- end
-
- def incomplete_actions
- TRACKS[track][:incomplete_actions]
- end
-
- def series
- TRACKS[track][:interval_days].index(interval)
- end
end
end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index fdab2a07990..1af26377b71 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -187,12 +187,12 @@ module Notes
namespace: project&.namespace,
user: user,
label: metric_key_path,
- context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis, key_path: metric_key_path).to_context]
+ context: [Gitlab::Usage::MetricDefinition.context_for(metric_key_path).to_context]
)
end
def tracking_data_for(note)
- label = Gitlab.ee? && note.author == User.visual_review_bot ? 'anonymous_visual_review_note' : 'note'
+ label = Gitlab.ee? && note.author == Users::Internal.visual_review_bot ? 'anonymous_visual_review_note' : 'note'
{
label: label,
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
index 9465b5218b0..6e92a887cdd 100644
--- a/app/services/notes/post_process_service.rb
+++ b/app/services/notes/post_process_service.rb
@@ -42,8 +42,6 @@ module Notes
note.project.execute_hooks(note_data, hooks_scope)
note.project.execute_integrations(note_data, hooks_scope)
- return unless Feature.enabled?(:group_mentions, note.project)
-
execute_group_mention_hooks(note, note_data, is_confidential)
end
diff --git a/app/services/notification_recipients/builder/base.rb b/app/services/notification_recipients/builder/base.rb
index 3fabec29c0d..afbf5747429 100644
--- a/app/services/notification_recipients/builder/base.rb
+++ b/app/services/notification_recipients/builder/base.rb
@@ -44,6 +44,7 @@ module NotificationRecipients
def add_recipients(users, type, reason)
if users.is_a?(ActiveRecord::Relation)
users = users.includes(:notification_settings)
+ .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/421821')
end
users = Array(users).compact
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 648067e3452..f1781b3d3c5 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -75,6 +75,19 @@ class NotificationService
end
end
+ def resource_access_tokens_about_to_expire(bot_user, token_names)
+ recipients = bot_user.resource_bot_owners.select { |owner| owner.can?(:receive_notifications) }
+ resource = bot_user.resource_bot_resource
+
+ recipients.each do |recipient|
+ mailer.resource_access_tokens_about_to_expire_email(
+ recipient,
+ resource,
+ token_names
+ ).deliver_later
+ end
+ end
+
# Notify the owner of the account when a new personal access token is created
def access_token_created(user, token_name)
return unless user.can?(:receive_notifications)
@@ -430,13 +443,14 @@ class NotificationService
def send_service_desk_notification(note)
return unless note.noteable_type == 'Issue'
return if note.confidential
+ return unless note.project.service_desk_enabled?
issue = note.noteable
recipients = issue.email_participants_emails
return unless recipients.any?
- support_bot = User.support_bot
+ support_bot = Users::Internal.support_bot
recipients.delete(issue.external_author) if note.author == support_bot
recipients.each do |recipient|
@@ -755,10 +769,6 @@ class NotificationService
end
end
- def in_product_marketing(user_id, group_id, track, series)
- mailer.in_product_marketing_email(user_id, group_id, track, series).deliver_later
- end
-
def approve_mr(merge_request, current_user)
approve_mr_email(merge_request, merge_request.target_project, current_user)
end
diff --git a/app/services/packages/debian/generate_distribution_service.rb b/app/services/packages/debian/generate_distribution_service.rb
index 9feb860ae87..05f6d9af581 100644
--- a/app/services/packages/debian/generate_distribution_service.rb
+++ b/app/services/packages/debian/generate_distribution_service.rb
@@ -12,7 +12,7 @@ module Packages
DEFAULT_LEASE_TIMEOUT = 1.hour.to_i.freeze
# From https://salsa.debian.org/ftp-team/dak/-/blob/991aaa27a7f7aa773bb9c0cf2d516e383d9cffa0/setup/core-init.d/080_metadatakeys#L9
- METADATA_KEYS = %w(
+ METADATA_KEYS = %w[
Package
Source
Binary
@@ -60,7 +60,7 @@ module Packages
Tag
Package-Type
Installer-Menu-Item
- ).freeze
+ ].freeze
def initialize(distribution)
@distribution = distribution
diff --git a/app/services/packages/npm/generate_metadata_service.rb b/app/services/packages/npm/generate_metadata_service.rb
index e1795079513..8eaac547f7e 100644
--- a/app/services/packages/npm/generate_metadata_service.rb
+++ b/app/services/packages/npm/generate_metadata_service.rb
@@ -4,6 +4,7 @@ module Packages
module Npm
class GenerateMetadataService
include API::Helpers::RelatedResourcesHelpers
+ include Gitlab::Utils::StrongMemoize
# Allowed fields are those defined in the abbreviated form
# defined here: https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
@@ -13,6 +14,8 @@ module Packages
def initialize(name, packages)
@name = name
@packages = packages
+ @dependencies = {}
+ @dependency_ids = Hash.new { |h, key| h[key] = {} }
end
def execute(only_dist_tags: false)
@@ -21,7 +24,7 @@ module Packages
private
- attr_reader :name, :packages
+ attr_reader :name, :packages, :dependencies, :dependency_ids
def metadata(only_dist_tags)
result = { dist_tags: dist_tags }
@@ -38,9 +41,11 @@ module Packages
package_versions = {}
packages.each_batch do |relation|
- batched_packages = relation.including_dependency_links
- .preload_files
- .preload_npm_metadatum
+ load_dependencies(relation)
+ load_dependency_ids(relation)
+
+ batched_packages = relation.preload_files
+ .preload_npm_metadatum
batched_packages.each do |package|
package_file = package.installable_package_files.last
@@ -82,15 +87,17 @@ module Packages
end
def build_package_dependencies(package)
- dependencies = Hash.new { |h, key| h[key] = {} }
-
- package.dependency_links.each do |dependency_link|
- dependency = dependency_link.dependency
- dependencies[dependency_link.dependency_type][dependency.name] = dependency.version_pattern
+ dependency_ids[package.id].each_with_object(Hash.new { |h, key| h[key] = {} }) do |(type, ids), memo|
+ ids.each do |id|
+ memo[inverted_dependency_types[type]].merge!(dependencies[id])
+ end
end
+ end
- dependencies
+ def inverted_dependency_types
+ Packages::DependencyLink.dependency_types.invert.stringify_keys
end
+ strong_memoize_attr :inverted_dependency_types
def sorted_versions
versions = packages.pluck_versions.compact
@@ -106,6 +113,31 @@ module Packages
json = package.npm_metadatum&.package_json || {}
json.slice(*PACKAGE_JSON_ALLOWED_FIELDS)
end
+
+ def load_dependencies(packages)
+ Packages::Dependency
+ .id_in(
+ Packages::DependencyLink
+ .for_packages(packages)
+ .select_dependency_id
+ )
+ .id_not_in(dependencies.keys)
+ .each_batch do |relation|
+ relation.each do |dependency|
+ dependencies[dependency.id] = { dependency.name => dependency.version_pattern }
+ end
+ end
+ end
+
+ def load_dependency_ids(packages)
+ Packages::DependencyLink
+ .dependency_ids_grouped_by_type(packages)
+ .each_batch(column: :package_id) do |relation|
+ relation.each do |dependency_link|
+ dependency_ids[dependency_link.package_id] = dependency_link.dependency_ids_by_type
+ end
+ end
+ end
end
end
end
diff --git a/app/services/packages/nuget/check_duplicates_service.rb b/app/services/packages/nuget/check_duplicates_service.rb
new file mode 100644
index 00000000000..7ad9038d7c1
--- /dev/null
+++ b/app/services/packages/nuget/check_duplicates_service.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module Packages
+ module Nuget
+ class CheckDuplicatesService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ ExtractionError = Class.new(StandardError)
+
+ def execute
+ return ServiceResponse.success if package_settings_allow_duplicates? || !target_package_is_duplicate?
+
+ ServiceResponse.error(
+ message: 'A package with the same name and version already exists',
+ reason: :conflict
+ )
+ rescue ExtractionError => e
+ ServiceResponse.error(message: e.message, reason: :bad_request)
+ end
+
+ private
+
+ def package_settings_allow_duplicates?
+ package_settings.nuget_duplicates_allowed? || package_settings.class.duplicates_allowed?(existing_package)
+ end
+
+ def target_package_is_duplicate?
+ existing_package.name.casecmp(metadata[:package_name]) == 0 &&
+ (existing_package.version.casecmp(metadata[:package_version]) == 0 ||
+ existing_package.normalized_nuget_version&.casecmp(metadata[:package_version]) == 0)
+ end
+
+ def package_settings
+ project.namespace.package_settings
+ end
+ strong_memoize_attr :package_settings
+
+ def existing_package
+ ::Packages::Nuget::PackageFinder
+ .new(
+ current_user,
+ project,
+ package_name: metadata[:package_name],
+ package_version: metadata[:package_version]
+ )
+ .execute
+ .first
+ end
+ strong_memoize_attr :existing_package
+
+ def metadata
+ if remote_package_file?
+ ExtractMetadataContentService
+ .new(nuspec_file_content)
+ .execute
+ .payload
+ else # to cover the case when package file is on disk not in object storage
+ MetadataExtractionService
+ .new(mock_package_file)
+ .execute
+ .payload
+ end
+ end
+ strong_memoize_attr :metadata
+
+ def remote_package_file?
+ params[:remote_url].present?
+ end
+
+ def nuspec_file_content
+ ExtractRemoteMetadataFileService
+ .new(params[:remote_url])
+ .execute
+ .payload
+ rescue ExtractRemoteMetadataFileService::ExtractionError => e
+ raise ExtractionError, e.message
+ end
+
+ def mock_package_file
+ ::Packages::PackageFile.new(
+ params
+ .slice(:file, :file_name)
+ .merge(package: ::Packages::Package.nuget.build)
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/packages/nuget/extract_metadata_file_service.rb b/app/services/packages/nuget/extract_metadata_file_service.rb
index 61e4892fee7..cc040a45016 100644
--- a/app/services/packages/nuget/extract_metadata_file_service.rb
+++ b/app/services/packages/nuget/extract_metadata_file_service.rb
@@ -3,14 +3,12 @@
module Packages
module Nuget
class ExtractMetadataFileService
- include Gitlab::Utils::StrongMemoize
-
ExtractionError = Class.new(StandardError)
MAX_FILE_SIZE = 4.megabytes.freeze
- def initialize(package_file_id)
- @package_file_id = package_file_id
+ def initialize(package_file)
+ @package_file = package_file
end
def execute
@@ -21,12 +19,7 @@ module Packages
private
- attr_reader :package_file_id
-
- def package_file
- ::Packages::PackageFile.find_by_id(package_file_id)
- end
- strong_memoize_attr :package_file
+ attr_reader :package_file
def valid_package_file?
package_file &&
@@ -41,7 +34,7 @@ module Packages
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|
+ 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
diff --git a/app/services/packages/nuget/extract_remote_metadata_file_service.rb b/app/services/packages/nuget/extract_remote_metadata_file_service.rb
new file mode 100644
index 00000000000..37624002ce7
--- /dev/null
+++ b/app/services/packages/nuget/extract_remote_metadata_file_service.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+module Packages
+ module Nuget
+ class ExtractRemoteMetadataFileService
+ include Gitlab::Utils::StrongMemoize
+
+ ExtractionError = Class.new(StandardError)
+
+ MAX_FILE_SIZE = 4.megabytes.freeze
+ METADATA_FILE_EXTENSION = '.nuspec'
+ MAX_FRAGMENTS = 5 # nuspec file is usually in the first 2 fragments but we buffer 5 max
+
+ def initialize(remote_url)
+ @remote_url = remote_url
+ end
+
+ def execute
+ raise ExtractionError, 'invalid file url' if remote_url.blank?
+
+ if nuspec_file_content.blank? || !nuspec_file_content.instance_of?(String)
+ raise ExtractionError, 'nuspec file not found'
+ end
+
+ ServiceResponse.success(payload: nuspec_file_content)
+ end
+
+ private
+
+ attr_reader :remote_url
+
+ def nuspec_file_content
+ fragments = []
+
+ Gitlab::HTTP.get(remote_url, stream_body: true, allow_object_storage: true) do |fragment|
+ break if fragments.size >= MAX_FRAGMENTS
+
+ fragments << fragment
+ joined_fragments = fragments.join
+
+ next if joined_fragments.exclude?(METADATA_FILE_EXTENSION)
+
+ nuspec_content = extract_nuspec_file(joined_fragments)
+
+ break nuspec_content if nuspec_content.present?
+ end
+ end
+ strong_memoize_attr :nuspec_file_content
+
+ def extract_nuspec_file(fragments)
+ StringIO.open(fragments) do |io|
+ Zip::InputStream.open(io) do |zip|
+ process_zip_entries(zip)
+ end
+ rescue Zip::Error => e
+ raise ExtractionError, "Error opening zip stream: #{e.message}"
+ end
+ end
+
+ def process_zip_entries(zip)
+ while (entry = zip.get_next_entry) # rubocop:disable Lint/AssignmentInCondition
+ next unless entry.name.end_with?(METADATA_FILE_EXTENSION)
+
+ raise ExtractionError, 'nuspec file too big' if entry.size > MAX_FILE_SIZE
+
+ return extract_file_content(entry)
+ end
+ end
+
+ def extract_file_content(entry)
+ Tempfile.create('extract_remote_metadata_file_service') do |file|
+ entry.extract(file.path) { true } # allow #extract to overwrite the file
+ file.read
+ end
+ rescue Zip::DecompressionError
+ '' # Ignore decompression errors and continue reading the next fragment
+ rescue Zip::EntrySizeError => e
+ raise ExtractionError, "nuspec file has the wrong entry size: #{e.message}"
+ end
+ end
+ end
+end
diff --git a/app/services/packages/nuget/metadata_extraction_service.rb b/app/services/packages/nuget/metadata_extraction_service.rb
index e1ee29ef2c6..2c758a5ec20 100644
--- a/app/services/packages/nuget/metadata_extraction_service.rb
+++ b/app/services/packages/nuget/metadata_extraction_service.rb
@@ -3,8 +3,8 @@
module Packages
module Nuget
class MetadataExtractionService
- def initialize(package_file_id)
- @package_file_id = package_file_id
+ def initialize(package_file)
+ @package_file = package_file
end
def execute
@@ -13,18 +13,18 @@ module Packages
private
- attr_reader :package_file_id
+ attr_reader :package_file
- def nuspec_file_content
- ExtractMetadataFileService
- .new(package_file_id)
+ def metadata
+ ExtractMetadataContentService
+ .new(nuspec_file_content)
.execute
.payload
end
- def metadata
- ExtractMetadataContentService
- .new(nuspec_file_content)
+ def nuspec_file_content
+ ExtractMetadataFileService
+ .new(package_file)
.execute
.payload
end
diff --git a/app/services/packages/nuget/odata_package_entry_service.rb b/app/services/packages/nuget/odata_package_entry_service.rb
new file mode 100644
index 00000000000..0cdcc38de16
--- /dev/null
+++ b/app/services/packages/nuget/odata_package_entry_service.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Packages
+ module Nuget
+ 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
+ end
+
+ def execute
+ ServiceResponse.success(payload: package_entry)
+ end
+
+ private
+
+ attr_reader :project, :params
+
+ def package_entry
+ <<-XML.squish
+ <entry xmlns='http://www.w3.org/2005/Atom' xmlns:d='http://schemas.microsoft.com/ado/2007/08/dataservices' xmlns:georss='http://www.georss.org/georss' xmlns:gml='http://www.opengis.net/gml' xmlns:m='http://schemas.microsoft.com/ado/2007/08/dataservices/metadata' xml:base="#{xml_base}">
+ <id>#{id_url}</id>
+ <category term='V2FeedPackage' scheme='http://schemas.microsoft.com/ado/2007/08/dataservices/scheme'/>
+ <title type='text'>#{params[:package_name]}</title>
+ <content type='application/zip' src="#{download_url}"/>
+ <m:properties>
+ <d:Version>#{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}')"
+ 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
+ else
+ params[:package_version]
+ 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
+ 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 73a52ea569f..258f8c8f6aa 100644
--- a/app/services/packages/nuget/update_package_from_metadata_service.rb
+++ b/app/services/packages/nuget/update_package_from_metadata_service.rb
@@ -148,7 +148,7 @@ module Packages
end
def metadata
- ::Packages::Nuget::MetadataExtractionService.new(@package_file.id).execute.payload
+ ::Packages::Nuget::MetadataExtractionService.new(@package_file).execute.payload
end
strong_memoize_attr :metadata
diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb
index c8ccbe1465e..10aef87332a 100644
--- a/app/services/preview_markdown_service.rb
+++ b/app/services/preview_markdown_service.rb
@@ -17,7 +17,7 @@ class PreviewMarkdownService < BaseService
private
def quick_action_types
- %w(Issue MergeRequest Commit WorkItem)
+ %w[Issue MergeRequest Commit WorkItem]
end
def explain_quick_actions(text)
diff --git a/app/services/projects/apple_target_platform_detector_service.rb b/app/services/projects/apple_target_platform_detector_service.rb
index ec4c16a1416..087bb1f22e1 100644
--- a/app/services/projects/apple_target_platform_detector_service.rb
+++ b/app/services/projects/apple_target_platform_detector_service.rb
@@ -20,7 +20,7 @@ module Projects
# > AppleTargetPlatformDetectorService.new(multiplatform_project).execute
# => [:ios, :osx, :tvos, :watchos]
class AppleTargetPlatformDetectorService < BaseService
- BUILD_CONFIG_FILENAMES = %w(project.pbxproj *.xcconfig).freeze
+ BUILD_CONFIG_FILENAMES = %w[project.pbxproj *.xcconfig].freeze
# For the current iteration, we only want to detect when the project targets
# iOS. In the future, we can use the same logic to detect projects that
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 45557d03502..61b09de1643 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).seconds
+ ChronicDuration.parse(older_than, use_complete_matcher: true).seconds
end
end
end
diff --git a/app/services/projects/container_repository/gitlab/delete_tags_service.rb b/app/services/projects/container_repository/gitlab/delete_tags_service.rb
index 530cf87c338..a5b7f4bbb6f 100644
--- a/app/services/projects/container_repository/gitlab/delete_tags_service.rb
+++ b/app/services/projects/container_repository/gitlab/delete_tags_service.rb
@@ -39,7 +39,7 @@ module Projects
end
end
- @deleted_tags.any? ? success(deleted: @deleted_tags) : error('could not delete tags')
+ @deleted_tags.any? ? success(deleted: @deleted_tags) : error("could not delete tags: #{@tag_names.join(', ')}".truncate(1000))
end
end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index bca78b88630..e4987438c57 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -144,6 +144,8 @@ module Projects
end
def create_project_settings
+ Gitlab::Pages.add_unique_domain_to(project)
+
@project.project_setting.save if @project.project_setting.changed?
end
@@ -223,22 +225,26 @@ module Projects
end
def save_project_and_import_data
- ApplicationRecord.transaction do
- @project.create_or_update_import_data(data: @import_data[:data], credentials: @import_data[:credentials]) if @import_data
+ Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
+ %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
- # Avoid project callbacks being triggered multiple times by saving the parent first.
- # See https://github.com/rails/rails/issues/41701.
- Namespaces::ProjectNamespace.create_from_project!(@project) if @project.valid?
+ # Avoid project callbacks being triggered multiple times by saving the parent first.
+ # See https://github.com/rails/rails/issues/41701.
+ Namespaces::ProjectNamespace.create_from_project!(@project) if @project.valid?
- if @project.saved?
- Integration.create_from_active_default_integrations(@project, :project_id)
+ if @project.saved?
+ Integration.create_from_active_default_integrations(@project, :project_id)
- @project.create_labels unless @project.gitlab_project_import?
+ @project.create_labels unless @project.gitlab_project_import?
- next if @project.import?
+ next if @project.import?
- unless @project.create_repository(default_branch: default_branch)
- raise 'Failed to create repository'
+ unless @project.create_repository(default_branch: default_branch)
+ raise 'Failed to create repository'
+ end
end
end
end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 0ae6fcb4d97..a2a2f9d2800 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -255,7 +255,11 @@ module Projects
# We need to remove them when a project is deleted
# rubocop: disable CodeReuse/ActiveRecord
def destroy_project_bots!
- project.members.includes(:user).references(:user).merge(User.project_bot).each do |member|
+ members = project.members
+ .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/422405')
+ .includes(:user).references(:user).merge(User.project_bot)
+
+ members.each do |member|
Users::DestroyService.new(current_user).execute(member.user, skip_authorization: true)
end
end
diff --git a/app/services/projects/download_service.rb b/app/services/projects/download_service.rb
index 22104409199..d67b7832bf8 100644
--- a/app/services/projects/download_service.rb
+++ b/app/services/projects/download_service.rb
@@ -28,7 +28,7 @@ module Projects
end
def http?(url)
- url =~ /\A#{URI::DEFAULT_PARSER.make_regexp(%w(http https))}\z/
+ url =~ /\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/
end
def valid_domain?(url)
diff --git a/app/services/projects/hashed_storage/migrate_attachments_service.rb b/app/services/projects/hashed_storage/migrate_attachments_service.rb
index 023f8494d99..40c4fd5376c 100644
--- a/app/services/projects/hashed_storage/migrate_attachments_service.rb
+++ b/app/services/projects/hashed_storage/migrate_attachments_service.rb
@@ -6,7 +6,7 @@ module Projects
extend ::Gitlab::Utils::Override
# List of paths that can be excluded while evaluation if a target can be discarded
- DISCARDABLE_PATHS = %w(tmp tmp/cache tmp/work).freeze
+ DISCARDABLE_PATHS = %w[tmp tmp/cache tmp/work].freeze
def initialize(project:, old_disk_path:, logger: nil)
super
diff --git a/app/services/projects/import_error_filter.rb b/app/services/projects/import_error_filter.rb
index 737b794484d..a0fc5149bb4 100644
--- a/app/services/projects/import_error_filter.rb
+++ b/app/services/projects/import_error_filter.rb
@@ -4,7 +4,7 @@ module Projects
# Used by project imports, it removes any potential paths
# included in an error message that could be stored in the DB
class ImportErrorFilter
- ERROR_MESSAGE_FILTER = /[^\s]*#{File::SEPARATOR}[^\s]*(?=(\s|\z))/.freeze
+ ERROR_MESSAGE_FILTER = /[^\s]*#{File::SEPARATOR}[^\s]*(?=(\s|\z))/
FILTER_MESSAGE = '[FILTERED]'
def self.filter_message(message)
diff --git a/app/services/projects/in_product_marketing_campaign_emails_service.rb b/app/services/projects/in_product_marketing_campaign_emails_service.rb
index 249a2d89fc1..a549d8f594e 100644
--- a/app/services/projects/in_product_marketing_campaign_emails_service.rb
+++ b/app/services/projects/in_product_marketing_campaign_emails_service.rb
@@ -26,13 +26,9 @@ module Projects
sent_email_records.save!
end
- # rubocop: disable CodeReuse/ActiveRecord
def project_users
- @project_users ||= project.users
- .where(email_opted_in: true)
- .merge(Users::InProductMarketingEmail.without_campaign(campaign))
+ @project_users ||= project.users.merge(Users::InProductMarketingEmail.without_campaign(campaign))
end
- # rubocop: enable CodeReuse/ActiveRecord
def project_users_max_access_levels
ids = project_users.map(&:id)
diff --git a/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb b/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb
index 09fec9939b9..0efe9fb16f6 100644
--- a/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb
@@ -9,7 +9,7 @@ module Projects
include Gitlab::Utils::StrongMemoize
HEAD_REV = 'HEAD'
- LFS_ENDPOINT_PATTERN = /^\t?url\s*=\s*(.+)$/.freeze
+ LFS_ENDPOINT_PATTERN = /^\t?url\s*=\s*(.+)$/
LFS_BATCH_API_ENDPOINT = '/info/lfs/objects/batch'
LfsObjectDownloadListError = Class.new(StandardError)
@@ -101,7 +101,7 @@ module Projects
# The import url must end with '.git' here we ensure it is
def default_endpoint_uri
@default_endpoint_uri ||= import_uri.dup.tap do |uri|
- path = uri.path.gsub(%r(/$), '')
+ path = uri.path.gsub(%r{/$}, '')
path += '.git' unless path.ends_with?('.git')
uri.path = path + LFS_BATCH_API_ENDPOINT
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 642ec37619f..3d08039942b 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -110,36 +110,40 @@ module Projects
end
def proceed_to_transfer
- Project.transaction do
- project.expire_caches_before_rename(@old_path)
+ Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
+ %w[routes redirect_routes], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424282'
+ ) do
+ Project.transaction do
+ project.expire_caches_before_rename(@old_path)
- # Apply changes to the project
- update_namespace_and_visibility(@new_namespace)
- project.reconcile_shared_runners_setting!
- project.save!
+ # Apply changes to the project
+ update_namespace_and_visibility(@new_namespace)
+ project.reconcile_shared_runners_setting!
+ project.save!
- # Notifications
- project.send_move_instructions(@old_path)
+ # Notifications
+ project.send_move_instructions(@old_path)
- # Directories on disk
- move_project_folders(project)
+ # Directories on disk
+ move_project_folders(project)
- transfer_missing_group_resources(@old_group)
+ transfer_missing_group_resources(@old_group)
- # Move uploads
- move_project_uploads(project)
+ # Move uploads
+ move_project_uploads(project)
- update_integrations
+ update_integrations
- remove_paid_features
+ remove_paid_features
- project.old_path_with_namespace = @old_path
+ project.old_path_with_namespace = @old_path
- update_repository_configuration(@new_path)
+ update_repository_configuration(@new_path)
- remove_issue_contacts
+ remove_issue_contacts
- execute_system_hooks
+ execute_system_hooks
+ end
end
update_pending_builds
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 403f645392c..dc92c501b8c 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -2,10 +2,12 @@
module Projects
class UpdatePagesService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
# old deployment can be cached by pages daemon
# so we need to give pages daemon some time update cache
# 10 minutes is enough, but 30 feels safer
- OLD_DEPLOYMENTS_DESTRUCTION_DELAY = 30.minutes.freeze
+ OLD_DEPLOYMENTS_DESTRUCTION_DELAY = 30.minutes
attr_reader :build, :deployment_update
@@ -18,9 +20,7 @@ module Projects
def execute
register_attempt
- # Create status notifying the deployment of pages
- @commit_status = build_commit_status
- ::Ci::Pipelines::AddJobService.new(@build.pipeline).execute!(@commit_status) do |job|
+ ::Ci::Pipelines::AddJobService.new(@build.pipeline).execute!(commit_status) do |job|
job.enqueue!
job.run!
end
@@ -31,13 +31,10 @@ module Projects
deployment = create_pages_deployment(artifacts_path, build)
break error('The uploaded artifact size does not match the expected value') unless deployment
+ break error(deployment_update.errors.first.full_message) unless deployment_update.valid?
- if deployment_update.valid?
- update_project_pages_deployment(deployment)
- success
- else
- error(deployment_update.errors.first.full_message)
- end
+ update_project_pages_deployment(deployment)
+ success
end
rescue StandardError => e
error(e.message)
@@ -47,7 +44,7 @@ module Projects
private
def success
- @commit_status.success
+ commit_status.success
@project.mark_pages_as_deployed
publish_deployed_event
super
@@ -56,15 +53,14 @@ module Projects
def error(message)
register_failure
log_error("Projects::UpdatePagesService: #{message}")
- @commit_status.allow_failure = !deployment_update.latest?
- @commit_status.description = message
- @commit_status.drop(:script_failure)
+ commit_status.allow_failure = !deployment_update.latest?
+ commit_status.description = message
+ commit_status.drop(:script_failure)
super
end
- def build_commit_status
- stage = create_stage
-
+ # Create status notifying the deployment of pages
+ def commit_status
GenericCommitStatus.new(
user: build.user,
ci_stage: stage,
@@ -73,26 +69,22 @@ module Projects
stage_idx: stage.position
)
end
+ strong_memoize_attr :commit_status
# rubocop: disable Performance/ActiveRecordSubtransactionMethods
- def create_stage
+ def stage
build.pipeline.stages.safe_find_or_create_by(name: 'deploy', pipeline_id: build.pipeline.id) do |stage|
stage.position = GenericCommitStatus::EXTERNAL_STAGE_IDX
stage.project = build.project
end
end
+ strong_memoize_attr :commit_status
# rubocop: enable Performance/ActiveRecordSubtransactionMethods
def create_pages_deployment(artifacts_path, build)
- sha256 = build.job_artifacts_archive.file_sha256
File.open(artifacts_path) do |file|
- deployment = project.pages_deployments.create!(
- file: file,
- file_count: deployment_update.entries_count,
- file_sha256: sha256,
- ci_build_id: build.id,
- root_directory: build.options[:publish]
- )
+ attributes = pages_deployment_attributes(file, build)
+ deployment = project.pages_deployments.create!(**attributes)
break if deployment.size != file.size || deployment.file.size != file.size
@@ -100,21 +92,28 @@ module Projects
end
end
+ # overridden on EE
+ def pages_deployment_attributes(file, build)
+ {
+ file: file,
+ file_count: deployment_update.entries_count,
+ file_sha256: build.job_artifacts_archive.file_sha256,
+ ci_build_id: build.id,
+ root_directory: build.options[:publish]
+ }
+ end
+
def update_project_pages_deployment(deployment)
project.update_pages_deployment!(deployment)
+
+ 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 ref
- build.ref
- end
-
- def artifacts
- build.artifacts_file.path
+ deployment.id)
end
def register_attempt
@@ -126,12 +125,14 @@ module Projects
end
def pages_deployments_total_counter
- @pages_deployments_total_counter ||= Gitlab::Metrics.counter(:pages_deployments_total, "Counter of GitLab Pages deployments triggered")
+ Gitlab::Metrics.counter(:pages_deployments_total, "Counter of GitLab Pages deployments triggered")
end
+ strong_memoize_attr :pages_deployments_total_counter
def pages_deployments_failed_total_counter
- @pages_deployments_failed_total_counter ||= Gitlab::Metrics.counter(:pages_deployments_failed_total, "Counter of GitLab Pages deployments which failed")
+ Gitlab::Metrics.counter(:pages_deployments_failed_total, "Counter of GitLab Pages deployments which failed")
end
+ strong_memoize_attr :pages_deployments_failed_total_counter
def publish_deployed_event
event = ::Pages::PageDeployedEvent.new(data: {
@@ -144,3 +145,5 @@ module Projects
end
end
end
+
+::Projects::UpdatePagesService.prepend_mod
diff --git a/app/services/projects/update_repository_storage_service.rb b/app/services/projects/update_repository_storage_service.rb
index f5f6bb85995..799ae5677c3 100644
--- a/app/services/projects/update_repository_storage_service.rb
+++ b/app/services/projects/update_repository_storage_service.rb
@@ -80,8 +80,8 @@ module Projects
end
def pool_repository_exists_for?(shard_name:, pool_repository:)
- PoolRepository.by_source_project_and_shard_name(
- pool_repository.source_project,
+ PoolRepository.by_disk_path_and_shard_name(
+ pool_repository.disk_path,
shard_name
).exists?
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 8639e2f833f..e5e39247dbf 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -53,13 +53,7 @@ module Projects
def add_pages_unique_domain
return unless params.dig(:project_setting_attributes, :pages_unique_domain_enabled)
- # If the project used a unique domain once, it'll always use the same
- return if project.project_setting.pages_unique_domain_in_database.present?
-
- params[:project_setting_attributes][:pages_unique_domain] = Gitlab::Pages::RandomDomain.generate(
- project_path: project.path,
- namespace_path: project.parent.full_path
- )
+ Gitlab::Pages.add_unique_domain_to(project)
end
def validate!
@@ -112,6 +106,7 @@ module Projects
# overridden by EE module
end
+ # overridden by EE module
def remove_unallowed_params
params.delete(:emails_enabled) unless can?(current_user, :set_emails_disabled, project)
@@ -119,11 +114,11 @@ module Projects
end
def after_update
- todos_features_changes = %w(
+ todos_features_changes = %w[
issues_access_level
merge_requests_access_level
repository_access_level
- )
+ ]
project_changed_feature_keys = project.project_feature.previous_changes.keys
if project.visibility_level_previous_changes && project.private?
diff --git a/app/services/releases/destroy_service.rb b/app/services/releases/destroy_service.rb
index ff2b3a7bd18..41b421662ef 100644
--- a/app/services/releases/destroy_service.rb
+++ b/app/services/releases/destroy_service.rb
@@ -7,6 +7,8 @@ module Releases
return error(_('Access Denied'), 403) unless allowed?
if release.destroy
+ update_catalog_resource!
+
success(tag: existing_tag, release: release)
else
error(release.errors.messages || '400 Bad request', 400)
@@ -15,6 +17,14 @@ module Releases
private
+ def update_catalog_resource!
+ return unless project.catalog_resource
+
+ return unless project.catalog_resource.versions.none?
+
+ project.catalog_resource.update!(state: 'draft')
+ end
+
def allowed?
Ability.allowed?(current_user, :destroy_release, release)
end
diff --git a/app/services/repositories/base_service.rb b/app/services/repositories/base_service.rb
index b262b4a1f7b..bf7ac2e5fd8 100644
--- a/app/services/repositories/base_service.rb
+++ b/app/services/repositories/base_service.rb
@@ -29,7 +29,7 @@ class Repositories::BaseService < BaseService
end
def move_error(path)
- error = %{Repository "#{path}" could not be moved}
+ error = %(Repository "#{path}" could not be moved)
log_error(error)
error(error)
diff --git a/app/services/repository_archive_clean_up_service.rb b/app/services/repository_archive_clean_up_service.rb
index 0fb7dfdb85f..4258898c665 100644
--- a/app/services/repository_archive_clean_up_service.rb
+++ b/app/services/repository_archive_clean_up_service.rb
@@ -37,7 +37,7 @@ class RepositoryArchiveCleanUpService
private
def clean_up_old_archives
- run(%W(find #{path} -mindepth 1 -maxdepth #{MAX_ARCHIVE_DEPTH} -type f \( -name \*.tar -o -name \*.bz2 -o -name \*.tar.gz -o -name \*.zip \) -mmin +#{mmin} -delete))
+ run(%W[find #{path} -mindepth 1 -maxdepth #{MAX_ARCHIVE_DEPTH} -type f \( -name \*.tar -o -name \*.bz2 -o -name \*.tar.gz -o -name \*.zip \) -mmin +#{mmin} -delete])
end
def clean_up_empty_directories
@@ -45,7 +45,7 @@ class RepositoryArchiveCleanUpService
end
def clean_up_empty_directories_with_depth(depth)
- run(%W(find #{path} -mindepth #{depth} -maxdepth #{depth} -type d -empty -delete))
+ run(%W[find #{path} -mindepth #{depth} -maxdepth #{depth} -type d -empty -delete])
end
def run(cmd)
diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb
index 1fea894a599..1c496aa5e77 100644
--- a/app/services/resource_access_tokens/create_service.rb
+++ b/app/services/resource_access_tokens/create_service.rb
@@ -60,7 +60,7 @@ module ResourceAccessTokens
strong_memoize_attr :username_and_email_generator
def has_permission_to_create?
- %w(project group).include?(resource_type) && can?(current_user, :create_resource_access_tokens, resource)
+ %w[project group].include?(resource_type) && can?(current_user, :create_resource_access_tokens, resource)
end
def create_user
diff --git a/app/services/resource_access_tokens/revoke_service.rb b/app/services/resource_access_tokens/revoke_service.rb
index 2aaf4cc31d2..46c71b04632 100644
--- a/app/services/resource_access_tokens/revoke_service.rb
+++ b/app/services/resource_access_tokens/revoke_service.rb
@@ -38,7 +38,7 @@ module ResourceAccessTokens
end
def can_destroy_token?
- %w(project group).include?(resource.class.name.downcase) && can?(current_user, :destroy_resource_access_tokens, resource)
+ %w[project group].include?(resource.class.name.downcase) && can?(current_user, :destroy_resource_access_tokens, resource)
end
def find_member
diff --git a/app/services/resource_events/base_change_timebox_service.rb b/app/services/resource_events/base_change_timebox_service.rb
index ba7c9d90713..d0b0f635ed2 100644
--- a/app/services/resource_events/base_change_timebox_service.rb
+++ b/app/services/resource_events/base_change_timebox_service.rb
@@ -14,7 +14,7 @@ module ResourceEvents
track_event
- resource.expire_note_etag_cache
+ resource.broadcast_notes_changed
end
private
diff --git a/app/services/resource_events/change_labels_service.rb b/app/services/resource_events/change_labels_service.rb
index 69e68922b91..f0ebf7fb40b 100644
--- a/app/services/resource_events/change_labels_service.rb
+++ b/app/services/resource_events/change_labels_service.rb
@@ -31,7 +31,7 @@ module ResourceEvents
end
create_timeline_events_from(added_labels: added_labels, removed_labels: removed_labels)
- resource.expire_note_etag_cache
+ resource.broadcast_notes_changed
return unless resource.is_a?(Issue)
diff --git a/app/services/resource_events/change_state_service.rb b/app/services/resource_events/change_state_service.rb
index a396f7a1907..303d666c8e2 100644
--- a/app/services/resource_events/change_state_service.rb
+++ b/app/services/resource_events/change_state_service.rb
@@ -23,7 +23,7 @@ module ResourceEvents
created_at: resource.system_note_timestamp
)
- resource.expire_note_etag_cache
+ resource.broadcast_notes_changed
end
private
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index f4c0a743ef0..24549b1498b 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -6,7 +6,7 @@ module Search
include Gitlab::Utils::StrongMemoize
DEFAULT_SCOPE = 'projects'
- ALLOWED_SCOPES = %w(projects issues merge_requests milestones users).freeze
+ ALLOWED_SCOPES = %w[projects issues merge_requests milestones users].freeze
attr_accessor :current_user, :params
diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb
index 73d46a9ba70..24613dc2564 100644
--- a/app/services/search/project_service.rb
+++ b/app/services/search/project_service.rb
@@ -5,7 +5,7 @@ module Search
include Search::Filter
include Gitlab::Utils::StrongMemoize
- ALLOWED_SCOPES = %w(blobs issues merge_requests wiki_blobs commits notes milestones users).freeze
+ ALLOWED_SCOPES = %w[blobs issues merge_requests wiki_blobs commits notes milestones users].freeze
attr_accessor :project, :current_user, :params
diff --git a/app/services/service_desk/custom_email_verifications/base_service.rb b/app/services/service_desk/custom_email_verifications/base_service.rb
index fe456e4d3f3..e92700022f1 100644
--- a/app/services/service_desk/custom_email_verifications/base_service.rb
+++ b/app/services/service_desk/custom_email_verifications/base_service.rb
@@ -3,6 +3,8 @@
module ServiceDesk
module CustomEmailVerifications
class BaseService < ::BaseProjectService
+ include ::ServiceDesk::CustomEmails::Logger
+
attr_reader :settings
def initialize(project:, current_user: nil, params: {})
@@ -35,15 +37,21 @@ module ServiceDesk
end
def error_response(message)
+ log_warning(error_message: message)
ServiceResponse.error(message: message)
end
def error_not_verified(error_identifier)
+ log_info(error_message: error_identifier.to_s)
ServiceResponse.error(
message: _('ServiceDesk|Custom email address could not be verified.'),
reason: error_identifier.to_s
)
end
+
+ def log_category
+ 'custom_email_verification'
+ end
end
end
end
diff --git a/app/services/service_desk/custom_email_verifications/create_service.rb b/app/services/service_desk/custom_email_verifications/create_service.rb
index db518bfdf24..9c5721446a1 100644
--- a/app/services/service_desk/custom_email_verifications/create_service.rb
+++ b/app/services/service_desk/custom_email_verifications/create_service.rb
@@ -17,6 +17,7 @@ module ServiceDesk
if ramp_up_error
handle_error_case
else
+ log_info
ServiceResponse.success
end
end
@@ -63,11 +64,11 @@ module ServiceDesk
end
def error_settings_missing
- error_response(_('ServiceDesk|Service Desk setting missing'))
+ error_response(s_('ServiceDesk|Service Desk setting missing'))
end
def error_user_not_authorized
- error_response(_('ServiceDesk|User cannot manage project.'))
+ error_response(s_('ServiceDesk|User cannot manage project.'))
end
end
end
diff --git a/app/services/service_desk/custom_email_verifications/update_service.rb b/app/services/service_desk/custom_email_verifications/update_service.rb
index 813624cde23..5ef36ce0576 100644
--- a/app/services/service_desk/custom_email_verifications/update_service.rb
+++ b/app/services/service_desk/custom_email_verifications/update_service.rb
@@ -24,6 +24,7 @@ module ServiceDesk
else
verification.mark_as_finished!
+ log_info
ServiceResponse.success
end
end
@@ -75,15 +76,15 @@ module ServiceDesk
end
def error_parameter_missing
- error_response(_('ServiceDesk|Service Desk setting or verification object missing'))
+ error_response(s_('ServiceDesk|Service Desk setting or verification object missing'))
end
def error_already_finished
- error_response(_('ServiceDesk|Custom email address has already been verified.'))
+ error_response(s_('ServiceDesk|Custom email address has already been verified.'))
end
def error_already_failed
- error_response(_('ServiceDesk|Custom email address verification has already been processed and failed.'))
+ error_response(s_('ServiceDesk|Custom email address verification has already been processed and failed.'))
end
end
end
diff --git a/app/services/service_desk/custom_emails/base_service.rb b/app/services/service_desk/custom_emails/base_service.rb
index 62152f31012..91f4100a8ca 100644
--- a/app/services/service_desk/custom_emails/base_service.rb
+++ b/app/services/service_desk/custom_emails/base_service.rb
@@ -3,6 +3,8 @@
module ServiceDesk
module CustomEmails
class BaseService < ::BaseProjectService
+ include Logger
+
private
def legitimate_user?
@@ -34,6 +36,7 @@ module ServiceDesk
end
def error_response(message)
+ log_warning(error_message: message)
ServiceResponse.error(message: message)
end
end
diff --git a/app/services/service_desk/custom_emails/create_service.rb b/app/services/service_desk/custom_emails/create_service.rb
index c3ca98a0259..305f5b3fa11 100644
--- a/app/services/service_desk/custom_emails/create_service.rb
+++ b/app/services/service_desk/custom_emails/create_service.rb
@@ -25,6 +25,7 @@ module ServiceDesk
# we don't use its response here.
create_verification
+ log_info
ServiceResponse.success
end
diff --git a/app/services/service_desk/custom_emails/destroy_service.rb b/app/services/service_desk/custom_emails/destroy_service.rb
index 1aa5994edd8..abbe39646aa 100644
--- a/app/services/service_desk/custom_emails/destroy_service.rb
+++ b/app/services/service_desk/custom_emails/destroy_service.rb
@@ -13,6 +13,7 @@ module ServiceDesk
project.reset
project.service_desk_setting&.update!(custom_email: nil, custom_email_enabled: false)
+ log_info
ServiceResponse.success
end
diff --git a/app/services/service_desk_settings/update_service.rb b/app/services/service_desk_settings/update_service.rb
index 61cb6fce11f..182022beb1d 100644
--- a/app/services/service_desk_settings/update_service.rb
+++ b/app/services/service_desk_settings/update_service.rb
@@ -2,12 +2,19 @@
module ServiceDeskSettings
class UpdateService < BaseService
+ include ::ServiceDesk::CustomEmails::Logger
+
def execute
settings = ServiceDeskSetting.safe_find_or_create_by!(project_id: project.id)
params[:project_key] = nil if params[:project_key].blank?
+ # We want to know when custom email got enabled
+ write_log_message = params[:custom_email_enabled].present? && !settings.custom_email_enabled?
+
if settings.update(params)
+ log_info if write_log_message
+
ServiceResponse.success
else
ServiceResponse.error(message: settings.errors.full_messages.to_sentence)
diff --git a/app/services/service_response.rb b/app/services/service_response.rb
index 86efc01bd30..fbc5660315b 100644
--- a/app/services/service_response.rb
+++ b/app/services/service_response.rb
@@ -2,18 +2,22 @@
class ServiceResponse
def self.success(message: nil, payload: {}, http_status: :ok)
- new(status: :success,
- message: message,
- payload: payload,
- http_status: http_status)
+ new(
+ status: :success,
+ message: message,
+ payload: payload,
+ http_status: http_status
+ )
end
def self.error(message:, payload: {}, http_status: nil, reason: nil)
- new(status: :error,
- message: message,
- payload: payload,
- http_status: http_status,
- reason: reason)
+ new(
+ status: :error,
+ message: message,
+ payload: payload,
+ http_status: http_status,
+ reason: reason
+ )
end
attr_reader :status, :message, :http_status, :payload, :reason
diff --git a/app/services/snippets/update_service.rb b/app/services/snippets/update_service.rb
index 662e31a93aa..8cc6458227f 100644
--- a/app/services/snippets/update_service.rb
+++ b/app/services/snippets/update_service.rb
@@ -2,7 +2,7 @@
module Snippets
class UpdateService < Snippets::BaseService
- COMMITTABLE_ATTRIBUTES = %w(file_name content).freeze
+ COMMITTABLE_ATTRIBUTES = %w[file_name content].freeze
UpdateError = Class.new(StandardError)
diff --git a/app/services/spam/akismet_service.rb b/app/services/spam/akismet_service.rb
index d31b904f549..fd2b3c9a441 100644
--- a/app/services/spam/akismet_service.rb
+++ b/app/services/spam/akismet_service.rb
@@ -50,8 +50,10 @@ module Spam
attr_accessor :owner_name, :owner_email
def akismet_client
- @akismet_client ||= ::Akismet::Client.new(Gitlab::CurrentSettings.akismet_api_key,
- Gitlab.config.gitlab.url)
+ @akismet_client ||= ::Akismet::Client.new(
+ Gitlab::CurrentSettings.akismet_api_key,
+ Gitlab.config.gitlab.url
+ )
end
def akismet_enabled?
diff --git a/app/services/spam/spam_action_service.rb b/app/services/spam/spam_action_service.rb
index 5c510990b2d..6ec8d09c37c 100644
--- a/app/services/spam/spam_action_service.rb
+++ b/app/services/spam/spam_action_service.rb
@@ -76,10 +76,9 @@ module Spam
spam_verdict_service.execute.tap do |result|
case result
when BLOCK_USER
- # TODO: improve BLOCK_USER handling, non-existent until now
- # https://gitlab.com/gitlab-org/gitlab/-/issues/329666
target.spam!
create_spam_log
+ ban_user!
when DISALLOW
target.spam!
create_spam_log
@@ -119,6 +118,12 @@ module Spam
target.spam_log = spam_log
end
+ def ban_user!
+ UserCustomAttribute.set_banned_by_spam_log(target.spam_log)
+
+ user.ban!
+ end
+
def spam_verdict_service
context = {
action: action,
@@ -131,12 +136,13 @@ module Spam
referer: spam_params&.referer
}
- SpamVerdictService.new(target: target,
- user: user,
- options: options,
- context: context,
- extra_features: extra_features
- )
+ SpamVerdictService.new(
+ target: target,
+ user: user,
+ options: options,
+ context: context,
+ extra_features: extra_features
+ )
end
def noteable_type
diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb
index 639d99ad906..9efe51b43b8 100644
--- a/app/services/spam/spam_verdict_service.rb
+++ b/app/services/spam/spam_verdict_service.rb
@@ -36,16 +36,17 @@ module Spam
# The target can override the verdict via the `allow_possible_spam` application setting
final_verdict = OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM if override_via_allow_possible_spam?(verdict: final_verdict)
- logger.info(class: self.class.name,
- akismet_verdict: akismet_verdict,
- spam_check_verdict: spamcheck_verdict,
- spam_check_rtt: external_spam_check_round_trip_time.real,
- final_verdict: final_verdict,
- username: user.username,
- user_id: user.id,
- target_type: target.class.to_s,
- project_id: target.project_id
- )
+ logger.info(
+ class: self.class.name,
+ akismet_verdict: akismet_verdict,
+ spam_check_verdict: spamcheck_verdict,
+ spam_check_rtt: external_spam_check_round_trip_time.real,
+ final_verdict: final_verdict,
+ username: user.username,
+ user_id: user.id,
+ target_type: target.class.to_s,
+ project_id: target.project_id
+ )
final_verdict
end
diff --git a/app/services/submodules/update_service.rb b/app/services/submodules/update_service.rb
index a6011a920bd..4a573e595ce 100644
--- a/app/services/submodules/update_service.rb
+++ b/app/services/submodules/update_service.rb
@@ -26,11 +26,13 @@ module Submodules
end
def create_commit!
- repository.update_submodule(current_user,
- @submodule,
- @commit_sha,
- message: @commit_message,
- branch: @branch_name)
+ repository.update_submodule(
+ current_user,
+ @submodule,
+ @commit_sha,
+ message: @commit_message,
+ branch: @branch_name
+ )
rescue ArgumentError, TypeError
raise ValidationError, 'Invalid parameters'
end
diff --git a/app/services/suggestions/create_service.rb b/app/services/suggestions/create_service.rb
index 239cd86e0ec..d7c261f1c25 100644
--- a/app/services/suggestions/create_service.rb
+++ b/app/services/suggestions/create_service.rb
@@ -9,20 +9,22 @@ module Suggestions
def execute
return unless @note.supports_suggestion?
- suggestions = Gitlab::Diff::SuggestionsParser.parse(@note.note,
- project: @note.project,
- position: @note.position)
+ suggestions = Gitlab::Diff::SuggestionsParser.parse(
+ @note.note,
+ project: @note.project,
+ position: @note.position
+ )
- rows =
- suggestions.map.with_index do |suggestion, index|
- creation_params =
- suggestion.to_hash.slice(:from_content,
- :to_content,
- :lines_above,
- :lines_below)
+ rows = suggestions.map.with_index do |suggestion, index|
+ creation_params = suggestion.to_hash.slice(
+ :from_content,
+ :to_content,
+ :lines_above,
+ :lines_below
+ )
- creation_params.merge!(note_id: @note.id, relative_order: index)
- end
+ creation_params.merge!(note_id: @note.id, relative_order: index)
+ end
rows.in_groups_of(100, false) do |rows|
ApplicationRecord.legacy_bulk_insert('suggestions', rows) # rubocop:disable Gitlab/BulkInsert
diff --git a/app/services/system_notes/alert_management_service.rb b/app/services/system_notes/alert_management_service.rb
index 994e3174668..1a1dd84491a 100644
--- a/app/services/system_notes/alert_management_service.rb
+++ b/app/services/system_notes/alert_management_service.rb
@@ -14,7 +14,7 @@ module SystemNotes
def create_new_alert(monitoring_tool)
body = "logged an alert from **#{monitoring_tool}**"
- create_note(NoteSummary.new(noteable, project, User.alert_bot, body, action: 'new_alert_added'))
+ create_note(NoteSummary.new(noteable, project, Users::Internal.alert_bot, body, action: 'new_alert_added'))
end
# Called when the status of an AlertManagement::Alert has changed
@@ -61,7 +61,7 @@ module SystemNotes
def log_resolving_alert(monitoring_tool)
body = "logged a recovery alert from **#{monitoring_tool}**"
- create_note(NoteSummary.new(noteable, project, User.alert_bot, body, action: 'new_alert_added'))
+ create_note(NoteSummary.new(noteable, project, Users::Internal.alert_bot, body, action: 'new_alert_added'))
end
end
end
diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb
index 61a4316e8ae..04ae734a8fe 100644
--- a/app/services/system_notes/issuables_service.rb
+++ b/app/services/system_notes/issuables_service.rb
@@ -32,8 +32,7 @@ module SystemNotes
#
# Returns the created Note object
def relate_issuable(noteable_ref)
- issuable_type = noteable.to_ability_name.humanize(capitalize: false)
- body = "marked this #{issuable_type} as related to #{noteable_ref.to_reference(noteable.resource_parent)}"
+ body = "marked this #{noteable_name} as related to #{noteable_ref.to_reference(noteable.resource_parent)}"
track_issue_event(:track_issue_related_action)
@@ -351,12 +350,12 @@ module SystemNotes
# Returns the created Note object
def change_issue_confidentiality
if noteable.confidential
- body = 'made the issue confidential'
+ body = "made the #{noteable_name} confidential"
action = 'confidential'
track_issue_event(:track_issue_made_confidential_action)
else
- body = 'made the issue visible to everyone'
+ body = "made the #{noteable_name} visible to everyone"
action = 'visible'
track_issue_event(:track_issue_made_visible_action)
@@ -534,6 +533,12 @@ module SystemNotes
issue_activity_counter.public_send(event_name, author: author, project: project || noteable.project) # rubocop: disable GitlabSecurity/PublicSend
end
+
+ def noteable_name
+ name = noteable.try(:issue_type) || noteable.to_ability_name
+
+ name.humanize(capitalize: false)
+ end
end
end
diff --git a/app/services/todos/destroy/destroyed_issuable_service.rb b/app/services/todos/destroy/destroyed_issuable_service.rb
index 759c430ec7a..6ba286458df 100644
--- a/app/services/todos/destroy/destroyed_issuable_service.rb
+++ b/app/services/todos/destroy/destroyed_issuable_service.rb
@@ -8,7 +8,7 @@ module Todos
# Since we are moving towards work items, in some instances we create todos with
# `target_type: WorkItem` in other instances we still create todos with `target_type: Issue`
# So when an issue/work item is deleted, we just make sure to delete todos for both target types
- BOUND_TARGET_TYPES = %w(Issue WorkItem).freeze
+ BOUND_TARGET_TYPES = %w[Issue WorkItem].freeze
def initialize(target_id, target_type)
@target_id = target_id
diff --git a/app/services/todos/destroy/entity_leave_service.rb b/app/services/todos/destroy/entity_leave_service.rb
index 5b04d2fd3af..387c5ce063a 100644
--- a/app/services/todos/destroy/entity_leave_service.rb
+++ b/app/services/todos/destroy/entity_leave_service.rb
@@ -8,7 +8,7 @@ module Todos
attr_reader :user, :entity
def initialize(user_id, entity_id, entity_type)
- unless %w(Group Project).include?(entity_type)
+ unless %w[Group Project].include?(entity_type)
raise ArgumentError, "#{entity_type} is not an entity user can leave"
end
diff --git a/app/services/users/activity_service.rb b/app/services/users/activity_service.rb
index 24aa4aa1061..b490df6a134 100644
--- a/app/services/users/activity_service.rb
+++ b/app/services/users/activity_service.rb
@@ -30,11 +30,9 @@ module Users
return if Gitlab::Database.read_only?
today = Date.today
-
return if user.last_activity_on == today
- lease = Gitlab::ExclusiveLease.new("activity_service:#{user.id}",
- timeout: LEASE_TIMEOUT)
+ lease = Gitlab::ExclusiveLease.new("activity_service:#{user.id}", timeout: LEASE_TIMEOUT)
return unless lease.try_obtain
user.update_attribute(:last_activity_on, today)
diff --git a/app/services/users/authorized_build_service.rb b/app/services/users/authorized_build_service.rb
index 5029105b087..446c897fe5a 100644
--- a/app/services/users/authorized_build_service.rb
+++ b/app/services/users/authorized_build_service.rb
@@ -12,7 +12,7 @@ module Users
end
def signup_params
- super + [:skip_confirmation]
+ super + [:skip_confirmation, :external]
end
end
end
diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb
index 04a11f41eb1..b51684c6899 100644
--- a/app/services/users/build_service.rb
+++ b/app/services/users/build_service.rb
@@ -5,8 +5,8 @@ module Users
ALLOWED_USER_TYPES = %i[project_bot security_policy_bot].freeze
delegate :user_default_internal_regex_enabled?,
- :user_default_internal_regex_instance,
- to: :'Gitlab::CurrentSettings.current_application_settings'
+ :user_default_internal_regex_instance,
+ to: :'Gitlab::CurrentSettings.current_application_settings'
def initialize(current_user, params = {})
@current_user = current_user
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index d4c00a4dcec..a0e1167836b 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -59,9 +59,6 @@ module Users
Groups::DestroyService.new(group, current_user).execute
end
- namespace = user.namespace
- namespace.prepare_for_destroy
-
user.personal_projects.each do |project|
success = ::Projects::DestroyService.new(project, current_user).execute
raise DestroyError, "Project #{project.id} can't be deleted" unless success
@@ -70,9 +67,11 @@ module Users
yield(user) if block_given?
hard_delete = options.fetch(:hard_delete, false)
- Users::GhostUserMigration.create!(user: user,
- initiator_user: current_user,
- hard_delete: hard_delete)
+ Users::GhostUserMigration.create!(
+ user: user,
+ initiator_user: current_user,
+ hard_delete: hard_delete
+ )
update_metrics
end
diff --git a/app/services/users/migrate_records_to_ghost_user_in_batches_service.rb b/app/services/users/migrate_records_to_ghost_user_in_batches_service.rb
index d294312cc30..e05f308343d 100644
--- a/app/services/users/migrate_records_to_ghost_user_in_batches_service.rb
+++ b/app/services/users/migrate_records_to_ghost_user_in_batches_service.rb
@@ -12,9 +12,11 @@ module Users
ghost_user_migrations.each do |job|
break if execution_tracker.over_limit?
- service = Users::MigrateRecordsToGhostUserService.new(job.user,
- job.initiator_user,
- execution_tracker)
+ service = Users::MigrateRecordsToGhostUserService.new(
+ job.user,
+ job.initiator_user,
+ execution_tracker
+ )
service.execute(hard_delete: job.hard_delete)
rescue Gitlab::Utils::ExecutionTracker::ExecutionTimeOutError
# no-op
diff --git a/app/services/users/migrate_records_to_ghost_user_service.rb b/app/services/users/migrate_records_to_ghost_user_service.rb
index 5d518803315..06950292fea 100644
--- a/app/services/users/migrate_records_to_ghost_user_service.rb
+++ b/app/services/users/migrate_records_to_ghost_user_service.rb
@@ -18,7 +18,7 @@ module Users
@user = user
@initiator_user = initiator_user
@execution_tracker = execution_tracker
- @ghost_user = User.ghost
+ @ghost_user = Users::Internal.ghost
end
def execute(hard_delete: false)
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index 197260a80ca..32acc3f170d 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -82,15 +82,17 @@ module Users
attr_reader :incorrect_auth_found_callback, :missing_auth_found_callback
def log_refresh_details(remove, add)
- Gitlab::AppJsonLogger.info(event: 'authorized_projects_refresh',
- user_id: user.id,
- 'authorized_projects_refresh.source': source,
- 'authorized_projects_refresh.rows_deleted_count': remove.length,
- 'authorized_projects_refresh.rows_added_count': add.length,
- # most often there's only a few entries in remove and add, but limit it to the first 5
- # entries to avoid flooding the logs
- 'authorized_projects_refresh.rows_deleted_slice': remove.first(5),
- 'authorized_projects_refresh.rows_added_slice': add.first(5).map(&:values))
+ Gitlab::AppJsonLogger.info(
+ event: 'authorized_projects_refresh',
+ user_id: user.id,
+ 'authorized_projects_refresh.source': source,
+ 'authorized_projects_refresh.rows_deleted_count': remove.length,
+ 'authorized_projects_refresh.rows_added_count': add.length,
+ # most often there's only a few entries in remove and add, but limit it to the first 5
+ # entries to avoid flooding the logs
+ 'authorized_projects_refresh.rows_deleted_slice': remove.first(5),
+ 'authorized_projects_refresh.rows_added_slice': add.first(5).map(&:values)
+ )
end
end
end
diff --git a/app/services/users/upsert_credit_card_validation_service.rb b/app/services/users/upsert_credit_card_validation_service.rb
index 61cf598f178..62df676db25 100644
--- a/app/services/users/upsert_credit_card_validation_service.rb
+++ b/app/services/users/upsert_credit_card_validation_service.rb
@@ -7,8 +7,10 @@ module Users
end
def execute
+ user_id = params.fetch(:user_id)
+
@params = {
- user_id: params.fetch(:user_id),
+ user_id: user_id,
credit_card_validated_at: params.fetch(:credit_card_validated_at),
expiration_date: get_expiration_date(params),
last_digits: Integer(params.fetch(:credit_card_mask_number), 10),
@@ -16,7 +18,9 @@ module Users
holder_name: params.fetch(:credit_card_holder_name)
}
- ::Users::CreditCardValidation.upsert(@params)
+ credit_card = Users::CreditCardValidation.find_or_initialize_by_user(user_id)
+
+ credit_card.update(@params.except(:user_id))
ServiceResponse.success(message: 'CreditCardValidation was set')
rescue ActiveRecord::InvalidForeignKey, ActiveRecord::NotNullViolation => e
diff --git a/app/services/webauthn/authenticate_service.rb b/app/services/webauthn/authenticate_service.rb
index 52437a77df8..7855b509595 100644
--- a/app/services/webauthn/authenticate_service.rb
+++ b/app/services/webauthn/authenticate_service.rb
@@ -40,8 +40,8 @@ module Webauthn
# (which is done in #verify_webauthn_credential)
def validate_webauthn_credential(webauthn_credential)
webauthn_credential.type == WebAuthn::TYPE_PUBLIC_KEY &&
- webauthn_credential.raw_id && webauthn_credential.id &&
- webauthn_credential.raw_id == WebAuthn.standard_encoder.decode(webauthn_credential.id)
+ webauthn_credential.raw_id && webauthn_credential.id &&
+ webauthn_credential.raw_id == WebAuthn.standard_encoder.decode(webauthn_credential.id)
end
##
@@ -53,9 +53,10 @@ module Webauthn
rp_id = webauthn_credential.client_extension_outputs['appid'] ? WebAuthn.configuration.origin : URI(WebAuthn.configuration.origin).host
webauthn_credential.response.verify(
encoder.decode(challenge),
- public_key: encoder.decode(stored_credential.public_key),
- sign_count: stored_credential.counter,
- rp_id: rp_id)
+ public_key: encoder.decode(stored_credential.public_key),
+ sign_count: stored_credential.counter,
+ rp_id: rp_id
+ )
end
end
end
diff --git a/app/services/work_items/callbacks/award_emoji.rb b/app/services/work_items/callbacks/award_emoji.rb
index 6344813d4b9..9ff5b6d049d 100644
--- a/app/services/work_items/callbacks/award_emoji.rb
+++ b/app/services/work_items/callbacks/award_emoji.rb
@@ -15,7 +15,8 @@ module WorkItems
def execute_emoji_service(action, name)
class_name = {
add: ::AwardEmojis::AddService,
- remove: ::AwardEmojis::DestroyService
+ remove: ::AwardEmojis::DestroyService,
+ toggle: ::AwardEmojis::ToggleService
}
raise_error(invalid_action_error(action)) unless class_name.key?(action)
diff --git a/app/services/work_items/create_service.rb b/app/services/work_items/create_service.rb
index 903736cf662..354a33a0384 100644
--- a/app/services/work_items/create_service.rb
+++ b/app/services/work_items/create_service.rb
@@ -32,8 +32,11 @@ module WorkItems
end
def before_create(work_item)
- execute_widgets(work_item: work_item, callback: :before_create_callback,
- widget_params: @widget_params)
+ execute_widgets(
+ work_item: work_item,
+ callback: :before_create_callback,
+ widget_params: @widget_params
+ )
super
end
@@ -41,8 +44,11 @@ module WorkItems
def transaction_create(work_item)
super.tap do |save_result|
if save_result
- execute_widgets(work_item: work_item, callback: :after_create_in_transaction,
- widget_params: @widget_params)
+ execute_widgets(
+ work_item: work_item,
+ callback: :after_create_in_transaction,
+ widget_params: @widget_params
+ )
end
end
end
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 6a9ddd5c83d..f313881470a 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
@@ -25,7 +25,7 @@ module WorkItems
end
def previous_related_issuables
- @related_issues ||= issuable.related_issues(current_user).to_a
+ @related_issues ||= issuable.linked_work_items(authorize: false).to_a
end
private
diff --git a/app/services/work_items/related_work_item_links/destroy_service.rb b/app/services/work_items/related_work_item_links/destroy_service.rb
new file mode 100644
index 00000000000..6d1920d01b2
--- /dev/null
+++ b/app/services/work_items/related_work_item_links/destroy_service.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module WorkItems
+ module RelatedWorkItemLinks
+ class DestroyService < BaseService
+ def initialize(work_item, user, params)
+ @work_item = work_item
+ @current_user = user
+ @params = params.dup
+ @failed_ids = []
+ @removed_ids = []
+ end
+
+ def execute
+ return error(_('No work item found.'), 403) unless can?(current_user, :admin_work_item_link, work_item)
+ return error(_('No work item IDs provided.'), 409) if params[:item_ids].empty?
+
+ destroy_links_for(params[:item_ids])
+
+ if removed_ids.any?
+ success(message: response_message, items_removed: removed_ids, items_with_errors: failed_ids.flatten)
+ else
+ error(error_message)
+ end
+ end
+
+ private
+
+ attr_reader :work_item, :current_user, :failed_ids, :removed_ids
+
+ def destroy_links_for(item_ids)
+ destroy_links(source: work_item, target: item_ids, direction: :target)
+ destroy_links(source: item_ids, target: work_item, direction: :source)
+ end
+
+ def destroy_links(source:, target:, direction:)
+ WorkItems::RelatedWorkItemLink.for_source_and_target(source, target).each do |link|
+ linked_item = link.try(direction)
+
+ if can?(current_user, :admin_work_item_link, linked_item)
+ link.destroy!
+ removed_ids << linked_item.id
+ create_notes(link)
+ else
+ failed_ids << linked_item.id
+ end
+ end
+ end
+
+ def create_notes(link)
+ SystemNoteService.unrelate_issuable(link.source, link.target, current_user)
+ SystemNoteService.unrelate_issuable(link.target, link.source, current_user)
+ end
+
+ def error_message
+ not_linked = params[:item_ids] - (removed_ids + failed_ids)
+ error_messages = []
+
+ if failed_ids.any?
+ error_messages << format(
+ _('%{item_ids} could not be removed due to insufficient permissions'), item_ids: failed_ids.to_sentence
+ )
+ end
+
+ if not_linked.any?
+ error_messages << format(
+ _('%{item_ids} could not be removed due to not being linked'), item_ids: not_linked.to_sentence
+ )
+ end
+
+ return '' unless error_messages.any?
+
+ format(_('IDs with errors: %{error_messages}.'), error_messages: error_messages.join(', '))
+ end
+
+ def response_message
+ success_message = format(_('Successfully unlinked IDs: %{item_ids}.'), item_ids: removed_ids.to_sentence)
+
+ return success_message unless error_message.present?
+
+ "#{success_message} #{error_message}"
+ end
+ end
+ end
+end
diff --git a/app/uploaders/design_management/design_v432x230_uploader.rb b/app/uploaders/design_management/design_v432x230_uploader.rb
index 975050c26e4..0f1ebfed4aa 100644
--- a/app/uploaders/design_management/design_v432x230_uploader.rb
+++ b/app/uploaders/design_management/design_v432x230_uploader.rb
@@ -20,7 +20,7 @@ module DesignManagement
#
# We currently choose not to resize `image/svg+xml` for security reasons.
# See https://gitlab.com/gitlab-org/gitlab/issues/207740#note_302766171
- MIME_TYPE_ALLOWLIST = %w(image/png image/jpeg image/bmp image/gif).freeze
+ MIME_TYPE_ALLOWLIST = %w[image/png image/jpeg image/bmp image/gif].freeze
process resize_to_fit: [432, 230]
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 87a624ddb60..c28f0893c56 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -20,8 +20,8 @@ class FileUploader < GitlabUploader
'!?\[.*?\]\(/uploads/(?P<secret>[0-9a-f]{32})/(?P<file>.*?)\)'
)
- DYNAMIC_PATH_PATTERN = %r{.*(?<secret>\b(\h{10}|\h{32}))\/(?<identifier>.*)}.freeze
- VALID_SECRET_PATTERN = %r{\A\h{10,32}\z}.freeze
+ DYNAMIC_PATH_PATTERN = %r{.*(?<secret>\b(\h{10}|\h{32}))\/(?<identifier>.*)}
+ VALID_SECRET_PATTERN = %r{\A\h{10,32}\z}
InvalidSecret = Class.new(StandardError)
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index 06bf742a22d..c1ca535b336 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -5,7 +5,7 @@ class GitlabUploader < CarrierWave::Uploader::Base
class_attribute :storage_location_identifier
- PROTECTED_METHODS = %i(filename cache_dir work_dir store_dir).freeze
+ PROTECTED_METHODS = %i[filename cache_dir work_dir store_dir].freeze
ObjectNotReadyError = Class.new(StandardError)
diff --git a/app/uploaders/packages/nuget/symbol_uploader.rb b/app/uploaders/packages/nuget/symbol_uploader.rb
new file mode 100644
index 00000000000..1d6ec9a8de8
--- /dev/null
+++ b/app/uploaders/packages/nuget/symbol_uploader.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Packages
+ module Nuget
+ class SymbolUploader < GitlabUploader
+ include ObjectStorage::Concern
+
+ storage_location :packages
+
+ alias_method :upload, :model
+
+ def store_dir
+ dynamic_segment
+ end
+
+ private
+
+ def dynamic_segment
+ raise ObjectNotReadyError, 'Packages::Nuget::Symbol model not ready' unless model.object_storage_key
+
+ model.object_storage_key
+ end
+ end
+ end
+end
diff --git a/app/validators/addressable_url_validator.rb b/app/validators/addressable_url_validator.rb
index 3e6ec0b6f29..6dcc089fa73 100644
--- a/app/validators/addressable_url_validator.rb
+++ b/app/validators/addressable_url_validator.rb
@@ -51,7 +51,7 @@ class AddressableUrlValidator < ActiveModel::EachValidator
# tasks that uses that url won't work.
# See https://gitlab.com/gitlab-org/gitlab-foss/issues/66723
BLOCKER_VALIDATE_OPTIONS = {
- schemes: %w(http https),
+ schemes: %w[http https],
ports: [],
allow_localhost: true,
allow_local_network: true,
diff --git a/app/validators/certificate_fingerprint_validator.rb b/app/validators/certificate_fingerprint_validator.rb
index 79d78653ec7..c4e317c3e79 100644
--- a/app/validators/certificate_fingerprint_validator.rb
+++ b/app/validators/certificate_fingerprint_validator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class CertificateFingerprintValidator < ActiveModel::EachValidator
- FINGERPRINT_PATTERN = /\A([a-zA-Z0-9]{2}[\s\-:]?){16,}\z/.freeze
+ FINGERPRINT_PATTERN = /\A([a-zA-Z0-9]{2}[\s\-:]?){16,}\z/
def validate_each(record, attribute, value)
unless value.try(:match, FINGERPRINT_PATTERN)
diff --git a/app/validators/duration_validator.rb b/app/validators/duration_validator.rb
index defd28d7d3b..bcdcf665cba 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)
+ ChronicDuration.parse(value, use_complete_matcher: true)
rescue ChronicDuration::DurationParseError
if options[:message]
record.errors.add(:base, options[:message])
diff --git a/app/validators/gitlab/zoom_url_validator.rb b/app/validators/gitlab/zoom_url_validator.rb
index c752cec07c2..d3da3977697 100644
--- a/app/validators/gitlab/zoom_url_validator.rb
+++ b/app/validators/gitlab/zoom_url_validator.rb
@@ -8,7 +8,7 @@ module Gitlab
# @example usage
# validates :url, 'gitlab/zoom_url': true
class ZoomUrlValidator < ActiveModel::EachValidator
- ALLOWED_SCHEMES = %w(https).freeze
+ ALLOWED_SCHEMES = %w[https].freeze
def validate_each(record, attribute, value)
links_count = Gitlab::ZoomLinkExtractor.new(value).links.size
diff --git a/app/validators/json_schema_validator.rb b/app/validators/json_schema_validator.rb
index 9c246a114f6..2ef011df73e 100644
--- a/app/validators/json_schema_validator.rb
+++ b/app/validators/json_schema_validator.rb
@@ -10,9 +10,9 @@
# end
#
class JsonSchemaValidator < ActiveModel::EachValidator
- FILENAME_ALLOWED = /\A[a-z0-9_-]*\Z/.freeze
+ FILENAME_ALLOWED = /\A[a-z0-9_-]*\Z/
FilenameError = Class.new(StandardError)
- BASE_DIRECTORY = %w(app validators json_schemas).freeze
+ BASE_DIRECTORY = %w[app validators json_schemas].freeze
def initialize(options)
raise ArgumentError, "Expected 'filename' as an argument" unless options[:filename]
diff --git a/app/validators/json_schemas/pinned_nav_items.json b/app/validators/json_schemas/pinned_nav_items.json
index 60dee5cc463..aaeb2fe8bda 100644
--- a/app/validators/json_schemas/pinned_nav_items.json
+++ b/app/validators/json_schemas/pinned_nav_items.json
@@ -16,6 +16,13 @@
"type": "string"
},
"uniqueItems": true
+ },
+ "organization": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "uniqueItems": true
}
},
"additionalProperties": false
diff --git a/app/validators/json_schemas/scan_result_policy_project_approval_settings.json b/app/validators/json_schemas/scan_result_policy_project_approval_settings.json
new file mode 100644
index 00000000000..5a81c61ede4
--- /dev/null
+++ b/app/validators/json_schemas/scan_result_policy_project_approval_settings.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "description": "Scan result policy project_approval_settings",
+ "type": "object",
+ "properties": {
+ "prevent_approval_by_author": {
+ "type": "boolean"
+ },
+ "prevent_approval_by_commit_author": {
+ "type": "boolean"
+ },
+ "remove_approvals_with_new_commit": {
+ "type": "boolean"
+ },
+ "require_password_to_approve": {
+ "type": "boolean"
+ },
+ "block_unprotecting_branches": {
+ "type": "boolean"
+ }
+ }
+}
diff --git a/app/validators/line_code_validator.rb b/app/validators/line_code_validator.rb
index e1abccc1dff..54f9272a2ae 100644
--- a/app/validators/line_code_validator.rb
+++ b/app/validators/line_code_validator.rb
@@ -4,7 +4,7 @@
#
# Custom validator for GitLab line codes.
class LineCodeValidator < ActiveModel::EachValidator
- PATTERN = /\A[a-z0-9]+_\d+_\d+\z/.freeze
+ PATTERN = /\A[a-z0-9]+_\d+_\d+\z/
def validate_each(record, attribute, value)
unless PATTERN.match?(value)
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
deleted file mode 100644
index aa5543700a7..00000000000
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ /dev/null
@@ -1,39 +0,0 @@
-- reporter = abuse_report.reporter
-- user = abuse_report.user
-%tr
- %th.d-block.d-sm-none
- %strong= _('User')
- %td
- - if user
- = link_to user.name, user
- .light.small
- = html_escape(_('Joined %{time_ago}')) % { time_ago: time_ago_with_tooltip(user.created_at).html_safe }
- - else
- = _('(removed)')
- %td
- - if reporter
- %strong.subheading.d-block.d-sm-none
- = _('Reported by %{reporter}').html_safe % { reporter: reporter ? link_to(reporter.name, reporter) : _('(removed)') }
- .light.gl-display-none.gl-sm-display-block
- = link_to(reporter.name, reporter)
- .light.small
- = time_ago_with_tooltip(abuse_report.created_at)
- - else
- = _('(removed)')
- %td
- %strong.subheading.d-block.d-sm-none
- = _('Message')
- .message
- = markdown_field(abuse_report, :message)
- %td
- - if user && user != current_user
- = render Pajamas::ButtonComponent.new(href: admin_abuse_report_path(abuse_report, remove_user: true), variant: :danger, block: true, button_options: { data: { confirm: _("USER %{user} WILL BE REMOVED! Are you sure?") % { user: user.name }, confirm_btn_variant: "danger", remote: true, method: :delete }, class: "js-remove-tr" }) do
- = _('Remove user & report')
- - if user.blocked?
- = render Pajamas::ButtonComponent.new(href: block_admin_user_path(user), block: true, disabled: true, button_options: { data: { confirm: _('USER WILL BE BLOCKED! Are you sure?'), method: :put } }) do
- = _('Already blocked')
- - else
- = render Pajamas::ButtonComponent.new(href: block_admin_user_path(user), block: true, button_options: { data: { confirm: _('USER WILL BE BLOCKED! Are you sure?'), method: :put } }) do
- = _('Block user')
- = render Pajamas::ButtonComponent.new(href: [:admin, abuse_report], block: true, button_options: { data: { remote: true, method: :delete }, class: "js-remove-tr" }) do
- = _('Remove report')
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index fee3a846849..ea2d4f3b4af 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -2,35 +2,5 @@
%h1.page-title.gl-font-size-h-display= _('Abuse Reports')
-- if Feature.enabled?(:abuse_reports_list)
- #js-abuse-reports-list-app{ data: abuse_reports_list_data(@abuse_reports) }
- = gl_loading_icon(css_class: 'gl-my-5', size: 'md')
-- else
- .row-content-block.second-block
- = form_tag admin_abuse_reports_path, method: :get, class: 'filter-form' do
- .filter-categories.flex-fill
- .filter-item.inline
- = dropdown_tag(user_dropdown_label(params[:user_id], 'User'),
- options: { toggle_class: 'js-filter-submit js-user-search',
- title: _('Filter by user'), filter: true, filterInput: 'input#user-search',
- dropdown_class: 'dropdown-menu-selectable dropdown-menu-user js-filter-submit',
- placeholder: _('Search users'),
- data: { current_user: true, field_name: 'user_id' }})
-
- .abuse-reports
- - if @abuse_reports.present?
- .table-holder
- %table.table.responsive-table
- %thead.d-none.d-md-table-header-group
- %tr
- %th= _('User')
- %th= _('Reported by')
- %th.wide= _('Message')
- %th= _('Action')
- = render @abuse_reports
- = paginate @abuse_reports, theme: 'gitlab'
- - else
- .empty-state
- .text-center
- %h4= _("There are no abuse reports!")
- %h3= emoji_icon('tada')
+#js-abuse-reports-list-app{ data: abuse_reports_list_data(@abuse_reports) }
+ = gl_loading_icon(css_class: 'gl-my-5', size: 'md')
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 d8d6af606ac..b65649b5a07 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -19,25 +19,6 @@
= 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' }
.form-group
- = f.label :max_export_size, _('Maximum export size (MiB)'), class: 'label-light'
- = f.number_field :max_export_size, class: 'form-control gl-form-input', title: _('Maximum size of export files.'), data: { toggle: 'tooltip', container: 'body' }
- %span.form-text.text-muted= _('Set to 0 for no size limit.')
- .form-group
- = f.label :max_import_size, _('Maximum import size (MiB)'), class: 'label-light'
- = f.number_field :max_import_size, class: 'form-control gl-form-input', title: _('Maximum size of import files.'), data: { toggle: 'tooltip', container: 'body' }
- %span.form-text.text-muted= _('Only effective when remote storage is enabled. Set to 0 for no size limit.')
- .form-group
- = f.label :max_import_remote_file_size, s_('Import|Maximum import remote file size (MB)'), class: 'label-light'
- = f.number_field :max_import_remote_file_size, class: 'form-control gl-form-input', title: s_('Import|Maximum remote file size for imports from external object storages. For example, AWS S3.'), data: { toggle: 'tooltip', container: 'body' }
- %span.form-text.text-muted= _('Set to 0 for no size limit.')
- .form-group
- = f.label :bulk_import_max_download_file_size, s_('BulkImport|Direct transfer maximum download file size (MB)'), class: 'label-light'
- = f.number_field :bulk_import_max_download_file_size, class: 'form-control gl-form-input', title: s_('BulkImport|Maximum download file size when importing from source GitLab instances by direct transfer.'), data: { toggle: 'tooltip', container: 'body' }
- .form-group
- = f.label :max_decompressed_archive_size, s_('Import|Maximum decompressed size (MiB)'), class: 'label-light'
- = f.number_field :max_decompressed_archive_size, class: 'form-control gl-form-input', title: s_('Import|Maximum size of decompressed archive.'), data: { toggle: 'tooltip', container: 'body' }
- %span.form-text.text-muted= _('Set to 0 for no size limit.')
- .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' }
%span.form-text.text-muted#session_expire_delay_help_block= _('Restart GitLab to apply changes.')
@@ -53,7 +34,7 @@
.form-group
= f.label :user_oauth_applications, _('User OAuth applications'), class: 'label-bold'
- = f.gitlab_ui_checkbox_component :user_oauth_applications, _('Allow users to register any application to use GitLab as an OAuth provider')
+ = f.gitlab_ui_checkbox_component :user_oauth_applications, _('Allow users to register any application to use GitLab as an OAuth provider. This setting does not affect group-level OAuth applications.')
.form-group
= f.label :user_default_external, _('New users set to external'), class: 'label-bold'
= f.gitlab_ui_checkbox_component :user_default_external, _('Newly-registered users are external by default')
diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml
index 2f31eb5f6d1..65049fa5466 100644
--- a/app/views/admin/application_settings/_email.html.haml
+++ b/app/views/admin/application_settings/_email.html.haml
@@ -16,9 +16,6 @@
= render_if_exists 'admin/application_settings/email_additional_text_setting', form: f
.form-group
- = f.gitlab_ui_checkbox_component :in_product_marketing_emails_enabled, _('Enable in-product marketing emails'), help_text: _('Send emails to help guide new users through the onboarding process.')
-
- .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)
diff --git a/app/views/admin/application_settings/_gitpod.html.haml b/app/views/admin/application_settings/_gitpod.html.haml
index 988153d45a4..1f56487cea4 100644
--- a/app/views/admin/application_settings/_gitpod.html.haml
+++ b/app/views/admin/application_settings/_gitpod.html.haml
@@ -6,7 +6,7 @@
= _('Gitpod')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
- .gl-text-secondary
+ .gl-text-secondary.gl-mb-5
#js-gitpod-settings-help-text{ data: {"message" => gitpod_enable_description, "message-url" => "https://gitpod.io/" } }
= link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information')
diff --git a/app/views/admin/application_settings/_import_and_export.html.haml b/app/views/admin/application_settings/_import_and_export.html.haml
new file mode 100644
index 00000000000..8e321406bf8
--- /dev/null
+++ b/app/views/admin/application_settings/_import_and_export.html.haml
@@ -0,0 +1,43 @@
+= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-import-export-settings'), html: { class: 'fieldset-form', id: 'import-export-settings' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :import_sources, s_('AdminSettings|Import sources'), class: 'label-bold gl-mb-0'
+ %span.form-text.gl-mt-0.gl-mb-3#import-sources-help
+ - tag_pair_github_docs = tag_pair(link_to('', help_page_path('integration/github'), target: '_blank', rel: 'noopener noreferrer'), :github_docs_link_start, :github_docs_link_end)
+ - tag_pair_bitbucket_docs = tag_pair(link_to('', help_page_path('integration/bitbucket'), target: '_blank', rel: 'noopener noreferrer'), :bitbucket_docs_link_start, :bitbucket_docs_link_end)
+ = safe_format(s_('AdminSettings|Code can be imported from enabled sources during project creation. OmniAuth must be configured for GitHub %{github_docs_link_start}%{icon}%{github_docs_link_end} and Bitbucket %{bitbucket_docs_link_start}%{icon}%{bitbucket_docs_link_end}.'), tag_pair_github_docs, tag_pair_bitbucket_docs, icon: sprite_icon('question-o'))
+ = hidden_field_tag 'application_setting[import_sources][]'
+ - import_sources_checkboxes(f).each do |source|
+ = source
+ .form-group{ data: { testid: 'project-export' } }
+ = f.label :project_export, s_('AdminSettings|Project export'), class: 'label-bold'
+ = f.gitlab_ui_checkbox_component :project_export_enabled, s_('AdminSettings|Enabled')
+ .form-group{ data: { testid: 'bulk-import' } }
+ = f.label :bulk_import, s_('AdminSettings|Allow migrating GitLab groups and projects by direct transfer'), class: 'gl-font-weight-bold'
+ = f.gitlab_ui_checkbox_component :bulk_import_enabled, s_('AdminSettings|Enabled')
+ .form-group
+ = f.label :max_export_size, _('Maximum export size (MiB)'), class: 'label-light'
+ = f.number_field :max_export_size, class: 'form-control gl-form-input', title: _('Maximum size of export files.'), data: { toggle: 'tooltip', container: 'body' }
+ %span.form-text.text-muted= _('Set to 0 for no size limit.')
+ .form-group
+ = f.label :max_import_size, _('Maximum import size (MiB)'), class: 'label-light'
+ = f.number_field :max_import_size, class: 'form-control gl-form-input', title: _('Maximum size of import files.'), data: { toggle: 'tooltip', container: 'body' }
+ %span.form-text.text-muted= _('Only effective when remote storage is enabled. Set to 0 for no size limit.')
+ .form-group
+ = f.label :max_import_remote_file_size, s_('Import|Maximum import remote file size (MiB)'), class: 'label-light'
+ = f.number_field :max_import_remote_file_size, class: 'form-control gl-form-input', title: s_('Import|Maximum remote file size for imports from external object storages. For example, AWS S3.'), data: { toggle: 'tooltip', container: 'body' }
+ %span.form-text.text-muted= _('Set to 0 for no size limit.')
+ .form-group
+ = f.label :bulk_import_max_download_file_size, s_('BulkImport|Direct transfer maximum download file size (MiB)'), class: 'label-light'
+ = f.number_field :bulk_import_max_download_file_size, class: 'form-control gl-form-input', title: s_('BulkImport|Maximum download file size when importing from source GitLab instances by direct transfer.'), data: { toggle: 'tooltip', container: 'body' }
+ .form-group
+ = f.label :max_decompressed_archive_size, s_('Import|Maximum decompressed file size for archives from imports (MiB)'), class: 'label-light'
+ = f.number_field :max_decompressed_archive_size, class: 'form-control gl-form-input', title: s_('Import|Maximum size of decompressed archive.'), data: { toggle: 'tooltip', container: 'body' }
+ %span.form-text.text-muted= _('Set to 0 for no size limit.')
+ .form-group
+ = f.label :decompress_archive_file_timeout, s_('Import|Timeout for decompressing archived files (seconds)'), class: 'label-light'
+ = f.number_field :decompress_archive_file_timeout, class: 'form-control gl-form-input', title: s_('Import|Timeout for decompressing archived files.'), data: { toggle: 'tooltip', container: 'body' }
+ %span.form-text.text-muted= _('Set to 0 to disable timeout.')
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml
index 1eb6b747704..d5f2c6afee3 100644
--- a/app/views/admin/application_settings/_pages.html.haml
+++ b/app/views/admin/application_settings/_pages.html.haml
@@ -19,28 +19,25 @@
= f.label :max_pages_size, _('Maximum size of pages (MiB)'), class: 'label-bold'
= f.number_field :max_pages_size, class: 'form-control gl-form-input'
.form-text.text-muted
- - pages_link_url = help_page_path('administration/pages/index', anchor: 'set-maximum-size-of-gitlab-pages-site-in-a-project')
- - pages_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: pages_link_url }
- = s_('AdminSettings|Set the maximum size of GitLab Pages per project (0 for unlimited). %{link_start}Learn more.%{link_end}').html_safe % { link_start: pages_link_start, link_end: '</a>'.html_safe }
+ - link = link_to('', help_page_path('administration/pages/index', anchor: 'set-maximum-size-of-gitlab-pages-site-in-a-project'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(s_('AdminSettings|Set the maximum size of GitLab Pages per project (0 for unlimited). %{link_start}Learn more.%{link_end}'), tag_pair(link, :link_start, :link_end))
.form-group
= f.label :max_pages_custom_domains_per_project, s_('AdminSettings|Maximum number of custom domains per project'), class: 'label-bold'
= f.number_field :max_pages_custom_domains_per_project, class: 'form-control gl-form-input'
.form-text.text-muted
- - pages_link_url = help_page_path('administration/pages/index', anchor: 'set-maximum-number-of-gitlab-pages-custom-domains-for-a-project')
- - pages_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: pages_link_url }
- = s_('AdminSettings|Set the maximum number of GitLab Pages custom domains per project (0 for unlimited). %{link_start}Learn more.%{link_end}').html_safe % { link_start: pages_link_start, link_end: '</a>'.html_safe }
+ - link = link_to('', help_page_path('administration/pages/index', anchor: 'set-maximum-number-of-gitlab-pages-custom-domains-for-a-project'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(s_('AdminSettings|Set the maximum number of GitLab Pages custom domains per project (0 for unlimited). %{link_start}Learn more.%{link_end}'), tag_pair(link, :link_start, :link_end))
%h5
= s_("AdminSettings|Configure Let's Encrypt")
%p
- - lets_encrypt_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: "https://letsencrypt.org/" }
- = _("%{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end} is a free, automated, and open certificate authority (CA) that issues digital certificates to enable HTTPS (SSL/TLS) for sites.").html_safe % { lets_encrypt_link_start: lets_encrypt_link_start, lets_encrypt_link_end: '</a>'.html_safe }
+ - link = link_to('', "https://letsencrypt.org/", target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(_("%{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end} is a free, automated, and open certificate authority (CA) that issues digital certificates to enable HTTPS (SSL/TLS) for sites."), tag_pair(link, :lets_encrypt_link_start, :lets_encrypt_link_end))
.form-group
= f.label :lets_encrypt_notification_email, s_("AdminSettings|Let's Encrypt email"), class: 'label-bold'
= f.text_field :lets_encrypt_notification_email, class: 'form-control gl-form-input'
.form-text.text-muted
- - pages_link_url = help_page_path('administration/pages/index', anchor: 'lets-encrypt-integration')
- - pages_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: pages_link_url }
- = s_("AdminSettings|A Let's Encrypt account will be configured for this GitLab instance using this email address. You will receive emails to warn of expiring certificates. %{link_start}Learn more.%{link_end}").html_safe % { link_start: pages_link_start, link_end: '</a>'.html_safe }
+ - link = link_to('', help_page_path('administration/pages/index', anchor: 'lets-encrypt-integration'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(s_("AdminSettings|A Let's Encrypt account will be configured for this GitLab instance using this email address. You will receive emails to warn of expiring certificates. %{link_start}Learn more.%{link_end}"), tag_pair(link, :link_start, :link_end))
.form-group
- terms_of_service_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: lets_encrypt_terms_of_service_admin_application_settings_path }
= f.gitlab_ui_checkbox_component :lets_encrypt_terms_of_service_accepted,
diff --git a/app/views/admin/application_settings/_protected_paths.html.haml b/app/views/admin/application_settings/_protected_paths.html.haml
index 3a7a951d137..cd17e4bdec3 100644
--- a/app/views/admin/application_settings/_protected_paths.html.haml
+++ b/app/views/admin/application_settings/_protected_paths.html.haml
@@ -4,7 +4,7 @@
%fieldset
.form-group
= f.gitlab_ui_checkbox_component :throttle_protected_paths_enabled,
- _('Enable rate limiting for POST requests to the specified paths'),
+ _('Enable rate limiting for requests to the specified paths'),
help_text: _('Helps reduce request volume for protected paths.')
.form-group
= f.label :throttle_protected_paths_requests_per_period, 'Maximum requests per period per user', class: 'label-bold'
@@ -14,11 +14,14 @@
= f.number_field :throttle_protected_paths_period_in_seconds, class: 'form-control gl-form-input'
.form-group
= f.label :protected_paths, class: 'label-bold' do
- = _('Paths to protect with rate limiting')
+ = _('Paths with rate limiting for POST requests')
= f.text_area :protected_paths_raw, placeholder: '/users/sign_in,/users/password', class: 'form-control gl-form-input', rows: 10
+ .form-group
+ = f.label :protected_paths_for_get_request, class: 'label-bold' do
+ = _('Paths with rate limiting for GET requests')
+ = f.text_area :protected_paths_for_get_request_raw, class: 'form-control gl-form-input', rows: 10
%span.form-text.text-muted
- - relative_url_link = 'https://docs.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab'
- - relative_url_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: relative_url_link }
- = _('All paths are relative to the GitLab URL. Do not include %{relative_url_link_start}relative URLs%{relative_url_link_end}.').html_safe % { relative_url_link_start: relative_url_link_start, relative_url_link_end: '</a>'.html_safe }
+ - link = link_to('', 'https://docs.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab', target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(_('All paths are relative to the GitLab URL. Do not include %{relative_url_link_start}relative URLs%{relative_url_link_end}.'), tag_pair(link, :relative_url_link_start, :relative_url_link_end))
= f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_search_limits.html.haml b/app/views/admin/application_settings/_search_limits.html.haml
index 396c263dd5d..b318f7e5a20 100644
--- a/app/views/admin/application_settings/_search_limits.html.haml
+++ b/app/views/admin/application_settings/_search_limits.html.haml
@@ -12,5 +12,11 @@
= f.label :search_rate_limit_unauthenticated, _('Maximum number of requests per minute for an unauthenticated IP address'), class: 'label-bold'
= f.number_field :search_rate_limit_unauthenticated, class: 'form-control gl-form-input'
+ .form-group
+ = f.label :search_rate_limit_allowlist, _('Users to exclude from the rate limit'), class: 'label-bold'
+ = f.text_area :search_rate_limit_allowlist_raw, class: 'form-control gl-form-input', rows: 5, aria: { describedBy: 'search-rate-limit-allowlist-field-description' }
+ .form-text.text-muted{ id: 'search-rate-limit-allowlist-field-description' }
+ = _('List of users who are allowed to exceed the rate limit. Example: username1, username2')
+
= f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
diff --git a/app/views/admin/application_settings/_sentry.html.haml b/app/views/admin/application_settings/_sentry.html.haml
index 20164cfe88d..9f2a40e4e54 100644
--- a/app/views/admin/application_settings/_sentry.html.haml
+++ b/app/views/admin/application_settings/_sentry.html.haml
@@ -17,4 +17,12 @@
= 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-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
+
= f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_silent_mode_settings_form.html.haml b/app/views/admin/application_settings/_silent_mode_settings_form.html.haml
new file mode 100644
index 00000000000..92b4174842f
--- /dev/null
+++ b/app/views/admin/application_settings/_silent_mode_settings_form.html.haml
@@ -0,0 +1,11 @@
+%section.settings.no-animate#js-silent-mode-toggle{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
+ = s_('SilentMode|Silent mode')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p.gl-text-secondary
+ = s_('SilentMode|Suppress outbound communication, such as emails, from GitLab.')
+ = link_to _('Learn more.'), help_page_path('administration/silent_mode/index'), target: '_blank', rel: 'noopener noreferrer'
+ .settings-content
+ #js-silent-mode-settings{ data: { "silent-mode-enabled" => @application_setting.silent_mode_enabled.to_s } }
diff --git a/app/views/admin/application_settings/_snowplow.html.haml b/app/views/admin/application_settings/_snowplow.html.haml
index 6f9aad56ce8..1b90432e1f3 100644
--- a/app/views/admin/application_settings/_snowplow.html.haml
+++ b/app/views/admin/application_settings/_snowplow.html.haml
@@ -6,7 +6,7 @@
= 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/snowplow/index'), target: '_blank', rel: 'noopener noreferrer')
+ - help_link = link_to('', help_page_path('development/internal_analytics/snowplow/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
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
index 0455394444c..5a3814ca83d 100644
--- a/app/views/admin/application_settings/_usage.html.haml
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -12,7 +12,7 @@
help_text: _("GitLab informs you if a new version is available. %{link_start}What information does GitLab Inc. collect?%{link_end}").html_safe % { link_start: help_link_start, link_end: link_end }
.form-group
- can_be_configured = @application_setting.usage_ping_can_be_configured?
- - service_ping_link_start = link_start % { url: help_page_path('development/service_ping/index') }
+ - service_ping_link_start = link_start % { url: help_page_path('development/internal_analytics/service_ping/index') }
- deactivating_service_ping_link_start = link_start % { url: help_page_path('administration/settings/usage_statistics', anchor: 'disable-usage-statistics-with-the-configuration-file') }
- usage_ping_help_text = s_('AdminSettings|To help improve GitLab and its user experience, GitLab periodically collects usage information. %{link_start}What information is shared with GitLab Inc.?%{link_end}').html_safe % { link_start: service_ping_link_start, link_end: link_end }
- 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 }
@@ -35,19 +35,10 @@
checkbox_options: { id: 'application_setting_usage_ping_features_enabled' },
label_options: { id: 'service_ping_features_label' }
.form-text.gl-text-gray-500.gl-pl-6
- %p.gl-mb-3= s_('AdminSettings|Registration Features include:')
- - email_from_gitlab_path = help_page_path('administration/email_from_gitlab')
- - repo_size_limit_path = help_page_path('administration/settings/account_and_limit_settings', anchor: 'repository-size-limit')
- - restrict_ip_path = help_page_path('user/group/access_and_permissions', anchor: 'restrict-group-access-by-ip-address')
- - email_from_gitlab_link = link_start % { url: email_from_gitlab_path }
- - repo_size_limit_link = link_start % { url: repo_size_limit_path }
- - restrict_ip_link = link_start % { url: restrict_ip_path }
- %ul
- %li
- = s_('AdminSettings|Email from GitLab - email users right from the Admin Area. %{link_start}Learn more%{link_end}.').html_safe % { link_start: email_from_gitlab_link, link_end: link_end }
- %li
- = s_('AdminSettings|Limit project size at a global, group, and project level. %{link_start}Learn more%{link_end}.').html_safe % { link_start: repo_size_limit_link, link_end: link_end }
- %li
- = s_('AdminSettings|Restrict group access by IP address. %{link_start}Learn more%{link_end}.').html_safe % { link_start: restrict_ip_link, link_end: link_end }
+ %p.gl-mb-3
+ - registration_features_gitlab_path = help_page_path('administration/settings/usage_statistics', anchor: 'registration-features-program')
+ - registration_features_gitlab_link = link_to('', registration_features_gitlab_path, target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(s_('AdminSettings|For a list of included Registration Features, see %{link_start}the documentation%{link_end}.'), tag_pair(registration_features_gitlab_link, :link_start, :link_end))
+
= f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index 7142128d2cd..624f5a48c3a 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -19,33 +19,17 @@
= s_('AdminSettings|Restricted visibility levels')
%small.form-text.text-gl-muted
= s_('AdminSettings|Prevent non-administrators from using the selected visibility levels for groups, projects and snippets.')
+ = s_('AdminSettings|The selected level must be different from the selected default group and project visibility.')
+ = link_to _('Learn more.'), help_page_path('administration/settings/visibility_and_access_controls', anchor: 'restrict-visibility-levels'), target: '_blank', rel: 'noopener noreferrer'
= hidden_field_tag 'application_setting[restricted_visibility_levels][]'
.gl-form-checkbox-group
- restricted_level_checkboxes(f).each do |checkbox|
= checkbox
- .form-group
- = f.label :import_sources, s_('AdminSettings|Import sources'), class: 'label-bold gl-mb-0'
- %span.form-text.gl-mt-0.gl-mb-3#import-sources-help
- = _('Code can be imported from enabled sources during project creation. OmniAuth must be configured for GitHub')
- = link_to sprite_icon('question-o'), help_page_path("integration/github")
- and Bitbucket
- = link_to sprite_icon('question-o'), help_page_path("integration/bitbucket")
- = hidden_field_tag 'application_setting[import_sources][]'
- - import_sources_checkboxes(f).each do |source|
- = source
= render_if_exists 'admin/application_settings/ldap_access_setting', form: f
= render_if_exists 'admin/application_settings/saml_group_locks_setting', form: f
- .form-group{ data: { testid: 'project-export' } }
- = f.label :project_export, s_('AdminSettings|Project export'), class: 'label-bold'
- = f.gitlab_ui_checkbox_component :project_export_enabled, s_('AdminSettings|Enabled')
-
- .form-group{ data: { testid: 'bulk-import' } }
- = f.label :bulk_import, s_('AdminSettings|Allow migrating GitLab groups and projects by direct transfer'), class: 'gl-font-weight-bold'
- = f.gitlab_ui_checkbox_component :bulk_import_enabled, s_('AdminSettings|Enabled')
-
.form-group
%label.label-bold= _('Enabled Git access protocols')
= select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml
index 6ae9c58ffcd..5aa2684f084 100644
--- a/app/views/admin/application_settings/general.html.haml
+++ b/app/views/admin/application_settings/general.html.haml
@@ -10,7 +10,7 @@
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p.gl-text-secondary
- = _('Set visibility of project contents. Configure import sources and Git access protocols.')
+ = s_('AdminSettings|Set visibility of project contents and configure Git access protocols.')
.settings-content
= render 'visibility_and_access'
@@ -25,6 +25,17 @@
.settings-content
= render 'account_and_limit'
+%section.settings.as-import-export.no-animate#js-import-export-settings{ class: ('expanded' if expanded_by_default?), data: { testid: 'admin-import-export-settings' } }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
+ = _('Import and export settings')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle '}) do
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p.gl-text-secondary
+ = _('Configure import sources and settings related to import and export features.')
+ .settings-content
+ = render 'import_and_export'
+
%section.settings.as-diff-limits.no-animate#js-merge-request-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
@@ -89,6 +100,7 @@
= render 'terminal'
= render_if_exists 'admin/application_settings/maintenance_mode_settings_form'
+= render 'admin/application_settings/silent_mode_settings_form'
= render 'admin/application_settings/gitpod'
= render 'admin/application_settings/kroki'
= render 'admin/application_settings/mailgun'
diff --git a/app/views/admin/dev_ops_report/_score.html.haml b/app/views/admin/dev_ops_report/_score.html.haml
index 208afefc73b..a504563ad91 100644
--- a/app/views/admin/dev_ops_report/_score.html.haml
+++ b/app/views/admin/dev_ops_report/_score.html.haml
@@ -1,6 +1,6 @@
- service_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
- if !service_ping_enabled
- #js-devops-service-ping-disabled{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_service_ping_path: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/service_ping/index.md') } }
+ #js-devops-service-ping-disabled{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_service_ping_path: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/internal_analytics/service_ping/index.md') } }
- else
#js-devops-score{ data: { devops_score_metrics: devops_score_metrics(@metric).to_json, no_data_image_path: image_path('dev_ops_report_no_data.svg'), devops_score_intro_image_path: image_path('dev_ops_report_overview.svg') } }
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index b708564e23a..2da28910af3 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -41,6 +41,6 @@
- else
.gl-mt-5
- = 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
= render Pajamas::ButtonComponent.new(href: admin_group_path(@group)) do
= _('Cancel')
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index 20d24161c57..0c4bf91f545 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -1,11 +1,11 @@
- group = local_assigns.fetch(:group)
-%li.group-row.gl-py-3.gl-align-items-center{ class: 'gl-display-flex!', data: { qa_selector: 'group_row_content' } }
+%li.group-row.gl-py-3.gl-align-items-center{ class: 'gl-display-flex!', data: { testid: 'group-row-content' } }
= render Pajamas::AvatarComponent.new(group, size: 32, alt: '')
.gl-min-w-0.gl-flex-grow-1.gl-ml-3
.title
- = link_to [:admin, group], class: 'group-name', data: { qa_selector: 'group_name_link' } do
+ = link_to [:admin, group], class: 'group-name', data: { testid: 'group-name-link' } do
= group.full_name
- if group.description.present?
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 2a49b9c5ad8..9f42897d1da 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -7,7 +7,7 @@
= hidden_field_tag :sort, @sort
.search-holder
.search-field-holder
- = search_field_tag :name, params[:name].presence, class: "form-control search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: 'Search by name', data: { qa_selector: 'group_search_field' }
+ = search_field_tag :name, params[:name].presence, class: "form-control search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: 'Search by name', data: { testid: 'group-search-field' }
= sprite_icon('search', css_class: 'search-icon')
= render "shared/groups/dropdown", options_hash: admin_groups_sort_options_hash
= render Pajamas::ButtonComponent.new(variant: :confirm, href: new_admin_group_path) do
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 5f5f6c98663..f7a49c88d78 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -8,7 +8,7 @@
= _('Group: %{group_name}') % { group_name: @group.full_name }
= render Pajamas::ButtonComponent.new(href: admin_group_edit_path(@group),
- button_options: { class: 'gl-float-right', data: { qa_selector: 'edit_group_link' }},
+ button_options: { class: 'gl-float-right', data: { testid: 'edit-group-link' }},
icon: 'pencil') do
= _('Edit')
%hr
diff --git a/app/views/admin/hook_logs/show.html.haml b/app/views/admin/hook_logs/show.html.haml
index 0ccde159905..096fae8d457 100644
--- a/app/views/admin/hook_logs/show.html.haml
+++ b/app/views/admin/hook_logs/show.html.haml
@@ -1,5 +1,5 @@
- page_title _('Request details')
-%h1.page-title.gl-font-size-h-display
+%h2.page-title.gl-font-size-h-display
= _("Request details")
%hr
diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml
index a24cd000464..921232125ff 100644
--- a/app/views/admin/identities/_identity.html.haml
+++ b/app/views/admin/identities/_identity.html.haml
@@ -7,6 +7,8 @@
= saml_group_link(identity)
%td
= identity.extern_uid
+ %td
+ = '-'
%td{ class: 'gl-py-0!' }
- button_classes = 'has-tooltip gl-my-3'
= render Pajamas::ButtonComponent.new(category: :tertiary,
diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml
index 1bb14969939..8077f0e15ca 100644
--- a/app/views/admin/identities/index.html.haml
+++ b/app/views/admin/identities/index.html.haml
@@ -10,6 +10,7 @@
%th{ class: 'gl-border-t-0!' }= s_('Identity|Provider ID')
%th{ class: 'gl-border-t-0!' }= _('Group')
%th{ class: 'gl-border-t-0!' }= _('Identifier')
+ %th{ class: 'gl-border-t-0!' }= s_('Identity|Active')
%th{ class: 'gl-border-t-0!' }= _('Actions')
- if identity_cells_to_render?(@identities, @user)
= render_if_exists partial: 'admin/identities/scim_identity', collection: scim_identities_collection(@user)
@@ -17,6 +18,6 @@
- else
%tbody
%tr
- %td{ colspan: '5' }
+ %td{ colspan: '6' }
.text-center.my-2
= _('This user has no identities')
diff --git a/app/views/admin/jobs/index.html.haml b/app/views/admin/jobs/index.html.haml
index c5632e0d70b..b8a9ad32259 100644
--- a/app/views/admin/jobs/index.html.haml
+++ b/app/views/admin/jobs/index.html.haml
@@ -1,30 +1,7 @@
- add_page_specific_style 'page_bundles/ci_status'
-- add_page_specific_style 'page_bundles/admin/jobs_index'
- breadcrumb_title _("Jobs")
- page_title _("Jobs")
-- if Feature.enabled?(:admin_jobs_vue)
- #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 } }
-- else
- .top-area
- .scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full
- %button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') }
- = sprite_icon('chevron-lg-left', size: 12)
- %button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') }
- = sprite_icon('chevron-lg-right', size: 12)
- - build_path_proc = ->(scope) { admin_jobs_path(scope: scope) }
- = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
-
- - if @all_builds.running_or_pending.any?
- #js-stop-jobs-modal
- .nav-controls
- = render Pajamas::ButtonComponent.new(variant: :danger, button_options: { id: 'js-stop-jobs-button', data: { url: cancel_all_admin_jobs_path } }) do
- = s_('AdminArea|Stop all jobs')
-
- .row-content-block.second-block
- #{(@scope || 'all').capitalize} jobs
-
- %ul.content-list.builds-content-list.admin-builds-table
- = render "projects/jobs/table", builds: @builds, admin: true
+#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 } }
diff --git a/app/views/admin/topics/_form.html.haml b/app/views/admin/topics/_form.html.haml
index c3b5161d617..2638e45c9eb 100644
--- a/app/views/admin/topics/_form.html.haml
+++ b/app/views/admin/topics/_form.html.haml
@@ -20,7 +20,7 @@
= f.label :description, _("Description")
.js-markdown-editor{ data: { render_markdown_path: preview_markdown_admin_topics_path,
markdown_docs_path: help_page_path('user/markdown'),
- qa_selector: 'topic_form_description',
+ testid: 'topic-form-description',
form_field_placeholder: _('Write a description…'),
supports_quick_actions: 'false',
enable_autocomplete: 'false',
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
index 4979f7e28e7..5c80b3b4352 100644
--- a/app/views/admin/users/_access_levels.html.haml
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -9,7 +9,7 @@
= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control gl-form-input gl-form-input-sm'
.form-group.gl-form-group{ role: 'group' }
- = f.gitlab_ui_checkbox_component :can_create_group, s_('AdminUsers|Can create group')
+ = f.gitlab_ui_checkbox_component :can_create_group, s_('AdminUsers|Can create top level group')
= f.gitlab_ui_checkbox_component :private_profile, s_('AdminUsers|Private profile')
%fieldset.form-group.gl-form-group
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index 8f7741b8a32..f6b7db2032f 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -31,7 +31,7 @@
- if impersonation_enabled?
.gl-p-2
%span.btn-group{ class: !@can_impersonate ? 'has-tooltip' : nil, title: @impersonation_error_text }
- = render Pajamas::ButtonComponent.new(disabled: !@can_impersonate, method: :post, href: impersonate_admin_user_path(@user), button_options: { data: { qa_selector: 'impersonate_user_link', testid: 'impersonate_user_link' } }) do
+ = render Pajamas::ButtonComponent.new(disabled: !@can_impersonate, method: :post, href: impersonate_admin_user_path(@user), button_options: { data: { testid: 'impersonate-user-link' } }) do
= _('Impersonate')
- if can_force_email_confirmation?(@user)
.gl-p-2
@@ -48,5 +48,5 @@
= gl_tab_link_to _("SSH keys"), keys_admin_user_path(@user)
= gl_tab_link_to _("Identities"), admin_user_identities_path(@user)
- if impersonation_enabled?
- = gl_tab_link_to _("Impersonation Tokens"), admin_user_impersonation_tokens_path(@user), data: { qa_selector: 'impersonation_tokens_tab' }
+ = gl_tab_link_to _("Impersonation Tokens"), admin_user_impersonation_tokens_path(@user), data: { testid: 'impersonation-tokens-tab' }
.gl-mb-3
diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml
index 213d5847986..d4a9009a0cf 100644
--- a/app/views/admin/users/_users.html.haml
+++ b/app/views/admin/users/_users.html.haml
@@ -35,7 +35,7 @@
= gl_tab_link_to admin_users_path(filter: "banned"), { item_active: active_when(params[:filter] == 'banned'), class: 'gl-border-0!' } do
= s_('AdminUsers|Banned')
= gl_tab_counter_badge(limited_counter_with_delimiter(User.banned))
- = gl_tab_link_to admin_users_path(filter: "blocked_pending_approval"), { item_active: active_when(params[:filter] == 'blocked_pending_approval'), class: 'filter-blocked-pending-approval gl-border-0!', data: { qa_selector: 'pending_approval_tab' } } do
+ = gl_tab_link_to admin_users_path(filter: "blocked_pending_approval"), { item_active: active_when(params[:filter] == 'blocked_pending_approval'), class: 'filter-blocked-pending-approval gl-border-0!', data: { testid: 'pending-approval-tab' } } do
= s_('AdminUsers|Pending approval')
= gl_tab_counter_badge(limited_counter_with_delimiter(User.blocked_pending_approval))
= gl_tab_link_to admin_users_path(filter: "deactivated"), { item_active: active_when(params[:filter] == 'deactivated'), class: 'gl-border-0!' } do
@@ -56,7 +56,7 @@
= hidden_field_tag "filter", h(params[:filter])
.search-holder
.search-field-holder.gl-mb-4
- = search_field_tag :search_query, params[:search_query], placeholder: s_('AdminUsers|Search by name, email, or username'), class: 'form-control search-text-input js-search-input', spellcheck: false, data: { qa_selector: 'user_search_field' }
+ = search_field_tag :search_query, params[:search_query], placeholder: s_('AdminUsers|Search by name, email, or username'), class: 'form-control search-text-input js-search-input', spellcheck: false, data: { testid: 'user-search-field' }
- if @sort.present?
= hidden_field_tag :sort, @sort
= sprite_icon('search', css_class: 'search-icon')
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index a4ae29bed81..4cc3e12a8ad 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -45,7 +45,7 @@
= link_button_to nil, remove_email_admin_user_path(@user, email), data: { confirm: _("Are you sure you want to remove %{email}?") % { email: email.email }, 'confirm-btn-variant': 'danger' }, method: :delete, class: 'float-right', title: _('Remove secondary email'), id: "remove_email_#{email.id}", variant: :danger, size: :small, icon: 'close'
%li
%span.light ID:
- %strong{ data: { qa_selector: 'user_id_content' } }
+ %strong{ data: { testid: 'user-id-content' } }
= @user.id
%li
%span.light= _('Namespace ID:')
@@ -71,7 +71,7 @@
= render_if_exists 'admin/users/provisioned_by', user: @user
%li
- %span.light= _('Can create groups:')
+ %span.light= _('Can create top level groups:')
%strong
= @user.can_create_group ? _('Yes') : _('No')
%li
diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml
index cdf25a9348c..c46aabf2604 100644
--- a/app/views/ci/runner/_how_to_setup_runner.html.haml
+++ b/app/views/ci/runner/_how_to_setup_runner.html.haml
@@ -8,13 +8,13 @@
= _("Register the runner with this URL:")
%br
%code#coordinator_address= root_url(only_path: false)
- = clipboard_button(target: '#coordinator_address', title: _("Copy URL"))
+ = deprecated_clipboard_button(target: '#coordinator_address', title: _("Copy URL"))
%br
%br
= _("And this registration token:")
%br
%code#registration_token{ data: {testid: 'registration_token' } }= registration_token
- = clipboard_button(target: '#registration_token', title: _("Copy token"))
+ = deprecated_clipboard_button(target: '#registration_token', title: _("Copy token"))
.gl-mt-3.gl-mb-3
= render Pajamas::ButtonComponent.new(variant: :default, method: :put, href: reset_token_url, button_options: { id: 'Reset registration token', data: { confirm: _("Are you sure you want to reset the registration token?") } }) do
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
deleted file mode 100644
index a710655aa20..00000000000
--- a/app/views/ci/variables/_variable_row.html.haml
+++ /dev/null
@@ -1,36 +0,0 @@
-- form_field = local_assigns.fetch(:form_field, nil)
-- variable = local_assigns.fetch(:variable, nil)
-
-- id = variable&.id
-- variable_type = variable&.variable_type
-- key = variable&.key
-- value = variable&.value
-
-- id_input_name = "#{form_field}[variables_attributes][][id]"
-- destroy_input_name = "#{form_field}[variables_attributes][][_destroy]"
-- variable_type_input_name = "#{form_field}[variables_attributes][][variable_type]"
-- key_input_name = "#{form_field}[variables_attributes][][key]"
-- value_input_name = "#{form_field}[variables_attributes][][secret_value]"
-
-%li.js-row.ci-variable-row{ data: { is_persisted: "#{!id.nil?}" } }
- .ci-variable-row-body.border-bottom
- %input.js-ci-variable-input-id{ type: "hidden", name: id_input_name, value: id }
- %input.js-ci-variable-input-destroy{ type: "hidden", name: destroy_input_name }
- %select.js-ci-variable-input-variable-type.ci-variable-body-item.form-control.select-control.custom-select.table-section.section-15{ name: variable_type_input_name }
- = options_for_select(ci_variable_type_options, variable_type)
- %input.js-ci-variable-input-key.ci-variable-body-item.form-control.gl-form-input.table-section.section-15{ type: "text",
- name: key_input_name,
- value: key,
- placeholder: s_('CiVariables|Input variable key') }
- .ci-variable-body-item.gl-show-field-errors.table-section.section-15.border-top-0.p-0
- .form-control.js-secret-value-placeholder.overflow-hidden{ class: ('hide' unless id) }
- = '*' * 17
- %textarea.js-ci-variable-input-value.js-secret-value.form-control.gl-form-input{ class: ('hide' if id),
- rows: 1,
- name: value_input_name,
- placeholder: s_('CiVariables|Input variable value') }
- = value
- %p.masking-validation-error.gl-field-error.hide
- = s_("CiVariables|Cannot use Masked Variable with current value")
- = link_to sprite_icon('question-o'), help_page_path('ci/variables/index', anchor: 'mask-a-cicd-variable'), target: '_blank', rel: 'noopener noreferrer'
- = render Pajamas::ButtonComponent.new(icon: 'close', button_options: { class: 'js-row-remove-button ci-variable-row-remove-button table-section', 'aria-label': s_('CiVariables|Remove variable row') })
diff --git a/app/views/clusters/clusters/_provider_details_form.html.haml b/app/views/clusters/clusters/_provider_details_form.html.haml
index 59706b6d8c4..4b7164f9845 100644
--- a/app/views/clusters/clusters/_provider_details_form.html.haml
+++ b/app/views/clusters/clusters/_provider_details_form.html.haml
@@ -1,7 +1,7 @@
= gitlab_ui_form_for cluster, url: update_cluster_url_path, html: { class: 'js-provider-details gl-show-field-errors', role: 'form' },
as: :cluster do |field|
.form-group
- - copy_name_btn = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'),
+ - copy_name_btn = deprecated_clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'),
class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold required'
.input-group.gl-field-error-anchor
@@ -12,7 +12,7 @@
= field.fields_for :platform_kubernetes, platform do |platform_field|
.form-group
- - copy_api_url = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'),
+ - copy_api_url = deprecated_clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'),
class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
= platform_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-bold required'
.input-group.gl-field-error-anchor
@@ -22,7 +22,7 @@
append: copy_api_url
.form-group
- - copy_ca_cert_btn = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'),
+ - copy_ca_cert_btn = deprecated_clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'),
class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
= platform_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-bold'
.input-group.gl-field-error-anchor
diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml
index ea7cd75152d..ddf3bd5ae07 100644
--- a/app/views/dashboard/groups/_groups.html.haml
+++ b/app/views/dashboard/groups/_groups.html.haml
@@ -1,2 +1,2 @@
.js-groups-list-holder
- #js-groups-tree{ data: { endpoint: dashboard_groups_path(format: :json), path: dashboard_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } }
+ #js-groups-tree{ data: { endpoint: dashboard_groups_path(format: :json), path: dashboard_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap', groups_empty_state_illustration: image_path('illustrations/empty-state/empty-groups-md.svg') } }
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index 7f004e405a7..c5abc964fda 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -3,8 +3,4 @@
= render_dashboard_ultimate_trial(current_user)
= render 'dashboard/groups_head'
-- if params[:filter].blank? && @groups.empty?
- .empty-state
- = render 'shared/groups/empty_state'
-- else
- = render 'groups'
+= render 'groups'
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 1cd8015934e..181c79e7bd0 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -1,11 +1,11 @@
%li.todo.gl-hover-border-blue-200.gl-hover-bg-blue-50.gl-hover-cursor-pointer.gl-relative{ class: "todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo) }
.gl-display-flex.gl-flex-direction-column.gl-sm-flex-direction-row.gl-sm-align-items-center
- .todo-item.gl-overflow-hidden.gl-overflow-x-auto.gl-align-self-center.gl-w-full{ data: { qa_selector: "todo_item_container" } }
+ .todo-item.gl-overflow-hidden.gl-overflow-x-auto.gl-align-self-center.gl-w-full{ data: { testid: "todo-item-container" } }
.todo-title.gl-pt-2.gl-pb-3.gl-px-2.gl-md-mb-1.gl-font-sm.gl-text-secondary
= todo_target_state_pill(todo)
- %span.todo-target-title{ data: { qa_selector: "todo_target_title_content" }, :id => dom_id(todo) + "_describer" }
+ %span.todo-target-title{ :id => dom_id(todo) + "_describer" }
= todo_target_title(todo)
- if !todo.for_design? && !todo.member_access_requested?
@@ -25,7 +25,7 @@
= author_avatar(todo, size: 24)
.todo-note
- if todo_author_display?(todo)
- .author-name.bold.gl-display-inline{ data: { qa_selector: "todo_author_name_content" } }<
+ .author-name.bold.gl-display-inline{ data: { testid: "todo-author-name-content" } }<
- if todo.author
= link_to_author(todo, self_added: todo.self_added?)
- else
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index c5f70397fad..3feb30085c0 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -36,7 +36,7 @@
.filter-item.gl-m-2
- if params[:group_id].present?
= hidden_field_tag(:group_id, params[:group_id])
- = dropdown_tag(group_dropdown_label(params[:group_id], _("Group")), options: { toggle_class: 'js-group-search js-filter-submit gl-xs-w-full!', title: s_("Todos|Filter by group"), filter: true, filterInput: 'input#group-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-group js-filter-submit', placeholder: _("Search groups"), data: { default_label: _("Group"), display: 'static', qa_selector: 'group_dropdown' } })
+ = dropdown_tag(group_dropdown_label(params[:group_id], _("Group")), options: { toggle_class: 'js-group-search js-filter-submit gl-xs-w-full!', title: s_("Todos|Filter by group"), filter: true, filterInput: 'input#group-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-group js-filter-submit', placeholder: _("Search groups"), data: { default_label: _("Group"), display: 'static', testid: 'group-dropdown' } })
.filter-item.gl-m-2
- if params[:project_id].present?
= hidden_field_tag(:project_id, params[:project_id])
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 345a1cc0225..e6551adffde 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -1,6 +1,6 @@
= gitlab_ui_form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'gl-p-5 gl-show-field-errors js-arkose-labs-form', aria: { live: 'assertive' }, data: { testid: 'sign-in-form' }}) do |f|
.form-group
- = f.label :login, _('Username or email')
+ = f.label :login, _('Username or primary email')
= f.text_field :login, value: @invite_email, class: 'form-control gl-form-input js-username-field', autocomplete: 'username', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', required: true, title: _('This field is required.'), data: { qa_selector: 'login_field', testid: 'username-field' }
.form-group
= f.label :password, _('Password')
diff --git a/app/views/devise/shared/_email_opted_in.html.haml b/app/views/devise/shared/_email_opted_in.html.haml
deleted file mode 100644
index d8ed0028222..00000000000
--- a/app/views/devise/shared/_email_opted_in.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-- return unless Gitlab.com?
-
-.gl-mb-3.js-email-opt-in.hidden
- .gl-font-weight-bold.gl-mb-3
- = _('Email updates (optional)')
- = f.gitlab_ui_checkbox_component :email_opted_in, _("I'd like to receive updates about GitLab via email")
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index 8f2c2c58790..73b9a3d5c5a 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -3,7 +3,7 @@
- if restyle_login_page_enabled && (any_form_based_providers_enabled? || password_authentication_enabled_for_web?)
.omniauth-divider.gl-display-flex.gl-align-items-center
- = _("or")
+ = _("or sign in with")
.gl-mt-5.gl-px-5{ class: restyle_login_page_enabled ? 'omniauth-container gl-text-center gl-ml-auto gl-mr-auto' : 'omniauth-container gl-py-5' }
- if !restyle_login_page_enabled
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 6d37257232b..bf1b604465b 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -17,7 +17,7 @@
class: 'form-control gl-form-input top js-block-emoji js-validate-length',
data: { max_length: max_first_name_length,
max_length_message: s_('SignUp|First name is too long (maximum is %{max_length} characters).') % { max_length: max_first_name_length },
- qa_selector: 'new_user_first_name_field' },
+ testid: 'new-user-first-name-field' },
required: true,
title: _('This field is required.')
.col.form-group
@@ -26,7 +26,7 @@
class: 'form-control gl-form-input top js-block-emoji js-validate-length',
data: { max_length: max_last_name_length,
max_length_message: s_('SignUp|Last name is too long (maximum is %{max_length} characters).') % { max_length: max_last_name_length },
- qa_selector: 'new_user_last_name_field' },
+ testid: 'new-user-last-name-field' },
required: true,
title: _('This field is required.')
.username.form-group
@@ -44,7 +44,7 @@
= f.label :email, _('Email')
= f.email_field :email,
class: 'form-control gl-form-input middle js-validate-email',
- data: { qa_selector: 'new_user_email_field' },
+ data: { testid: 'new-user-email-field' },
required: true,
title: _('Please provide a valid email address.')
%p.validation-hint.gl-field-hint.text-secondary= _('We recommend a work email address.')
@@ -56,7 +56,7 @@
%input.form-control.gl-form-input.js-password{ data: { id: "#{form_resource_name}_password",
title: s_('SignUp|Minimum length is %{minimum_password_length} characters.') % { minimum_password_length: @minimum_password_length },
minimum_password_length: @minimum_password_length,
- qa_selector: 'new_user_password_field',
+ testid: 'new-user-password-field',
autocomplete: 'new-password',
name: "#{form_resource_name}[password]" } }
%p.gl-field-hint-valid.text-secondary= s_('SignUp|Minimum length is %{minimum_password_length} characters.') % { minimum_password_length: @minimum_password_length }
@@ -69,7 +69,7 @@
- elsif show_recaptcha_sign_up?
= recaptcha_tags nonce: content_security_policy_nonce
- = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { qa_selector: 'new_user_register_button' }}) do
+ = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { testid: 'new-user-register-button' }}) do
= button_text
= render 'devise/shared/terms_of_service_notice', button_text: button_text
diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
index fcd52f33121..76a805d4d1b 100644
--- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
@@ -5,4 +5,5 @@
= form_tag path do
%input{ :name => "_method", :type => "hidden", :value => "delete" }/
- = submit_tag _('Revoke'), class: 'gl-button btn btn-danger btn-sm', aria: { label: s_('AuthorizedApplication|Revoke application') }, data: { confirm: s_('AuthorizedApplication|Are you sure you want to revoke this application?'), confirm_btn_variant: 'danger' }
+ = render Pajamas::ButtonComponent.new(type: :submit, variant: :danger, size: :small, button_options: { aria: { label: s_('AuthorizedApplication|Revoke application') }, data: { confirm: s_('AuthorizedApplication|Are you sure you want to revoke this application?'), confirm_btn_variant: 'danger' } }) do
+ = _('Revoke')
diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder
index c429bbbb610..43a545c4b4e 100644
--- a/app/views/events/_event.atom.builder
+++ b/app/views/events/_event.atom.builder
@@ -6,7 +6,7 @@ event = event.present
event_url = event_feed_url(event)
xml.entry do
- xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}"
+ xml.id "tag:#{request.host},#{event.created_at.to_date.iso8601}:#{event.id}"
xml.link href: event_url if event_url
xml.title truncate(event_feed_title(event), length: 80)
xml.updated event.updated_at.xmlschema
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 4a6b7fcfa84..0ad969116e0 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -6,13 +6,11 @@
.event-title.d-flex.flex-wrap
= inline_event_icon(event)
- - many_refs = event.ref_count.to_i > 1
- %span.event-type.d-inline-block.gl-mr-2.pushed= many_refs ? "#{event.action_name} #{event.ref_count} #{event.ref_type.pluralize}" : "#{event.action_name} #{event.ref_type}"
- - unless many_refs
+ %span.event-type.d-inline-block.gl-mr-2.pushed= event.push_activity_description
+ - unless event.batch_push?
%span.gl-mr-2.text-truncate
- commits_link = project_commits_path(project, event.ref_name)
- - should_link = event.tag? ? project.repository.tag_exists?(event.ref_name) : project.repository.branch_exists?(event.ref_name)
- = link_to_if should_link, event.ref_name, commits_link, class: 'ref-name'
+ = link_to_if event.linked_to_reference?, event.ref_name, commits_link, class: 'ref-name'
= render "events/event_scope", event: event
diff --git a/app/views/explore/groups/_groups.html.haml b/app/views/explore/groups/_groups.html.haml
index 3291129fd69..37e5be521b4 100644
--- a/app/views/explore/groups/_groups.html.haml
+++ b/app/views/explore/groups/_groups.html.haml
@@ -1,3 +1,3 @@
.js-groups-list-holder
- #js-groups-tree{ data: { endpoint: explore_groups_path(format: :json), path: explore_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } }
+ #js-groups-tree{ data: { endpoint: explore_groups_path(format: :json), path: explore_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap', groups_empty_state_illustration: image_path('illustrations/empty-state/empty-groups-md.svg') } }
= gl_loading_icon(size: 'md', css_class: 'gl-mt-6')
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index 213346b4cc2..ddb82411add 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -22,7 +22,4 @@
%p= _("Below you will find all the groups that are public.")
%p= _("You can easily contribute to them by requesting to join these groups.")
-- if params[:filter].blank? && @groups.empty?
- .nothing-here-block= _("No public groups")
-- else
- = render 'groups'
+= render 'groups'
diff --git a/app/views/groups/_import_group_from_another_instance_panel.html.haml b/app/views/groups/_import_group_from_another_instance_panel.html.haml
index 8c2434ca4a0..c35bbce6ba7 100644
--- a/app/views/groups/_import_group_from_another_instance_panel.html.haml
+++ b/app/views/groups/_import_group_from_another_instance_panel.html.haml
@@ -13,10 +13,9 @@
= s_('GroupsNew|Importing groups by direct transfer is currently disabled.')
- if current_user.admin?
- - admin_link_start = '<a href="%{url}">'.html_safe % { url: general_admin_application_settings_path(anchor: 'js-visibility-settings') }
- - admin_link_end = '</a>'.html_safe
+ - admin_link = link_to('', general_admin_application_settings_path(anchor: 'js-visibility-settings'))
- = s_('GroupsNew|Please %{admin_link_start}enable it in the Admin settings%{admin_link_end}.').html_safe % { admin_link_start: admin_link_start, admin_link_end: admin_link_end }
+ = safe_format(s_('GroupsNew|Please %{admin_link_start}enable it in the Admin settings%{admin_link_end}.'), tag_pair(admin_link, :admin_link_start, :admin_link_end))
- else
= s_('GroupsNew|Please ask your Administrator to enable it in the Admin settings.')
@@ -25,9 +24,8 @@
= render Pajamas::AlertComponent.new(dismissible: false,
variant: :warning) do |c|
- c.with_body do
- - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index.md', anchor: 'migrated-group-items') }
- - docs_link_end = '</a>'.html_safe
- = s_('GroupsNew|Not all group items are migrated. %{docs_link_start}What items are migrated%{docs_link_end}?').html_safe % { docs_link_start: docs_link_start, docs_link_end: docs_link_end }
+ - docs_link = link_to('', help_page_path('user/group/import/index.md', anchor: 'migrated-group-items'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(s_('GroupsNew|Not all group items are migrated. %{docs_link_start}What items are migrated%{docs_link_end}?'), tag_pair(docs_link, :docs_link_start, :docs_link_end))
%p.gl-mt-3
= s_('GroupsNew|Provide credentials for the source instance to import from. You can provide this instance as a source to move groups in this instance.')
@@ -41,9 +39,9 @@
.form-group.gl-display-flex.gl-flex-direction-column
= f.label :bulk_import_gitlab_access_token, s_('GroupsNew|Personal access token'), for: 'import_gitlab_token'
.gl-font-weight-normal
- - pat_link_start = '<a href="%{url}" target="_blank">'.html_safe % { url: help_page_path('user/profile/personal_access_tokens') }
- - short_living_link_start = '<a href="%{url}" target="_blank">'.html_safe % { url: help_page_path('security/token_overview', anchor: 'security-considerations') }
- = s_('GroupsNew|Create a token with %{code_start}api%{code_end} and %{code_start}read_repository%{code_end} scopes in the %{pat_link_start}user settings%{pat_link_end} of the source GitLab instance. For %{short_living_link_start}security reasons%{short_living_link_end}, set a short expiration date for the token. Keep in mind that large migrations take more time.').html_safe % { code_start: '<code>'.html_safe, code_end: '</code>'.html_safe, pat_link_start: pat_link_start, pat_link_end: '</a>'.html_safe, short_living_link_start: short_living_link_start, short_living_link_end: '</a>'.html_safe }
+ - pat_link = link_to('', help_page_path('user/profile/personal_access_tokens'), target: '_blank')
+ - short_living_link = link_to('', help_page_path('security/token_overview', anchor: 'security-considerations'), target: '_blank')
+ = safe_format(s_('GroupsNew|Create a token with %{code_start}api%{code_end} and %{code_start}read_repository%{code_end} scopes in the %{pat_link_start}user settings%{pat_link_end} of the source GitLab instance. For %{short_living_link_start}security reasons%{short_living_link_end}, set a short expiration date for the token. Keep in mind that large migrations take more time.'), tag_pair('<code></code>'.html_safe, :code_start , :code_end), tag_pair(pat_link, :pat_link_start, :pat_link_end), tag_pair(short_living_link, :short_living_link_start, :short_living_link_end))
= f.text_field :bulk_import_gitlab_access_token, placeholder: s_('GroupsNew|e.g. h8d3f016698e...'), class: 'gl-form-input gl-mt-3 col-xs-12 col-sm-8',
required: true,
disabled: bulk_imports_disabled,
diff --git a/app/views/groups/dependency_proxies/show.html.haml b/app/views/groups/dependency_proxies/show.html.haml
index 8416cb81c95..02a024ed3b5 100644
--- a/app/views/groups/dependency_proxies/show.html.haml
+++ b/app/views/groups/dependency_proxies/show.html.haml
@@ -1,6 +1,7 @@
- page_title _("Dependency Proxy")
#js-dependency-proxy{ data: { group_path: @group.full_path,
+ endpoint: group_dependency_proxy_path(@group),
no_manifests_illustration: image_path('illustrations/docker-empty-state.svg'),
group_id: @group.id,
settings_path: group_settings_packages_and_registries_path(@group),
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index c11154cbd75..8d6eebc27b0 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -28,8 +28,8 @@
.settings-content
= render 'groups/settings/permissions'
-= render_if_exists 'groups/merge_requests', expanded: expanded, group: @group
-= render_if_exists 'groups/merge_request_approval_settings', expanded: expanded, group: @group, user: current_user
+= render_if_exists 'groups/settings/merge_requests/merge_requests', expanded: expanded, group: @group
+= render_if_exists 'groups/settings/merge_requests/merge_request_approval_settings', expanded: expanded, group: @group, user: current_user
= render_if_exists 'groups/analytics', expanded: expanded
%section.settings.no-animate#js-badge-settings{ class: ('expanded' if expanded) }
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index e5c66c2c432..fbfaaa49b39 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -15,7 +15,7 @@
= render_if_exists 'groups/group_members/create_service_account'
.js-invite-members-trigger{ data: { variant: 'confirm',
classes: 'gl-md-w-auto gl-w-full',
- trigger_source: 'group-members-page',
+ trigger_source: 'group_members_page',
display_text: _('Invite members') } }
= render 'groups/invite_groups_modal', group: @group, reload_page_on_submit: true
diff --git a/app/views/groups/labels/edit.html.haml b/app/views/groups/labels/edit.html.haml
index 9af842b01df..1a64ba0c27d 100644
--- a/app/views/groups/labels/edit.html.haml
+++ b/app/views/groups/labels/edit.html.haml
@@ -1,8 +1,9 @@
- add_to_breadcrumbs _("Labels"), group_labels_path(@group)
- breadcrumb_title _("Edit")
- page_title _("Edit"), @label.name, _("Labels")
+- show_lock_on_merge = @group.supports_lock_on_merge?
%h1.page-title.gl-font-size-h-display
= _('Edit Label')
-= render 'shared/labels/form', url: group_label_path(@group, @label), back_path: @previous_labels_path
+= render 'shared/labels/form', url: group_label_path(@group, @label), back_path: @previous_labels_path, show_lock_on_merge: show_lock_on_merge
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index e84fd7a8692..7665da08582 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -13,7 +13,7 @@
- @gfm_form = true
.js-markdown-editor{ data: { render_markdown_path: group_preview_markdown_path,
markdown_docs_path: help_page_path('user/markdown'),
- qa_selector: 'milestone_description_field',
+ testid: 'milestone-description-field',
form_field_placeholder: _('Write milestone description...'),
supports_quick_actions: 'false',
enable_autocomplete: 'true',
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index f4749617463..45fd98adbb9 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -37,7 +37,7 @@
= render_if_exists 'groups/settings/wiki', f: f, group: @group
= render 'groups/settings/lfs', f: f
= render_if_exists 'groups/settings/code_suggestions', f: f, group: @group
- = render_if_exists 'groups/settings/ai_related_settings', f: f, group: @group
+ = render_if_exists 'groups/settings/experimental_settings', f: f, group: @group
= render_if_exists 'groups/settings/ai_third_party_settings', f: f, group: @group
= render 'groups/settings/git_access_protocols', f: f, group: @group
= render 'groups/settings/project_creation_level', f: f, group: @group
diff --git a/app/views/groups/settings/applications/index.html.haml b/app/views/groups/settings/applications/index.html.haml
index da3257ca27d..595336d8abb 100644
--- a/app/views/groups/settings/applications/index.html.haml
+++ b/app/views/groups/settings/applications/index.html.haml
@@ -3,7 +3,7 @@
- @force_desktop_expanded_sidebar = true
= render 'shared/doorkeeper/applications/index',
- oauth_applications_enabled: user_oauth_applications?,
+ oauth_applications_enabled: true,
oauth_authorized_applications_enabled: false,
form_url: group_settings_applications_path(@group),
application_url: ->(application) { group_settings_application_path(@group, application) },
diff --git a/app/views/groups/work_items/index.html.haml b/app/views/groups/work_items/index.html.haml
index 2e3d3dda941..299a90b362d 100644
--- a/app/views/groups/work_items/index.html.haml
+++ b/app/views/groups/work_items/index.html.haml
@@ -1,4 +1,4 @@
- page_title s_('WorkItem|Work items')
- add_page_specific_style 'page_bundles/issuable_list'
-.js-work-items-list-root{ data: { full_path: @group.full_path } }
+.js-work-items-list-root{ data: work_items_list_data(@group) }
diff --git a/app/views/groups/work_items/show.html.haml b/app/views/groups/work_items/show.html.haml
new file mode 100644
index 00000000000..eb962cc0b69
--- /dev/null
+++ b/app/views/groups/work_items/show.html.haml
@@ -0,0 +1 @@
+.h1 Work Item
diff --git a/app/views/help/instance_configuration/_size_limits.html.haml b/app/views/help/instance_configuration/_size_limits.html.haml
index add484feac9..1f6379314b9 100644
--- a/app/views/help/instance_configuration/_size_limits.html.haml
+++ b/app/views/help/instance_configuration/_size_limits.html.haml
@@ -42,8 +42,8 @@
%td= _('Maximum snippet size')
%td= instance_configuration_human_size_cell(size_limits[:snippet_size_limit])
%tr
- %td= s_('Import|Maximum import remote file size (MB)')
+ %td= s_('Import|Maximum import remote file size (MiB)')
%td= instance_configuration_human_size_cell(size_limits[:max_import_remote_file_size])
%tr
- %td= s_('BulkImport|Direct transfer maximum download file size (MB)')
+ %td= s_('BulkImport|Direct transfer maximum download file size (MiB)')
%td= instance_configuration_human_size_cell(size_limits[:bulk_import_max_download_file_size])
diff --git a/app/views/import/bitbucket_server/new.html.haml b/app/views/import/bitbucket_server/new.html.haml
index 292dd9d071c..de94f142a40 100644
--- a/app/views/import/bitbucket_server/new.html.haml
+++ b/app/views/import/bitbucket_server/new.html.haml
@@ -24,4 +24,5 @@
.col-md-4
= password_field_tag :personal_access_token, '', class: 'form-control gl-form-input gl-mr-3', placeholder: _('Personal Access Token'), size: 40
.form-actions
- = submit_tag _('List your Bitbucket Server repositories'), class: 'gl-button btn btn-confirm'
+ = render Pajamas::ButtonComponent.new(type: 'submit', variant: :confirm) do
+ = _('List your Bitbucket Server repositories')
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 53ecad1b474..bbde5f2843b 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -4,7 +4,7 @@
%head{ omit_og ? { } : { prefix: "og: http://ogp.me/ns#" } }
%meta{ charset: "utf-8" }
%meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' }
- %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' }
+ %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
%title= page_title(site_name)
= Gon::Base.render_data(nonce: content_security_policy_nonce)
= yield :project_javascripts
@@ -48,6 +48,16 @@
= webpack_bundle_tag 'legacy_sentry'
= webpack_bundle_tag 'performance_bar' if performance_bar_enabled?
+ - if vite_enabled
+ %meta{ name: 'controller-path', content: controller_full_path }
+ - if Rails.env.development?
+ = vite_client_tag
+ = vite_javascript_tag "main"
+ - if Gitlab.ee?
+ = vite_javascript_tag "main_ee"
+ - if Gitlab.jh?
+ = vite_javascript_tag "main_jh"
+
= yield :page_specific_javascripts
= webpack_bundle_tag 'super_sidebar' if show_super_sidebar?
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 95627c2884a..f52ea801eef 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -20,7 +20,6 @@
.mobile-overlay
= dispensable_render_if_exists 'layouts/header/verification_reminder'
.alert-wrapper.gl-force-block-formatting-context
- = yield :code_suggestions_third_party_alert
= dispensable_render 'shared/new_nav_announcement'
= dispensable_render 'shared/outdated_browser'
= dispensable_render_if_exists "layouts/header/licensed_user_count_threshold"
@@ -37,6 +36,7 @@
= dispensable_render_if_exists "layouts/header/seat_count_alert"
= dispensable_render_if_exists "shared/namespace_user_cap_reached_alert"
= dispensable_render_if_exists "shared/new_user_signups_cap_reached_alert"
+ = dispensable_render_if_exists "shared/silent_mode_banner"
= yield :page_level_alert
= yield :group_invite_members_banner
- unless @hide_top_bar
diff --git a/app/views/layouts/_snowplow.html.haml b/app/views/layouts/_snowplow.html.haml
index 399a826d611..10b2002dfef 100644
--- a/app/views/layouts/_snowplow.html.haml
+++ b/app/views/layouts/_snowplow.html.haml
@@ -2,6 +2,8 @@
- namespace = @group || @project&.namespace || @namespace
= webpack_bundle_tag 'tracker'
+- if Gitlab.com? && Feature.enabled?(:browsersdk_tracking)
+ = webpack_bundle_tag 'analytics'
= javascript_tag do
:plain
window.snowplowOptions = #{Gitlab::Tracking.options(@group).to_json}
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 28cbdf0a7a1..451c66b074b 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -12,13 +12,8 @@
= render 'peek/bar'
= header_message
- - if show_super_sidebar? # TODO: Move this CSS to a better place
- - if current_user
- :css
- body {
- --header-height: 0px;
- }
- - else
+ - if show_super_sidebar?
+ - if !current_user
= render partial: "layouts/header/super_sidebar_logged_out"
- else
= render partial: "layouts/header/default", locals: { project: @project, group: @group }
diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml
index 5ad20478f51..d07daf0aab9 100644
--- a/app/views/layouts/errors.html.haml
+++ b/app/views/layouts/errors.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html{ lang: I18n.locale }
%head
- %meta{ :content => "width=device-width, initial-scale=1, maximum-scale=1", :name => "viewport" }
+ %meta{ :content => "width=device-width, initial-scale=1", :name => "viewport" }
%title= yield(:title)
%style
= Rails.application.assets_manifest.find_sources('errors.css').first.to_s.html_safe
diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml
index 83641fbb184..1d67ac942fa 100644
--- a/app/views/layouts/group.html.haml
+++ b/app/views/layouts/group.html.haml
@@ -21,7 +21,6 @@
= render 'groups/invite_members_modal', group: @group
= dispensable_render_if_exists "shared/web_hooks/group_web_hook_disabled_alert"
-= dispensable_render_if_exists "shared/code_suggestions_third_party_alert", source: @group
= dispensable_render_if_exists "shared/free_user_cap_alert", source: @group
= dispensable_render_if_exists "shared/unlimited_members_during_trial_alert", resource: @group
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index 7ce914cf660..75de13d4862 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -43,7 +43,7 @@
= link_to _("Switch to GitLab Next"), Gitlab::Saas.canary_toggle_com_url, data: { track_action: "click_link", track_label: "switch_to_canary", track_property: "navigation_top" }
%li.divider
- .js-new-nav-toggle{ data: { enabled: show_super_sidebar?.to_s, endpoint: profile_preferences_url} }
+ .js-new-nav-toggle{ data: { enabled: show_super_sidebar?.to_s, endpoint: profile_preferences_path} }
- if current_user_menu?(:sign_out)
%li.divider
diff --git a/app/views/layouts/header/_super_sidebar_logged_out.haml b/app/views/layouts/header/_super_sidebar_logged_out.haml
index 67322aced74..31dfdfb2bb3 100644
--- a/app/views/layouts/header/_super_sidebar_logged_out.haml
+++ b/app/views/layouts/header/_super_sidebar_logged_out.haml
@@ -44,4 +44,4 @@
- if allow_signup?
%li
= render Pajamas::ButtonComponent.new(href: new_user_registration_path, variant: :confirm) do
- = _('Register')
+ = Gitlab.com? ? _('Get free trial') : _('Register')
diff --git a/app/views/layouts/minimal.html.haml b/app/views/layouts/minimal.html.haml
index 5531c0ab23a..8b6a2a2f2a7 100644
--- a/app/views/layouts/minimal.html.haml
+++ b/app/views/layouts/minimal.html.haml
@@ -3,7 +3,7 @@
!!! 5
%html{ lang: I18n.locale, class: page_classes }
= render "layouts/head"
- %body{ data: body_data }
+ %body{ data: body_data, class: system_message_class }
= header_message
= render 'peek/bar'
= render 'layouts/published_experiments'
diff --git a/app/views/layouts/oauth_error.html.haml b/app/views/layouts/oauth_error.html.haml
index d5e0e8e9c1d..697bd9b5864 100644
--- a/app/views/layouts/oauth_error.html.haml
+++ b/app/views/layouts/oauth_error.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html{ lang: I18n.locale }
%head
- %meta{ :content => "width=device-width, initial-scale=1, maximum-scale=1", :name => "viewport" }
+ %meta{ :content => "width=device-width, initial-scale=1", :name => "viewport" }
%title= yield(:title)
= stylesheet_link_tag 'application_utilities'
%style
diff --git a/app/views/layouts/organization.html.haml b/app/views/layouts/organization.html.haml
index 5a357c6f805..7e1bf228876 100644
--- a/app/views/layouts/organization.html.haml
+++ b/app/views/layouts/organization.html.haml
@@ -1,6 +1,6 @@
-- page_title @organization.name
-- header_title @organization.name, organization_path(@organization)
-- nav "organization"
+- page_title @organization.name if @organization
+- header_title @organization.name, organization_path(@organization) if @organization
+- nav(%w[index new].include?(params[:action]) ? "your_work" : "organization")
- @left_sidebar = true
= render template: "layouts/application"
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 18ae3353f4d..1e85bb6cc3a 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -23,7 +23,6 @@
= render 'projects/invite_members_modal', project: @project
= dispensable_render_if_exists "shared/web_hooks/web_hook_disabled_alert"
-= dispensable_render_if_exists "projects/code_suggestions_third_party_alert", project: @project
= dispensable_render_if_exists "projects/free_user_cap_alert", project: @project
= dispensable_render_if_exists 'shared/unlimited_members_during_trial_alert', resource: @project
diff --git a/app/views/layouts/service_desk.html.haml b/app/views/layouts/service_desk.html.haml
index 13e9785317c..e9c5d51c674 100644
--- a/app/views/layouts/service_desk.html.haml
+++ b/app/views/layouts/service_desk.html.haml
@@ -24,11 +24,5 @@
%br
= link_to "Unsubscribe", @unsubscribe_url
- -# EE-specific start
- - if Gitlab::CurrentSettings.email_additional_text.present?
- %br
- %br
- = Gitlab::Utils.nlbr(Gitlab::CurrentSettings.email_additional_text)
- -# EE-specific end
-
+ = render_if_exists 'layouts/email_additional_text'
= html_footer_message
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
index 9a50e3e2eb2..29a561ae1a9 100644
--- a/app/views/layouts/terms.html.haml
+++ b/app/views/layouts/terms.html.haml
@@ -1,6 +1,7 @@
!!! 5
- add_page_specific_style 'page_bundles/terms'
- @hide_top_bar = true
+- @hide_top_bar_padding = true
- body_classes = [user_application_theme]
%html{ lang: I18n.locale, class: page_class }
= render "layouts/head"
diff --git a/app/views/notify/in_product_marketing_email.html.haml b/app/views/notify/in_product_marketing_email.html.haml
deleted file mode 100644
index a88d581c5de..00000000000
--- a/app/views/notify/in_product_marketing_email.html.haml
+++ /dev/null
@@ -1,51 +0,0 @@
-- if @message.series?
- %tr{ style: "background-color: #ffffff;" }
- %td{ style: "color: #424242; padding: 10px 30px; text-align: center; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;font-size: 16px; line-height: 22px; border: 1px solid #dddddd" }
- %p
- = @message.progress.html_safe
-%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
- %h2{ style: "font-size: 28px; line-height: 34px; color: #000000; padding: 0; font-weight: 400;" }
- = @message.subtitle
-%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
- - @message.body_line2&.tap do |line|
- %p{ style: "margin: 0 0 20px 0;" }
- = line.html_safe
-- if @message.cta_text
- %tr
- %td{ align: "center", style: "padding: 10px 20px 80px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" }
- .cta_link.cta_link_primary= @message.cta_link
-- else
- %tr
- %td{ style: "padding: 10px 20px 10px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; color:#000000; font-size: 16px; line-height: 20px;" }
- %table{ border: "0", cellpadding: "0", cellspacing: "0", width: "100%", style: "width: 100%; min-width: 100%;" }
- %tr
- %td{ width: "50%", style: "width: 50%; min-width: 50%; color: #000000; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; font-size: 16px; line-height: 100%; padding-bottom: 16px; text-align: left;", align: "left" }
- = @message.feedback_ratings(1)
- %td{ width: "50%", style: "width: 50%; min-width: 50%; color: #000000; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; font-size: 16px; line-height: 100%; padding-bottom: 16px; text-align: right;", align: "right" }
- = @message.feedback_ratings(5)
- %tr
- %td{ align: "center", style: "padding: 10px 1px 30px 1px;" }
- %table{ align: "center", cellpadding: "5", cellspacing: "0", width: "100%", style: "width: 100%; min-width: 100%; border: 1px solid #dae0ea; border-radius: 0; min-width: 100%; text-align: center; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; font-size: 16px;" }
- %tr
- - (1..5).each do |rating|
- %td{ height: "54", style: "border-left: 1px solid #dae0ea; padding-bottom: 0; width: 9% !important;", width: "9%" }
- %a{ href: @message.feedback_link(rating), style: "color: #424242; display: block; text-decoration: none;" }
- %span{ height: "54", style: "display: block; font-size: 18px; height: 22px; line-height: 22px; padding: 16px 0; width: 100%; text-decoration: none;" }
- = rating
- %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 50px 0;" }
- = @message.feedback_thanks
-- if @message.invite_members?
- %tr
- %td{ align: "center", style: "padding: 0 20px 80px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" }
- = @message.invite_text
- %br
- = @message.invite_link
diff --git a/app/views/notify/in_product_marketing_email.text.erb b/app/views/notify/in_product_marketing_email.text.erb
deleted file mode 100644
index 79a366eb1cc..00000000000
--- a/app/views/notify/in_product_marketing_email.text.erb
+++ /dev/null
@@ -1,36 +0,0 @@
-<%= @message.tagline %>
-
-<%= @message.title %>
-<%= @message.subtitle %>
-
-
-<%= @message.body_line1 %>
-
-<%= @message.body_line2 %>
-
-<% if @message.cta_text %>
-<%= @message.cta_link %>
-
-
-
-<% else %>
-<% (1..5).each do |rating| %>
-<%= "#{rating} - #{@message.feedback_ratings(rating).upcase} - #{@message.feedback_link(rating)}" %>
-<% end %>
-
-
-<%= @message.feedback_thanks %>
-<% end %>
-<% if @message.invite_members? %>
-<%= @message.invite_text %>
-<%= @message.invite_link %>
-<% end %>
-
-
-
-
-<%= @message.footer_links %>
-
-<%= @message.address %>
-
-<%= @message.unsubscribe %>
diff --git a/app/views/notify/member_access_granted_email.html.haml b/app/views/notify/member_access_granted_email.html.haml
index cce36f7b8a6..49a571f154e 100644
--- a/app/views/notify/member_access_granted_email.html.haml
+++ b/app/views/notify/member_access_granted_email.html.haml
@@ -8,11 +8,6 @@
%td.text-content
%p
= _('You have been granted %{access_level} access to the %{source_link} %{source_type}.').html_safe % { access_level: access_level, source_link: source_link, source_type: source_type }
- - if member.tasks_to_be_done.present?
- = s_("InviteEmail|You were assigned the following tasks:")
- %ul.list-style-position-inside
- - member.tasks_to_be_done.each do |task|
- %li= localized_tasks_to_be_done_choices[task]
%p
- leave_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: leave_link }
= _('If this was a mistake you can %{leave_link_start}leave the %{source_type}%{link_end}.').html_safe % { source_type: source_type, leave_link_start: leave_link_start, link_end: link_end }
diff --git a/app/views/notify/member_invited_email.html.haml b/app/views/notify/member_invited_email.html.haml
index 6d5207510da..21d0f8b9108 100644
--- a/app/views/notify/member_invited_email.html.haml
+++ b/app/views/notify/member_invited_email.html.haml
@@ -22,11 +22,6 @@
%p
- if member.created_by
= html_escape(s_("InviteEmail|%{inviter} invited you to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}")) % placeholders.merge({ inviter: (link_to inviter_name, user_url(member.created_by)).html_safe })
- - if member.tasks_to_be_done.present?
- = s_("InviteEmail|and has assigned you the following tasks:")
- %ul.list-style-position-inside
- - member.tasks_to_be_done.each do |task|
- %li= localized_tasks_to_be_done_choices[task]
- else
= html_escape(s_("InviteEmail|You are invited to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}")) % placeholders
%p.invite-actions
diff --git a/app/views/notify/new_email_address_added_email.haml b/app/views/notify/new_email_address_added_email.html.haml
index 6d00aaedfd5..6d00aaedfd5 100644
--- a/app/views/notify/new_email_address_added_email.haml
+++ b/app/views/notify/new_email_address_added_email.html.haml
diff --git a/app/views/notify/new_email_address_added_email.erb b/app/views/notify/new_email_address_added_email.text.erb
index 3af1953c902..3af1953c902 100644
--- a/app/views/notify/new_email_address_added_email.erb
+++ b/app/views/notify/new_email_address_added_email.text.erb
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
new file mode 100644
index 00000000000..e4e34f6c8ee
--- /dev/null
+++ b/app/views/notify/resource_access_tokens_about_to_expire_email.html.haml
@@ -0,0 +1,13 @@
+%p
+ = _('Hi %{username}!') % { username: sanitize_name(@user.name) }
+%p
+ = _('One or more of your resource access tokens will expire in %{days_to_expire} or less:') % { days_to_expire: pluralize(@days_to_expire, _('day')) }
+%p
+ #{@resource.class.name.titleize}: #{@resource.full_path}
+%p
+ %ul
+ - @token_names.each do |token|
+ %li= token
+%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 }
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
new file mode 100644
index 00000000000..bea74f09129
--- /dev/null
+++ b/app/views/notify/resource_access_tokens_about_to_expire_email.text.erb
@@ -0,0 +1,11 @@
+<%= _('Hi %{username}!') % { username: sanitize_name(@user.name) } %>
+
+<%= _('One or more of your resource access tokens will expire in %{days_to_expire} or less:') % { days_to_expire: pluralize(@days_to_expire, _('day')) } %>
+
+<%= "#{@resource.class.name.titleize}: #{@resource.full_path}" %>
+
+<% @token_names.each do |token| %>
+ - <%= token %>
+<% end %>
+
+<%= _('You can create a new one or check them in your access token settings: %{target_url}') % { target_url: @target_url } %>
diff --git a/app/views/organizations/organizations/groups_and_projects.html.haml b/app/views/organizations/organizations/groups_and_projects.html.haml
index 8890f4b1ce5..a993e1c9404 100644
--- a/app/views/organizations/organizations/groups_and_projects.html.haml
+++ b/app/views/organizations/organizations/groups_and_projects.html.haml
@@ -1,3 +1,3 @@
- page_title _('Groups and projects')
-#js-organizations-groups-and-projects
+#js-organizations-groups-and-projects{ data: { app_data: organization_groups_and_projects_app_data } }
diff --git a/app/views/organizations/organizations/index.html.haml b/app/views/organizations/organizations/index.html.haml
new file mode 100644
index 00000000000..04a90b7589f
--- /dev/null
+++ b/app/views/organizations/organizations/index.html.haml
@@ -0,0 +1,2 @@
+- page_title s_('Organization|Organizations')
+- header_title _("Your work"), root_path
diff --git a/app/views/organizations/organizations/new.html.haml b/app/views/organizations/organizations/new.html.haml
new file mode 100644
index 00000000000..4d7f552c87b
--- /dev/null
+++ b/app/views/organizations/organizations/new.html.haml
@@ -0,0 +1,3 @@
+- page_title s_('Organization|New organization')
+- header_title _("Your work"), root_path
+- add_to_breadcrumbs s_('Organization|Organizations'), organizations_path
diff --git a/app/views/organizations/organizations/show.html.haml b/app/views/organizations/organizations/show.html.haml
index 8ba2a3d96ac..2ce4c0688ae 100644
--- a/app/views/organizations/organizations/show.html.haml
+++ b/app/views/organizations/organizations/show.html.haml
@@ -1,2 +1,4 @@
- page_title s_('Organization|Organization overview')
- @skip_current_level_breadcrumb = true
+
+#js-organizations-show{ data: { app_data: organization_show_app_data(@organization) } }
diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml
index 2714193d1d1..982199d3d6f 100644
--- a/app/views/profiles/gpg_keys/index.html.haml
+++ b/app/views/profiles/gpg_keys/index.html.haml
@@ -28,7 +28,7 @@
%h4.gl-mt-0
= _('Add a GPG key')
%p
- - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/repository/gpg_signed_commits/index.md') }
+ - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/repository/signed_commits/gpg.md') }
= _('Add a GPG key for secure access to GitLab. %{help_link_start}Learn more%{help_link_end}.').html_safe % {help_link_start: help_link_start, help_link_end: '</a>'.html_safe }
= render 'form'
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index d5193a424ef..1307c388041 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
- = 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!')
+ = 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!')
= 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/notifications/_email_settings.html.haml b/app/views/profiles/notifications/_email_settings.html.haml
index 60f366f8878..6848426306b 100644
--- a/app/views/profiles/notifications/_email_settings.html.haml
+++ b/app/views/profiles/notifications/_email_settings.html.haml
@@ -1,6 +1,3 @@
-- form = local_assigns.fetch(:form)
.js-notification-email-listbox-input.gl-mb-3{ data: { label: _('Global notification email'), name: 'user[notification_email]', emails: @user.public_verified_emails.to_json, empty_value_text: _('Use primary email (%{email})') % { email: @user.email }, value: @user.notification_email, disabled: local_assigns.fetch(:email_change_disabled, nil) } }
.help-block
= local_assigns.fetch(:help_text, nil)
-.form-group
- = form.gitlab_ui_checkbox_component :email_opted_in, _('Receive product marketing emails')
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 2c7ef2b7e0e..87945f66ae7 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -19,8 +19,8 @@
= _('You can specify notification level per group or per project.')
.gl-mt-0
- = gitlab_ui_form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications gl-mt-3' } do |f|
- = render_if_exists 'profiles/notifications/email_settings', form: f
+ = gitlab_ui_form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications gl-mt-3 gl-mb-6' } do |f|
+ = render_if_exists 'profiles/notifications/email_settings'
= label_tag :global_notification_level, _('Global notification level'), class: "label-bold gl-mb-0"
.gl-text-secondary.gl-mb-3
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 681d4e087f3..a6534a16e86 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -67,6 +67,13 @@
= link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'behavior'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.label :layout, class: 'label-bold' do
+ = s_('Preferences|Keyboard shortcuts')
+ - shortcuts_help_link = link_to('', help_page_path('user/shortcuts'), target: '_blank', rel: 'noopener noreferrer')
+ = f.gitlab_ui_checkbox_component :keyboard_shortcuts_enabled,
+ s_('Preferences|Enable keyboard shortcuts'),
+ help_text: safe_format(s_('Preferences|%{link_start}List of keyboard shortcuts%{link_end}'), tag_pair(shortcuts_help_link, :link_start, :link_end))
+ .form-group
+ = f.label :layout, class: 'label-bold' do
= s_('Preferences|Layout width')
= f.gitlab_ui_radio_component :layout, layout_choices[0][1], layout_choices[0][0], help_text: fixed_help_text
= f.gitlab_ui_radio_component :layout, layout_choices[1][1], layout_choices[1][0], help_text: fluid_help_text
@@ -140,7 +147,7 @@
%p.gl-text-secondary
= s_('Preferences|Configure how dates and times display for you.')
= succeed '.' do
- = link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'time-preferences'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'show-exact-times-instead-of-relative-times'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.gitlab_ui_checkbox_component :time_display_relative,
s_('Preferences|Use relative times'),
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index 20fb2b43c63..58c760c54e8 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -9,8 +9,8 @@
- c.with_body do
%p
- - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/import_export') }
- = _('Export this project with all its related data in order to move it to a new GitLab instance. When the exported file is ready, you can download it from this page or from the download link in the email notification you will receive. You can then import it when creating a new project. %{link_start}Learn more.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+ - link = link_to('', help_page_path('user/project/settings/import_export'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(_('Export this project with all its related data in order to move it to a new GitLab instance. When the exported file is ready, you can download it from this page or from the download link in the email notification you will receive. You can then import it when creating a new project. %{link_start}Learn more.%{link_end}'), tag_pair(link, :link_start, :link_end))
.gl-mb-0
%p.gl-font-weight-bold= _('The following items will be exported:')
%ul
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 4ac30547ce3..759ec541af5 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -18,7 +18,7 @@
- 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 }
- = clipboard_button(title: s_('ProjectPage|Copy project ID'), text: @project.id)
+ = deprecated_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
diff --git a/app/views/projects/_invite_members_empty_project.html.haml b/app/views/projects/_invite_members_empty_project.html.haml
index 18d06c7d0bb..d6cab06f773 100644
--- a/app/views/projects/_invite_members_empty_project.html.haml
+++ b/app/views/projects/_invite_members_empty_project.html.haml
@@ -6,4 +6,4 @@
.js-invite-members-trigger{ data: { variant: 'confirm',
classes: 'gl-mb-8 gl-xs-w-full',
display_text: s_('InviteMember|Invite members'),
- trigger_source: 'project-empty-page' } }
+ trigger_source: 'project_empty_page' } }
diff --git a/app/views/projects/_merge_request_merge_checks_settings.html.haml b/app/views/projects/_merge_request_merge_checks_settings.html.haml
deleted file mode 100644
index bb7a7731067..00000000000
--- a/app/views/projects/_merge_request_merge_checks_settings.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-- form = local_assigns.fetch(:form)
-
-.form-group
- %b= s_('ProjectSettings|Merge checks')
- %p.text-secondary= s_('ProjectSettings|These checks must pass before merge requests can be merged.')
- = render 'projects/merge_request_pipelines_and_threads_options', form: form, project: @project
- = render_if_exists 'projects/merge_request_merge_checks_status_checks', form: form, project: @project
- = render_if_exists 'projects/merge_request_merge_checks_jira_enforcement', form: form, project: @project
diff --git a/app/views/projects/_merge_request_merge_suggestions_settings.html.haml b/app/views/projects/_merge_request_merge_suggestions_settings.html.haml
deleted file mode 100644
index eb2fc05686c..00000000000
--- a/app/views/projects/_merge_request_merge_suggestions_settings.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-- form = local_assigns.fetch(:form)
-
-.form-group
- %b= s_('ProjectSettings|Merge suggestions')
- %p.text-secondary
- = s_('ProjectSettings|The commit message used when applying merge request suggestions.')
- .mb-2
- = form.text_field :suggestion_commit_message, class: 'form-control mb-2', placeholder: Gitlab::Suggestions::CommitMessage::DEFAULT_SUGGESTION_COMMIT_MESSAGE
- %p.form-text.text-muted
- = s_('ProjectSettings|Leave empty to use default template.')
- = sprintf(s_('ProjectSettings|Maximum %{maxLength} characters.'), { maxLength: Project::MAX_SUGGESTIONS_TEMPLATE_LENGTH })
- - configure_the_commit_message_for_applied_suggestions_help_link_url = help_page_path('user/project/merge_requests/reviews/suggestions.md', anchor: 'configure-the-commit-message-for-applied-suggestions')
- - configure_the_commit_message_for_applied_suggestions_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: configure_the_commit_message_for_applied_suggestions_help_link_url }
- = s_('ProjectSettings|%{link_start}What variables can I use?%{link_end}').html_safe % { link_start: configure_the_commit_message_for_applied_suggestions_help_link_start, link_end: '</a>'.html_safe }
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
deleted file mode 100644
index 728ff597860..00000000000
--- a/app/views/projects/_merge_request_settings.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-- form = local_assigns.fetch(:form)
-
-= render 'projects/merge_request_merge_method_settings', project: @project, form: form
-
-= render 'projects/merge_request_merge_options_settings', project: @project, form: form
-
-= render 'projects/merge_request_squash_options_settings', form: form
-
-= render 'projects/merge_request_merge_checks_settings', project: @project, form: form
-
-= render 'projects/merge_request_merge_suggestions_settings', project: @project, form: form
-
-= render 'projects/merge_request_merge_commit_template', project: @project, form: form
-
-= render 'projects/merge_request_squash_commit_template', project: @project, form: form
-
-- if @project.forked?
- = render 'projects/merge_request_target_project_settings', project: @project, form: form
diff --git a/app/views/projects/_service_desk_settings.html.haml b/app/views/projects/_service_desk_settings.html.haml
index c2382a66132..3dbc4c0fad7 100644
--- a/app/views/projects/_service_desk_settings.html.haml
+++ b/app/views/projects/_service_desk_settings.html.haml
@@ -12,7 +12,7 @@
enabled: "#{@project.service_desk_enabled}",
issue_tracker_enabled: "#{@project.project_feature.issues_enabled?}",
incoming_email: (@project.service_desk_incoming_address if @project.service_desk_enabled),
- service_desk_email: (@project.service_desk_custom_address if @project.service_desk_enabled),
+ service_desk_email: (@project.service_desk_alias_address if @project.service_desk_enabled),
service_desk_email_enabled: "#{Gitlab::Email::ServiceDeskEmail.enabled?}",
selected_template: "#{@project.service_desk_setting&.issue_template_key}",
selected_file_template_project_id: "#{@project.service_desk_setting&.file_template_project_id}",
diff --git a/app/views/projects/_transfer.html.haml b/app/views/projects/_transfer.html.haml
index fe84a83c43c..cf0634ee411 100644
--- a/app/views/projects/_transfer.html.haml
+++ b/app/views/projects/_transfer.html.haml
@@ -8,15 +8,15 @@
.gl-new-card-title-wrapper
%h4.gl-new-card-title.warning-title= _('Transfer project')
%p.gl-new-card-description
- - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'transfer-a-project-to-another-namespace') }
- = _("Transfer your project into another namespace. %{link_start}Learn more.%{link_end}").html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+ - link = link_to('', help_page_path('user/project/settings/index', anchor: 'transfer-a-project-to-another-namespace'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(_("Transfer your project into another namespace. %{link_start}Learn more.%{link_end}"), tag_pair(link, :link_start, :link_end))
- c.with_body do
= form_for @project, url: transfer_project_path(@project), method: :put, html: { class: 'js-project-transfer-form', id: form_id } do |f|
.form-group.gl-mb-0
%p
- - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'rename-a-repository') }
- = _("A project’s repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}").html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+ - link = link_to('', help_page_path('user/project/settings/index', anchor: 'rename-a-repository'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(_("A project’s repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}"), tag_pair(link, :link_start, :link_end))
%p= _('When you transfer your project to a group, you can easily manage multiple projects, view usage quotas for storage, pipeline minutes, and users, and start a trial or upgrade to a paid tier.')
%p
= _("Don't have a group?")
diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml
index 674b21b66b9..6a4760c3954 100644
--- a/app/views/projects/activity.html.haml
+++ b/app/views/projects/activity.html.haml
@@ -1,6 +1,4 @@
- page_title _("Activity")
-= render_if_exists 'shared/ultimate_feature_removal_banner', project: @project
-
= render 'projects/last_push'
= render 'projects/activity'
diff --git a/app/views/projects/artifacts/_tree_directory.html.haml b/app/views/projects/artifacts/_tree_directory.html.haml
index c68cc19f6c1..56105c4cac3 100644
--- a/app/views/projects/artifacts/_tree_directory.html.haml
+++ b/app/views/projects/artifacts/_tree_directory.html.haml
@@ -3,6 +3,6 @@
%tr.tree-item{ 'data-link' => path_to_directory }
%td.tree-item-file-name
= tree_icon('folder', '755', directory.name)
- = link_to path_to_directory, class: 'str-truncated', data: { qa_selector: 'directory_name_link', qa_directory_name: directory.name } do
+ = link_to path_to_directory, class: 'str-truncated', data: { testid: 'directory-name-link', qa_directory_name: directory.name } do
%span= directory.name
%td
diff --git a/app/views/projects/blob/_header_content.html.haml b/app/views/projects/blob/_header_content.html.haml
index 9cd2f583fdd..8c132a16797 100644
--- a/app/views/projects/blob/_header_content.html.haml
+++ b/app/views/projects/blob/_header_content.html.haml
@@ -3,7 +3,7 @@
.js-table-contents
= blob_icon blob.mode, blob.name
- %strong.file-title-name.gl-word-break-all{ data: { qa_selector: 'file_name_content' } }
+ %strong.file-title-name.gl-word-break-all{ data: { testid: 'file-name-content' } }
= blob.name
= copy_file_path_button(blob.path)
diff --git a/app/views/projects/blob/viewers/_markup.html.haml b/app/views/projects/blob/viewers/_markup.html.haml
index 703ffa8896e..01f730db33e 100644
--- a/app/views/projects/blob/viewers/_markup.html.haml
+++ b/app/views/projects/blob/viewers/_markup.html.haml
@@ -1,3 +1,3 @@
- blob = viewer.blob
-.file-content.md
+.file-content.js-markup-content.md
= markup(blob.name, blob.data, viewer.banzai_render_context)
diff --git a/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml b/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml
index 9c3f9b6c9fd..b7bc43d08d8 100644
--- a/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml
+++ b/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml
@@ -7,5 +7,3 @@
%ul
- viewer.errors.each do |error|
%li= error
-
-= link_to _('Learn more'), help_page_path('operations/metrics/dashboards/index.md')
diff --git a/app/views/projects/blob/viewers/_metrics_dashboard_yml_loading.html.haml b/app/views/projects/blob/viewers/_metrics_dashboard_yml_loading.html.haml
index 5e355ecc4b8..1f5086dc3bd 100644
--- a/app/views/projects/blob/viewers/_metrics_dashboard_yml_loading.html.haml
+++ b/app/views/projects/blob/viewers/_metrics_dashboard_yml_loading.html.haml
@@ -1,4 +1,2 @@
= gl_loading_icon(inline: true, css_class: "mr-1")
= _('Metrics Dashboard YAML definition') + '…'
-
-= link_to _('Learn more'), help_page_path('operations/metrics/dashboards/yaml.md')
diff --git a/app/views/projects/branch_defaults/_branch_names_fields.html.haml b/app/views/projects/branch_defaults/_branch_names_fields.html.haml
index 4e4a72c154f..3e77cb51a85 100644
--- a/app/views/projects/branch_defaults/_branch_names_fields.html.haml
+++ b/app/views/projects/branch_defaults/_branch_names_fields.html.haml
@@ -6,7 +6,7 @@
.form-group
.gl-mb-2
- = f.text_field :issue_branch_template, class: 'form-control gl-mb-2', placeholder: "%{id}-%{title}"
+ = f.text_field :issue_branch_template, class: 'form-control gl-mb-2 gl-form-input-xl', placeholder: "%{id}-%{title}"
%p.form-text.text-muted
= s_('ProjectSettings|Leave empty to use default template.')
= sprintf(s_('ProjectSettings|Maximum %{maxLength} characters.'), { maxLength: Issue::MAX_BRANCH_TEMPLATE })
diff --git a/app/views/projects/branch_defaults/_default_branch_fields.html.haml b/app/views/projects/branch_defaults/_default_branch_fields.html.haml
index e4f51725f1a..2c59e187d30 100644
--- a/app/views/projects/branch_defaults/_default_branch_fields.html.haml
+++ b/app/views/projects/branch_defaults/_default_branch_fields.html.haml
@@ -6,7 +6,8 @@
.form-group
= f.label :default_branch, _("Default branch"), class: 'label-bold'
%p= s_('ProjectSettings|All merge requests and commits are made against this branch unless you specify a different one.')
- .js-select-default-branch{ data: { default_branch: @project.default_branch, project_id: @project.id } }
+ .gl-form-input-xl
+ .js-select-default-branch{ data: { default_branch: @project.default_branch, project_id: @project.id } }
.form-group
- help_text = _("When merge requests and commits in the default branch close, any issues they reference also close.")
diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml
index db5d1ff5693..b5679bc512c 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
- = clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default")
+ = deprecated_clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default")
= 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
- = clipboard_button(target: '#http_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default")
+ = deprecated_clipboard_button(target: '#http_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default")
= render_if_exists 'projects/buttons/geo'
= render_if_exists 'projects/buttons/kerberos_clone_field'
%li.divider.mt-2
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 24d063d3b4d..e79a91eddaf 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -45,11 +45,12 @@
= gl_loading_icon(inline: true, css_class: 'gl-vertical-align-middle')
- if can?(current_user, :read_pipeline, @last_pipeline)
+ - status = @last_pipeline.detailed_status(current_user)
.well-segment.pipeline-info
.js-commit-pipeline-status{ data: { full_path: @project.full_path, iid: @last_pipeline.iid, graphql_resource_etag: graphql_etag_pipeline_path(@last_pipeline) } }
#{ _('Pipeline') }
= link_to "##{@last_pipeline.id}", project_pipeline_path(@project, @last_pipeline.id)
- = ci_label_for_status(@last_pipeline.status)
+ = status&.label
- if @last_pipeline.stages_count.nonzero?
#{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), @last_pipeline.stages_count) }
.js-commit-pipeline-mini-graph{ data: { stages: @last_pipeline_stages.to_json.html_safe, full_path: @project.full_path, iid: @last_pipeline.iid, graphql_resource_etag: graphql_etag_pipeline_path(@last_pipeline) } }
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index 5b99a88f29e..6aefc2eaa8b 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -17,17 +17,17 @@
- if signature.x509?
= render partial: "projects/commit/x509/certificate_details", locals: { signature: signature }
- = link_to(_('Learn more about X.509 signed commits'), help_page_path('user/project/repository/x509_signed_commits/index.md'), class: 'gl-link gl-display-block')
+ = link_to(_('Learn more about X.509 signed commits'), help_page_path('user/project/repository/signed_commits/x509.md'), class: 'gl-link gl-display-block')
- elsif signature.ssh?
= _('SSH key fingerprint:')
%span.gl-font-monospace= signature.key_fingerprint_sha256 || _('Unknown')
- = link_to(_('Learn about signing commits with SSH keys.'), help_page_path('user/project/repository/ssh_signed_commits/index.md'), class: 'gl-link gl-display-block gl-mt-3')
+ = link_to(_('Learn about signing commits with SSH keys.'), help_page_path('user/project/repository/signed_commits/ssh.md'), class: 'gl-link gl-display-block gl-mt-3')
- else
= _('GPG Key ID:')
%span.gl-font-monospace= signature.gpg_key_primary_keyid
- = link_to(_('Learn about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gl-link gl-display-block gl-mt-3')
+ = link_to(_('Learn about signing commits'), help_page_path('user/project/repository/signed_commits/index.md'), class: 'gl-link gl-display-block gl-mt-3')
%a.signature-badge.gl-display-inline-block.gl-ml-4{ role: 'button', tabindex: 0, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
= gl_badge_tag label, variant: variant
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 13a406d442d..c42d0fe9931 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -76,5 +76,5 @@
.commit-sha-group.btn-group.d-none.d-sm-flex
.label.label-monospace.monospace
= commit.short_id
- = clipboard_button(text: commit.id, title: _("Copy commit SHA"), class: "gl-button btn btn-default btn-icon", container: "body")
+ = clipboard_button(text: commit.id, category: :primary, size: :medium, title: _("Copy commit SHA"))
= link_to_browse_code(project, commit)
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index 4a29402bfe7..38633c9e5f1 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -1,6 +1,6 @@
- breadcrumb_title s_("CompareRevisions|Compare revisions")
-- page_title _("CompareRevisions|Compare revisions")
+- page_title s_("CompareRevisions|Compare revisions")
.prepend-top-20
#js-compare-selector{ data: project_compare_selector_data(@project, @merge_request, @compare_params) }
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
deleted file mode 100644
index 509ed62b39d..00000000000
--- a/app/views/projects/deployments/_commit.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-.table-mobile-content
- .branch-commit.cgray
- - if deployment.ref
- %span.icon-container.gl-display-inline-block
- = deployment.tag? ? sprite_icon('tag', css_class: 'sprite') : sprite_icon('fork', css_class: 'sprite')
- = link_to deployment.ref, project_ref_path(@project, deployment.ref), class: "ref-name"
- .icon-container.commit-icon
- = custom_icon("icon_commit")
- = link_to deployment.short_sha, project_commit_path(@project, deployment.sha), class: "commit-sha mr-0"
-
- %p.commit-title.flex-truncate-parent
- %span.flex-truncate-child
- - if commit_title = deployment.commit_title
- = author_avatar(deployment.commit, size: 20)
- = link_to_markdown commit_title, project_commit_path(@project, deployment.sha), class: "commit-row-message cgray"
- - else
- = _("Can't find HEAD commit for this branch")
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
deleted file mode 100644
index e3688c8d323..00000000000
--- a/app/views/projects/deployments/_deployment.html.haml
+++ /dev/null
@@ -1,49 +0,0 @@
-.gl-responsive-table-row.deployment{ role: 'row' }
- .table-section.section-15{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' }= _("Status")
- .table-mobile-content
- = render_deployment_status(deployment)
-
- .table-section.section-10{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' }= _("ID")
- %strong.table-mobile-content{ data: { testid: 'deployment-id' } } ##{deployment.iid}
-
- .table-section.section-10{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' }= _("Triggerer")
- .table-mobile-content
- - if deployment.deployed_by
- = user_avatar(user: deployment.deployed_by, size: 26, css_class: "mr-0 float-none")
-
- .table-section.section-25{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' }= _("Commit")
- = render 'projects/deployments/commit', deployment: deployment
-
- .table-section.section-10.build-column{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' }= _("Job")
- - if deployment.deployable
- .table-mobile-content
- .flex-truncate-parent
- .flex-truncate-child.has-tooltip.gl-white-space-normal.gl-md-white-space-nowrap{ :title => "#{deployment.deployable.name} (##{deployment.deployable.id})", data: { container: 'body' } }
- = link_to deployment_path(deployment), class: 'build-link' do
- #{deployment.deployable.name} (##{deployment.deployable.id})
- - else
- = gl_badge_tag s_('Deployment|API'), { variant: :info }, { class: 'gl-cursor-help', data: { toggle: 'tooltip' }, title: s_('Deployment|This deployment was created using the API') }
-
- .table-section.section-10{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' }= _("Created")
- %span.table-mobile-content.flex-truncate-parent
- %span.flex-truncate-child
- = time_ago_with_tooltip(deployment.created_at)
-
- .table-section.section-10{ role: 'gridcell' }
- .table-mobile-header{ role: 'rowheader' }= _("Deployed")
- - if deployment.deployed_at
- %span.table-mobile-content.flex-truncate-parent
- %span.flex-truncate-child
- = time_ago_with_tooltip(deployment.deployed_at)
-
- .table-section.section-10.table-button-footer{ role: 'gridcell' }
- .btn-group.table-action-buttons
- = render 'projects/deployments/actions', deployment: deployment
- = render 'projects/deployments/rollback', deployment: deployment
- = render_if_exists 'projects/deployments/approvals', deployment: deployment
diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml
deleted file mode 100644
index e50fa1fa0f7..00000000000
--- a/app/views/projects/deployments/_rollback.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-- if deployment.deployable && can?(current_user, :play_job, deployment.deployable)
- - tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
- - icon = deployment.last? ? 'repeat' : 'redo'
- = render Pajamas::ButtonComponent.new(icon: icon, button_options: { title: tooltip, class: 'js-confirm-rollback-modal-button has-tooltip', data: { environment_name: @environment.name, commit_short_sha: deployment.short_sha, commit_url: project_commit_path(@project, deployment.sha), is_last_deployment: deployment.last?.to_s, retry_path: retry_project_job_path(@environment.project, deployment.deployable) } })
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index afca27c5430..17e55699615 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -5,7 +5,7 @@
- if diff_file.submodule?
%span
- = sprite_icon('archive')
+ = sprite_icon('folder-git', file_icon: true)
%strong.file-title-name
= submodule_link(diff_file.blob, diff_file.content_sha, diff_file.repository)
@@ -23,7 +23,7 @@
%strong.file-title-name.has-tooltip.gl-word-break-all{ data: { title: diff_file.new_path, container: 'body' } }
= new_path
- else
- %strong.file-title-name.has-tooltip.gl-word-break-all{ data: { title: diff_file.file_path, container: 'body', qa_selector: 'file_name_content' } }
+ %strong.file-title-name.has-tooltip.gl-word-break-all{ data: { title: diff_file.file_path, container: 'body', testid: 'file-name-content' } }
= diff_file.file_path
- if diff_file.deleted_file?
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 662f1bb158d..0158018ecc0 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -5,8 +5,6 @@
- reduce_visibility_form_id = 'reduce-visibility-form'
- @force_desktop_expanded_sidebar = true
-= render_if_exists 'shared/ultimate_feature_removal_banner', project: @project
-
= render Pajamas::AlertComponent.new(title: _('GitLab Pages has moved'),
alert_options: { class: 'gl-my-5', data: { feature_id: Users::CalloutsHelper::PAGES_MOVED_CALLOUT, dismiss_endpoint: callouts_path, defer_links: 'true' } }) do |c|
- c.with_body do
@@ -91,8 +89,8 @@
.gl-new-card-title-wrapper
%h4.gl-new-card-title.warning-title= _('Change path')
%p.gl-new-card-description
- - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'rename-a-repository') }
- = _("A project’s repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}").html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+ - link = link_to('', help_page_path('user/project/settings/index', anchor: 'rename-a-repository'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(_("A project’s repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}"), tag_pair(link, :link_start, :link_end))
- c.with_body do
= render 'projects/errors'
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index e97cae911d9..46ec430cadb 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -8,33 +8,4 @@
#environments-detail-view{ data: { details: environments_detail_data_json(current_user, @project, @environment) } }
#environments-detail-view-header
- - if Feature.enabled?(:environment_details_vue, @project)
- #environment_details_page
- - else
- .environments-container
- - if @deployments.blank?
- .empty-state
- .text-content
- %h4.state-title
- = _("You don't have any deployments right now.")
- %p
- = html_escape(_("Define environments in the deploy stage(s) in %{code_open}.gitlab-ci.yml%{code_close} to track deployments here.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- .text-center
- = render Pajamas::ButtonComponent.new(variant: :confirm, href: help_page_path("ci/environments/index.md")) do
- = _('Read more')
-
- - else
- .table-holder.gl-overflow-visible
- .ci-table.environments{ role: 'grid' }
- .gl-responsive-table-row.table-row-header{ role: 'row' }
- .table-section.section-15{ role: 'columnheader' }= _('Status')
- .table-section.section-10{ role: 'columnheader' }= _('ID')
- .table-section.section-10{ role: 'columnheader' }= _('Triggerer')
- .table-section.section-25{ role: 'columnheader' }= _('Commit')
- .table-section.section-10{ role: 'columnheader' }= _('Job')
- .table-section.section-10{ role: 'columnheader' }= _('Created')
- .table-section.section-10{ role: 'columnheader' }= _('Deployed')
-
- = render @deployments
-
- = paginate @deployments, theme: 'gitlab'
+ #environment_details_page
diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml
index 2eaf89be4ef..f8025f7c462 100644
--- a/app/views/projects/hook_logs/show.html.haml
+++ b/app/views/projects/hook_logs/show.html.haml
@@ -1,7 +1,7 @@
- add_to_breadcrumbs _('Webhook Settings'), namespace_project_hooks_path
- page_title _('Webhook Logs')
-%h1.page-title.gl-font-size-h-display
+%h2.page-title.gl-font-size-h-display
= _("Request details")
%hr
diff --git a/app/views/projects/incidents/show.html.haml b/app/views/projects/incidents/show.html.haml
index 6d733dc46df..400c07835cd 100644
--- a/app/views/projects/incidents/show.html.haml
+++ b/app/views/projects/incidents/show.html.haml
@@ -1,9 +1,17 @@
-- @content_class = "limit-container-width" unless fluid_layout
- add_to_breadcrumbs _("Incidents"), project_incidents_path(@project)
- breadcrumb_title @issue.to_reference
+
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Incidents")
+- page_description @issue.description_html
+- page_card_attributes @issue.card_attributes
+- if @issue.relocation_target
+ - page_canonical_link @issue.relocation_target.present(current_user: current_user).web_url
+
- add_page_specific_style 'page_bundles/design_management'
- add_page_specific_style 'page_bundles/incidents'
+- add_page_specific_style 'page_bundles/issuable'
- add_page_specific_style 'page_bundles/issues_show'
-= render 'projects/issuable/show', issuable: @issue
+- @content_class = "limit-container-width" unless fluid_layout
+
+= render 'projects/issues/details_content', issuable: @issue
diff --git a/app/views/projects/issuable/_show.html.haml b/app/views/projects/issuable/_show.html.haml
deleted file mode 100644
index e502457808d..00000000000
--- a/app/views/projects/issuable/_show.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-- api_awards_path = local_assigns.fetch(:api_awards_path, nil)
-- page_description issuable.description_html
-- page_card_attributes issuable.card_attributes
-- if issuable.relocation_target
- - page_canonical_link issuable.relocation_target.present(current_user: current_user).web_url
-- add_page_specific_style 'page_bundles/issuable'
-
-= render "projects/issues/service_desk/alert_moved_from_service_desk", issue: issuable
-
-= render 'shared/issue_type/details_content', issuable: issuable, api_awards_path: api_awards_path
diff --git a/app/views/shared/issue_type/_details_content.html.haml b/app/views/projects/issues/_details_content.html.haml
index 249e296b41a..51ffb68f4e5 100644
--- a/app/views/shared/issue_type/_details_content.html.haml
+++ b/app/views/projects/issues/_details_content.html.haml
@@ -1,14 +1,12 @@
- related_branches_path = related_branches_project_issue_path(@project, issuable)
- api_awards_path = local_assigns.fetch(:api_awards_path, nil)
+= render "projects/issues/service_desk/alert_moved_from_service_desk", issue: issuable
+
.issue-details.issuable-details.js-issue-details
.detail-page-description.content-block.js-detail-page-description.gl-pt-3.gl-pb-0.gl-border-none
#js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json,
- header_actions_data: issue_header_actions_data(@project, issuable, current_user, @issuable_sidebar).to_json,
- issuable_id: issuable.id,
- full_path: @project.full_path,
- register_path: new_user_registration_path(redirect_to_referer: 'yes'),
- sign_in_path: new_session_path(:user, redirect_to_referer: 'yes') } }
+ header_actions_data: issue_header_actions_data(@project, issuable, current_user, @issuable_sidebar).to_json } }
.title-container
%h1.title.page-title.gl-font-size-h-display= markdown_field(issuable, :title)
- if issuable.description.present?
@@ -18,10 +16,10 @@
= edited_time_ago_with_tooltip(issuable, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
.js-issue-widgets
- = render 'shared/issue_type/emoji_block', issuable: issuable, api_awards_path: api_awards_path
+ = render 'projects/issues/emoji_block', issuable: issuable, api_awards_path: api_awards_path
.js-issue-widgets
- = render 'shared/issue_type/sentry_stack_trace', issuable: issuable
+ = render 'projects/issues/sentry_stack_trace', issuable: issuable
= render 'projects/issues/design_management'
@@ -29,7 +27,10 @@
= render_if_exists 'projects/issues/linked_resources'
= render 'projects/issues/related_issues'
- #js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: issuable.iid)), project_namespace: @project.namespace.path, project_path: @project.path } }
+ #js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: issuable.iid)),
+ has_closing_merge_request: (issuable.merge_requests_count(current_user) != 0).to_s,
+ project_namespace: @project.namespace.path,
+ project_path: @project.path } }
- if can?(current_user, :read_code, @project)
- add_page_startup_api_call related_branches_path
diff --git a/app/views/shared/issue_type/_emoji_block.html.haml b/app/views/projects/issues/_emoji_block.html.haml
index 7eb3c0f5c9f..7eb3c0f5c9f 100644
--- a/app/views/shared/issue_type/_emoji_block.html.haml
+++ b/app/views/projects/issues/_emoji_block.html.haml
diff --git a/app/views/projects/issues/_related_issues.html.haml b/app/views/projects/issues/_related_issues.html.haml
index 2409c61fbf2..73a88e63a4e 100644
--- a/app/views/projects/issues/_related_issues.html.haml
+++ b/app/views/projects/issues/_related_issues.html.haml
@@ -4,6 +4,7 @@
full_path: @project.full_path,
has_issue_weights_feature: @project.licensed_feature_available?(:issue_weights).to_s,
help_path: help_page_path('user/project/issues/related_issues'),
+ issuable_type: @issue.issue_type,
show_categorized_issues: @project.licensed_feature_available?(:blocked_issues).to_s,
has_iterations_feature: @project.licensed_feature_available?(:iterations).to_s,
report_abuse_path: add_category_abuse_reports_path } }
diff --git a/app/views/shared/issue_type/_sentry_stack_trace.html.haml b/app/views/projects/issues/_sentry_stack_trace.html.haml
index 40b29a74b53..40b29a74b53 100644
--- a/app/views/shared/issue_type/_sentry_stack_trace.html.haml
+++ b/app/views/projects/issues/_sentry_stack_trace.html.haml
diff --git a/app/views/projects/issues/service_desk.html.haml b/app/views/projects/issues/service_desk.html.haml
index 2d17719a8c2..4453cb2e538 100644
--- a/app/views/projects/issues/service_desk.html.haml
+++ b/app/views/projects/issues/service_desk.html.haml
@@ -2,10 +2,11 @@
- page_title _("Service Desk")
- add_page_specific_style 'page_bundles/issuable_list'
+- add_page_specific_style 'page_bundles/issues_list'
- content_for :breadcrumbs_extra do
= render "projects/issues/service_desk/nav_btns", show_export_button: false, show_rss_button: false
-- support_bot_attrs = { service_desk_enabled: @project.service_desk_enabled?, **UserSerializer.new.represent(User.support_bot) }.to_json
+- support_bot_attrs = { service_desk_enabled: @project.service_desk_enabled?, **UserSerializer.new.represent(Users::Internal.support_bot) }.to_json
.js-service-desk-issues.service-desk-issues{ data: { support_bot: support_bot_attrs } }
- if ::Feature.enabled?(:service_desk_vue_list, @project)
diff --git a/app/views/projects/issues/service_desk/_issue.html.haml b/app/views/projects/issues/service_desk/_issue.html.haml
index 5b98712d3eb..66b2eabac9d 100644
--- a/app/views/projects/issues/service_desk/_issue.html.haml
+++ b/app/views/projects/issues/service_desk/_issue.html.haml
@@ -48,10 +48,10 @@
.issuable-meta
%ul.controls
- if issue.closed? && issue.moved?
- %li.issuable-status
+ %li
= render Pajamas::BadgeComponent.new(_('Closed (moved)'), size: 'sm', variant: 'info')
- elsif issue.closed?
- %li.issuable-status
+ %li
= render Pajamas::BadgeComponent.new(_('Closed'), size: 'sm', variant: 'info')
- if issue.assignees.any?
%li.gl-display-flex
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 7e8bf4ae57f..457eaf5e194 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,10 +1,18 @@
-- @content_class = "limit-container-width" unless fluid_layout
- add_to_breadcrumbs _("Issues"), project_issues_path(@project)
- breadcrumb_title @issue.to_reference
+
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues")
+- page_description @issue.description_html
+- page_card_attributes @issue.card_attributes
+- if @issue.relocation_target
+ - page_canonical_link @issue.relocation_target.present(current_user: current_user).web_url
+
- add_page_specific_style 'page_bundles/design_management'
- add_page_specific_style 'page_bundles/incidents'
+- add_page_specific_style 'page_bundles/issuable'
- add_page_specific_style 'page_bundles/issues_show'
- add_page_specific_style 'page_bundles/work_items'
-= render 'projects/issuable/show', issuable: @issue, api_awards_path: award_emoji_issue_api_path(@issue)
+- @content_class = "limit-container-width" unless fluid_layout
+
+= render 'projects/issues/details_content', issuable: @issue, api_awards_path: award_emoji_issue_api_path(@issue)
diff --git a/app/views/projects/jobs/_table.html.haml b/app/views/projects/jobs/_table.html.haml
deleted file mode 100644
index 0bb512b4035..00000000000
--- a/app/views/projects/jobs/_table.html.haml
+++ /dev/null
@@ -1,37 +0,0 @@
-- admin = local_assigns.fetch(:admin, false)
-
-- if builds.blank?
- - if @project
- .row.empty-state
- .col-12
- .svg-content.svg-250
- = image_tag('jobs-empty-state.svg')
- .col-12
- .text-content.gl-text-center
- %h4
- = s_('Jobs|Use jobs to automate your tasks')
- %p
- = s_('Jobs|Jobs are the building blocks of a GitLab CI/CD pipeline. Each job has a specific task, like testing code. To set up jobs in a CI/CD pipeline, add a CI/CD configuration file to your project.')
- = link_button_to s_('Jobs|Create CI/CD configuration file'), project_ci_pipeline_editor_path(project), class: 'js-empty-state-button', variant: :confirm
- - else
- .nothing-here-block= s_('Jobs|No jobs to show')
-- else
- .table-holder
- %table.table.ci-table.builds-page
- %thead
- %tr
- %th= _('Status')
- %th= _('Name')
- %th= _('Job')
- %th= _('Pipeline')
- - if admin
- %th= _('Project')
- %th= _('Runner')
- %th= _('Stage')
- %th= _('Duration')
- %th= _('Coverage')
- %th
-
- = render partial: "projects/ci/builds/build", collection: builds, as: :build, locals: { commit_sha: true, ref: true, pipeline_link: true, stage: true, allow_retry: true, admin: admin }
-
- = paginate_collection(builds)
diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml
index 4f4609e6016..ce8b3f70204 100644
--- a/app/views/projects/labels/edit.html.haml
+++ b/app/views/projects/labels/edit.html.haml
@@ -1,8 +1,9 @@
- add_to_breadcrumbs _("Labels"), project_labels_path(@project)
- breadcrumb_title _("Edit")
- page_title _("Edit"), @label.name, _("Labels")
+- show_lock_on_merge = @project.supports_lock_on_merge?
%h1.page-title.gl-font-size-h-display
= _('Edit Label')
-= render 'shared/labels/form', url: project_label_path(@project, @label), back_path: project_labels_path(@project)
+= render 'shared/labels/form', url: project_label_path(@project, @label), back_path: project_labels_path(@project), show_lock_on_merge: show_lock_on_merge
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 8855e8024b3..4b27b344498 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -3,7 +3,6 @@
- search = params[:search]
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present?
-= render_if_exists 'shared/ultimate_feature_removal_banner', project: @project
- if labels_or_filters
#js-promote-label-modal
diff --git a/app/views/projects/merge_requests/_code_dropdown.html.haml b/app/views/projects/merge_requests/_code_dropdown.html.haml
index 4cab6fac388..bfa33f26453 100644
--- a/app/views/projects/merge_requests/_code_dropdown.html.haml
+++ b/app/views/projects/merge_requests/_code_dropdown.html.haml
@@ -1,6 +1,6 @@
.gl-md-ml-3.dropdown.gl-dropdown{ class: "gl-display-none! gl-md-display-flex!" }
#js-check-out-modal{ data: how_merge_modal_data(@merge_request) }
- = button_tag type: 'button', class: "btn dropdown-toggle btn-confirm gl-button gl-dropdown-toggle", data: { toggle: 'dropdown', qa_selector: 'mr_code_dropdown' } do
+ = button_tag type: 'button', class: "btn dropdown-toggle btn-confirm gl-button gl-dropdown-toggle", data: { toggle: 'dropdown', testid: 'mr-code-dropdown' } do
%span.gl-dropdown-button-text= _('Code')
= sprite_icon "chevron-down", size: 16, css_class: "dropdown-icon gl-icon gl-ml-2 gl-mr-0!"
.dropdown-menu.dropdown-menu-right
@@ -16,7 +16,7 @@
= _('Check out branch')
- if current_user
%li.gl-dropdown-item
- = link_to ide_merge_request_path(@merge_request), class: 'dropdown-item', target: '_blank', data: { qa_selector: 'open_in_web_ide_button' } do
+ = link_to ide_merge_request_path(@merge_request), class: 'dropdown-item', target: '_blank', data: { testid: 'open-in-web-ide-button' } do
.gl-dropdown-item-text-wrapper
= _('Open in Web IDE')
- if Gitlab::CurrentSettings.gitpod_enabled && current_user&.gitpod_enabled
@@ -30,10 +30,10 @@
%header.dropdown-header
= _('Download')
%li.gl-dropdown-item
- = link_to merge_request_path(@merge_request, format: :patch), class: 'dropdown-item', download: '', data: { qa_selector: 'download_email_patches_menu_item' } do
+ = link_to merge_request_path(@merge_request, format: :patch), class: 'dropdown-item', download: '', data: { testid: 'download-email-patches-menu-item' } do
.gl-dropdown-item-text-wrapper
= _('Patches')
%li.gl-dropdown-item
- = link_to merge_request_path(@merge_request, format: :diff), class: 'dropdown-item', download: '', data: { qa_selector: 'download_plain_diff_menu_item' } do
+ = link_to merge_request_path(@merge_request, format: :diff), class: 'dropdown-item', download: '', data: { testid: 'download-plain-diff-menu-item' } do
.gl-dropdown-item-text-wrapper
= _('Plain diff')
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 7b815d996e0..4a7aa9a86ab 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -45,8 +45,10 @@
.issuable-meta
%ul.controls.d-flex.align-items-end
- 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
- = render Pajamas::BadgeComponent.new(_('Merged'), size: 'sm', variant: 'info')
+ %a.has-tooltip{ href: "#{merge_request_path(merge_request)}#widget-state", title: merged_at }
+ = render Pajamas::BadgeComponent.new(_('Merged'), size: 'sm', variant: 'info')
- elsif merge_request.closed?
%li.d-none.d-sm-flex
= render Pajamas::BadgeComponent.new(_('Closed'), size: 'sm', variant: 'danger')
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index dfa582f4c60..f0e7df8a379 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -15,7 +15,7 @@
.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-issuable-header-warnings{ data: { hidden: @merge_request.hidden?.to_s } }
+ .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)
diff --git a/app/views/projects/merge_requests/_page.html.haml b/app/views/projects/merge_requests/_page.html.haml
index 69e2487152e..dfb18b52021 100644
--- a/app/views/projects/merge_requests/_page.html.haml
+++ b/app/views/projects/merge_requests/_page.html.haml
@@ -28,12 +28,12 @@
.merge-request-tabs-holder{ class: "#{'js-tabs-affix' unless ENV['RAILS_ENV'] == 'test'} #{'gl-static' if moved_mr_sidebar_enabled?}" }
.merge-request-tabs-container.gl-display-flex.gl-justify-content-space-between{ class: "#{'is-merge-request' if Feature.enabled?(:moved_mr_sidebar, @project) && !fluid_layout}" }
%ul.merge-request-tabs.nav-tabs.nav.nav-links.gl-display-flex.gl-flex-nowrap.gl-m-0.gl-p-0{ class: "#{'gl-w-full gl-lg-w-auto!' if Feature.enabled?(:moved_mr_sidebar, @project)}" }
- = render "projects/merge_requests/tabs/tab", class: "notes-tab", qa_selector: "notes_tab" do
+ = render "projects/merge_requests/tabs/tab", class: "notes-tab", testid: "notes-tab" do
= tab_link_for @merge_request, :show, force_link: @commit.present? do
= _("Overview")
= gl_badge_tag @merge_request.related_notes.user.count, { size: :sm }, { class: 'js-discussions-count' }
- if @merge_request.source_project
- = render "projects/merge_requests/tabs/tab", name: "commits", class: "commits-tab", qa_selector: "commits_tab" do
+ = render "projects/merge_requests/tabs/tab", name: "commits", class: "commits-tab", testid: "commits-tab" do
= tab_link_for @merge_request, :commits do
= _("Commits")
= gl_badge_tag tab_count_display(@merge_request, @commits_count), { size: :sm }, { class: 'js-commits-count' }
@@ -42,7 +42,7 @@
= tab_link_for @merge_request, :pipelines do
= _("Pipelines")
= gl_badge_tag @number_of_pipelines, { size: :sm }, { class: 'js-pipelines-mr-count' }
- = render "projects/merge_requests/tabs/tab", name: "diffs", class: "diffs-tab js-diffs-tab", id: "diffs-tab", qa_selector: "diffs_tab" do
+ = render "projects/merge_requests/tabs/tab", name: "diffs", class: "diffs-tab js-diffs-tab", id: "diffs-tab", testid: "diffs-tab" do
= tab_link_for @merge_request, :diffs do
= _("Changes")
= gl_badge_tag tab_count_display(@merge_request, @diffs_count), { size: :sm }
@@ -61,6 +61,7 @@
= render "projects/merge_requests/tabs/pane", id: "notes", class: "notes voting_notes" do
%div{ class: "#{'merge-request-overview' if moved_mr_sidebar_enabled?}" }
%section
+ = render_if_exists "projects/merge_requests/diff_summary"
.issuable-discussion.js-vue-notes-event
- if @merge_request.description.present?
.detail-page-description.gl-pb-0
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index 07bae4d2396..e6bd0b05f00 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -14,7 +14,7 @@
= gitlab_ui_form_for [@project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form js-requires-input" } do |f|
- if params[:nav_source].present?
= hidden_field_tag(:nav_source, params[:nav_source])
- .js-merge-request-new-compare.row{ 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
+ .js-merge-request-new-compare.row{ data: mr_compare_form_data(current_user, @merge_request) }
.col-lg-6
.card-new-merge-request
%h2.gl-font-size-h2
@@ -31,4 +31,4 @@
= form_errors(@merge_request)
.row
.col-12
- = f.submit _('Compare branches and continue'), data: { qa_selector: 'compare_branches_button' }, pajamas_button: true
+ = f.submit _('Compare branches and continue'), data: { testid: 'compare-branches-button' }, pajamas_button: true
diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml
index a7151421acb..996928ba377 100644
--- a/app/views/projects/merge_requests/creations/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml
@@ -50,7 +50,7 @@
= _("Pipelines")
= gl_badge_tag @pipelines.size, { size: :sm }, { class: 'gl-tab-counter-badge' }
%li.diffs-tab
- = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tabvue', qa_selector: 'diffs_tab'} do
+ = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tabvue', testid: 'diffs-tab'} do
= _("Changes")
= gl_badge_tag @merge_request.diff_size, { size: :sm }, { class: 'gl-tab-counter-badge' }
diff --git a/app/views/projects/merge_requests/tabs/_tab.html.haml b/app/views/projects/merge_requests/tabs/_tab.html.haml
index 9d942da8098..f6c8f4cd87b 100644
--- a/app/views/projects/merge_requests/tabs/_tab.html.haml
+++ b/app/views/projects/merge_requests/tabs/_tab.html.haml
@@ -1,8 +1,8 @@
- tab_name = local_assigns.fetch(:name, nil)
- tab_class = local_assigns.fetch(:class, nil)
-- qa_selector = local_assigns.fetch(:qa_selector, nil)
+- testid = local_assigns.fetch(:testid, nil)
- id = local_assigns.fetch(:id, nil)
-- attrs = { class: [tab_class, ("active" if params[:tab] == tab_name)], data: { qa_selector: qa_selector } }
+- attrs = { class: [tab_class, ("active" if params[:tab] == tab_name)], data: { testid: testid } }
- attrs[:id] = id if id.present?
%li{ attrs }
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index a592062a17d..abf2949938c 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -15,7 +15,7 @@
- @gfm_form = true
.js-markdown-editor{ data: { render_markdown_path: preview_markdown_path(@project),
markdown_docs_path: help_page_path('user/markdown'),
- qa_selector: 'milestone_description_field',
+ testid: 'milestone-description-field',
form_field_placeholder: _('Write milestone description...'),
supports_quick_actions: 'false',
enable_autocomplete: 'true',
diff --git a/app/views/projects/mirrors/_authentication_method.html.haml b/app/views/projects/mirrors/_authentication_method.html.haml
index 54e1f1a8b20..c4cf128a62a 100644
--- a/app/views/projects/mirrors/_authentication_method.html.haml
+++ b/app/views/projects/mirrors/_authentication_method.html.haml
@@ -1,14 +1,17 @@
- mirror = f.object
-- auth_options = [[_('Password'), 'password'], [_('SSH public key'), 'ssh_public_key']]
+- auth_options = [[_('Username and Password'), 'password'], [_('SSH public key'), 'ssh_public_key']]
.form-group
= f.label :auth_method, _('Authentication method'), class: 'label-bold'
= f.select :auth_method,
options_for_select(auth_options, mirror.auth_method),
- {}, { class: "custom-select gl-form-select js-mirror-auth-type", data: { qa_selector: 'authentication_method_field' } }
+ {}, { class: "custom-select gl-form-select js-mirror-auth-type gl-max-w-34 gl-display-block", data: { qa_selector: 'authentication_method_field' } }
= f.hidden_field :auth_method, value: "password", class: "js-hidden-mirror-auth-type"
.form-group
- .well-password-auth.collapse.js-well-password-auth
+ = f.label :user, _('Username'), class: 'label-bold'
+ = f.text_field :user, class: 'form-control gl-form-input gl-form-input-xl', value: nil, autocomplete: 'off', required: false, autocorrect: 'off', autocapitalize: 'off', spellcheck: false, data: { testid: 'username-field' }
+.well-password-auth.collapse.js-well-password-auth
+ .form-group
= f.label :password, _("Password"), class: "label-bold"
- = f.password_field :password, class: 'form-control gl-form-input js-mirror-password-field', autocomplete: 'off', data: { qa_selector: 'password_field' }
+ = f.password_field :password, class: 'form-control gl-form-input js-mirror-password-field gl-form-input-xl', autocomplete: 'off', data: { testid: 'password-field' }
diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml
index 2bd2c7cac44..5a8710a64b0 100644
--- a/app/views/projects/mirrors/_instructions.html.haml
+++ b/app/views/projects/mirrors/_instructions.html.haml
@@ -4,7 +4,7 @@
= html_escape(_('The repository must be accessible over %{code_open}http://%{code_close},
%{code_open}https://%{code_close}, %{code_open}ssh://%{code_close} or %{code_open}git://%{code_close}.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%li= html_escape(_('When using the %{code_open}http://%{code_close} or %{code_open}https://%{code_close} protocols, please provide the exact URL to the repository. HTTP redirects will not be followed.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- %li= html_escape(_('Include the username in the URL if required: %{code_open}https://username@gitlab.company.com/group/project.git%{code_close}.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+ %li= html_escape(_('Do not include the username in the URL, use the username field below if required: %{code_open}https://gitlab.company.com/group/project.git%{code_close}.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%li
- minutes = Gitlab.config.gitlab_shell.git_timeout / 60
= _("The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination.") % { number_of_minutes: minutes }
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index a1c89a9dd30..00837ce1c73 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -35,7 +35,7 @@
%div= form_errors(@project)
.form-group.has-feedback
= label_tag :url, _('Git repository URL'), class: 'label-light'
- = text_field_tag :url, nil, class: 'form-control gl-form-input js-mirror-url js-repo-url', placeholder: _('Input the remote repository URL'), required: true, pattern: "(#{protocols}):\/\/.+", autocomplete: 'new-password', data: { qa_selector: 'mirror_repository_url_field' }
+ = text_field_tag :url, nil, class: 'form-control gl-form-input js-mirror-url js-repo-url gl-form-input-xl', placeholder: _('Input the remote repository URL'), required: true, pattern: "(#{protocols}):\/\/.+", autocomplete: 'new-password', data: { qa_selector: 'mirror_repository_url_field' }
= render 'projects/mirrors/instructions'
diff --git a/app/views/projects/mirrors/_mirror_repos_form.html.haml b/app/views/projects/mirrors/_mirror_repos_form.html.haml
index 1322e677d5a..8378a74311f 100644
--- a/app/views/projects/mirrors/_mirror_repos_form.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_form.html.haml
@@ -1,7 +1,7 @@
.form-group
= label_tag :mirror_direction, _('Mirror direction'), class: 'label-light'
.select-wrapper
- = select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control gl-form-select select-control js-mirror-direction', disabled: true, data: { qa_selector: 'mirror_direction_field' }
+ = select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control gl-form-select select-control js-mirror-direction gl-max-w-34 gl-display-block', disabled: true, data: { qa_selector: 'mirror_direction_field' }
= sprite_icon('chevron-down', css_class: "gl-icon gl-absolute gl-top-3 gl-right-3 gl-text-gray-200")
= render partial: "projects/mirrors/mirror_repos_push", locals: { f: f }
diff --git a/app/views/projects/mirrors/_mirror_repos_list.html.haml b/app/views/projects/mirrors/_mirror_repos_list.html.haml
index 0debd13709d..59611db941f 100644
--- a/app/views/projects/mirrors/_mirror_repos_list.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_list.html.haml
@@ -34,7 +34,7 @@
- if mirror_settings_enabled
.btn-group.mirror-actions-group{ role: 'group' }
- if mirror.ssh_key_auth?
- = clipboard_button(text: mirror.ssh_public_key, class: 'gl-button btn btn-default btn-icon', title: _('Copy SSH public key'), qa_selector: 'copy_public_key_button')
+ = clipboard_button(text: mirror.ssh_public_key, variant: :default, category: :primary, size: :medium, title: _('Copy SSH public key'), testid: 'copy_public_key_button')
= render 'shared/remote_mirror_update_button', remote_mirror: mirror
= render Pajamas::ButtonComponent.new(variant: :danger,
icon: 'remove',
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 59a21cecd39..bf288d3601b 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -29,9 +29,8 @@
= render Pajamas::CardComponent.new(card_options: { class: 'gl-my-5' }) do |c|
- c.with_body do
%div
- - contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing'
- - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: contributing_templates_url }
- = _('Learn how to %{link_start}contribute to the built-in templates%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+ - link = link_to('', 'https://gitlab.com/gitlab-org/project-templates/contributing', target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(_('Learn how to %{link_start}contribute to the built-in templates%{link_end}'), tag_pair(link, :link_start, :link_end))
= gitlab_ui_form_for @project, html: { class: 'new_project' } do |f|
.project-template
.form-group
diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml
index e3cc9199352..54d1bf012f3 100644
--- a/app/views/projects/notes/_more_actions_dropdown.html.haml
+++ b/app/views/projects/notes/_more_actions_dropdown.html.haml
@@ -5,7 +5,7 @@
= 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' }})
%ul.dropdown-menu.more-actions-dropdown.dropdown-open-left
%li
- = clipboard_button(text: noteable_note_url(note), title: _('Copy reference'), button_text: _('Copy link'), class: 'btn-clipboard', hide_tooltip: true, hide_button_icon: true)
+ = 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)
- unless is_current_user
.gl-ml-n2
.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) } }
diff --git a/app/views/projects/pages/_pages_settings.html.haml b/app/views/projects/pages/_pages_settings.html.haml
index b1ec7a362b7..1aa8148dfed 100644
--- a/app/views/projects/pages/_pages_settings.html.haml
+++ b/app/views/projects/pages/_pages_settings.html.haml
@@ -1,11 +1,7 @@
-- can_edit_max_page_size = can?(current_user, :update_max_pages_size)
-- can_enforce_https_only = Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
-
= gitlab_ui_form_for @project, url: project_pages_path(@project), html: { class: 'inline', title: pages_https_only_title } do |f|
- - if can_edit_max_page_size
- = render_if_exists 'shared/pages/max_pages_size_input', form: f
+ = render_if_exists 'shared/pages/max_pages_size_input', form: f
- - if can_enforce_https_only
+ - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
.form-group
= f.gitlab_ui_checkbox_component :pages_https_only,
s_('GitLabPages|Force HTTPS (requires valid certificates)'),
@@ -24,5 +20,14 @@
%p.gl-pl-6
= s_("GitLabPages|When enabled, a unique domain is generated to access pages.").html_safe
+ - if can?(current_user, :pages_multiple_versions, @project)
+ .form-group
+ = f.fields_for :project_setting do |settings|
+ = settings.gitlab_ui_checkbox_component :pages_multiple_versions_enabled,
+ s_('GitLabPages|Use multiple versions'),
+ label_options: { class: 'label-bold' }
+ %p.gl-pl-6
+ = s_("GitLabPages|When enabled, you can create multiple versions of your pages site.").html_safe
+
.gl-mt-3
= f.submit s_('GitLabPages|Save changes'), pajamas_button: true
diff --git a/app/views/projects/pages_domains/_dns.html.haml b/app/views/projects/pages_domains/_dns.html.haml
index 0edce28bb9d..9ca9360199d 100644
--- a/app/views/projects/pages_domains/_dns.html.haml
+++ b/app/views/projects/pages_domains/_dns.html.haml
@@ -9,7 +9,7 @@
.input-group
= text_field_tag :domain_dns, dns_record , class: "monospace js-select-on-focus form-control", readonly: true
.input-group-append
- = clipboard_button(target: '#domain_dns', class: 'btn-default input-group-text d-none d-sm-block')
+ = deprecated_clipboard_button(target: '#domain_dns', class: 'btn-default input-group-text d-none d-sm-block')
%p.form-text.text-muted
= _("To access this domain create a new DNS record")
- if verification_enabled
@@ -25,7 +25,7 @@
.input-group
= text_field_tag :domain_verification, domain_presenter.verification_record, class: "monospace js-select-on-focus form-control", readonly: true
.input-group-append
- = clipboard_button(target: '#domain_verification', class: 'btn-default d-none d-sm-block')
+ = deprecated_clipboard_button(target: '#domain_verification', class: 'btn-default d-none d-sm-block')
%p.form-text.text-muted
- link_to_help = link_to(_('verify ownership'), help_page_path('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: '4-verify-the-domains-ownership'))
= _("To %{link_to_help} of your domain, add the above key to a TXT record within your DNS configuration within seven days.").html_safe % { link_to_help: link_to_help }
diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml
deleted file mode 100644
index df85963218d..00000000000
--- a/app/views/projects/pipeline_schedules/_form.html.haml
+++ /dev/null
@@ -1,43 +0,0 @@
-= gitlab_ui_form_for [@project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "js-pipeline-schedule-form pipeline-schedule-form" } do |f|
- = form_errors(@schedule)
- .form-group.row
- .col-md-9
- = f.label :description, _('Description'), class: 'label-bold'
- = f.text_field :description, class: 'form-control gl-form-input', required: true, autofocus: true, placeholder: s_('PipelineSchedules|Provide a short description for this pipeline')
- .form-group.row
- .col-md-9
- = f.label :cron, _('Interval Pattern'), class: 'label-bold'
- #interval-pattern-input{ data: { initial_interval: @schedule.cron, daily_limit: @schedule.daily_limit } }
- .form-group.row
- .col-md-9{ data: { testid: 'schedule-timezone' } }
- = f.label :cron_timezone, _("Cron Timezone")
- .js-timezone-dropdown{ data: { timezone_data: timezone_data.to_json, initial_value: @schedule.cron_timezone, name: 'schedule[cron_timezone]' } }
-
- .form-group.row
- .col-md-9
- = f.label :ref, _('Target branch or tag'), class: 'label-bold'
- %div{ data: { testid: 'schedule-target-ref' } }
- .js-target-ref-dropdown{ data: { project_id: @project.id, default_branch: @project.default_branch } }
- = f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true
- .form-group.row.js-ci-variable-list-section
- .col-md-9
- %label.label-bold
- #{ s_('PipelineSchedules|Variables') }
- %ul.ci-variable-list
- - @schedule.variables.each do |variable|
- = render 'ci/variables/variable_row', form_field: 'schedule', variable: variable
- = render 'ci/variables/variable_row', form_field: 'schedule'
- - if @schedule.variables.size > 0
- = render Pajamas::ButtonComponent.new(category: :secondary, variant: :confirm, button_options: { class: 'gl-mt-3 js-secret-value-reveal-button', data: { secret_reveal_status: "#{@schedule.variables.size == 0}" }}) do
- - if @schedule.variables.size == 0
- = n_('Hide value', 'Hide values', @schedule.variables.size)
- - else
- = n_('Reveal value', 'Reveal values', @schedule.variables.size)
- .form-group.row
- .col-md-9
- = f.label :active, s_('PipelineSchedules|Activated'), class: 'label-bold'
- %div
- = f.gitlab_ui_checkbox_component :active, _('Active'), checkbox_options: { value: @schedule.active, required: false }
- .footer-block
- = f.submit _('Save pipeline schedule'), pajamas_button: true
- = link_button_to _('Cancel'), pipeline_schedules_path(@project)
diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
deleted file mode 100644
index a050808f13c..00000000000
--- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
+++ /dev/null
@@ -1,45 +0,0 @@
-- if pipeline_schedule
- %tr.pipeline-schedule-table-row
- %td{ role: 'cell', data: { label: _('Description') } }
- %div
- = pipeline_schedule.description
- %td.branch-name-cell.gl-text-truncate{ role: 'cell', data: { label: s_("PipelineSchedules|Target") } }
- %div
- - if pipeline_schedule.for_tag?
- = sprite_icon('tag', size: 12, css_class: 'gl-vertical-align-middle!')
- - else
- = sprite_icon('fork', size: 12, css_class: 'gl-vertical-align-middle!')
- - if pipeline_schedule.ref.present?
- = link_to pipeline_schedule.ref_for_display, project_ref_path(@project, pipeline_schedule.ref_for_display), class: "ref-name"
- %td{ role: 'cell', data: { label: _("Last Pipeline") } }
- %div
- - if pipeline_schedule.last_pipeline
- .status-icon-container{ class: "ci-status-icon-#{pipeline_schedule.last_pipeline.status}" }
- = link_to project_pipeline_path(@project, pipeline_schedule.last_pipeline.id) do
- = ci_icon_for_status(pipeline_schedule.last_pipeline.status)
- %span.gl-text-blue-500! ##{pipeline_schedule.last_pipeline.id}
- - else
- = s_("PipelineSchedules|None")
- %td.gl-text-gray-500{ role: 'cell', data: { label: s_("PipelineSchedules|Next Run") }, 'data-testid': 'next-run-cell' }
- %div
- - if pipeline_schedule.active? && pipeline_schedule.next_run_at
- = time_ago_with_tooltip(pipeline_schedule.real_next_run)
- - else
- = s_("PipelineSchedules|Inactive")
- %td{ role: 'cell', data: { label: _("Owner") } }
- %div
- - if pipeline_schedule.owner
- = render Pajamas::AvatarComponent.new(pipeline_schedule.owner, size: 24, class: "gl-mr-2")
- = link_to user_path(pipeline_schedule.owner) do
- = pipeline_schedule.owner&.name
- %td{ role: 'cell', data: { label: _('Actions') } }
- .float-right.btn-group
- - if can?(current_user, :play_pipeline_schedule, pipeline_schedule)
- = link_button_to nil, play_pipeline_schedule_path(pipeline_schedule), method: :post, title: _('Play'), icon: 'play'
- - if can?(current_user, :admin_pipeline_schedule, pipeline_schedule) && pipeline_schedule.owner != current_user
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-take-ownership-button has-tooltip', title: s_('PipelineSchedule|Take ownership to edit'), data: { url: take_ownership_pipeline_schedule_path(pipeline_schedule) } }) do
- = s_('PipelineSchedules|Take ownership')
- - if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
- = link_button_to nil, edit_pipeline_schedule_path(pipeline_schedule), title: _('Edit'), icon: 'pencil'
- - if can?(current_user, :admin_pipeline_schedule, pipeline_schedule)
- = link_button_to nil, pipeline_schedule_path(pipeline_schedule), title: _('Delete'), method: :delete, aria: { label: _('Delete pipeline schedule') }, data: { confirm: _("Are you sure you want to delete this pipeline schedule?"), confirm_btn_variant: 'danger' }, variant: :danger, icon: 'remove'
diff --git a/app/views/projects/pipeline_schedules/_table.html.haml b/app/views/projects/pipeline_schedules/_table.html.haml
deleted file mode 100644
index 2f96ac6a534..00000000000
--- a/app/views/projects/pipeline_schedules/_table.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-.table-holder
- %table.table.ci-table.responsive-table.b-table.gl-table.b-table-stacked-md{ role: 'table' }
- %thead{ role: 'rowgroup' }
- %tr{ role: 'row' }
- %th.table-th-transparent.border-bottom{ role: 'cell', style: 'width: 34%' }= _("Description")
- %th.table-th-transparent.border-bottom{ role: 'cell' }= s_("PipelineSchedules|Target")
- %th.table-th-transparent.border-bottom{ role: 'cell' }= _("Last Pipeline")
- %th.table-th-transparent.border-bottom{ role: 'cell' }= s_("PipelineSchedules|Next Run")
- %th.table-th-transparent.border-bottom{ role: 'cell' }= _("Owner")
- %th.table-th-transparent.border-bottom{ role: 'cell' }
- %tbody{ role: 'rowgroup' }
- = render partial: "pipeline_schedule", collection: @schedules
diff --git a/app/views/projects/pipeline_schedules/_tabs.html.haml b/app/views/projects/pipeline_schedules/_tabs.html.haml
deleted file mode 100644
index f825ef35902..00000000000
--- a/app/views/projects/pipeline_schedules/_tabs.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-= gl_tabs_nav({ class: 'gl-display-flex gl-flex-grow-1 gl-border-0' }) do
- = gl_tab_link_to schedule_path_proc.call(nil), { item_active: active_when(scope.nil?) } do
- = s_("PipelineSchedules|All")
- = gl_tab_counter_badge(number_with_delimiter(all_schedules.count(:id)), { class: 'js-totalbuilds-count' })
-
- = gl_tab_link_to schedule_path_proc.call('active'), { item_active: active_when(scope == 'active') } do
- = s_("PipelineSchedules|Active")
- = gl_tab_counter_badge(number_with_delimiter(all_schedules.active.count(:id)))
-
- = gl_tab_link_to schedule_path_proc.call('inactive'), { item_active: active_when(scope == 'inactive') } do
- = s_("PipelineSchedules|Inactive")
- = gl_tab_counter_badge(number_with_delimiter(all_schedules.inactive.count(:id)))
diff --git a/app/views/projects/pipeline_schedules/edit.html.haml b/app/views/projects/pipeline_schedules/edit.html.haml
index 4e1ae53a101..647c0272852 100644
--- a/app/views/projects/pipeline_schedules/edit.html.haml
+++ b/app/views/projects/pipeline_schedules/edit.html.haml
@@ -1,12 +1,8 @@
- add_to_breadcrumbs _("Schedules"), pipeline_schedules_path(@project)
- breadcrumb_title "##{@schedule.id}"
- page_title _("Edit"), @schedule.description, _("Pipeline Schedule")
-- add_page_specific_style 'page_bundles/pipeline_schedules'
%h1.page-title.gl-font-size-h-display
= _("Edit Pipeline Schedule")
-- if Feature.enabled?(:pipeline_schedules_vue, @project)
- #pipeline-schedules-form-edit{ data: js_pipeline_schedules_form_data(@project, @schedule) }
-- else
- = render "form"
+#pipeline-schedules-form-edit{ data: js_pipeline_schedules_form_data(@project, @schedule) }
diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml
index 5051fc6a5f5..15a80b6c7b1 100644
--- a/app/views/projects/pipeline_schedules/index.html.haml
+++ b/app/views/projects/pipeline_schedules/index.html.haml
@@ -1,28 +1,4 @@
- breadcrumb_title _("Schedules")
- page_title _("Pipeline Schedules")
-- add_page_specific_style 'page_bundles/pipeline_schedules'
-- add_page_specific_style 'page_bundles/ci_status'
-- add_page_specific_style 'page_bundles/merge_request'
-#pipeline-schedules-callout{ data: { docs_url: help_page_path('ci/pipelines/schedules'), illustration_url: image_path('illustrations/pipeline_schedule_callout.svg') } }
-
-- if Feature.enabled?(:pipeline_schedules_vue, @project)
- #pipeline-schedules-app{ data: { full_path: @project.full_path, pipelines_path: project_pipelines_path(@project), new_schedule_path: new_project_pipeline_schedule_path(@project) } }
-- else
- .top-area
- - schedule_path_proc = ->(scope) { pipeline_schedules_path(@project, scope: scope) }
- = render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope
-
- - if can?(current_user, :create_pipeline_schedule, @project)
- .nav-controls
- = link_button_to new_project_pipeline_schedule_path(@project), variant: :confirm do
- = _('New schedule')
-
- - if @schedules.present?
- %ul.content-list
- = render partial: "table"
- - else
- .nothing-here-block
- = _("No schedules")
-
- #pipeline-take-ownership-modal
+#pipeline-schedules-app{ data: { full_path: @project.full_path, pipelines_path: project_pipelines_path(@project), new_schedule_path: new_project_pipeline_schedule_path(@project) } }
diff --git a/app/views/projects/pipeline_schedules/new.html.haml b/app/views/projects/pipeline_schedules/new.html.haml
index ef99a79b06f..2cd65521ae9 100644
--- a/app/views/projects/pipeline_schedules/new.html.haml
+++ b/app/views/projects/pipeline_schedules/new.html.haml
@@ -1,14 +1,10 @@
- breadcrumb_title _('Schedules')
- @breadcrumb_link = namespace_project_pipeline_schedules_path(@project.namespace, @project)
- page_title _("New Pipeline Schedule")
-- add_page_specific_style 'page_bundles/pipeline_schedules'
- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
%h1.page-title.gl-font-size-h-display
= _("Schedule a new pipeline")
-- if Feature.enabled?(:pipeline_schedules_vue, @project)
- #pipeline-schedules-form-new{ data: js_pipeline_schedules_form_data(@project, @schedule) }
-- else
- = render "form"
+#pipeline-schedules-form-new{ data: js_pipeline_schedules_form_data(@project, @schedule) }
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 6b6aaaad802..3eb24873daf 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,8 +1,6 @@
- add_page_specific_style 'page_bundles/members'
- page_title _("Members")
-= render_if_exists 'shared/ultimate_feature_removal_banner', project: @project
-
.row.gl-mt-3
.col-lg-12
.gl-display-flex.gl-flex-wrap
@@ -26,7 +24,7 @@
- if can_admin_project_member?(@project)
.js-invite-members-trigger{ data: { variant: 'confirm',
classes: 'gl-md-w-auto gl-w-full gl-md-ml-3 gl-md-mt-0 gl-mt-3',
- trigger_source: 'project-members-page',
+ trigger_source: 'project_members_page',
display_text: _('Invite members') } }
- else
- if project_can_be_shared?
diff --git a/app/views/projects/protected_tags/_create_protected_tag.html.haml b/app/views/projects/protected_tags/_create_protected_tag.html.haml
index ef3974b04b5..1dc31503db9 100644
--- a/app/views/projects/protected_tags/_create_protected_tag.html.haml
+++ b/app/views/projects/protected_tags/_create_protected_tag.html.haml
@@ -1,9 +1,5 @@
- content_for :create_access_levels do
.create_access_levels-container
- = dropdown_tag('Select',
- options: { toggle_class: 'js-allowed-to-create js-multiselect wide',
- dropdown_class: 'dropdown-menu-selectable capitalize-header',
- dropdown_qa_selector: 'access_levels_content', dropdown_testid: 'allowed-to-create-dropdown',
- data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes', qa_selector: 'access_levels_dropdown' }})
+ .js-allowed-to-create
= render 'projects/protected_tags/shared/create_protected_tag'
diff --git a/app/views/projects/protected_tags/_protected_tag_create_access_levels.haml b/app/views/projects/protected_tags/_protected_tag_create_access_levels.haml
index 30b9e3e9005..389a88293a5 100644
--- a/app/views/projects/protected_tags/_protected_tag_create_access_levels.haml
+++ b/app/views/projects/protected_tags/_protected_tag_create_access_levels.haml
@@ -1,8 +1,5 @@
- protected_tag = local_assigns.fetch(:protected_tag)
- create_access_level = local_assigns.fetch(:create_access_level)
-- dropdown_label = create_access_level.first&.humanize || 'Select'
= hidden_field_tag "allowed_to_create_#{protected_tag.id}", create_access_level.first&.access_level
-= dropdown_tag(dropdown_label,
- options: { toggle_class: 'js-allowed-to-create js-multiselect', dropdown_class: 'dropdown-menu-selectable capitalize-header js-allowed-to-create-container',
- data: { field_name: "allowed_to_create_#{protected_tag.id}", preselected_items: access_levels_data(create_access_level) }})
+.js-allowed-to-create{ data: { preselected_items: access_levels_data(create_access_level).to_json } }
diff --git a/app/views/projects/settings/access_tokens/index.html.haml b/app/views/projects/settings/access_tokens/index.html.haml
index b81c3bc9704..ea3ad370fb5 100644
--- a/app/views/projects/settings/access_tokens/index.html.haml
+++ b/app/views/projects/settings/access_tokens/index.html.haml
@@ -18,9 +18,8 @@
= _('Project access token creation is disabled in this group.')
- root_group = @project.group.root_ancestor
- if current_user.can?(:admin_group, root_group)
- - group_settings_link = edit_group_path(root_group)
- - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_settings_link }
- = _('You can enable project access token creation in %{link_start}group settings%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+ - link = link_to('', edit_group_path(root_group), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(_('You can enable project access token creation in %{link_start}group settings%{link_end}.'), tag_pair(link, :link_start, :link_end))
= html_escape(_('You can still use and manage existing tokens. %{link_start}Learn more.%{link_end}')) % { link_start: help_link_start, link_end: '</a>'.html_safe }
#js-new-access-token-app{ data: { access_token_type: type } }
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 6de39058455..17953e3bc14 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -25,9 +25,9 @@
%p.gl-text-secondary
- auto_devops_url = help_page_path('topics/autodevops/index')
- quickstart_url = help_page_path('topics/autodevops/cloud_deployments/auto_devops_with_gke')
- - auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url }
- - quickstart_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: quickstart_url }
- = s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe, quickstart_start: quickstart_start, quickstart_end: '</a>'.html_safe }
+ - auto_devops_link = link_to('', auto_devops_url, target: '_blank', rel: 'noopener noreferrer')
+ - quickstart_link = link_to('', quickstart_url, target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}'), tag_pair(auto_devops_link, :auto_devops_start, :auto_devops_end), tag_pair(quickstart_link, :quickstart_start, :quickstart_end))
.settings-content
= render 'autodevops_form', auto_devops_enabled: @project.auto_devops_enabled?
@@ -44,10 +44,7 @@
= _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
= link_to s_('What is GitLab Runner?'), 'https://docs.gitlab.com/runner/', target: '_blank', rel: 'noopener noreferrer'
.settings-content
- - if Feature.enabled?(:project_runners_vue_ui, @project)
- #js-project-runners{ data: { project_full_path: @project.full_path } }
- - else
- = render 'projects/runners/settings'
+ = render 'projects/runners/settings'
- if Gitlab::CurrentSettings.current_application_settings.keep_latest_artifact?
%section.settings.no-animate#js-artifacts-settings{ class: ('expanded' if expanded) }
diff --git a/app/views/projects/settings/integrations/_form.html.haml b/app/views/projects/settings/integrations/_form.html.haml
index 97c7729de44..d2df01c22bb 100644
--- a/app/views/projects/settings/integrations/_form.html.haml
+++ b/app/views/projects/settings/integrations/_form.html.haml
@@ -15,9 +15,12 @@
= render 'shared/integrations/slack_notifications_deprecation_alert'
%h2.gl-mb-0.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(s_('FeatureFlags|Active'), variant: 'success')
+ = 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/integrations/index.html.haml b/app/views/projects/settings/integrations/index.html.haml
index 6c0c99543cc..dca028d6167 100644
--- a/app/views/projects/settings/integrations/index.html.haml
+++ b/app/views/projects/settings/integrations/index.html.haml
@@ -6,7 +6,7 @@
%section.js-search-settings-section
%h3= _('Integrations')
- - integrations_link_start = '<a href="%{url}">'.html_safe % { url: help_page_url('user/project/integrations/index') }
- - webhooks_link_start = '<a href="%{url}">'.html_safe % { url: project_hooks_path(@project) }
- %p.gl-text-secondary= _("%{integrations_link_start}Integrations%{link_end} enable you to make third-party applications part of your GitLab workflow. If the available integrations don't meet your needs, consider using a %{webhooks_link_start}webhook%{link_end}.").html_safe % { integrations_link_start: integrations_link_start, webhooks_link_start: webhooks_link_start, link_end: '</a>'.html_safe }
+ - integrations_link = link_to('', help_page_url('user/project/integrations/index'))
+ - webhooks_link = link_to('', project_hooks_path(@project))
+ %p.gl-text-secondary= safe_format(_("%{integrations_link_start}Integrations%{link_end} enable you to make third-party applications part of your GitLab workflow. If the available integrations don't meet your needs, consider using a %{webhooks_link_start}webhook%{link_end}."), tag_pair(integrations_link, :integrations_link_start, :link_end), tag_pair(webhooks_link, :webhooks_link_start, :link_end))
= render 'shared/integrations/index', integrations: @integrations
diff --git a/app/views/projects/settings/merge_requests/_merge_request_merge_checks_settings.html.haml b/app/views/projects/settings/merge_requests/_merge_request_merge_checks_settings.html.haml
new file mode 100644
index 00000000000..fa9b39e0846
--- /dev/null
+++ b/app/views/projects/settings/merge_requests/_merge_request_merge_checks_settings.html.haml
@@ -0,0 +1,8 @@
+- form = local_assigns.fetch(:form)
+
+.form-group
+ %b= s_('ProjectSettings|Merge checks')
+ %p.text-secondary= s_('ProjectSettings|These checks must pass before merge requests can be merged.')
+ = render 'projects/settings/merge_requests/merge_request_pipelines_and_threads_options', form: form, project: @project
+ = render_if_exists 'projects/settings/merge_requests/merge_request_merge_checks_status_checks', form: form, project: @project
+ = render_if_exists 'projects/settings/merge_requests/merge_request_merge_checks_jira_enforcement', form: form, project: @project
diff --git a/app/views/projects/_merge_request_merge_commit_template.html.haml b/app/views/projects/settings/merge_requests/_merge_request_merge_commit_template.html.haml
index 502014b7279..da1965f549c 100644
--- a/app/views/projects/_merge_request_merge_commit_template.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_merge_commit_template.html.haml
@@ -9,6 +9,5 @@
%p.form-text.text-muted
= s_('ProjectSettings|Leave empty to use default template.')
= sprintf(s_('ProjectSettings|Maximum %{maxLength} characters.'), { maxLength: Project::MAX_COMMIT_TEMPLATE_LENGTH })
- - configure_the_merge_commit_message_help_link_url = help_page_path('user/project/merge_requests/commit_templates.md')
- - configure_the_merge_commit_message_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: configure_the_merge_commit_message_help_link_url }
- = s_('ProjectSettings|%{link_start}What variables can I use?%{link_end}').html_safe % { link_start: configure_the_merge_commit_message_help_link_start, link_end: '</a>'.html_safe }
+ - link = link_to('', help_page_path('user/project/merge_requests/commit_templates.md'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(s_('ProjectSettings|%{link_start}What variables can I use?%{link_end}'), tag_pair(link, :link_start, :link_end))
diff --git a/app/views/projects/_merge_request_merge_method_settings.html.haml b/app/views/projects/settings/merge_requests/_merge_request_merge_method_settings.html.haml
index dd32d3f9d92..dd32d3f9d92 100644
--- a/app/views/projects/_merge_request_merge_method_settings.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_merge_method_settings.html.haml
diff --git a/app/views/projects/_merge_request_merge_options_settings.html.haml b/app/views/projects/settings/merge_requests/_merge_request_merge_options_settings.html.haml
index e91c001ea3d..db7e59d6e2a 100644
--- a/app/views/projects/_merge_request_merge_options_settings.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_merge_options_settings.html.haml
@@ -3,8 +3,8 @@
.form-group#project-merge-options{ data: { project_full_path: @project.full_path } }
%b= s_('ProjectSettings|Merge options')
%p.text-secondary= s_('ProjectSettings|Additional settings that influence how and when merges are done.')
- = render_if_exists 'projects/merge_pipelines_settings', form: form
- = render_if_exists 'projects/merge_trains_settings', form: form
+ = render_if_exists 'projects/settings/merge_requests/merge_pipelines_settings', form: form
+ = render_if_exists 'projects/settings/merge_requests/merge_trains_settings', form: form
= form.gitlab_ui_checkbox_component :resolve_outdated_diff_discussions,
s_('ProjectSettings|Automatically resolve merge request diff threads when they become outdated')
= form.gitlab_ui_checkbox_component :printing_merge_request_link_enabled,
diff --git a/app/views/projects/settings/merge_requests/_merge_request_merge_suggestions_settings.html.haml b/app/views/projects/settings/merge_requests/_merge_request_merge_suggestions_settings.html.haml
new file mode 100644
index 00000000000..501288f727b
--- /dev/null
+++ b/app/views/projects/settings/merge_requests/_merge_request_merge_suggestions_settings.html.haml
@@ -0,0 +1,13 @@
+- form = local_assigns.fetch(:form)
+
+.form-group
+ %b= s_('ProjectSettings|Merge suggestions')
+ %p.text-secondary
+ = s_('ProjectSettings|The commit message used when applying merge request suggestions.')
+ .mb-2
+ = form.text_field :suggestion_commit_message, class: 'form-control mb-2', placeholder: Gitlab::Suggestions::CommitMessage::DEFAULT_SUGGESTION_COMMIT_MESSAGE
+ %p.form-text.text-muted
+ = s_('ProjectSettings|Leave empty to use default template.')
+ = sprintf(s_('ProjectSettings|Maximum %{maxLength} characters.'), { maxLength: Project::MAX_SUGGESTIONS_TEMPLATE_LENGTH })
+ - link = link_to('', help_page_path('user/project/merge_requests/reviews/suggestions.md', anchor: 'configure-the-commit-message-for-applied-suggestions'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(s_('ProjectSettings|%{link_start}What variables can I use?%{link_end}'), tag_pair(link, :link_start, :link_end))
diff --git a/app/views/projects/_merge_request_pipelines_and_threads_options.html.haml b/app/views/projects/settings/merge_requests/_merge_request_pipelines_and_threads_options.html.haml
index a9609434f15..a9609434f15 100644
--- a/app/views/projects/_merge_request_pipelines_and_threads_options.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_pipelines_and_threads_options.html.haml
diff --git a/app/views/projects/settings/merge_requests/_merge_request_settings.html.haml b/app/views/projects/settings/merge_requests/_merge_request_settings.html.haml
new file mode 100644
index 00000000000..9449fc7f4ae
--- /dev/null
+++ b/app/views/projects/settings/merge_requests/_merge_request_settings.html.haml
@@ -0,0 +1,18 @@
+- form = local_assigns.fetch(:form)
+
+= render 'projects/settings/merge_requests/merge_request_merge_method_settings', project: @project, form: form
+
+= render 'projects/settings/merge_requests/merge_request_merge_options_settings', project: @project, form: form
+
+= render 'projects/settings/merge_requests/merge_request_squash_options_settings', form: form
+
+= render 'projects/settings/merge_requests/merge_request_merge_checks_settings', project: @project, form: form
+
+= render 'projects/settings/merge_requests/merge_request_merge_suggestions_settings', project: @project, form: form
+
+= render 'projects/settings/merge_requests/merge_request_merge_commit_template', project: @project, form: form
+
+= render 'projects/settings/merge_requests/merge_request_squash_commit_template', project: @project, form: form
+
+- if @project.forked?
+ = render 'projects/settings/merge_requests/merge_request_target_project_settings', project: @project, form: form
diff --git a/app/views/projects/_merge_request_settings_description_text.html.haml b/app/views/projects/settings/merge_requests/_merge_request_settings_description_text.html.haml
index 123520acad8..123520acad8 100644
--- a/app/views/projects/_merge_request_settings_description_text.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_settings_description_text.html.haml
diff --git a/app/views/projects/_merge_request_squash_commit_template.html.haml b/app/views/projects/settings/merge_requests/_merge_request_squash_commit_template.html.haml
index 4d1b89bea83..bc6530b927c 100644
--- a/app/views/projects/_merge_request_squash_commit_template.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_squash_commit_template.html.haml
@@ -9,6 +9,5 @@
%p.form-text.text-muted
= s_('ProjectSettings|Leave empty to use default template.')
= sprintf(s_('ProjectSettings|Maximum %{maxLength} characters.'), { maxLength: Project::MAX_COMMIT_TEMPLATE_LENGTH })
- - configure_the_squash_commit_message_help_link_url = help_page_path('user/project/merge_requests/commit_templates.md')
- - configure_the_squash_commit_message_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: configure_the_squash_commit_message_help_link_url }
- = s_('ProjectSettings|%{link_start}What variables can I use?%{link_end}').html_safe % { link_start: configure_the_squash_commit_message_help_link_start, link_end: '</a>'.html_safe }
+ - link = link_to('', help_page_path('user/project/merge_requests/commit_templates.md'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(s_('ProjectSettings|%{link_start}What variables can I use?%{link_end}'), tag_pair(link, :link_start, :link_end))
diff --git a/app/views/projects/_merge_request_squash_options_settings.html.haml b/app/views/projects/settings/merge_requests/_merge_request_squash_options_settings.html.haml
index 372c0723600..372c0723600 100644
--- a/app/views/projects/_merge_request_squash_options_settings.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_squash_options_settings.html.haml
diff --git a/app/views/projects/_merge_request_target_project_settings.html.haml b/app/views/projects/settings/merge_requests/_merge_request_target_project_settings.html.haml
index 6f2917f24e0..6f2917f24e0 100644
--- a/app/views/projects/_merge_request_target_project_settings.html.haml
+++ b/app/views/projects/settings/merge_requests/_merge_request_target_project_settings.html.haml
diff --git a/app/views/projects/settings/merge_requests/show.html.haml b/app/views/projects/settings/merge_requests/show.html.haml
index ef528d17fc9..01af028f30c 100644
--- a/app/views/projects/settings/merge_requests/show.html.haml
+++ b/app/views/projects/settings/merge_requests/show.html.haml
@@ -5,15 +5,16 @@
%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' } }
.settings-header
%h4= _('Merge requests')
- = render_if_exists 'projects/merge_request_settings_description_text'
+ = render_if_exists 'projects/settings/merge_requests/merge_request_settings_description_text'
.settings-content
= render_if_exists 'shared/promotions/promote_mr_features'
= gitlab_ui_form_for @project, url: project_settings_merge_requests_path(@project), html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' }
- = render 'projects/merge_request_settings', form: f
+ = render 'projects/settings/merge_requests/merge_request_settings', form: f
= f.submit _('Save changes'), class: "rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' }, pajamas_button: true
= render_if_exists 'projects/settings/merge_requests/merge_request_approvals_settings', expanded: true
= render_if_exists 'projects/settings/merge_requests/suggested_reviewers_settings', expanded: true
+= render_if_exists 'projects/settings/merge_requests/target_branch_rules_settings'
diff --git a/app/views/projects/settings/operations/_alert_management.html.haml b/app/views/projects/settings/operations/_alert_management.html.haml
index 398c7758d66..c29cedd8250 100644
--- a/app/views/projects/settings/operations/_alert_management.html.haml
+++ b/app/views/projects/settings/operations/_alert_management.html.haml
@@ -3,7 +3,7 @@
- add_page_specific_style 'page_bundles/alert_management_settings'
- add_page_specific_style 'page_bundles/incident_management_list'
-%section.settings.no-animate#js-alert-management-settings{ class: ('expanded' if expanded), data: { qa_selector: 'alerts_settings_content' } }
+%section.settings.no-animate#js-alert-management-settings{ class: ('expanded' if expanded), data: { testid: 'alerts-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Alerts')
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 8a35db357ee..c76fa5e2220 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -6,7 +6,6 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity")
-= render_if_exists 'shared/ultimate_feature_removal_banner', project: @project
= render_if_exists 'shared/promotions/promote_mobile_devops', project: @project
= render partial: 'flash_messages', locals: { project: @project }
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 53c3d16ee64..281eac6c773 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -11,9 +11,8 @@
= s_('TagsPage|New Tag')
%p.gl-text-secondary
- - link_start = '<a href="%{url}">'.html_safe % { url: new_namespace_project_release_path }
- - link_end = '</a>'.html_safe
- = s_('TagsPage|Do you want to create a release with the new tag? You can do that in the %{link_start}New release page%{link_end}.').html_safe % { link_start: link_start, link_end: link_end }
+ - link = link_to('', new_namespace_project_release_path)
+ = safe_format(s_('TagsPage|Do you want to create a release with the new tag? You can do that in the %{link_start}New release page%{link_end}.'), tag_pair(link, :link_start, :link_end))
= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "common-note-form tag-form js-quick-submit js-requires-input" do
.form-group.row
diff --git a/app/views/projects/tracing/index.html.haml b/app/views/projects/tracing/index.html.haml
deleted file mode 100644
index ae6608cf343..00000000000
--- a/app/views/projects/tracing/index.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-- page_title _('Tracing')
-
-#js-tracing{ data: { view_model: observability_tracing_view_model(@project) } }
-
diff --git a/app/views/projects/tracing/show.html.haml b/app/views/projects/tracing/show.html.haml
deleted file mode 100644
index 4ba316a0b5c..00000000000
--- a/app/views/projects/tracing/show.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-- page_title _('Trace Details')
-- add_to_breadcrumbs _('Tracing'), project_tracing_index_path(@project)
-
-#js-tracing-details{ data: { view_model: observability_tracing_details_model(@project, @trace_id) } }
-
diff --git a/app/views/projects/usage_quotas/index.html.haml b/app/views/projects/usage_quotas/index.html.haml
index 616af3f3338..6f2a2aacf66 100644
--- a/app/views/projects/usage_quotas/index.html.haml
+++ b/app/views/projects/usage_quotas/index.html.haml
@@ -1,9 +1,6 @@
- page_title s_("UsageQuota|Usage")
-- add_page_specific_style 'page_bundles/projects_usage_quotas'
- @force_desktop_expanded_sidebar = true
-= render_if_exists 'shared/ultimate_feature_removal_banner', project: @project
-
= render Pajamas::AlertComponent.new(title: _('Repository usage recalculation started'),
variant: :info,
alert_options: { class: 'js-recalculation-started-alert gl-mt-4 gl-mb-5 gl-display-none' }) do |c|
diff --git a/app/views/protected_branches/_create_protected_branch.html.haml b/app/views/protected_branches/_create_protected_branch.html.haml
index 799f6aa6031..bc5da1653bf 100644
--- a/app/views/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/protected_branches/_create_protected_branch.html.haml
@@ -1,14 +1,9 @@
- content_for :merge_access_levels do
.merge_access_levels-container
- = dropdown_tag(_('Select'),
- options: { toggle_class: 'js-allowed-to-merge wide',
- dropdown_class: 'dropdown-menu-selectable capitalize-header', dropdown_qa_selector: 'allowed_to_merge_dropdown_content', dropdown_testid: 'allowed-to-merge-dropdown',
- data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes', qa_selector: 'select_allowed_to_merge_dropdown' }})
+ .js-allowed-to-merge
+
- content_for :push_access_levels do
.push_access_levels-container
- = dropdown_tag(_('Select'),
- options: { toggle_class: "js-allowed-to-push js-multiselect wide",
- dropdown_class: 'dropdown-menu-selectable capitalize-header', dropdown_qa_selector: 'allowed_to_push_dropdown_content' , dropdown_testid: 'allowed-to-push-dropdown',
- data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes', qa_selector: 'select_allowed_to_push_dropdown' }})
+ .js-allowed-to-push
= render 'protected_branches/shared/create_protected_branch', protected_branch_entity: protected_branch_entity
diff --git a/app/views/protected_branches/shared/_index.html.haml b/app/views/protected_branches/shared/_index.html.haml
index dccfefc1cb8..8e72563182c 100644
--- a/app/views/protected_branches/shared/_index.html.haml
+++ b/app/views/protected_branches/shared/_index.html.haml
@@ -13,6 +13,13 @@
.settings-content
.js-alert-protected-branch-created-container.gl-mt-5
+ = render Pajamas::AlertComponent.new(variant: :warning,
+ alert_options: { class: 'gl-mb-5' },
+ dismissible: false) do |c|
+ - c.with_body do
+ = s_("ProtectedBranch|Giving merge rights to a protected branch also gives elevated permissions for certain CI/CD features.")
+ = link_to s_("ProtectedBranch|What are the security implications?"), help_page_path('ci/pipelines/index', anchor: 'pipeline-security-on-protected-branches'), target: '_blank', rel: 'noopener noreferrer'
+
= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card js-toggle-container' }, header_options: { class: 'gl-new-card-header gl-flex-direction-column' }, body_options: { class: 'gl-new-card-body gl-px-0' }) do |c|
- c.with_header do
.gl-new-card-title-wrapper.gl-justify-content-space-between
diff --git a/app/views/protected_branches/shared/_update_protected_branch.html.haml b/app/views/protected_branches/shared/_update_protected_branch.html.haml
index ad61f557bb8..e4c8b779447 100644
--- a/app/views/protected_branches/shared/_update_protected_branch.html.haml
+++ b/app/views/protected_branches/shared/_update_protected_branch.html.haml
@@ -3,17 +3,13 @@
%td.merge_access_levels-container
= hidden_field_tag "allowed_to_merge_#{protected_branch.id}", merge_access_levels.first&.access_level
- = dropdown_tag((merge_access_levels.first&.humanize || 'Select') ,
- options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
- data: { field_name: "allowed_to_merge_#{protected_branch.id}", preselected_items: access_levels_data(merge_access_levels) }})
+ .js-allowed-to-merge{ data: { preselected_items: access_levels_data(merge_access_levels).to_json } }
= render_if_exists 'protected_branches/shared/user_merge_access_levels', protected_branch: protected_branch
= render_if_exists 'protected_branches/shared/group_merge_access_levels', protected_branch: protected_branch
%td.push_access_levels-container
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", push_access_levels.first&.access_level
- = dropdown_tag((push_access_levels.first&.humanize || 'Select') ,
- options: { toggle_class: "js-allowed-to-push js-multiselect", dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
- data: { field_name: "allowed_to_push_#{protected_branch.id}", preselected_items: access_levels_data(push_access_levels) }})
+ .js-allowed-to-push{ data: { preselected_items: access_levels_data(push_access_levels).to_json } }
= render_if_exists 'protected_branches/shared/user_push_access_levels', protected_branch: protected_branch
= render_if_exists 'protected_branches/shared/group_push_access_levels', protected_branch: protected_branch
diff --git a/app/views/registrations/welcome/show.html.haml b/app/views/registrations/welcome/show.html.haml
index caaa209a702..0f161855cdb 100644
--- a/app/views/registrations/welcome/show.html.haml
+++ b/app/views/registrations/welcome/show.html.haml
@@ -33,7 +33,7 @@
= 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 'devise/shared/email_opted_in', f: f
+ = render_if_exists "registrations/welcome/opt_in_to_email"
.row
.form-group.col-sm-12.gl-mb-0
- if partial_exists? "registrations/welcome/button"
diff --git a/app/views/repository_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml
index dfcd1c6b19f..d6c57b98ea9 100644
--- a/app/views/repository_check_mailer/notify.html.haml
+++ b/app/views/repository_check_mailer/notify.html.haml
@@ -7,4 +7,4 @@
%p
= _("You are receiving this message because you are a GitLab administrator for %{url}.") % { url: Gitlab.config.gitlab.url }
-= render_if_exists 'repository_check_mailer/email_additional_text'
+= render_if_exists 'shared/additional_email_text'
diff --git a/app/views/repository_check_mailer/notify.text.haml b/app/views/repository_check_mailer/notify.text.haml
index a2e04fa710f..dc316d3e2be 100644
--- a/app/views/repository_check_mailer/notify.text.haml
+++ b/app/views/repository_check_mailer/notify.text.haml
@@ -4,4 +4,4 @@
= _("You are receiving this message because you are a GitLab administrator for %{url}.") % { url: Gitlab.config.gitlab.url }
-= render_if_exists 'repository_check_mailer/email_additional_text'
+= render_if_exists 'shared/additional_email_text'
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 7399f51d7f8..a1839b3dd39 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -1,3 +1,3 @@
.gl-w-full.gl-flex-grow-1.gl-overflow-x-hidden
- = render partial: 'search/results_status' unless @search_objects.to_a.empty?
+ = render partial: 'search/results_status'
= render partial: 'search/results_list'
diff --git a/app/views/search/_results_list.html.haml b/app/views/search/_results_list.html.haml
index ff79f003e7d..fb96672cf99 100644
--- a/app/views/search/_results_list.html.haml
+++ b/app/views/search/_results_list.html.haml
@@ -8,7 +8,7 @@
- elsif @search_objects.blank?
= render partial: "search/results/empty"
- else
- - statusBarClass = !show_super_sidebar? ? 'gl-md-pl-5' : ''
+ - statusBarClass = !show_super_sidebar? ? 'gl-lg-pl-5' : ''
.section{ class: statusBarClass }
- if @scope == 'commits'
diff --git a/app/views/search/_results_status.html.haml b/app/views/search/_results_status.html.haml
index 6fc07d35296..8417b66eb34 100644
--- a/app/views/search/_results_status.html.haml
+++ b/app/views/search/_results_status.html.haml
@@ -1,28 +1,33 @@
-- return unless @search_service_presenter.show_results_status?
-- statusBarClass = !show_super_sidebar? ? 'gl-md-pl-5' : ''
+- statusBarClass = !show_super_sidebar? ? 'gl-lg-pl-5' : ''
+- statusBarClass = statusBarClass + ' gl-lg-display-none' if @search_objects.to_a.empty?
.section{ class: statusBarClass }
.search-results-status
.gl-display-flex.gl-flex-direction-column
- .gl-p-5.gl-display-flex
- .gl-md-display-flex.gl-text-left.gl-align-items-center.gl-flex-grow-1.gl-white-space-nowrap.gl-max-w-full
- - unless @search_service_presenter.without_count?
- .gl-text-truncate
- = search_entries_info(@search_objects, @scope, @search_term)
- - unless @search_service_presenter.show_snippets?
- - if @project
- - link_to_project = link_to(@project.full_name, @project, class: 'ml-md-1 gl-text-truncate search-wrap-f-md-down')
- - if @scope == 'blobs'
- = _("in")
- .mx-md-1
- #js-blob-ref-switcher{ data: { "project-id" => @project.id, "ref" => repository_ref(@project), "field-name": "repository_ref" } }
- = s_('SearchCodeResults|of %{link_to_project}').html_safe % { link_to_project: link_to_project }
- - else
- = _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project }
- - elsif @group
- - link_to_group = link_to(@group.name, @group, class: 'ml-md-1')
- = _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
- - if @search_service_presenter.show_sort_dropdown?
- .gl-md-display-flex.gl-flex-direction-column
- #js-search-sort{ data: { "search-sort-options" => search_sort_options.to_json } }
- %hr.gl-mb-5.gl-mt-0.gl-border-gray-100.gl-w-full
+ .gl-p-5.gl-display-flex.gl-flex-wrap
+ - unless @search_objects.to_a.empty?
+ .gl-display-flex.gl-text-left.gl-flex-grow-1.gl-flex-shrink-1.gl-white-space-nowrap.gl-flex-wrap.gl-sm-w-half
+ %p.gl-text-truncate.gl-my-auto
+ - unless @search_service_presenter.without_count?
+ = search_entries_info(@search_objects, @scope, @search_term)
+ - unless @search_service_presenter.show_snippets?
+ - if @project
+ - link_to_project = link_to(@project.full_name, @project, class: 'search-wrap-f-md-down')
+ - if @scope == 'blobs'
+ = _("in")
+ .mx-md-1.gl-my-auto
+ #js-blob-ref-switcher{ data: { "project-id" => @project.id, "ref" => repository_ref(@project), "field-name": "repository_ref" } }
+ %p.gl-text-truncate.gl-my-auto
+ = s_('SearchCodeResults|of %{link_to_project}').html_safe % { link_to_project: link_to_project }
+ - else
+ = _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project }
+ - elsif @group
+ - link_to_group = link_to(@group.name, @group, class: 'ml-md-1')
+ = _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
+ .gl-display-flex.gl-my-3.gl-flex-grow-1.gl-flex-shrink-1.gl-justify-content-end
+ = render Pajamas::ButtonComponent.new(category: 'primary', icon: 'filter', button_options: {id: 'js-open-mobile-filters', class: 'gl-lg-display-none'}) do
+ = s_('GlobalSearch|Filters')
+ - if @search_service_presenter.show_sort_dropdown? && !@search_objects.to_a.empty?
+ .gl-ml-3
+ #js-search-sort{ data: { "search-sort-options" => search_sort_options.to_json } }
+ %hr.gl-mb-5.gl-mt-0.gl-border-gray-100.gl-w-full
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index 16ca829a6d4..2fd6e4a5ca5 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -22,7 +22,7 @@
= 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 } }
-.results.gl-md-display-flex.gl-mt-0
- #js-search-sidebar{ class: search_bar_classes, data: { navigation_json: search_navigation_json } }
+.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
= render 'search/results'
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 48ae1f7eb1d..dde4ec3cf52 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -18,5 +18,5 @@
= 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' }
.input-group-append
- = clipboard_button(target: '#clone_url', title: _("Copy URL"), class: "input-group-text gl-button btn-default btn-clipboard")
+ = clipboard_button(target: '#clone_url', title: _("Copy URL"), variant: :default, category: :primary, size: :medium)
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index 5058455dcd7..05b376003bc 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -15,6 +15,9 @@
.gl-my-1
= markdown_field(label, :description)
%ul.label-links.gl-m-0.gl-p-0.gl-white-space-nowrap
+ - if label.lock_on_merge
+ %li.inline.gl-mr-3.gl-mt-1
+ .label-badge.gl-bg-orange-50= _('Lock on merge')
- if force_priority
%li.js-priority-badge.inline.gl-mr-3.gl-mt-1
.label-badge.gl-bg-blue-50= _('Prioritized')
diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg
index 83f6fe5c16c..dfc35856366 100644
--- a/app/views/shared/_logo.svg
+++ b/app/views/shared/_logo.svg
@@ -1,4 +1,4 @@
-<svg class="tanuki-logo" width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<svg 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/_mobile_clone_panel.html.haml b/app/views/shared/_mobile_clone_panel.html.haml
index aa3043b8fd6..c594cee326e 100644
--- a/app/views/shared/_mobile_clone_panel.html.haml
+++ b/app/views/shared/_mobile_clone_panel.html.haml
@@ -3,7 +3,7 @@
- http_copy_label = _('Copy %{http_label} clone URL') % { http_label: gitlab_config.protocol.upcase }
.btn-group.mobile-git-clone.js-mobile-git-clone.btn-block
- = clipboard_button(button_text: default_clone_label, text: default_url_to_repo(project), hide_button_icon: true, class: "gl-button btn-confirm flex-fill bold justify-content-center input-group-text clone-dropdown-btn js-clone-dropdown-label")
+ = clipboard_button(button_text: default_clone_label, size: :medium, category: :primary, variant: :confirm, text: default_url_to_repo(project), hide_button_icon: true, class: "clone-dropdown-btn js-clone-dropdown-label")
%button.btn.gl-button.btn-confirm.dropdown-toggle.js-dropdown-toggle.flex-grow-0.d-flex-center.w-auto.ml-0{ type: "button", data: { toggle: "dropdown" } }
= sprite_icon("chevron-down", css_class: "dropdown-btn-icon icon")
%ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown{ data: { dropdown: true } }
diff --git a/app/views/shared/_outdated_browser.html.haml b/app/views/shared/_outdated_browser.html.haml
index 79d0231536b..e62e3bb4a6c 100644
--- a/app/views/shared/_outdated_browser.html.haml
+++ b/app/views/shared/_outdated_browser.html.haml
@@ -3,5 +3,5 @@
- c.with_body do
= s_('OutdatedBrowser|GitLab may not work properly, because you are using an outdated web browser.')
%br
- - browser_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('install/requirements', anchor: 'supported-web-browsers') }
- = s_('OutdatedBrowser|Please install a %{browser_link_start}supported web browser%{browser_link_end} for a better experience.').html_safe % { browser_link_start: browser_link_start, browser_link_end: '</a>'.html_safe }
+ - link = link_to('', help_page_path('install/requirements', anchor: 'supported-web-browsers'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(s_('OutdatedBrowser|Please install a %{browser_link_start}supported web browser%{browser_link_end} for a better experience.'), tag_pair(link, :browser_link_start, :browser_link_end))
diff --git a/app/views/shared/_silent_mode_banner.html.haml b/app/views/shared/_silent_mode_banner.html.haml
new file mode 100644
index 00000000000..10e5d05fad2
--- /dev/null
+++ b/app/views/shared/_silent_mode_banner.html.haml
@@ -0,0 +1,9 @@
+- return unless ::Gitlab::SilentMode.enabled?
+
+= content_for :page_level_alert do
+ %div{ class: [container_class, @content_class, 'gl-pt-5!'] }
+ = render Pajamas::AlertComponent.new(title: s_('SilentMode|Silent mode is enabled'),
+ dismissible: false,
+ variant: :warning) do |c|
+ - c.with_body do
+ = s_('SilentMode|All outbound communications are blocked. %{link_start}Learn more%{link_end}.').html_safe % { link_start: "<a href='#{help_page_path('administration/silent_mode/index')}' target='_blank' rel='noopener noreferrer'>".html_safe, link_end: '</a>'.html_safe }
diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml
index 763ae5a498b..3cf13222f4e 100644
--- a/app/views/shared/_visibility_level.html.haml
+++ b/app/views/shared/_visibility_level.html.haml
@@ -5,9 +5,8 @@
= f.label :visibility_level, _('Visibility level'), class: 'label-bold gl-mb-0'
%p
= _('Who can see this group?')
- - visibility_docs_path = help_page_path('user/public_access')
- - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: visibility_docs_path }
- = _('%{docs_link_start}Learn about visibility levels.%{docs_link_end}').html_safe % { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe }
+ - link = link_to('', help_page_path('user/public_access'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(_('%{docs_link_start}Learn about visibility levels.%{docs_link_end}'), tag_pair(link, :docs_link_start, :docs_link_end))
- if can_change_visibility_level
= render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model)
- else
diff --git a/app/views/shared/builds/_build_output.html.haml b/app/views/shared/builds/_build_output.html.haml
deleted file mode 100644
index a3b7d4926f8..00000000000
--- a/app/views/shared/builds/_build_output.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-%pre.build-log
- %code.bash.js-build-output
- .build-loader-animation.js-build-refresh
- .dot
- .dot
- .dot
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
deleted file mode 100644
index 8f2b9fc06e3..00000000000
--- a/app/views/shared/builds/_tabs.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-- count_badge_classes = 'gl-display-none gl-sm-display-inline-flex'
-
-= gl_tabs_nav({class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full nav gl-border-b-0', data: { testid: 'jobs-tabs' } }) do
- = gl_tab_link_to build_path_proc.call(nil), { item_active: scope.nil? } do
- = _('All')
- = gl_tab_counter_badge(limited_counter_with_delimiter(all_builds), { class: count_badge_classes })
- = gl_tab_link_to build_path_proc.call('pending'), { item_active: scope == 'pending' } do
- = _('Pending')
- = gl_tab_counter_badge(limited_counter_with_delimiter(all_builds.pending), { class: count_badge_classes })
- = gl_tab_link_to build_path_proc.call('running'), { item_active: scope == 'running' } do
- = _('Running')
- = gl_tab_counter_badge(limited_counter_with_delimiter(all_builds.running), { class: count_badge_classes })
- = gl_tab_link_to build_path_proc.call('finished'), { item_active: scope == 'finished' } do
- = _('Finished')
- = gl_tab_counter_badge(limited_counter_with_delimiter(all_builds.finished), { class: count_badge_classes })
diff --git a/app/views/shared/deploy_keys/_index.html.haml b/app/views/shared/deploy_keys/_index.html.haml
index 650e50e0312..5188c530672 100644
--- a/app/views/shared/deploy_keys/_index.html.haml
+++ b/app/views/shared/deploy_keys/_index.html.haml
@@ -5,8 +5,8 @@
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
- - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/deploy_keys/index') }
- = _("Add deploy keys to grant read/write access to this repository. %{link_start}What are deploy keys?%{link_end}").html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+ - link = link_to('', help_page_path('user/project/deploy_keys/index'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(_("Add deploy keys to grant read/write access to this repository. %{link_start}What are deploy keys?%{link_end}"), tag_pair(link, :link_start, :link_end))
.settings-content
= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card js-toggle-container' }, body_options: { class: 'gl-new-card-body gl-px-0' }) do |c|
- c.with_body do
diff --git a/app/views/shared/deploy_keys/_project_group_form.html.haml b/app/views/shared/deploy_keys/_project_group_form.html.haml
index c633088b26a..bb4295779cd 100644
--- a/app/views/shared/deploy_keys/_project_group_form.html.haml
+++ b/app/views/shared/deploy_keys/_project_group_form.html.haml
@@ -1,30 +1,30 @@
-= gitlab_ui_form_for [@project.namespace, @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input container" } do |f|
+= gitlab_ui_form_for [@project.namespace, @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input" } do |f|
= form_errors(@deploy_keys.new_key)
- .form-group.row
+ .form-group
%h4.gl-my-0= s_('DeployKeys|Add new deploy key')
- .form-group.row
+ .form-group
= f.label :title, class: "label-bold"
- = f.text_field :title, class: 'form-control gl-form-input', required: true, data: { testid: 'deploy-key-title-field' }
- .form-group.row
+ = f.text_field :title, class: 'form-control gl-form-input gl-form-input-xl', required: true, data: { testid: 'deploy-key-title-field' }
+ .form-group
= f.label :key, class: "label-bold"
- = f.text_area :key, class: 'form-control gl-form-input', rows: 5, required: true, data: { testid: 'deploy-key-field' }
- .form-group.row
- %p.light.gl-mb-0
+ = f.text_area :key, class: 'form-control gl-form-input gl-form-input-xl gl-h-auto!', rows: 5, required: true, data: { testid: 'deploy-key-field' }
+ .form-text.text-muted
= _('Paste a public key here.')
= link_to _('How do I generate it?'), help_page_path("user/ssh")
= f.fields_for :deploy_keys_projects do |deploy_keys_project_form|
- .form-group.row
+ .form-group
= deploy_keys_project_form.gitlab_ui_checkbox_component :can_push, _('Grant write permissions to this key'),
help_text: _('Allow this key to push to this repository')
- .form-group.row
+ .form-group
= f.label :expires_at, _('Expiration date (optional)'), class: 'label-bold'
- = f.gitlab_ui_datepicker :expires_at, data: { testid: 'deploy-key-expires-at-field' }, value: f.object.expires_at
- %p.form-text.text-muted= ssh_key_expires_field_description
+ .gl-form-input-xl
+ = f.gitlab_ui_datepicker :expires_at, data: { testid: 'deploy-key-expires-at-field' }, value: f.object.expires_at
+ .form-text.text-muted= ssh_key_expires_field_description
- .form-group.row.gl-mb-0
+ .form-group.gl-mb-0
= f.submit _("Add key"), data: { testid: "add-deploy-key-button"}, pajamas_button: true
= render Pajamas::ButtonComponent.new(button_options: { type: 'reset', class: 'gl-ml-3 js-toggle-button' }) do
= _('Cancel')
diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml
index 8821804ce6b..bb7e0d774cc 100644
--- a/app/views/shared/deploy_tokens/_form.html.haml
+++ b/app/views/shared/deploy_tokens/_form.html.haml
@@ -1,7 +1,6 @@
%p
- - group_deploy_tokens_help_link_url = help_page_path('user/project/deploy_tokens/index.md')
- - group_deploy_tokens_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_deploy_tokens_help_link_url }
- = s_('DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}').html_safe % { link_start: group_deploy_tokens_help_link_start, link_end: '</a>'.html_safe }
+ - link = link_to('', help_page_path('user/project/deploy_tokens/index.md'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(s_('DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}'), tag_pair(link, :link_start, :link_end))
= gitlab_ui_form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post, remote: true do |f|
diff --git a/app/views/shared/deploy_tokens/_new_deploy_token.html.haml b/app/views/shared/deploy_tokens/_new_deploy_token.html.haml
index 9c82d5685f8..25c277ea0ea 100644
--- a/app/views/shared/deploy_tokens/_new_deploy_token.html.haml
+++ b/app/views/shared/deploy_tokens/_new_deploy_token.html.haml
@@ -7,7 +7,7 @@
.input-group
= text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { qa_selector: 'deploy_token_user_field' }
.input-group-append
- = clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username'), placement: 'left')
+ = deprecated_clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username'), placement: 'left')
%span.deploy-token-help-block.gl-mt-2.text-success
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/deploy_tokens/index.md') }
- link_end = "</a>".html_safe
@@ -17,7 +17,7 @@
.input-group
= text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { qa_selector: 'deploy_token_field' }
.input-group-append
- = clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token'), placement: 'left')
+ = deprecated_clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token'), placement: 'left')
%span.deploy-token-help-block.gl-mt-2.text-danger
- i_start = "<i>".html_safe
- i_end = "</i>".html_safe
diff --git a/app/views/shared/doorkeeper/applications/_show.html.haml b/app/views/shared/doorkeeper/applications/_show.html.haml
index 4101a456f32..d309a335166 100644
--- a/app/views/shared/doorkeeper/applications/_show.html.haml
+++ b/app/views/shared/doorkeeper/applications/_show.html.haml
@@ -10,7 +10,7 @@
.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-group-append
- = clipboard_button(target: '#application_id', title: _("Copy ID"), class: "gl-button btn btn-default")
+ = clipboard_button(target: "#application_id", title: _("Copy ID"), category: :primary, size: :medium)
%tr
%td
= _('Secret')
diff --git a/app/views/shared/form_elements/_description.html.haml b/app/views/shared/form_elements/_description.html.haml
index 75f678dea5c..b05befce24e 100644
--- a/app/views/shared/form_elements/_description.html.haml
+++ b/app/views/shared/form_elements/_description.html.haml
@@ -21,7 +21,7 @@
.js-markdown-editor{ data: { render_markdown_path: preview_url,
markdown_docs_path: help_page_path('user/markdown'),
quick_actions_docs_path: help_page_path('user/project/quick_actions'),
- qa_selector: 'issuable_form_description_field',
+ testid: 'issuable-form-description-field',
form_field_placeholder: placeholder,
autofocus: 'false',
form_field_classes: 'js-gfm-input markdown-area note-textarea rspec-issuable-form-description' } }
diff --git a/app/views/shared/groups/_empty_state.html.haml b/app/views/shared/groups/_empty_state.html.haml
deleted file mode 100644
index aaba9697fea..00000000000
--- a/app/views/shared/groups/_empty_state.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-.row.gl-align-items-center.gl-justify-content-center
- .order-md-2
- = custom_icon("icon_empty_groups")
-
- .text-content.order-md-1{ class: 'gl-m-0!' }
- %h4= s_("GroupsEmptyState|A group is a collection of several projects.")
- %p= s_("GroupsEmptyState|If you organize your projects under a group, it works like a folder.")
- %p= s_("GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group.")
diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml
index 1971c2da913..e8cb93f8037 100644
--- a/app/views/shared/hook_logs/_content.html.haml
+++ b/app/views/shared/hook_logs/_content.html.haml
@@ -1,6 +1,6 @@
%span.gl-display-flex.gl-align-items-center
- %h4
- POST
+ %h5
+ = gl_badge_tag "POST", { size: :sm }, { variant: :info }
= hook_log.url
= gl_badge_tag hook_log.trigger.singularize.titleize, { size: :sm }, { class: 'gl-ml-3' }
@@ -16,19 +16,28 @@
- c.with_body do
= _('Error: %{error}') % { error: hook_log.internal_error_message }
-%h4= _('Response')
-= render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
+%span.gl-display-flex.gl-align-items-center
+ %h3
+ = _('Response')
+ = render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
+
%pre.gl-mt-3
- :escaped
- #{hook_log.response_body}
+ - if hook_log.response_body.blank?
+ = s_('Webhooks|Response body is empty')
+ - else
+ :escaped
+ #{hook_log.response_body}
-%h5= _('Headers')
+%h4= _('Headers')
%pre
- - hook_log.response_headers.each do |k, v|
- <span class="gl-font-weight-bold">#{k}:</span> #{v}
- %br
+ - if hook_log.response_headers.blank?
+ = s_('Webhooks|Response headers data is empty')
+ - else
+ - hook_log.response_headers.each do |k, v|
+ <span class="gl-font-weight-bold">#{k}:</span> #{v}
+ %br
-%h4.gl-mt-6= _('Request')
+%h3.gl-mt-6= _('Request')
%pre
- if hook_log.oversize?
= _('Request data is too large')
@@ -36,7 +45,7 @@
:escaped
#{Gitlab::Json.pretty_generate(hook_log.request_data)}
-%h5= _('Headers')
+%h4= _('Headers')
%pre
- hook_log.request_headers.each do |k, v|
<span class="gl-font-weight-bold">#{k}:</span> #{v}
diff --git a/app/views/shared/icons/_icon_empty_groups.svg b/app/views/shared/icons/_icon_empty_groups.svg
deleted file mode 100644
index cf378145e59..00000000000
--- a/app/views/shared/icons/_icon_empty_groups.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg width="249" height="368" viewBox="891 156 249 368" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="131" height="162" rx="10"/><mask id="e" x="0" y="0" width="131" height="162" fill="#fff"><use xlink:href="#a"/></mask><path d="M223.616 127.958V108.96c0-4.416-3.584-8-8.005-8h-23.985c-2.778 0-5.98 2.014-7.18 4.5l-5.07 10.5h-49.763c-5.527 0-9.996 4.475-9.996 9.997v53.005c0 5.513 4.475 9.997 9.996 9.997h84.01c5.525 0 9.994-4.477 9.994-9.998v-51.004z" id="b"/><mask id="f" x="0" y="0" width="104" height="88" fill="#fff"><use xlink:href="#b"/></mask><path d="M47 25h.996C53.52 25 58 29.472 58 34.99v20.02C58 60.526 53.52 65 47.996 65H10.004C4.48 65 0 60.528 0 55.01V34.99C0 29.474 4.48 25 10.004 25H11v-7c0-9.94 8.06-18 18-18s18 8.06 18 18v7zm-6 0H17v-7c0-6.627 5.373-12 12-12s12 5.373 12 12v7z" id="c"/><mask id="g" x="0" y="0" width="58" height="65" fill="#fff"><use xlink:href="#c"/></mask><path d="M0 10.008C0 4.48 4.476 0 10 0h218c5.523 0 10 4.473 10 10.008v140.94c0 5.53-4.062 11.882-9.08 14.196l-100.84 46.5c-5.015 2.31-13.142 2.312-18.16 0l-100.84-46.5C4.064 162.832 0 156.484 0 150.95V10.007z" id="d"/><mask id="h" x="0" y="0" width="238" height="213.417" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(891 156)"><g transform="rotate(8 -266.528 490.3)"><use stroke="#E5E5E5" mask="url(#e)" stroke-width="8" fill="#FFF" xlink:href="#a"/><rect fill="#FC8A51" x="20" y="31" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="60" y="31" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="36" y="31" width="20" height="4" rx="2"/><rect fill="#6B4FBB" x="20" y="65" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="44" y="65" width="20" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="80" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="80" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="48" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="60" y="80" width="12" height="4" rx="2"/><rect fill="#6B4FBB" x="52" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="68" y="48" width="12" height="4" rx="2"/></g><use stroke="#B5A7DD" mask="url(#f)" stroke-width="8" fill="#FFF" transform="rotate(5 171.616 144.96)" xlink:href="#b"/><path d="M58 132c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#C1E7D0"/><path d="M90.143 132c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M74.686 133.875l-3.18-3.18c-.29-.29-.77-.296-1.06-.005l-1.55 1.55c-.287.287-.29.766.004 1.06l4.92 4.92c.504.504 1.32.504 1.823 0l.654-.653 7.804-7.804c.3-.3.29-.77-.005-1.067l-1.578-1.58c-.302-.3-.775-.298-1.068-.004l-6.764 6.763z" fill="#31AF64"/><path d="M4 66c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18S4 75.94 4 66z" fill="#D5ECF7"/><path d="M36.143 66c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M22 55.714c5.68 0 10.286 4.605 10.286 10.286 0 5.68-4.605 10.286-10.286 10.286-3.45 0-6.505-1.7-8.37-4.307L22 66V55.714z" fill="#2D9FD8"/><g transform="rotate(-8 748.533 18.147)"><use stroke="#FDE5D8" mask="url(#g)" stroke-width="8" fill="#FFF" xlink:href="#c"/><path d="M31 46.584c1.766-.772 3-2.534 3-4.584 0-2.76-2.24-5-5-5s-5 2.24-5 5c0 2.05 1.234 3.812 3 4.584v3.42c0 1.1.895 1.996 2 1.996 1.112 0 2-.894 2-1.997v-3.42z" fill="#FC8A51"/></g><g transform="translate(0 154)"><use stroke="#E5E5E5" mask="url(#h)" stroke-width="8" fill="#FFF" xlink:href="#d"/><g opacity=".3"><path d="M141.837 104.53l-2.56-7.993-5.074-15.843c-.26-.815-1.398-.815-1.66 0l-5.074 15.843h-16.85l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33 22.16-16.33c.61-.452.866-1.25.632-1.98" fill="#A1A1A1"/><path fill="#5C5C5C" d="M119.044 122.84l8.425-26.303h-16.85l8.424 26.304"/><path fill="#787878" d="M119.044 122.84l-8.425-26.303H98.81l20.232 26.304"/><path fill="#787878" d="M119.044 122.84l8.425-26.303h11.807l-20.233 26.304"/><path d="M98.812 96.537l-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33L98.81 96.538z" fill="#A1A1A1"/><path d="M98.812 96.537h11.807l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843z" fill="#5C5C5C"/><path d="M139.277 96.537l2.56 7.993c.234.73-.022 1.528-.634 1.98l-22.16 16.33 20.234-26.303z" fill="#A1A1A1"/><path d="M139.277 96.537H127.47l5.074-15.843c.26-.815 1.398-.815 1.66 0l5.073 15.843z" fill="#5C5C5C"/></g><path d="M57 18.29c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H41c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H77c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm17 24.693c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm202 32.923c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm202 32.923c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm-202 0c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm202 32.922c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm179.023 19.555c-.988.452-1.388 1.55-.894 2.454.493.904 1.694 1.27 2.682.82l14.31-6.545c.99-.452 1.39-1.55.896-2.454-.494-.902-1.696-1.27-2.684-.817l-14.31 6.544zm-32.2 14.723c-.987.452-1.388 1.55-.894 2.454.493.904 1.695 1.27 2.683.818l14.31-6.544c.99-.45 1.39-1.55.895-2.454-.494-.903-1.695-1.27-2.683-.818l-14.31 6.544zm-32.2 14.724c-.987.45-1.387 1.55-.893 2.454.494.903 1.695 1.27 2.683.818l14.31-6.544c.99-.452 1.39-1.55.896-2.454-.495-.904-1.697-1.27-2.685-.818l-14.31 6.544zm-23.67-2.023l-12.186-5.57c-.987-.452-2.19-.086-2.683.817-.494.904-.093 2.003.895 2.454l12.185 5.573c.754.345 1.57.645 2.438.898 1.052.307 2.177-.224 2.513-1.187.335-.962-.246-1.99-1.298-2.298-.677-.197-1.302-.426-1.864-.684zM62.57 168.437c-.988-.452-2.19-.086-2.683.818-.494.903-.094 2.002.894 2.454l14.31 6.544c.988.45 2.19.085 2.683-.818.494-.904.094-2.003-.894-2.454l-14.312-6.544zm-32.2-14.723c-.988-.452-2.19-.086-2.683.818-.494.904-.093 2.003.895 2.454l14.31 6.544c.988.452 2.19.086 2.684-.818.494-.903.093-2.002-.895-2.454l-14.312-6.543z" fill="#EEE"/></g><g><path d="M104 18c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#FADFD9"/><path d="M136.143 18c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M119.43 8.994c0-.707.57-1.28 1.283-1.28h2.574c.71 0 1.284.57 1.284 1.28v10.298c0 .706-.57 1.28-1.283 1.28h-2.574c-.71 0-1.284-.57-1.284-1.28V8.994zm0 15.433c0-.71.57-1.284 1.283-1.284h2.574c.71 0 1.284.57 1.284 1.284V27c0 .71-.57 1.286-1.283 1.286h-2.574c-.71 0-1.284-.57-1.284-1.285v-2.573z" fill="#E75E40"/></g><g><path d="M213 89c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#F6D4DC"/><path d="M245.143 89c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M231 86.348l-3.603-3.602c-.288-.29-.766-.286-1.063.01l-1.578 1.578c-.3.302-.3.773-.01 1.063L228.348 89l-3.602 3.603c-.29.288-.286.766.01 1.063l1.578 1.578c.302.3.773.3 1.063.01L231 91.652l3.603 3.602c.288.29.766.286 1.063-.01l1.578-1.578c.3-.302.3-.773.01-1.063L233.652 89l3.602-3.603c.29-.288.286-.766-.01-1.063l-1.578-1.578c-.302-.3-.773-.3-1.063-.01L231 86.348z" fill="#D22852"/></g></g></svg>
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 db1754c1864..5aaae5eb4ec 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
@@ -1,30 +1,21 @@
- pretty_name = @project&.full_name ? html_escape(@project&.full_name) : '<' + _('project name') + '>'
- run_actions_text = html_escape(s_("ProjectService|Perform common operations on GitLab project: %{project_name}")) % { project_name: pretty_name }
+- external_link_icon = sprite_icon('external-link')
%p= s_("ProjectService|To configure this integration, you should:")
-%ul.list-unstyled.indent-list
+%ol.indent-list
%li
- 1.
- = link_to help_page_url('user/project/integrations/mattermost_slash_commands', anchor: 'enable-custom-slash-commands-in-mattermost'), target: '_blank', rel: 'noopener noreferrer nofollow' do
- Enable custom slash commands
- = sprite_icon('external-link')
- on your Mattermost installation.
+ - enable_slash_commands_link_url = help_page_url('user/project/integrations/mattermost_slash_commands', anchor: 'enable-custom-slash-commands-in-mattermost')
+ - enable_slash_commands_link = link_to '', enable_slash_commands_link_url, target: '_blank', rel: 'noopener noreferrer'
+ = safe_format(s_('MattermostService|%{link_start}Enable custom slash commands %{icon}%{link_end} on your Mattermost installation.'), tag_pair(enable_slash_commands_link, :link_start, :link_end), icon: external_link_icon)
%li
- 2.
- = link_to help_page_url('user/project/integrations/mattermost_slash_commands', anchor: 'create-a-slash-command-in-mattermost'), target: '_blank', rel: 'noopener noreferrer nofollow' do
- Add a slash command
- = sprite_icon('external-link')
- in your Mattermost team with the options listed below.
+ - create_slash_commands_link_url = help_page_url('user/project/integrations/mattermost_slash_commands', anchor: 'create-a-slash-command-in-mattermost')
+ - create_slash_commands_link = link_to '', create_slash_commands_link_url, target: '_blank', rel: 'noopener noreferrer'
+ = safe_format(s_('MattermostService|%{link_start}Add a slash command %{icon}%{link_end} in your Mattermost team with the options listed below.'), tag_pair(create_slash_commands_link, :link_start, :link_end), icon: external_link_icon)
%li
- 3. Paste the token into the
- %strong Token
- field.
+ = safe_format(s_('MattermostService|Paste the token into the %{strong_start}Token%{strong_end} field.'), tag_pair(tag.strong, :strong_start, :strong_end))
%li
- 4. Select the
- %strong Active
- check box, then select
- %strong Save changes
- to start using GitLab inside Mattermost!
+ = safe_format(s_('MattermostService|Select the %{strong_start}Active%{strong_end} check box, then select %{strong_start}Save changes%{strong_end} to start using GitLab inside Mattermost!'), tag_pair(tag.strong, :strong_start, :strong_end))
%hr
@@ -34,14 +25,14 @@
.col-12.input-group
= text_field_tag :display_name, "GitLab / #{pretty_name}".html_safe, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#display_name', class: 'gl-button btn-default btn-icon input-group-text')
+ = clipboard_button(target: '#display_name', category: :primary, size: :medium)
.form-group
= label_tag :description, _('Description'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#description', class: 'gl-button btn-default btn-icon input-group-text')
+ = clipboard_button(target: '#description', category: :primary, size: :medium)
.form-group
= label_tag nil, s_('MattermostService|Command trigger word'), class: 'col-12 col-form-label label-bold'
@@ -59,7 +50,7 @@
.col-12.input-group
= text_field_tag :request_url, service_trigger_url(integration), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#request_url', class: 'gl-button btn-default btn-icon input-group-text')
+ = clipboard_button(target: '#request_url', category: :primary, size: :medium)
.form-group
= label_tag nil, s_('MattermostService|Request method'), class: 'col-12 col-form-label label-bold'
@@ -70,14 +61,14 @@
.col-12.input-group
= text_field_tag :response_username, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#response_username', class: 'gl-button btn-default btn-icon input-group-text')
+ = clipboard_button(target: '#response_username', category: :primary, size: :medium)
.form-group
= label_tag :response_icon, s_('MattermostService|Response icon'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#response_icon', class: 'gl-button btn-default btn-icon input-group-text')
+ = clipboard_button(target: '#response_icon', category: :primary, size: :medium)
.form-group
= label_tag nil, _('Autocomplete'), class: 'col-12 col-form-label label-bold'
@@ -88,11 +79,11 @@
.col-12.input-group
= text_field_tag :autocomplete_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#autocomplete_hint', class: 'gl-button btn-default btn-icon input-group-text')
+ = clipboard_button(target: '#autocomplete_hint', category: :primary, size: :medium)
.form-group
= label_tag :autocomplete_description, _('Autocomplete description'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#autocomplete_description', class: 'gl-button btn-default btn-icon input-group-text')
+ = clipboard_button(target: '#autocomplete_description', category: :primary, size: :medium)
diff --git a/app/views/shared/integrations/mattermost_slash_commands/_installation_info.html.haml b/app/views/shared/integrations/mattermost_slash_commands/_installation_info.html.haml
index 38adc69dd5e..ac14e1a6cd7 100644
--- a/app/views/shared/integrations/mattermost_slash_commands/_installation_info.html.haml
+++ b/app/views/shared/integrations/mattermost_slash_commands/_installation_info.html.haml
@@ -1,7 +1,5 @@
.services-installation-info
- unless integration.activated?
- .row
- .col-sm-9.offset-sm-3
- = link_to new_project_mattermost_path(@project), class: 'btn gl-button btn-lg' do
- = custom_icon('mattermost_logo', size: 15)
- = s_("MattermostService|Add to Mattermost")
+ = render Pajamas::ButtonComponent.new(href: new_project_mattermost_path(@project), button_text_classes: 'gl-display-flex gl-gap-2') do
+ = custom_icon('mattermost_logo')
+ = s_("MattermostService|Add to Mattermost")
diff --git a/app/views/shared/integrations/prometheus/_custom_metrics.html.haml b/app/views/shared/integrations/prometheus/_custom_metrics.html.haml
index a7a650aa95d..080d4b37354 100644
--- a/app/views/shared/integrations/prometheus/_custom_metrics.html.haml
+++ b/app/views/shared/integrations/prometheus/_custom_metrics.html.haml
@@ -3,7 +3,6 @@
.col-lg-3
%p
= s_('PrometheusService|Custom metrics require Prometheus installed on a cluster with environment scope "*" OR a manually configured Prometheus to be available.')
- = link_to s_('PrometheusService|More information'), help_page_path('operations/metrics/index.md'), target: '_blank', rel: "noopener noreferrer"
.col-lg-9
= render Pajamas::CardComponent.new(header_options: { class: 'gl-display-flex gl-align-items-center' }, body_options: { class: 'gl-p-0' }, card_options: { class: 'gl-mb-5 custom-monitored-metrics js-panel-custom-monitored-metrics', data: { active_custom_metrics: project_prometheus_metrics_path(project), environments_data: environments_list_data, service_active: "#{integration.active}" } }) do |c|
diff --git a/app/views/shared/integrations/prometheus/_metrics.html.haml b/app/views/shared/integrations/prometheus/_metrics.html.haml
index cb78faa383a..36e4c0d4b13 100644
--- a/app/views/shared/integrations/prometheus/_metrics.html.haml
+++ b/app/views/shared/integrations/prometheus/_metrics.html.haml
@@ -5,10 +5,9 @@
.col-lg-3
%p
= s_('PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters.')
- = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus'), target: '_blank', rel: "noopener noreferrer"
.col-lg-9
- = render Pajamas::CardComponent.new(body_options: { class: 'gl-p-0' }, card_options: { class: 'gl-mb-5 js-panel-monitored-metrics', data: { active_metrics: active_common_project_prometheus_metrics_path(project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/index') }}) do |c|
+ = render Pajamas::CardComponent.new(body_options: { class: 'gl-p-0' }, card_options: { class: 'gl-mb-5 js-panel-monitored-metrics', data: { active_metrics: active_common_project_prometheus_metrics_path(project, :json) }}) do |c|
- c.with_header do
%strong
= s_('PrometheusService|Common metrics')
@@ -34,5 +33,4 @@
.flash-notice
.flash-text
= html_escape(s_("PrometheusService|To set up automatic monitoring, add the environment variable %{variable} to exporter's queries.")) % { variable: "<code>$CI_ENVIRONMENT_SLUG</code>".html_safe }
- = link_to s_('PrometheusService|More information'), help_page_path('operations/metrics/index.md')
%ul.list-unstyled.metrics-list.js-missing-var-metrics-list
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 43a240fa6fe..defaf50efea 100644
--- a/app/views/shared/integrations/slack_slash_commands/_help.html.haml
+++ b/app/views/shared/integrations/slack_slash_commands/_help.html.haml
@@ -40,7 +40,7 @@
.col-12.input-group
= text_field_tag :url, service_trigger_url(integration), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
- = clipboard_button(target: '#url', class: 'input-group-text')
+ = deprecated_clipboard_button(target: '#url', class: 'input-group-text')
.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
- = clipboard_button(target: '#customize_name', class: 'input-group-text')
+ = deprecated_clipboard_button(target: '#customize_name', class: 'input-group-text')
.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
- = clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
+ = deprecated_clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
.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
- = clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text')
+ = deprecated_clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text')
.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
- = clipboard_button(target: '#descriptive_label', class: 'input-group-text')
+ = deprecated_clipboard_button(target: '#descriptive_label', class: 'input-group-text')
%hr
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 42f035b99aa..f8d07a2f6de 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -51,12 +51,11 @@
.gl-mt-5{ class: (is_footer ? "footer-block" : "middle-block") }
- if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = issuable.project.present.contribution_guide_path)
.gl-mb-5
- - contribution_guidelines_start = '<strong><a href="%{url}">'.html_safe % {url: strip_tags(guide_url)}
- - contribution_guidelines_end = '</a></strong>'.html_safe
- = sanitize(html_escape(_('Please review the %{linkStart}contribution guidelines%{linkEnd} for this project.')) % { linkStart: contribution_guidelines_start, linkEnd: contribution_guidelines_end })
+ - contribution_guidelines = link_to('', strip_tags(guide_url))
+ = safe_format(_('Please review the %{strong_start}%{contribution_guidelines_start}contribution guidelines%{contribution_guidelines_end}%{strong_end} for this project.'), tag_pair('<strong></strong>'.html_safe, :strong_start, :strong_end), tag_pair(contribution_guidelines, :contribution_guidelines_start, :contribution_guidelines_end))
- if issuable.new_record?
- = form.submit "#{_('Create')} #{issuable.class.model_name.human.downcase}", pajamas_button: true, class: 'gl-mr-2 js-issuable-submit-button js-reset-autosave', data: { qa_selector: 'issuable_create_button', track_action: 'click_button', track_label: 'submit_mr', track_value: 0 }
+ = form.submit "#{_('Create')} #{issuable.class.model_name.human.downcase}", pajamas_button: true, class: 'gl-mr-2 js-issuable-submit-button js-reset-autosave', data: { testid: 'issuable-create-button', track_action: 'click_button', track_label: 'submit_mr', track_value: 0 }
- else
= form.submit _('Save changes'), pajamas_button: true, class: 'gl-mr-2 js-issuable-submit-button js-reset-autosave', data: { track_action: 'click_button', track_label: 'submit_mr', track_value: 0 }
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index d590c859945..86aaa5128a8 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -1,7 +1,7 @@
- type = local_assigns.fetch(:type)
- show_sorting_dropdown = local_assigns.fetch(:show_sorting_dropdown, true)
- disable_target_branch = local_assigns.fetch(:disable_target_branch, false)
-- placeholder = local_assigns[:placeholder] || _('Search or filter results...')
+- placeholder = local_assigns[:placeholder] || _('Search or filter results…')
- block_css_class = type != :productivity_analytics ? 'row-content-block second-block' : ''
.issues-filters
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 46710081307..93e1a53ccb4 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -46,7 +46,7 @@
.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
- .block.gl-collapse-empty{ data: { testid: 'iteration_container' } }<
+ .block.gl-collapse-empty{ data: { testid: 'iteration-container' } }<
= render_if_exists 'shared/issuable/iteration_select', can_edit: can_edit_issuable.to_s, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type
- if issuable_sidebar[:show_crm_contacts]
@@ -88,11 +88,11 @@
- if is_merge_request && !moved_sidebar_enabled
.sub-block.js-sidebar-source-branch
.sidebar-collapsed-icon.js-dont-change-state
- = 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')
+ = 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')
.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) }
- = 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')
+ = 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')
- if show_forwarding_email && !moved_sidebar_enabled
.block
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index a27bb506c87..0ffce0ac571 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -45,4 +45,4 @@
options: options,
wrapper_class: 'js-sidebar-assignee-dropdown',
track_label: 'edit_assignee',
- trigger_source: "#{issuable_type}-assignee-dropdown"
+ trigger_source: "#{issuable_type}_assignee_dropdown"
diff --git a/app/views/shared/issuable/_sidebar_reviewers.html.haml b/app/views/shared/issuable/_sidebar_reviewers.html.haml
index b360fac0a55..4b07d8d0850 100644
--- a/app/views/shared/issuable/_sidebar_reviewers.html.haml
+++ b/app/views/shared/issuable/_sidebar_reviewers.html.haml
@@ -42,4 +42,4 @@
options: options,
wrapper_class: 'js-sidebar-reviewer-dropdown',
track_label: 'edit_reviewer',
- trigger_source: "#{issuable_type}-reviewer-dropdown"
+ trigger_source: "#{issuable_type}_reviewer_dropdown"
diff --git a/app/views/shared/issuable/_status_box.html.haml b/app/views/shared/issuable/_status_box.html.haml
index cca51b48322..f2e4e22788a 100644
--- a/app/views/shared/issuable/_status_box.html.haml
+++ b/app/views/shared/issuable/_status_box.html.haml
@@ -1,8 +1,7 @@
- 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_status_class = issuable.open? ? 'issuable-status-badge-open' : issuable.merged? ? 'issuable-status-badge-merged' : 'issuable-status-badge-closed'
-- badge_classes = "js-mr-status-box issuable-status-badge gl-mr-3 gl-align-self-center #{badge_status_class} #{'gl-vertical-align-bottom' if issuable.is_a?(MergeRequest)}"
+- 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
diff --git a/app/views/shared/issuable/form/_default_templates.html.haml b/app/views/shared/issuable/form/_default_templates.html.haml
index 2dda0049c09..be6ca475f5c 100644
--- a/app/views/shared/issuable/form/_default_templates.html.haml
+++ b/app/views/shared/issuable/form/_default_templates.html.haml
@@ -1,5 +1,4 @@
.gl-mt-3.gl-text-secondary
- - template_link_url = help_page_path('user/project/description_templates')
- - template_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: template_link_url }
- = s_('Promotions|Add %{link_start} description templates %{link_end} to help your contributors to communicate effectively!').html_safe % { link_start: template_link_start, link_end: '</a>'.html_safe }
+ - link = link_to('', help_page_path('user/project/description_templates'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(s_('Promotions|Add %{link_start} description templates %{link_end} to help your contributors to communicate effectively!'), tag_pair(link, :link_start, :link_end))
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
index 36000f3cc67..242342d365e 100644
--- a/app/views/shared/issuable/form/_title.html.haml
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -3,7 +3,7 @@
%div{ data: { testid: 'issue-title-input-field' } }
= form.text_field :title, required: true, aria: { required: true }, maxlength: 255, autofocus: true,
- autocomplete: 'off', class: 'form-control pad', dir: 'auto', data: { qa_selector: 'issuable_form_title_field' }
+ autocomplete: 'off', class: 'form-control pad', dir: 'auto', data: { testid: 'issuable-form-title-field' }
- if issuable.respond_to?(:draft?)
.gl-pt-3
diff --git a/app/views/shared/issue_type/_details_header.html.haml b/app/views/shared/issue_type/_details_header.html.haml
deleted file mode 100644
index 558287480e1..00000000000
--- a/app/views/shared/issue_type/_details_header.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-- link = issue_closed_link(@issue, current_user, css_class: 'text-underline gl-reset-color!')
-- badge_classes = 'issuable-status-badge gl-mr-3'
-
-.detail-page-header
- .detail-page-header-body.gl-flex-wrap
- = gl_badge_tag({ variant: :info, icon: 'issue-closed', icon_classes: 'gl-mr-0!' }, { class: "#{issue_status_visibility(issuable, status_box: :closed)} #{badge_classes} issuable-status-badge-closed" }) do
- .gl-display-none.gl-sm-display-block.gl-ml-2
- = issue_closed_text(issuable, current_user)
- - if link
- %span.gl-pl-2.gl-sm-display-none
- = "(#{link})"
- = gl_badge_tag({ variant: :success, icon: 'issues', icon_classes: 'gl-mr-0!' }, { class: "#{issue_status_visibility(issuable, status_box: :open)} #{badge_classes} issuable-status-badge-open" }) do
- %span.gl-display-none.gl-sm-display-block.gl-ml-2
- = _('Open')
-
- #js-issuable-header-warnings{ data: { hidden: issue_hidden?(issuable).to_s } }
- = issuable_meta(issuable, @project)
-
- = render Pajamas::ButtonComponent.new(href: '#', icon: 'chevron-double-lg-left', button_options: { class: 'gl-ml-auto gl-display-block gl-sm-display-none! js-sidebar-toggle' })
-
- .js-issue-header-actions{ data: issue_header_actions_data(@project, issuable, current_user, @issuable_sidebar) }
diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml
index 899b2ed832e..53fbe3dac03 100644
--- a/app/views/shared/labels/_form.html.haml
+++ b/app/views/shared/labels/_form.html.haml
@@ -1,3 +1,5 @@
+- 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|
= form_errors(@label)
@@ -21,6 +23,13 @@
.form-text.text-muted
= _('Select a color from the color picker or from the presets below.')
= render_suggested_colors
+ - if show_lock_on_merge
+ .form-group.row
+ .col-12
+ = f.gitlab_ui_checkbox_component :lock_on_merge,
+ _('Lock label after a merge request is merged'),
+ help_text: label_lock_on_merge_help_text,
+ checkbox_options: { disabled: @label.lock_on_merge }
.gl-display-flex.gl-justify-content-space-between
%div
- if @label.persisted?
diff --git a/app/views/shared/members/_access_request_links.html.haml b/app/views/shared/members/_access_request_links.html.haml
index 94086636d5a..0d692ee753a 100644
--- a/app/views/shared/members/_access_request_links.html.haml
+++ b/app/views/shared/members/_access_request_links.html.haml
@@ -15,4 +15,4 @@
- elsif source.request_access_enabled && can?(current_user, :request_access, source)
= link_to _('Request Access'), polymorphic_path([:request_access, source, :members]),
method: :post,
- data: { qa_selector: 'request_access_link' }
+ data: { testid: 'request-access-link' }
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 1b0eeb424c2..9387d8d3ad1 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -162,10 +162,10 @@
- if milestone_ref.present?
.block.reference
.sidebar-collapsed-icon.js-dont-change-state
- = clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport')
+ = deprecated_clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport')
.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
= s_('MilestoneSidebar|Reference:')
%span{ title: milestone_ref }
= milestone_ref
- = clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport')
+ = deprecated_clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport')
diff --git a/app/views/shared/packages/_no_packages.html.haml b/app/views/shared/packages/_no_packages.html.haml
index 7cc8110fb6b..9f165a198d6 100644
--- a/app/views/shared/packages/_no_packages.html.haml
+++ b/app/views/shared/packages/_no_packages.html.haml
@@ -3,6 +3,5 @@
.text-content
%h4.text-center= _('There are no packages yet')
%p
- - no_packages_url = help_page_path('administration/packages/index')
- - no_packages_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: no_packages_url }
- = _('Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab.').html_safe % { no_packages_link_start: no_packages_link_start, no_packages_link_end: '</a>'.html_safe }
+ - link = link_to('', help_page_path('administration/packages/index'), target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(_('Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab.'), tag_pair(link, :no_packages_link_start, :no_packages_link_end))
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 95188cefdd1..ac5e65747d5 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -26,8 +26,8 @@
.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 } }
.gl-display-flex.gl-align-items-center.gl-flex-wrap
- %h2.gl-font-base.gl-line-height-20.gl-my-0
- = link_to project_path(project), class: 'text-plain gl-mr-3 js-prefetch-document' do
+ %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
%span.namespace-name.gl-font-weight-normal
- if project.namespace && !skip_namespace
= project.namespace.human_name
diff --git a/app/views/shared/web_hooks/_hook_errors.html.haml b/app/views/shared/web_hooks/_hook_errors.html.haml
index 0e5f6d844cd..bac2f1d7cb0 100644
--- a/app/views/shared/web_hooks/_hook_errors.html.haml
+++ b/app/views/shared/web_hooks/_hook_errors.html.haml
@@ -1,8 +1,5 @@
-- strong_start = '<strong>'.html_safe
-- strong_end = '</strong>'.html_safe
-- link_start = '<a href="%{url}">'.html_safe
-- link_end = '</a>'.html_safe
-
+- strong = { strong_start: '<strong>'.html_safe,
+ strong_end: '</strong>'.html_safe }
- if hook.rate_limited?
- placeholders = { limit: number_with_delimiter(hook.rate_limit),
root_namespace: hook.parent.root_namespace.path }
@@ -14,15 +11,11 @@
= render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook failed to connect'),
variant: :danger) do |c|
- c.with_body do
- = s_('Webhooks|The webhook failed to connect, and is disabled. To re-enable it, check %{strong_start}Recent events%{strong_end} for error details, then test your settings below.').html_safe % { strong_start: strong_start, strong_end: strong_end }
+ = safe_format(s_('Webhooks|The webhook failed to connect, and is disabled. To re-enable it, check %{strong_start}Recent events%{strong_end} for error details, then test your settings below.'), strong)
- elsif hook.temporarily_disabled?
- - help_path = help_page_path('user/project/integrations/webhooks', anchor: 'webhook-fails-or-multiple-webhook-requests-are-triggered')
- - placeholders = { strong_start: strong_start,
- strong_end: strong_end,
- retry_time: time_interval_in_words(hook.disabled_until - Time.now),
- help_link_start: link_start % { url: help_path },
- help_link_end: link_end }
+ - help_link = link_to('', help_page_path('user/project/integrations/webhooks', anchor: 'webhook-fails-or-multiple-webhook-requests-are-triggered'), target: '_blank', rel: 'noopener noreferrer')
+ - retry_time = { retry_time: time_interval_in_words(hook.disabled_until - Time.now) }
= render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook fails to connect'),
variant: :warning) do |c|
- c.with_body do
- = s_('Webhooks|The webhook %{help_link_start}failed to connect%{help_link_end}, and will retry in %{retry_time}. To re-enable it, check %{strong_start}Recent events%{strong_end} for error details, then test your settings below.').html_safe % placeholders
+ = safe_format(s_('Webhooks|The webhook %{help_link_start}failed to connect%{help_link_end}, and will retry in %{retry_time}. To re-enable it, check %{strong_start}Recent events%{strong_end} for error details, then test your settings below.'), retry_time, strong, tag_pair(help_link, :help_link_start, :help_link_end))
diff --git a/app/views/shared/web_hooks/_title_and_docs.html.haml b/app/views/shared/web_hooks/_title_and_docs.html.haml
index ae32dcea7cb..8ff41b6a1ca 100644
--- a/app/views/shared/web_hooks/_title_and_docs.html.haml
+++ b/app/views/shared/web_hooks/_title_and_docs.html.haml
@@ -1,4 +1,4 @@
-- webhooks_link_start = '<a href="%{url}">'.html_safe % { url: help_page_path(hook.help_path) }
+- webhooks_link = tag_pair(link_to('', help_page_path(hook.help_path), target: '_blank', rel: 'noopener noreferrer'), :webhooks_link_start, :webhooks_link_end)
.settings-sticky-header
.settings-sticky-header-inner
@@ -6,7 +6,7 @@
= page_title
- if @project
- - integrations_link_start = '<a href="%{url}">'.html_safe % { url: scoped_integrations_path(project: @project) }
- %p.gl-text-secondary= _("%{webhooks_link_start}%{webhook_type}%{link_end} enable you to send notifications to web applications in response to events in a group or project. We recommend using an %{integrations_link_start}integration%{link_end} in preference to a webhook.").html_safe % { webhooks_link_start: webhooks_link_start, webhook_type: hook.pluralized_name, integrations_link_start: integrations_link_start, link_end: '</a>'.html_safe }
+ - integrations_link = tag_pair(link_to('', scoped_integrations_path(project: @project)), :integrations_link_start, :integrations_link_end)
+ %p.gl-text-secondary= safe_format(_("%{webhooks_link_start}%{webhook_type}%{webhooks_link_end} enable you to send notifications to web applications in response to events in a group or project. We recommend using an %{integrations_link_start}integration%{integrations_link_end} in preference to a webhook."), webhooks_link, integrations_link, webhook_type: hook.pluralized_name)
- else
- %p.gl-text-secondary= _("%{webhooks_link_start}%{webhook_type}%{link_end} enable you to send notifications to web applications in response to events in a group or project.").html_safe % { webhooks_link_start: webhooks_link_start, webhook_type: hook.pluralized_name, link_end: '</a>'.html_safe }
+ %p.gl-text-secondary= safe_format(_("%{webhooks_link_start}%{webhook_type}%{webhooks_link_end} enable you to send notifications to web applications in response to events in a group or project."), webhooks_link, webhook_type: hook.pluralized_name)
diff --git a/app/views/users/_deletion_guidance.html.haml b/app/views/users/_deletion_guidance.html.haml
index 507fe126acb..64b4a8e1ae2 100644
--- a/app/views/users/_deletion_guidance.html.haml
+++ b/app/views/users/_deletion_guidance.html.haml
@@ -3,8 +3,8 @@
%ul
%li
%p
- - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path("user/profile/account/delete_account", anchor: "associated-records") }
- = _('Certain user content will be moved to a system-wide "Ghost User" in order to maintain content for posterity. For further information, please refer to the %{link_start}user account deletion documentation.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+ - link = link_to('', help_page_path("user/profile/account/delete_account", anchor: "associated-records"))
+ = safe_format(_('Certain user content will be moved to a system-wide "Ghost User" in order to maintain content for posterity. For further information, please refer to the %{link_start}user account deletion documentation.%{link_end}'), tag_pair(link, :link_start, :link_end))
- personal_projects_count = user.personal_projects.count
- unless personal_projects_count == 0
%li
diff --git a/app/views/users/_profile_basic_info.html.haml b/app/views/users/_profile_basic_info.html.haml
index 6de9e80008e..fb9721028d5 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 }
- = clipboard_button(title: s_('UserProfile|Copy user ID'), text: @user.id)
+ = deprecated_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/show.html.haml b/app/views/users/show.html.haml
index e2ddbb90213..a2f6b3da746 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -71,7 +71,7 @@
%p.gl-mb-4.gl-text-gray-500= s_("UserProfile|Pronounced as: %{pronunciation}") % { pronunciation: @user.pronunciation }
- if @user.status&.customized?
- .cover-status.gl-display-inline-flex.gl-align-items-center.gl-mb-3
+ .cover-status.gl-display-inline-flex.gl-align-items-baseline.gl-mb-3
= emoji_icon(@user.status.emoji, class: 'gl-mr-2')
= markdown_field(@user.status, :message)
= render "users/profile_basic_info"
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 60f233b8289..6ef7447b9da 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -570,23 +570,23 @@
:weight: 1
:idempotent: false
:tags: []
-- :name: cronjob:metrics_global_metrics_update
- :worker_name: Metrics::GlobalMetricsUpdateWorker
- :feature_category: :metrics
+- :name: cronjob:merge_requests_ensure_prepared
+ :worker_name: MergeRequests::EnsurePreparedWorker
+ :feature_category: :code_review_workflow
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
-- :name: cronjob:namespaces_in_product_marketing_emails
- :worker_name: Namespaces::InProductMarketingEmailsWorker
- :feature_category: :experimentation_activation
+- :name: cronjob:metrics_global_metrics_update
+ :worker_name: Metrics::GlobalMetricsUpdateWorker
+ :feature_category: :metrics
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent: false
+ :idempotent: true
:tags: []
- :name: cronjob:namespaces_prune_aggregation_schedules
:worker_name: Namespaces::PruneAggregationSchedulesWorker
@@ -2343,6 +2343,51 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: bitbucket_import_advance_stage
+ :worker_name: Gitlab::BitbucketImport::AdvanceStageWorker
+ :feature_category: :importers
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
+- :name: bitbucket_import_import_pull_request
+ :worker_name: Gitlab::BitbucketImport::ImportPullRequestWorker
+ :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
+ :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
+ :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
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
- :name: bitbucket_server_import_advance_stage
:worker_name: Gitlab::BitbucketServerImport::AdvanceStageWorker
:feature_category: :importers
@@ -2469,6 +2514,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: bulk_imports_finish_project_import
+ :worker_name: BulkImports::FinishProjectImportWorker
+ :feature_category: :importers
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: bulk_imports_pipeline
:worker_name: BulkImports::PipelineWorker
:feature_category: :importers
@@ -2613,6 +2667,15 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: database_lock_tables
+ :worker_name: Database::LockTablesWorker
+ :feature_category: :cell
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: default
:worker_name:
:feature_category: :not_owned
@@ -3306,15 +3369,6 @@
:weight: 1
:idempotent: false
:tags: []
-- :name: pages_invalidate_domain_cache
- :worker_name: Pages::InvalidateDomainCacheWorker
- :feature_category: :pages
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: true
- :tags: []
- :name: post_receive
:worker_name: PostReceive
:feature_category: :source_code_management
@@ -3470,7 +3524,7 @@
:tags: []
- :name: projects_record_target_platforms
:worker_name: Projects::RecordTargetPlatformsWorker
- :feature_category: :groups_and_projects
+ :feature_category: :experimentation_activation
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
@@ -3729,6 +3783,15 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: users_track_namespace_visits
+ :worker_name: Users::TrackNamespaceVisitsWorker
+ :feature_category: :navigation
+ :has_external_dependencies: false
+ :urgency: :throttled
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: web_hook
:worker_name: WebHookWorker
:feature_category: :webhooks
diff --git a/app/workers/background_migration/single_database_worker.rb b/app/workers/background_migration/single_database_worker.rb
index 2f797a24468..56800c03bbb 100644
--- a/app/workers/background_migration/single_database_worker.rb
+++ b/app/workers/background_migration/single_database_worker.rb
@@ -45,7 +45,10 @@ module BackgroundMigration
# lease on the class before giving up. See MR for more discussion.
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45298#note_434304956
def perform(class_name, arguments = [], lease_attempts = MAX_LEASE_ATTEMPTS)
- unless Feature.enabled?(:execute_background_migrations, type: :ops)
+ should_skip = Feature.enabled?(:disallow_database_ddl_feature_flags, type: :ops) ||
+ Feature.disabled?(:execute_background_migrations, type: :ops)
+
+ if should_skip
# Delay execution of background migrations
self.class.perform_in(BACKGROUND_MIGRATIONS_DELAY, class_name, arguments, lease_attempts)
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index f5baa220715..b937dbf298a 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -13,15 +13,5 @@ class BuildSuccessWorker # rubocop:disable Scalability/IdempotentWorker
queue_namespace :pipeline_processing
urgency :high
- def perform(build_id)
- Ci::Build.find_by_id(build_id).try do |build|
- stop_environment(build) if build.stops_environment? && build.stop_action_successful?
- end
- end
-
- private
-
- def stop_environment(build)
- build.persisted_environment.fire_state_event(:stop_complete)
- end
+ def perform(build_id); end
end
diff --git a/app/workers/bulk_import_worker.rb b/app/workers/bulk_import_worker.rb
index 6bce13c5ff0..83b881ee525 100644
--- a/app/workers/bulk_import_worker.rb
+++ b/app/workers/bulk_import_worker.rb
@@ -22,7 +22,7 @@ class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
@bulk_import.start! if @bulk_import.created?
created_entities.first(next_batch_size).each do |entity|
- BulkImports::CreatePipelineTrackersService.new(entity).execute!
+ create_tracker(entity)
entity.start!
@@ -51,7 +51,7 @@ class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
end
def all_entities_failed?
- entities.all? { |entity| entity.failed? }
+ entities.all?(&:failed?)
end
# A new BulkImportWorker job is enqueued to either
@@ -72,4 +72,55 @@ class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
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
diff --git a/app/workers/bulk_imports/finish_project_import_worker.rb b/app/workers/bulk_imports/finish_project_import_worker.rb
new file mode 100644
index 00000000000..815101c89f3
--- /dev/null
+++ b/app/workers/bulk_imports/finish_project_import_worker.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class FinishProjectImportWorker
+ include ApplicationWorker
+
+ feature_category :importers
+ sidekiq_options retry: 5
+ data_consistency :sticky
+
+ idempotent!
+
+ def perform(project_id)
+ project = Project.find_by_id(project_id)
+ return unless project
+
+ project.after_import
+ end
+ end
+end
diff --git a/app/workers/click_house/events_sync_worker.rb b/app/workers/click_house/events_sync_worker.rb
index 054e7763297..5b7398cb071 100644
--- a/app/workers/click_house/events_sync_worker.rb
+++ b/app/workers/click_house/events_sync_worker.rb
@@ -12,6 +12,47 @@ module ClickHouse
# the job is scheduled every 3 minutes and we will allow maximum 2.5 minutes runtime
MAX_TTL = 2.5.minutes.to_i
+ MAX_RUNTIME = 120.seconds
+ BATCH_SIZE = 500
+ INSERT_BATCH_SIZE = 5000
+ CSV_MAPPING = {
+ id: :id,
+ path: :path,
+ author_id: :author_id,
+ target_id: :target_id,
+ target_type: :target_type,
+ action: :raw_action,
+ created_at: :casted_created_at,
+ updated_at: :casted_updated_at
+ }.freeze
+
+ # transforms the traversal_ids to a String:
+ # Example: group_id/subgroup_id/group_or_projectnamespace_id/
+ PATH_COLUMN = <<~SQL
+ (
+ CASE
+ WHEN project_id IS NOT NULL THEN (SELECT array_to_string(traversal_ids, '/') || '/' FROM namespaces WHERE id = (SELECT project_namespace_id FROM projects WHERE id = events.project_id LIMIT 1) LIMIT 1)
+ WHEN group_id IS NOT NULL THEN (SELECT array_to_string(traversal_ids, '/') || '/' FROM namespaces WHERE id = events.group_id LIMIT 1)
+ ELSE ''
+ END
+ ) AS path
+ SQL
+
+ EVENT_PROJECTIONS = [
+ :id,
+ PATH_COLUMN,
+ :author_id,
+ :target_id,
+ :target_type,
+ 'action AS raw_action',
+ 'EXTRACT(epoch FROM created_at) AS casted_created_at',
+ 'EXTRACT(epoch FROM updated_at) AS casted_updated_at'
+ ].freeze
+
+ INSERT_EVENTS_QUERY = <<~SQL.squish
+ INSERT INTO events (#{CSV_MAPPING.keys.join(', ')})
+ SETTINGS async_insert=1, wait_for_async_insert=1 FORMAT CSV
+ SQL
def perform
unless enabled?
@@ -22,12 +63,15 @@ module ClickHouse
metadata = { status: :processed }
- # Prevent parallel jobs
begin
+ # Prevent parallel jobs
in_lock(self.class.to_s, ttl: MAX_TTL, retries: 0) do
- true
- end
+ loop { break unless next_batch }
+
+ metadata.merge!(records_inserted: context.total_record_count, reached_end_of_table: context.no_more_records?)
+ ClickHouse::SyncCursor.update_cursor_for(:events, context.last_processed_id) if context.last_processed_id
+ end
rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
# Skip retrying, just let the next worker to start after a few minutes
metadata = { status: :skipped }
@@ -38,8 +82,51 @@ module ClickHouse
private
+ def context
+ @context ||= ClickHouse::RecordSyncContext.new(
+ last_record_id: ClickHouse::SyncCursor.cursor_for(:events),
+ max_records_per_batch: INSERT_BATCH_SIZE,
+ runtime_limiter: Analytics::CycleAnalytics::RuntimeLimiter.new(MAX_RUNTIME)
+ )
+ end
+
def enabled?
ClickHouse::Client.configuration.databases[:main].present? && Feature.enabled?(:event_sync_worker_for_click_house)
end
+
+ def next_batch
+ context.new_batch!
+
+ CsvBuilder::Gzip.new(process_batch(context), CSV_MAPPING).render do |tempfile, rows_written|
+ unless rows_written == 0
+ ClickHouse::Client.insert_csv(INSERT_EVENTS_QUERY, File.open(tempfile.path),
+ :main)
+ end
+ end
+
+ !(context.over_time? || context.no_more_records?)
+ end
+
+ def process_batch(context)
+ Enumerator.new do |yielder|
+ has_data = false
+ # rubocop: disable CodeReuse/ActiveRecord
+ Event.where(Event.arel_table[:id].gt(context.last_record_id)).each_batch(of: BATCH_SIZE) do |relation|
+ has_data = true
+
+ relation.select(*EVENT_PROJECTIONS).each do |row|
+ yielder << row
+ context.last_processed_id = row.id
+
+ break if context.record_limit_reached?
+ end
+
+ break if context.over_time? || context.record_limit_reached?
+ end
+
+ context.no_more_records! if has_data == false
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
end
end
diff --git a/app/workers/concerns/gitlab/bitbucket_import/object_importer.rb b/app/workers/concerns/gitlab/bitbucket_import/object_importer.rb
new file mode 100644
index 00000000000..26e6e2675ed
--- /dev/null
+++ b/app/workers/concerns/gitlab/bitbucket_import/object_importer.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BitbucketImport
+ # ObjectImporter defines the base behaviour for every Sidekiq worker that
+ # imports a single resource such as a note or pull request.
+ module ObjectImporter
+ extend ActiveSupport::Concern
+
+ included do
+ include ApplicationWorker
+
+ data_consistency :always
+
+ feature_category :importers
+
+ worker_has_external_dependencies!
+
+ sidekiq_retries_exhausted do |msg|
+ args = msg['args']
+ jid = msg['jid']
+
+ # If a job is being exhausted we still want to notify the
+ # Gitlab::Import::AdvanceStageWorker to prevent the entire import from getting stuck
+ key = args.last
+ JobWaiter.notify(key, jid) if args.length == 3 && key && key.is_a?(String)
+ end
+ end
+
+ def perform(project_id, hash, notify_key)
+ project = Project.find_by_id(project_id)
+
+ return unless project
+
+ if project.import_state&.canceled?
+ info(project.id, message: 'project import canceled')
+ return
+ end
+
+ import(project, hash)
+ ensure
+ notify_waiter(notify_key)
+ end
+
+ private
+
+ # project - An instance of `Project` to import the data into.
+ # hash - A Hash containing the details of the object to import.
+ def import(project, hash)
+ info(project.id, message: 'importer started')
+
+ importer_class.new(project, hash).execute
+
+ info(project.id, message: 'importer finished')
+ rescue ActiveRecord::RecordInvalid => e
+ # We do not raise exception to prevent job retry
+ track_exception(project, e)
+ rescue StandardError => e
+ track_and_raise_exception(project, e)
+ end
+
+ def notify_waiter(key)
+ JobWaiter.notify(key, jid)
+ end
+
+ # Returns the class to use for importing the object.
+ def importer_class
+ raise NotImplementedError
+ end
+
+ def info(project_id, extra = {})
+ Logger.info(log_attributes(project_id, extra))
+ end
+
+ def log_attributes(project_id, extra = {})
+ extra.merge(
+ project_id: project_id,
+ importer: importer_class.name
+ )
+ end
+
+ def track_exception(project, exception, fail_import: false)
+ Gitlab::Import::ImportFailureService.track(
+ project_id: project.id,
+ error_source: importer_class.name,
+ exception: exception,
+ fail_import: fail_import
+ )
+ end
+
+ def track_and_raise_exception(project, exception, fail_import: false)
+ track_exception(project, exception, fail_import: fail_import)
+
+ raise(exception)
+ end
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/bitbucket_import/stage_methods.rb b/app/workers/concerns/gitlab/bitbucket_import/stage_methods.rb
new file mode 100644
index 00000000000..2885cc29532
--- /dev/null
+++ b/app/workers/concerns/gitlab/bitbucket_import/stage_methods.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BitbucketImport
+ module StageMethods
+ extend ActiveSupport::Concern
+
+ included do
+ include ApplicationWorker
+
+ worker_has_external_dependencies!
+
+ feature_category :importers
+
+ data_consistency :always
+
+ sidekiq_options dead: false, retry: 3
+
+ sidekiq_retries_exhausted do |msg, e|
+ Gitlab::Import::ImportFailureService.track(
+ project_id: msg['args'][0],
+ exception: e,
+ 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')
+
+ project = find_project(project_id)
+
+ return unless project
+
+ import(project)
+
+ info(project_id, message: 'stage finished')
+ rescue StandardError => e
+ Gitlab::Import::ImportFailureService.track(
+ project_id: project_id,
+ exception: e,
+ error_source: self.class.name,
+ fail_import: abort_on_failure
+ )
+
+ raise(e)
+ end
+
+ def find_project(id)
+ # If the project has been marked as failed we want to bail out
+ # automatically.
+ # rubocop: disable CodeReuse/ActiveRecord
+ Project.joins_import_state.where(import_state: { status: :started }).find_by_id(id)
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+
+ def abort_on_failure
+ false
+ end
+
+ private
+
+ def info(project_id, extra = {})
+ Logger.info(log_attributes(project_id, extra))
+ end
+
+ def log_attributes(project_id, extra = {})
+ extra.merge(
+ project_id: project_id,
+ import_stage: self.class.name
+ )
+ end
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/bitbucket_server_import/object_importer.rb b/app/workers/concerns/gitlab/bitbucket_server_import/object_importer.rb
index b209719479b..1090d82c922 100644
--- a/app/workers/concerns/gitlab/bitbucket_server_import/object_importer.rb
+++ b/app/workers/concerns/gitlab/bitbucket_server_import/object_importer.rb
@@ -23,7 +23,7 @@ module Gitlab
# 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)
- JobWaiter.notify(key, jid)
+ JobWaiter.notify(key, jid, ttl: Gitlab::Import::JOB_WAITER_TTL)
end
end
end
@@ -61,7 +61,7 @@ module Gitlab
end
def notify_waiter(key)
- JobWaiter.notify(key, jid)
+ JobWaiter.notify(key, jid, ttl: Gitlab::Import::JOB_WAITER_TTL)
end
# Returns the class to use for importing the object.
diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb
index 6cb9bd34969..e190ced5073 100644
--- a/app/workers/concerns/gitlab/github_import/object_importer.rb
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -27,7 +27,7 @@ module Gitlab
# 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)
- JobWaiter.notify(key, jid)
+ JobWaiter.notify(key, jid, ttl: Gitlab::Import::JOB_WAITER_TTL)
end
end
end
@@ -38,7 +38,7 @@ module Gitlab
# client - An instance of `Gitlab::GithubImport::Client`
# hash - A Hash containing the details of the object to import.
def import(project, client, hash)
- unless project.import_state&.in_progress?
+ if project.import_state&.completed?
info(
project.id,
message: 'Project import is no longer running. Stopping worker.',
diff --git a/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb b/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
index b40914770b5..f6feb6d1598 100644
--- a/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
+++ b/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
@@ -39,7 +39,7 @@ module Gitlab
end
def notify_waiter(key = nil)
- JobWaiter.notify(key, jid) if key
+ JobWaiter.notify(key, jid, ttl: Gitlab::Import::JOB_WAITER_TTL) if key
end
def reschedule_job(project, client, hash, notify_key)
diff --git a/app/workers/concerns/gitlab/github_import/stage_methods.rb b/app/workers/concerns/gitlab/github_import/stage_methods.rb
index a5287fcfbe2..75db5589415 100644
--- a/app/workers/concerns/gitlab/github_import/stage_methods.rb
+++ b/app/workers/concerns/gitlab/github_import/stage_methods.rb
@@ -9,7 +9,7 @@ module Gitlab
return unless (project = find_project(project_id))
- unless project.import_state&.in_progress?
+ if project.import_state&.completed?
info(
project_id,
message: 'Project import is no longer running. Stopping worker.',
diff --git a/app/workers/concerns/gitlab/import/notify_upon_death.rb b/app/workers/concerns/gitlab/import/notify_upon_death.rb
new file mode 100644
index 00000000000..ae726a673c2
--- /dev/null
+++ b/app/workers/concerns/gitlab/import/notify_upon_death.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+# NotifyUponDeath can be included into a worker class if it should
+# notify any JobWaiter instances upon being moved to the Sidekiq dead queue.
+#
+# Note that this will only notify the waiter upon graceful termination, a
+# SIGKILL will still result in the waiter _not_ being notified.
+#
+# Workers including this module must have jobs passed where the last
+# argument is the key to notify, as a String.
+module Gitlab
+ module Import
+ module NotifyUponDeath
+ extend ActiveSupport::Concern
+
+ included do
+ # If a job is being exhausted we still want to notify the
+ # Gitlab::Import::AdvanceStageWorker. This prevents the entire import from getting stuck
+ # just because 1 job threw too many errors.
+ sidekiq_retries_exhausted do |job|
+ args = job['args']
+ jid = job['jid']
+ key = args.last
+
+ next unless args.length == 3 && key.is_a?(String)
+
+ JobWaiter.notify(key, jid, ttl: Gitlab::Import::JOB_WAITER_TTL)
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/notify_upon_death.rb b/app/workers/concerns/gitlab/notify_upon_death.rb
deleted file mode 100644
index 66dc6270637..00000000000
--- a/app/workers/concerns/gitlab/notify_upon_death.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- # NotifyUponDeath can be included into a worker class if it should
- # notify any JobWaiter instances upon being moved to the Sidekiq dead queue.
- #
- # Note that this will only notify the waiter upon graceful termination, a
- # SIGKILL will still result in the waiter _not_ being notified.
- #
- # Workers including this module must have jobs passed where the last
- # argument is the key to notify, as a String.
- module NotifyUponDeath
- extend ActiveSupport::Concern
-
- included do
- # If a job is being exhausted we still want to notify the
- # Gitlab::Import::AdvanceStageWorker. This prevents the entire import from getting stuck
- # just because 1 job threw too many errors.
- sidekiq_retries_exhausted do |job|
- args = job['args']
- jid = job['jid']
-
- if args.length == 3 && (key = args.last) && key.is_a?(String)
- JobWaiter.notify(key, jid)
- end
- end
- end
- end
-end
diff --git a/app/workers/database/batched_background_migration/execution_worker.rb b/app/workers/database/batched_background_migration/execution_worker.rb
index 1bdc829418a..75798f0ab73 100644
--- a/app/workers/database/batched_background_migration/execution_worker.rb
+++ b/app/workers/database/batched_background_migration/execution_worker.rb
@@ -64,6 +64,8 @@ module Database
attr_accessor :database_name, :migration
def enabled?
+ return false if Feature.enabled?(:disallow_database_ddl_feature_flags, type: :ops)
+
Feature.enabled?(:execute_batched_migrations_on_schedule, type: :ops)
end
diff --git a/app/workers/database/batched_background_migration/single_database_worker.rb b/app/workers/database/batched_background_migration/single_database_worker.rb
index ebf63d34cbf..f73f8fd751b 100644
--- a/app/workers/database/batched_background_migration/single_database_worker.rb
+++ b/app/workers/database/batched_background_migration/single_database_worker.rb
@@ -27,6 +27,8 @@ module Database
# :nocov:
def enabled?
+ return false if Feature.enabled?(:disallow_database_ddl_feature_flags, type: :ops)
+
Feature.enabled?(:execute_batched_migrations_on_schedule, type: :ops)
end
diff --git a/app/workers/database/lock_tables_worker.rb b/app/workers/database/lock_tables_worker.rb
new file mode 100644
index 00000000000..12c8e508ad5
--- /dev/null
+++ b/app/workers/database/lock_tables_worker.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module Database
+ class LockTablesWorker
+ include ApplicationWorker
+
+ TableShouldNotBeLocked = Class.new(StandardError)
+
+ sidekiq_options retry: false
+ feature_category :cell
+ data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency
+ idempotent!
+
+ version 1
+
+ def perform(database_name, tables)
+ check_if_should_lock_database(database_name)
+
+ connection = ::Gitlab::Database.database_base_models_with_gitlab_shared[database_name].connection
+ check_if_should_lock_tables(tables, database_name, connection)
+
+ performed_actions = tables.map do |table_name|
+ lock_writes_manager(table_name, connection, database_name).lock_writes
+ end
+
+ log_extra_metadata_on_done(:performed_actions, performed_actions)
+ end
+
+ private
+
+ def check_if_should_lock_database(database_name)
+ raise TableShouldNotBeLocked, 'GitLab is not running in multiple database mode' unless
+ Gitlab::Database.database_mode == Gitlab::Database::MODE_MULTIPLE_DATABASES
+
+ raise TableShouldNotBeLocked, "database '#{database_name}' does not support locking writes on tables" unless
+ ::Gitlab::Database.database_base_models_with_gitlab_shared.include?(database_name)
+ end
+
+ def check_if_should_lock_tables(tables, database_name, connection)
+ tables.each do |table_name|
+ unless should_lock_writes_on_table?(connection, database_name, table_name)
+ raise TableShouldNotBeLocked, "table '#{table_name}' should not be locked on the database '#{database_name}'"
+ end
+ end
+ end
+
+ def should_lock_writes_on_table?(connection, database_name, table_name)
+ db_info = Gitlab::Database.all_database_connections.fetch(database_name)
+ table_schema = Gitlab::Database::GitlabSchema.table_schema!(table_name.to_s)
+
+ Gitlab::Database.gitlab_schemas_for_connection(connection).exclude?(table_schema) &&
+ db_info.lock_gitlab_schemas.include?(table_schema)
+ end
+
+ def lock_writes_manager(table_name, connection, database_name)
+ Gitlab::Database::LockWritesManager.new(
+ table_name: table_name,
+ connection: connection,
+ database_name: database_name,
+ with_retries: !connection.transaction_open?,
+ logger: nil,
+ dry_run: false
+ )
+ end
+ end
+end
diff --git a/app/workers/database/monitor_locked_tables_worker.rb b/app/workers/database/monitor_locked_tables_worker.rb
index 66296ea1c0d..4a23d25edf4 100644
--- a/app/workers/database/monitor_locked_tables_worker.rb
+++ b/app/workers/database/monitor_locked_tables_worker.rb
@@ -33,6 +33,13 @@ module Database
handle_lock_writes_result(tables_lock_info_per_db, result)
end
+ tables_lock_info_per_db.each do |database_name, database_results|
+ next if database_results[:tables_need_lock].empty?
+ break if Feature.disabled?(:lock_tables_in_monitoring, type: :ops)
+
+ LockTablesWorker.perform_async(database_name, database_results[:tables_need_lock])
+ end
+
log_extra_metadata_on_done(:results, tables_lock_info_per_db)
end
diff --git a/app/workers/environments/stop_job_success_worker.rb b/app/workers/environments/stop_job_success_worker.rb
index cc7d83512f3..93b743ee602 100644
--- a/app/workers/environments/stop_job_success_worker.rb
+++ b/app/workers/environments/stop_job_success_worker.rb
@@ -9,15 +9,15 @@ module Environments
feature_category :continuous_delivery
def perform(job_id, _params = {})
- Ci::Build.find_by_id(job_id).try do |build|
- stop_environment(build) if build.stops_environment? && build.stop_action_successful?
+ Ci::Processable.find_by_id(job_id).try do |job|
+ stop_environment(job) if job.stops_environment? && job.stop_action_successful?
end
end
private
- def stop_environment(build)
- build.persisted_environment.fire_state_event(:stop_complete)
+ def stop_environment(job)
+ job.persisted_environment.fire_state_event(:stop_complete)
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
new file mode 100644
index 00000000000..7f281352a1b
--- /dev/null
+++ b/app/workers/gitlab/bitbucket_import/advance_stage_worker.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BitbucketImport
+ # AdvanceStageWorker is a worker used by the BitBucket Importer to wait for a
+ # number of jobs to complete, without blocking a thread. Once all jobs have
+ # been completed this worker will advance the import process to the next
+ # stage.
+ class AdvanceStageWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+ include ::Gitlab::Import::AdvanceStage
+
+ data_consistency :delayed
+
+ sidekiq_options dead: false, retry: 3
+
+ feature_category :importers
+
+ loggable_arguments 1, 2
+
+ # The known importer stages and their corresponding Sidekiq workers.
+ STAGES = {
+ finish: Stage::FinishImportWorker
+ }.freeze
+
+ def find_import_state(project_id)
+ ProjectImportState.jid_by(project_id: project_id, status: :started)
+ end
+
+ private
+
+ def next_stage_worker(next_stage)
+ STAGES.fetch(next_stage.to_sym)
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/bitbucket_import/import_pull_request_worker.rb b/app/workers/gitlab/bitbucket_import/import_pull_request_worker.rb
new file mode 100644
index 00000000000..5b06ddf7079
--- /dev/null
+++ b/app/workers/gitlab/bitbucket_import/import_pull_request_worker.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BitbucketImport
+ class ImportPullRequestWorker # rubocop:disable Scalability/IdempotentWorker
+ include ObjectImporter
+
+ def importer_class
+ Importers::PullRequestImporter
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/bitbucket_import/stage/finish_import_worker.rb b/app/workers/gitlab/bitbucket_import/stage/finish_import_worker.rb
new file mode 100644
index 00000000000..a1c5f5787be
--- /dev/null
+++ b/app/workers/gitlab/bitbucket_import/stage/finish_import_worker.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BitbucketImport
+ module Stage
+ class FinishImportWorker # rubocop:disable Scalability/IdempotentWorker
+ include StageMethods
+
+ private
+
+ def import(project)
+ project.after_import
+
+ Gitlab::Import::Metrics.new(:bitbucket_importer, project).track_finished_import
+ 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
new file mode 100644
index 00000000000..e1f3b5ab79a
--- /dev/null
+++ b/app/workers/gitlab/bitbucket_import/stage/import_pull_requests_worker.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BitbucketImport
+ module Stage
+ class ImportPullRequestsWorker # 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::PullRequestsImporter
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/bitbucket_import/stage/import_repository_worker.rb b/app/workers/gitlab/bitbucket_import/stage/import_repository_worker.rb
new file mode 100644
index 00000000000..7c6503ae38f
--- /dev/null
+++ b/app/workers/gitlab/bitbucket_import/stage/import_repository_worker.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BitbucketImport
+ module Stage
+ class ImportRepositoryWorker # rubocop:disable Scalability/IdempotentWorker
+ include StageMethods
+
+ private
+
+ def import(project)
+ importer = importer_class.new(project)
+
+ importer.execute
+
+ ImportPullRequestsWorker.perform_async(project.id)
+ end
+
+ def importer_class
+ Importers::RepositoryImporter
+ end
+
+ def abort_on_failure
+ true
+ end
+ end
+ end
+ end
+end
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 1f17c98dff9..60e4c8fdad6 100644
--- a/app/workers/gitlab/github_gists_import/import_gist_worker.rb
+++ b/app/workers/gitlab/github_gists_import/import_gist_worker.rb
@@ -25,7 +25,7 @@ module Gitlab
# Gitlab::GithubGistsImport::FinishImportWorker to prevent
# the entire import from getting stuck
if args.length == 3 && (key = args.last) && key.is_a?(String)
- JobWaiter.notify(key, jid)
+ JobWaiter.notify(key, jid, ttl: Gitlab::Import::JOB_WAITER_TTL)
end
end
@@ -48,7 +48,7 @@ module Gitlab
)
end
- JobWaiter.notify(notify_key, jid)
+ JobWaiter.notify(notify_key, jid, ttl: Gitlab::Import::JOB_WAITER_TTL)
end
rescue StandardError => e
log_and_track_error(user_id, e, github_identifiers)
diff --git a/app/workers/gitlab/import/advance_stage.rb b/app/workers/gitlab/import/advance_stage.rb
index 9fc03efe9d0..5d5abc88388 100644
--- a/app/workers/gitlab/import/advance_stage.rb
+++ b/app/workers/gitlab/import/advance_stage.rb
@@ -15,7 +15,15 @@ module Gitlab
# next_stage - The name of the next stage to start when all jobs have been
# completed.
def perform(project_id, waiters, next_stage)
- return unless import_state = find_import_state(project_id)
+ import_state = find_import_state(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?
+ clear_waiter_caches(waiters)
+ return
+ end
new_waiters = wait_for_jobs(waiters)
@@ -56,6 +64,12 @@ module Gitlab
def next_stage_worker(next_stage)
raise NotImplementedError
end
+
+ def clear_waiter_caches(waiters)
+ waiters.each_key do |key|
+ JobWaiter.delete_key(key)
+ end
+ end
end
end
end
diff --git a/app/workers/gitlab/jira_import/import_issue_worker.rb b/app/workers/gitlab/jira_import/import_issue_worker.rb
index eabe988dfc2..2b676238a37 100644
--- a/app/workers/gitlab/jira_import/import_issue_worker.rb
+++ b/app/workers/gitlab/jira_import/import_issue_worker.rb
@@ -8,9 +8,9 @@ module Gitlab
data_consistency :always
sidekiq_options retry: 3
- include NotifyUponDeath
include Gitlab::JiraImport::QueueOptions
include Gitlab::Import::DatabaseHelpers
+ include Gitlab::Import::NotifyUponDeath
loggable_arguments 3
@@ -27,7 +27,7 @@ module Gitlab
JiraImport.increment_issue_failures(project_id)
ensure
# ensure we notify job waiter that the job has finished
- JobWaiter.notify(waiter_key, jid) if waiter_key
+ JobWaiter.notify(waiter_key, jid, ttl: Gitlab::Import::JOB_WAITER_TTL) if waiter_key
end
private
diff --git a/app/workers/incident_management/close_incident_worker.rb b/app/workers/incident_management/close_incident_worker.rb
index c820a8a97bf..011cb76442b 100644
--- a/app/workers/incident_management/close_incident_worker.rb
+++ b/app/workers/incident_management/close_incident_worker.rb
@@ -25,7 +25,7 @@ module IncidentManagement
private
def user
- @user ||= User.alert_bot
+ @user ||= Users::Internal.alert_bot
end
def close_incident(incident)
diff --git a/app/workers/incident_management/process_alert_worker_v2.rb b/app/workers/incident_management/process_alert_worker_v2.rb
index 04c02d17704..12434671527 100644
--- a/app/workers/incident_management/process_alert_worker_v2.rb
+++ b/app/workers/incident_management/process_alert_worker_v2.rb
@@ -32,7 +32,7 @@ module IncidentManagement
def create_issue_for(alert)
AlertManagement::CreateAlertIssueService
- .new(alert, User.alert_bot)
+ .new(alert, Users::Internal.alert_bot)
.execute
end
diff --git a/app/workers/loose_foreign_keys/cleanup_worker.rb b/app/workers/loose_foreign_keys/cleanup_worker.rb
index e6d0261b7f1..e1233a449e7 100644
--- a/app/workers/loose_foreign_keys/cleanup_worker.rb
+++ b/app/workers/loose_foreign_keys/cleanup_worker.rb
@@ -12,18 +12,23 @@ module LooseForeignKeys
idempotent!
def perform
+ connection_name, base_model = current_connection_name_and_base_model
+ modification_tracker, turbo_mode = initialize_modification_tracker_for(connection_name)
+
# Add small buffer on MAX_RUNTIME to account for single long running
# query or extra worker time after the cleanup.
- lock_ttl = ModificationTracker::MAX_RUNTIME + 20.seconds
+ lock_ttl = modification_tracker.max_runtime + 10.seconds
in_lock(self.class.name.underscore, ttl: lock_ttl, retries: 0) do
stats = {}
- connection_name, base_model = current_connection_name_and_base_model
-
Gitlab::Database::SharedModel.using_connection(base_model.connection) do
- stats = ProcessDeletedRecordsService.new(connection: base_model.connection).execute
+ stats = ProcessDeletedRecordsService.new(
+ connection: base_model.connection,
+ modification_tracker: modification_tracker
+ ).execute
stats[:connection] = connection_name
+ stats[:turbo_mode] = turbo_mode
end
log_extra_metadata_on_done(:stats, stats)
@@ -41,5 +46,16 @@ module LooseForeignKeys
connections_with_name = Gitlab::Database.database_base_models_with_gitlab_shared.to_a # this will never be empty
connections_with_name[minutes_since_epoch % connections_with_name.count]
end
+
+ def initialize_modification_tracker_for(connection_name)
+ turbo_mode = turbo_mode?(connection_name)
+ modification_tracker ||= turbo_mode ? TurboModificationTracker.new : ModificationTracker.new
+ [modification_tracker, turbo_mode]
+ end
+
+ def turbo_mode?(connection_name)
+ %w[main ci].include?(connection_name) &&
+ Feature.enabled?(:"loose_foreign_keys_turbo_mode_#{connection_name}", type: :ops)
+ end
end
end
diff --git a/app/workers/members_destroyer/unassign_issuables_worker.rb b/app/workers/members_destroyer/unassign_issuables_worker.rb
index 2e6ce0005fc..677a1be25ca 100644
--- a/app/workers/members_destroyer/unassign_issuables_worker.rb
+++ b/app/workers/members_destroyer/unassign_issuables_worker.rb
@@ -8,7 +8,7 @@ module MembersDestroyer
sidekiq_options retry: 3
- ENTITY_TYPES = %w(Group Project).freeze
+ ENTITY_TYPES = %w[Group Project].freeze
queue_namespace :unassign_issuables
feature_category :user_management
diff --git a/app/workers/merge_requests/ensure_prepared_worker.rb b/app/workers/merge_requests/ensure_prepared_worker.rb
new file mode 100644
index 00000000000..6dfe888f408
--- /dev/null
+++ b/app/workers/merge_requests/ensure_prepared_worker.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class EnsurePreparedWorker
+ include ApplicationWorker
+ include CronjobQueue
+
+ feature_category :code_review_workflow
+ idempotent!
+ deduplicate :until_executed
+ data_consistency :sticky
+
+ JOBS_PER_10_SECONDS = 5
+
+ def perform
+ return unless Feature.enabled?(:ensure_merge_requests_prepared)
+
+ scope = MergeRequest.recently_unprepared
+
+ iterator = Gitlab::Pagination::Keyset::Iterator.new(scope: scope)
+
+ index = 0
+ iterator.each_batch(of: JOBS_PER_10_SECONDS) do |merge_requests|
+ index += 1
+
+ NewMergeRequestWorker.bulk_perform_in_with_contexts(index * 10.seconds,
+ merge_requests,
+ arguments_proc: ->(merge_request) { [merge_request.id, merge_request.author_id] },
+ context_proc: ->(merge_request) { { project: merge_request.project } }
+ )
+ end
+ end
+ end
+end
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
index 3fcd7a3ad7a..a0594b15e31 100644
--- a/app/workers/merge_worker.rb
+++ b/app/workers/merge_worker.rb
@@ -29,3 +29,5 @@ class MergeWorker # rubocop:disable Scalability/IdempotentWorker
.execute(merge_request)
end
end
+
+MergeWorker.prepend_mod
diff --git a/app/workers/metrics/global_metrics_update_worker.rb b/app/workers/metrics/global_metrics_update_worker.rb
index 326403a2f8f..9b196f7213f 100644
--- a/app/workers/metrics/global_metrics_update_worker.rb
+++ b/app/workers/metrics/global_metrics_update_worker.rb
@@ -16,9 +16,7 @@ module Metrics
LEASE_TIMEOUT = 2.minutes
- def perform
- try_obtain_lease { ::Metrics::GlobalMetricsUpdateService.new.execute }
- end
+ def perform; end
def lease_timeout
LEASE_TIMEOUT
diff --git a/app/workers/namespaces/in_product_marketing_emails_worker.rb b/app/workers/namespaces/in_product_marketing_emails_worker.rb
deleted file mode 100644
index 470fba1227d..00000000000
--- a/app/workers/namespaces/in_product_marketing_emails_worker.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-module Namespaces
- class InProductMarketingEmailsWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
-
- feature_category :experimentation_activation
- urgency :low
-
- def perform
- return if paid_self_managed_instance?
- return if setting_disabled?
-
- Namespaces::InProductMarketingEmailsService.send_for_all_tracks_and_intervals
- end
-
- private
-
- def paid_self_managed_instance?
- false
- end
-
- def setting_disabled?
- !Gitlab::CurrentSettings.in_product_marketing_emails_enabled
- end
- end
-end
-
-Namespaces::InProductMarketingEmailsWorker.prepend_mod_with('Namespaces::InProductMarketingEmailsWorker')
diff --git a/app/workers/new_merge_request_worker.rb b/app/workers/new_merge_request_worker.rb
index 74239c5d968..e2e738f79a5 100644
--- a/app/workers/new_merge_request_worker.rb
+++ b/app/workers/new_merge_request_worker.rb
@@ -2,22 +2,23 @@
class NewMergeRequestWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ include NewIssuable
data_consistency :always
-
sidekiq_options retry: 3
- include NewIssuable
idempotent!
deduplicate :until_executed
feature_category :code_review_workflow
urgency :high
+
worker_resource_boundary :cpu
weight 2
def perform(merge_request_id, user_id)
return unless objects_found?(merge_request_id, user_id)
+ return if issuable.prepared?
MergeRequests::AfterCreateService
.new(project: issuable.target_project, current_user: user)
diff --git a/app/workers/pages/invalidate_domain_cache_worker.rb b/app/workers/pages/invalidate_domain_cache_worker.rb
deleted file mode 100644
index 1700b681b94..00000000000
--- a/app/workers/pages/invalidate_domain_cache_worker.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-module Pages
- class InvalidateDomainCacheWorker
- include Gitlab::EventStore::Subscriber
-
- idempotent!
-
- feature_category :pages
-
- def handle_event(event)
- domain_ids(event).each do |domain_id|
- ::Gitlab::Pages::CacheControl
- .for_domain(domain_id)
- .clear_cache
- end
-
- event.data.values_at(
- :root_namespace_id,
- :old_root_namespace_id,
- :new_root_namespace_id
- ).compact.uniq.each do |namespace_id|
- ::Gitlab::Pages::CacheControl
- .for_namespace(namespace_id)
- .clear_cache
- end
- end
-
- def domain_ids(event)
- ids = PagesDomain.ids_for_project(event.data[:project_id])
-
- ids << event.data[:domain_id] if event.data[:domain_id]
-
- ids
- end
- end
-end
diff --git a/app/workers/personal_access_tokens/expiring_worker.rb b/app/workers/personal_access_tokens/expiring_worker.rb
index de0bda82573..5f8316d184d 100644
--- a/app/workers/personal_access_tokens/expiring_worker.rb
+++ b/app/workers/personal_access_tokens/expiring_worker.rb
@@ -29,9 +29,21 @@ module PersonalAccessTokens
# rubocop: enable CodeReuse/ActiveRecord
- notification_service.access_token_about_to_expire(user, token_names)
+ message = if user.project_bot?
+ notification_service.resource_access_tokens_about_to_expire(user, token_names)
- Gitlab::AppLogger.info "#{self.class}: Notifying User #{user.id} about expiring tokens"
+ "Notifying Bot User resource owners about expiring tokens"
+ else
+ notification_service.access_token_about_to_expire(user, token_names)
+
+ "Notifying User about expiring tokens"
+ end
+
+ Gitlab::AppLogger.info(
+ message: message,
+ class: self.class,
+ user_id: user.id
+ )
expiring_user_tokens.each_batch do |expiring_tokens|
expiring_tokens.update_all(expire_notification_delivered: true)
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 4971dc3775f..5345714a010 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -164,7 +164,7 @@ class PostReceive
user: user,
property: 'source_code_pushes',
label: metric_path,
- context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis, key_path: metric_path).to_context]
+ context: [Gitlab::Usage::MetricDefinition.context_for(metric_path).to_context]
)
end
end
diff --git a/app/workers/projects/inactive_projects_deletion_cron_worker.rb b/app/workers/projects/inactive_projects_deletion_cron_worker.rb
index 31fdb3d9615..f16415af830 100644
--- a/app/workers/projects/inactive_projects_deletion_cron_worker.rb
+++ b/app/workers/projects/inactive_projects_deletion_cron_worker.rb
@@ -22,7 +22,7 @@ module Projects
return unless ::Gitlab::CurrentSettings.delete_inactive_projects?
@start_time ||= ::Gitlab::Metrics::System.monotonic_time
- admin_bot = ::User.admin_bot
+ admin_bot = ::Users::Internal.admin_bot
return unless admin_bot
diff --git a/app/workers/projects/record_target_platforms_worker.rb b/app/workers/projects/record_target_platforms_worker.rb
index 9ebc52f77d3..bbe0c63cfd1 100644
--- a/app/workers/projects/record_target_platforms_worker.rb
+++ b/app/workers/projects/record_target_platforms_worker.rb
@@ -6,9 +6,9 @@ module Projects
include ExclusiveLeaseGuard
LEASE_TIMEOUT = 1.hour.to_i
- APPLE_PLATFORM_LANGUAGES = %w(swift objective-c).freeze
+ APPLE_PLATFORM_LANGUAGES = %w[swift objective-c].freeze
- feature_category :groups_and_projects
+ feature_category :experimentation_activation
data_consistency :always
deduplicate :until_executed
urgency :low
diff --git a/app/workers/users/deactivate_dormant_users_worker.rb b/app/workers/users/deactivate_dormant_users_worker.rb
index 87566bff467..33c54f07521 100644
--- a/app/workers/users/deactivate_dormant_users_worker.rb
+++ b/app/workers/users/deactivate_dormant_users_worker.rb
@@ -15,7 +15,7 @@ module Users
return unless ::Gitlab::CurrentSettings.current_application_settings.deactivate_dormant_users
- admin_bot = User.admin_bot
+ admin_bot = Users::Internal.admin_bot
return unless admin_bot
deactivate_users(User.dormant, admin_bot)
diff --git a/app/workers/users/track_namespace_visits_worker.rb b/app/workers/users/track_namespace_visits_worker.rb
new file mode 100644
index 00000000000..5b2a7b7d0fa
--- /dev/null
+++ b/app/workers/users/track_namespace_visits_worker.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Users
+ class TrackNamespaceVisitsWorker
+ include ApplicationWorker
+
+ feature_category :navigation
+ data_consistency :delayed
+ urgency :throttled
+ idempotent!
+
+ GROUPS = 'groups'
+ PROJECTS = 'projects'
+
+ def perform(entity_type, entity_id, user_id, time)
+ return unless entity_id && user_id
+
+ case entity_type
+ when GROUPS
+ unless GroupVisit.visited_around?(entity_id: entity_id, user_id: user_id, time: time)
+ GroupVisit.create!(entity_id: entity_id, user_id: user_id, visited_at: time)
+ end
+ when PROJECTS
+ unless ProjectVisit.visited_around?(entity_id: entity_id, user_id: user_id, time: time)
+ ProjectVisit.create!(entity_id: entity_id, user_id: user_id, visited_at: time)
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/x509_issuer_crl_check_worker.rb b/app/workers/x509_issuer_crl_check_worker.rb
index 58084405769..71d2c398ca7 100644
--- a/app/workers/x509_issuer_crl_check_worker.rb
+++ b/app/workers/x509_issuer_crl_check_worker.rb
@@ -18,7 +18,7 @@ class X509IssuerCrlCheckWorker
def perform
@logger = Gitlab::GitLogger.build
- X509Issuer.all.find_each do |issuer|
+ X509Issuer.with_crl_url.find_each do |issuer|
with_context(related_class: X509IssuerCrlCheckWorker) do
update_certificates(issuer)
end